Skip to content

Instantly share code, notes, and snippets.

@jedahu
Last active February 5, 2019 14:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jedahu/9879269 to your computer and use it in GitHub Desktop.
Save jedahu/9879269 to your computer and use it in GitHub Desktop.
C# type-safe data validation
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
[ContractClass(typeof(IValidatorContract<,>))]
public interface IValidator<E, in A>
{
IEnumerable<E> Validate(A a);
}
[ContractClassFor(typeof(IValidator<,>))]
public abstract class IValidatorContract<E, A>
: IValidator<E, A>
{
IEnumerable<E> IValidator<E, A>.Validate(A a)
{
Contract.Requires(!ReferenceEquals(null, a));
Contract.Ensures(!ReferenceEquals(null, Contract.Result<IEnumerable<E>>()));
throw new System.NotImplementedException();
}
}
public sealed class ValidationException<E>
: Exception
{
private readonly E errors;
public ValidationException(E errors)
{
Contract.Requires(!ReferenceEquals(null, errors));
this.errors = errors;
}
public E Errors
{
get { return errors; }
}
}
[Pure]
public sealed class Valid<V, E, A>
where V : IValidator<E, A>, new()
{
private readonly A value;
private Valid(A value)
{
this.value = value;
}
public A Value { get { return value; }}
public static Valid<V, E, A> Validate(A a)
{
Contract.Requires(!ReferenceEquals(null, a));
Contract.Requires(
typeof (A).IsPrimitive
|| a is string
|| typeof (A).IsDefined(typeof (PureAttribute), false),
"Valid must be used with an immutable type."
+ " Make sure the type is immutable, then add"
+ " the System.Diagnostics.Contracts.Pure"
+ " attribute to its class.");
var errors = new V().Validate(a);
if (errors.Any())
{
throw new ValidationException<IEnumerable<E>>(
errors.ToList());
}
return new Valid<V, E, A>(a);
}
}
// The only way to get a Valid<V, E, A> is to call Valid<V, E, A>.Validate(A), so
// any method taking a Valid<V, E, A> argument is assured (at compile time) of getting
// a valid A.
using System.Collections.Generic;
using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Xunit;
public abstract class ValidLaws<V, E, A>
where V : IValidator<E, A>, new()
{
protected abstract Gen<A> ValidData { get; }
protected abstract Gen<A> InvalidData { get; }
protected abstract bool Eq(A a1, A a2);
[Property]
public void ValidDataValidates()
{
Spec.For(
ValidData,
data => Assert.DoesNotThrow(
() => Valid<V, E, A>.Validate(data)))
.QuickCheckThrowOnFailure();
}
[Property]
public void InvalidDataThrows()
{
Spec.For(
InvalidData,
data => Assert.Throws<ValidationException<IEnumerable<E>, A>>(
() => Valid<V, E, A>.Validate(data)))
.QuickCheckThrowOnFailure();
}
[Property]
public void Validate_on_valid_data_is_identity()
{
Spec.For(
ValidData,
data =>
Eq(data, Valid<V, E, A>.Validate(data).Value))
.QuickCheckThrowOnFailure();
}
}
public interface IInput<A>
{
A Value { get; }
string ID { get; }
}
[Pure]
public sealed class Form
{
public IInput<string> Name { get; }
public IInput<int> Age { get; }
}
public sealed class FormError
{
public FormError(string inputId, string errorMsg)
{
...;
}
public string InputID { get; }
public string ErrorMsg { get; }
}
public sealed class FormValidator
: IValidator<string, Form>
{
public IEnumerable<string> Validate(Form form)
{
if (string.IsNullOrEmpty(form.Name.Value))
yield return new FormError(form.Name.ID, "Name required.");
if (form.Age.Value < 0)
yield return new FormError(form.Age.ID, "Invalid age.");
}
}
public void MethodWithValidFormPrecondition(Valid<FormValidator, string, Form> validForm)
{
var form = validForm.Value;
...;
}
var form = ...;
try
{
var validForm = Valid<FormValidator, string, Form>.Validate(form);
MethodWithValidFormPrecondition(validForm);
}
catch (ValidationException<IEnumerable<E>> e)
{
RedisplayFormWithErrorsHighlighted(e.Errors);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment