Examples for validation
See original GitHub issueHi,
This is not an issue with the library, it is a request for examples on how to use validation. I already read the current validation tests, however, I feel it’s not enough for a beginner in the FP arts 😃. Particularly:
-
Add an example on how to use the ValidationContext and ValidationUnitContext. I am interested in passing additional data to the validators. Sometimes it is necessary to query databases to validate some piece of data.
-
Add examples on how to combine the results from validating child classes and parent classes. It would nice to also show an example where a validation is invoked only if a previous validation passed (perhaps add a & operator - I used bind below). I played with this stuff and here is some code that I ran in linqpad.
void Main()
{
// var x = (1, 2);
// x.Dump();
ValidateList(new List<string> {"", "1", "", "a", "200"}).Dump();
Parent parent = new Parent {
Children1 = new List<Child1> {
new Child1 {Id = "", Name = "Ai"},
new Child1 {Id = "1", Name = "X"},
},
Children2 = new List<Child2> {
new Child2 {Id = "", Name = "Ai", Child1Id = "100"},
new Child2 {Id = "1", Name = "Y", Child1Id = "1"},
},
};
ValidatorsParent(parent).Dump();
}
public class Error : NewType<Error, string>
{
public Error(string e) : base(e) { }
}
public static Validation<Error, List<String>> ValidateList(List<string> list)
{
//var errors = list.Map(s => Validators(s)).Filter(v => v.IsFail).SelectMany(value => value.FailToSeq()).ToList();//Map(v => v.FailToSeq().Head).ToList();
var errors = list.Map(s => Validators(s)).Bind(v => v.Match(Fail: errs => Some(errs), Succ: _ => None)).Bind(x => x).ToSeq(); //.Sequence();
return errors.Count() == 0 ? Validation<Error, List<String>>.Success(list) : Validation<Error, List<String>>.Fail(errors);
}
public static Validation<Error, string> NonEmpty(string str) =>
String.IsNullOrEmpty(str)
? Validation<Error, String>.Fail(Seq<Error>().Add(Error.New("Non empty string is required")))
: Validation<Error, String>.Success(str);
public static Validation<Error, string> StartsWithLetterDigit(string str) =>
!String.IsNullOrEmpty(str) && Char.IsLetter(str[0])
? Validation<Error, String>.Success(str)
: Validation<Error, String>.Fail(Seq<Error>().Add(Error.New($"{str} doesn't start with a letter")));
public static Validation<Error, string> Validators(string str) => NonEmpty(str).Bind(x => StartsWithLetterDigit(str));
public static MemberExpression ExtractMemberExpression<TSource, TProperty>(Expression<Func<TSource, TProperty>> expr)
{
MemberExpression me;
switch (expr.Body.NodeType)
{
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
var ue = expr.Body as UnaryExpression;
me = ue?.Operand as MemberExpression;
break;
default:
me = expr.Body as MemberExpression;
break;
}
return me;
}
public static PropertyInfo GetPropertyInfo<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
MemberExpression member = ExtractMemberExpression(propertyLambda);
if (member == null)
throw new ArgumentException($"Expression '{propertyLambda}' refers to a method, not a property.");
PropertyInfo propInfo = member.Member as PropertyInfo;
if (propInfo == null)
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
return propInfo;
}
public static Type TypeOf<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
return GetPropertyInfo(propertyLambda)
.PropertyType;
}
public static string NameOf<TSource, TProperty>(Expression<Func<TSource, TProperty>> propertyLambda)
{
return GetPropertyInfo(propertyLambda)
.Name;
}
public class Parent
{
public List<Child1> Children1 { get; set; }
public List<Child2> Children2 { get; set; }
}
public class Child1
{
public string Id { get; set; }
public string Name { get; set; }
}
public class Child2
{
public string Id { get; set; }
public string Name { get; set; }
public string Child1Id { get; set; }
}
public static Func<T, Validation<Error, T>> NonEmpty<T>(Expression<Func<T, string>> property)
{
PropertyInfo pi = GetPropertyInfo(property);
//string propertyName = NameOf(property);
return obj =>
{
string value = property.Compile().Invoke(obj);
return String.IsNullOrEmpty(value)
? Validation<Error, T>.Fail(Seq<Error>().Add(Error.New($"{pi.Name} of {pi.DeclaringType} is required")))
: Validation<Error, T>.Success(obj);
};
}
public static Func<T, Validation<Error, T>> ShouldStartWithVowel<T>(Expression<Func<T, string>> property)
{
PropertyInfo pi = GetPropertyInfo(property);
return obj =>
{
string value = property.Compile().Invoke(obj);
return String.IsNullOrEmpty(value) || !List('A', 'E', 'I', 'O', 'U', 'Y').Contains(value[0])
? Validation<Error, T>.Fail(Seq<Error>().Add(Error.New($"{pi.Name} of {pi.DeclaringType} is required and it should start with a vowel. Its value is invalid: '{value}'.")))
: Validation<Error, T>.Success(obj);
};
}
// More generic method
public static Func<T, Validation<Error, T>> CheckPredicate<T>(Func<T, bool> predicate, Func<T, string> errorMessage)
{
return obj =>
{
return predicate(obj)
? Validation<Error, T>.Success(obj)
: Validation<Error, T>.Fail(Seq<Error>().Add(Error.New(errorMessage(obj))));
};
}
public static Func<T, Validation<Error, T>> ShouldStartWithVowel2<T>(Expression<Func<T, string>> property)
{
PropertyInfo pi = GetPropertyInfo(property);
return obj =>
CheckPredicate<T>(
obj2 => {
string value = property.Compile().Invoke(obj2);
return !String.IsNullOrEmpty(value) && List('A', 'E', 'I', 'O', 'U', 'Y').Contains(value[0]);
},
obj2 => $"{pi.Name} of {pi.DeclaringType} is required and it should start with a vowel. Its value is invalid: '{property.Compile().Invoke(obj2)}'."
)(obj);
}
//public static List<Validation<Error, string>> Validators2 => new List<Validation<Error, string>> { NonEmpty, StartsWithLetterDigit};
public static Validation<Error, Child1> ValidatorsChild1(Child1 child1)
{
var v1 = NonEmpty((Child1 c) => c.Id)(child1);
var v2 = ShouldStartWithVowel2((Child1 c) => c.Name)(child1);
return v1 | v2;
}
public static Validation<Error, Child2> ValidateIds(Child2 child2, Parent parent)
{
return parent.Children1.Select(c => c.Id).Contains(child2.Child1Id)
? Validation<Error, Child2>.Success(child2)
: Validation<Error, Child2>.Fail(Seq<Error>().Add(Error.New($"Property Child1Id is invalid, it doesn't reference a Child1 Id: {child2.Child1Id}.")));
}
public static Validation<Error, Child2> ValidatorsChild2(Child2 child2, Parent parent)
{
var v1 = NonEmpty((Child2 c) => c.Id)(child2);
var v2 = ShouldStartWithVowel((Child2 c) => c.Name)(child2);
var v3 = ValidateIds(child2, parent);
return v1 | v2 | v3;
}
public static Seq<Error> CollectErrors<T>(IEnumerable<Validation<Error, T>> list)
{
//var errors = list.Map(s => Validators(s)).Filter(v => v.IsFail).SelectMany(value => value.FailToSeq()).ToList();//Map(v => v.FailToSeq().Head).ToList();
var errors = list.Bind(v => v.Match(Fail: errs => Some(errs), Succ: _ => None)).Bind(x => x).ToSeq(); //.Sequence();
//return errors.Count() == 0 ? Validation<Error, List<String>>.Success(list) : Validation<Error, List<String>>.Fail(errors);
return errors;
}
public static Validation<Error, Parent> ValidatorsParent(Parent parent)
{
// Is there a better way to do this?
var children1Errors = CollectErrors(parent.Children1.Map(c => ValidatorsChild1(c)));
var children2Errors = CollectErrors(parent.Children2.Map(c => ValidatorsChild2(c, parent)));
return children1Errors.Count == 0 && children2Errors.Count == 0
? Validation<Error, Parent>.Success(parent)
: Validation<Error, Parent>.Fail(children1Errors.Concat(children2Errors));
}
/*
{
var errors = validators
.Map(validate => validate(t))
.Bind(v => v.Match(Fail: errs => Some(errs.Head), Succ: _ => None))
.ToList();
return errors.Count == 0
? Success<Error, T>(t)
: errors.ToSeq();
};
*/
Overall I got it working, however, I get the feeling it can be improved.
Thanks
Issue Analytics
- State:
- Created 5 years ago
- Comments:8 (7 by maintainers)
Top GitHub Comments
Thank you for taking the time to review the code and for the comments!
Re: Dump() it is a function implemented in linqpad. I used linqpad to test the code. The using blocks are added in a separate dialog in linqpad, that’s why they are missing from the code - sorry for the confusion.
Yes, I started to read the Functional Programming in C# book. Just a side comment, I wish he used your library instead of writing his own code - it makes it harder when you want to adopt the techniques and use the FP style, the concepts are similar but the code is not exactly the same.
You make a very good point. Currently, my code is mostly imperative, however, I was looking at the validation classes to implement validation for configuration classes that receive values from the app.config file. My thought was to gradually introduce the FP style in the code. Validation is good start, I think.
Yes, your examples, NonnullString, NonemptyString etc. are very good. I can take it from here.
Thanks again
Your code contains two validation examples. I have finished going through the first one, the one that starts with
I think there is a key idea that you are missing. The purpose of validation is to guard the creation of strong types.
In the validation tests in Language Ext to which you linked, there is a type called
CreditCard
. Its constructor accepts any twostring
s and any twoint
s. However, having aCreditCard
instance is not the same as having an instance ofTuple<string, string, int, int>
. The difference is that the only call to the constructor ofCreditCard
went through many validation steps.The type safety of
CreditCard
is very good. It could be improved a bit using the smart constructor pattern. You can read more about this idea in Functional Programming in C# by Enrico Buonanno. He first mentions the idea in section 3.4.5 and then elaborates on it in section 8.5.1 in the context of validation.Alternatively, see below how the types I created have private constructors and factory methods involving validation.