question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Examples for validation

See original GitHub issue

Hi,

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:

  1. 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.

  2. 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:closed
  • Created 5 years ago
  • Comments:8 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
costa100commented, Feb 25, 2019

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.

I think there is a key idea that you are missing. The purpose of validation is to guard the creation of strong types.

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.

do you think you can improve it based on my review of your first example?

Yes, your examples, NonnullString, NonemptyString etc. are very good. I can take it from here.

Thanks again

1reaction
TysonMNcommented, Feb 24, 2019

Your code contains two validation examples. I have finished going through the first one, the one that starts with

ValidateList(new List<string> {"", "1", "", "a", "200"})

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 two strings and any two ints. However, having a CreditCard instance is not the same as having an instance of Tuple<string, string, int, int>. The difference is that the only call to the constructor of CreditCard 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.

using System.Collections.Generic;
using LanguageExt;
using static LanguageExt.Prelude;

public class Example {
  public static void Main() {
    var strings = new List<string> { "", "1", "", "a", "200" };
    var validatedStrings = strings
      .Map(Validate)
      .Map(v => v.Match(s => s.Value, errors => string.Join(", ", errors))) // just to more easily see what is inside the Validation<,> instances
      .ToSeq();
  }

  public static Validation<Error, AlphaStartingString> Validate(string s) => s
    .Apply(NonnullString.New)
    .Bind(NonemptyString.New)
    .Bind(AlphaStartingString.New);

  public sealed class NonnullString : Record<NonnullString> {
    public string Value { get; }
    private NonnullString(string value) => Value = value;
    public static Validation<Error, NonnullString> New(string value) => Optional(value)
      .Map(s => new NonnullString(s))
      .ToValidation(Error.New("Nonnull string is required"));
    public static implicit operator string(NonnullString self) => self.Value;
  }

  public sealed class NonemptyString : Record<NonemptyString> {
    public string Value { get; }
    private NonemptyString(string value) => Value = value;
    public static Validation<Error, NonemptyString> New(NonnullString nonnull) => Some(nonnull)
      .Filter(s => !string.IsNullOrEmpty(s))
      .Map(s => new NonemptyString(s))
      .ToValidation(Error.New("Nonempty string is required"));
    public static implicit operator string(NonemptyString self) => self.Value;
  }

  public sealed class AlphaStartingString : Record<AlphaStartingString> {
    public string Value { get; }
    private AlphaStartingString(string value) => Value = value;
    public static Validation<Error, AlphaStartingString> New(NonemptyString nonempty) => Some(nonempty)
      .Filter(s => s.Value[0].Apply(char.IsLetter))
      .Map(s => new AlphaStartingString(s))
      .ToValidation(Error.New($"{nonempty.Value} doesn't start with a letter"));
    public static implicit operator string(AlphaStartingString self) => self.Value;
  }

  public class Error : NewType<Error, string> {
    public Error(string e) : base(e) { }
  }

}
Read more comments on GitHub >

github_iconTop Results From Across the Web

37 Validating Statements (A Quick Cheat Sheet for When ...
I believe in you. ... I believe in us. ... We are going to get through this. ... How are you feeling today?...
Read more >
25 Examples of Validating Statements to Show Empathy
1. I too would feel that way if I were in your situation. · 2. I'm truly sorry you had to deal with…...
Read more >
50 {Vital} Examples of Validating Statements - Sunshyne Gray
Check out these 50 Important Examples of Validating Statements to help you deepen your relationships and experience more peace and joy!
Read more >
What Is Emotional Validation?
Emotional validation is the process of learning about, understanding, and expressing acceptance of another person's emotional experience.
Read more >
How To Validate Someone's Feelings Without Agreeing ...
Examples of Validating Statements · “Here's what I'm hearing you say (fact checking).” · “I can see how hard you are working.” ·...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found