Last active
February 5, 2019 14:11
-
-
Save jedahu/9879269 to your computer and use it in GitHub Desktop.
C# type-safe data validation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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