Using expressions to define validation rules

2019-09-04 01:07发布

问题:

I want to create a generic validation class, so I can do something like:

Validation v = new Validation();
v.AddRequired(x => this.Name);
v.AddRange(x => x.this.Age, 5, 65);

I'm unsure as to how to write the method definition and make the evaluation?

Where AddRequired would take a string and AddRange would take a numeric type (int, primarily but also double, decimal, etc)

回答1:

An approach would be:

class ObjectToBeValidated {
   public string Name { get; set; }
   public int Age { get; set; }
}

class Validation {
    private List<Expression<Func<ObjectToBeValidated, bool>>> requiredExpressions;
    private List<Expression<Func<ObjectToBeValidated, bool>>> rangeExpressions;

    public void AddRequired(Expression<Func<ObjectToBeValidated, string>> expression)
    {
        Expression<Func<ObjectToBeValidated, bool>> checkRequired = (p => !string.IsNullOrEmpty(expression.Compile().Invoke(p)));
        requiredExpressions.Add(checkRequired);
    }

    public void AddRange(Expression<Func<ObjectToBeValidated, int>> expression, int min, int max)
    {
        Func<ObjectToBeValidated, int> compiledFunc = expression.Compile();
        Expression<Func<ObjectToBeValidated, bool>> checkRange = (p => compiledFunc.Invoke(p) >= min && compiledFunc.Invoke(p) < max);
        rangeExpressions.Add(checkRange);
    }
}

This would just store your conditions in List<T>'s. You then would have to add a method to your Validation class to evaluate the expressions:

public bool IsValid(ObjectToBeValidated testObject)
    {
        return requiredExpressions.All(p => p.Compile().Invoke(testObject))
            && rangeExpressions.All(p => p.Compile().Invoke(testObject));
    }

Then use like this:

validator.AddRequired(p => p.Name);
validator.AddRange(p => p.Age, 6, 15);

var myObject = new ObjectToBeValidated();
var result = validator.IsValid(myObject);


回答2:

There should be some libraries available for this task. However, you can get some experience with lambdas by writing this yourself. I have made a draft implementation for the AddRange, I hope you can go further from here.

    public class Validation<T> {
        private List<RangeValidation> _rangeValidations = new List<RangeValidation>();

        public void AddRange(Func<T, int> func, int min, int max) {
            _rangeValidations.Add(new RangeValidation() {
                func = func,
                min = min,
                max = max
            });
        }

        public bool Validate(T obj) {
            foreach (var rangeValidation in _rangeValidations) {
                int value = rangeValidation.func(obj);
                if (value < rangeValidation.min || value > rangeValidation.max)
                    return false;
            }
            return true;
        }

        private class RangeValidation {
            public Func<T, int> func;
            public int min, max;
        }
    }


回答3:

Make Validation generic on the type of x, define methods taking Func<x,object> or some other type as needed, store these functions, and call them from the Validate(x) method:

class Validation<T> {
    private IList<Tuple<Func<T,IComparable>,IComparable,IComparable>> rangeChecks = new List<Tuple<Func<T,IComparable>,IComparable,IComparable>>();
    private IList<Func<T,object>> nullChecks = new List<Func<T,object>>;
    public AddRequired(Func<T,object> f) {
        nullChecks.Add(f);
    }
    public AddRange(Func<T,IComparable> f, IComparable low, IComparable high) {
        rangeChecks.Add(Tuple.Create(f, low, high));
    }
    public bool Validate(T x) {
        foreach(var t in rangeChecks) {
            var f = t.Item1;
            var low = t.Item2;
            var high = t.Item3;
            var val = f(x);
            if (v.CompareTo(low) < 0 || v.CompareTo(high) > 0) {
                return false;
            }
        }
        foreach (var t in nullChecks) {
            if (t(x) == null) {
                return false;
            }
        }
        return true;
    }
}

This implementation is very skeletal - it needs null checks in many places to be useful. In addition, it's not very efficient, because IComparable and object could wrap value types. However, the way of passing and storing Func<T,...> objects should give you an idea of how to implement the rest of it.