tags:

views:

361

answers:

2

So I have a simple validation rule pattern that I'm using to do validation on entity objects. Here is my ValidationRule class:

public class ValidationRule {

    public Func<object, bool> Rule { get; set; }
    public string ErrorMessage { get; set; }

    public ValidationRule(string errorMessage, Func<object, bool> rule) { 
        Rule = rule;
        ErrorMessage = errorMessage;
    }

    public bool IsValid(object obj) {
        return Rule(obj);
    }
}

I have a base class for my entity objects that encapsulate the methods to perform validation that looks like this:

public abstract class ModelBase {

    private List<ValidationRule> _validationRules;
    public List<ValidationRule> ValidationRules {
        get {
            if (_validationRules == null)
                _validationRules = new List<ValidationRule>();
            return _validationRules;
        }
        set { _validationRules = value; }
    }

    public ValidationResult Validate() {
        var result = new ValidationResult();
        rules.ForEach(r => {
            if (!r.IsValid(this))
                result.Errors.Add(
                    new ValidationError(r.ErrorMessage, r.PropertyName));             
            });
        return result;
    }
}

And now here's the real problem I'm trying to solve. When I create a new class that inherits from ModelBase adding validation rules is a little awkward. For example:

public class Client : ModelBase {

    public int ID{ get; set; }
    public string Name { get; set; }
    public Address MailingAddress { get; set; }

    public Client() {
        CreateValidationRules();
    }

    private void CreateValidationRules() {

        ValidationRules.Add(new ValidationRule("Client 'Name' is required.",
            c => !string.IsNullOrEmpty(((Client)c).Name)));
    }
}

Notice where I'm creating the validation rules list. Inside the lambda expression I have to cast "c" to "Client" because my rule is essentially Func<object, bool>. I've tried many ways to make this generic by doing something like ValidationRule<Client> but I always run into problems with calling Validate() in the ModelBase class. Any ideas on how to get around this casting?

+1  A: 

I suspect that ModelBase needs to become generic too. It's not clear to me exactly what's going on here though - aren't the validation rules going to be the same for every Client? Validation rules feel like they should be associated with a whole type, not to individual instances of a type. They'd be validated against a single instance, admittedly.

I wonder whether you shouldn't have a Validator<T> type, and create (once) a Validator<Client> which can then be validated against any instance of Client.

Jon Skeet
Is Predicate<object> preferred to Func<object, bool>?
bendewey
Personally I think it's clearer, yes. I'm not sure why LINQ uses Func<T, bool> for things like Where.
Jon Skeet
Predicate for one overload and Func for the other... nah. http://msdn.microsoft.com/en-us/library/system.linq.enumerable.where.aspx
David B
Yes, I take your point there. Arguably an IndexedPredicate would make that clearer. I just think that Predicate<T> is clear in intent than Func<T,bool>.
Jon Skeet
+3  A: 

You can make the ValidationRule class generic, but leave the parameter for the IsValid method as object and do the casting in the method. That way you get the generics without having to make ModelBase generic also.

You also need an interface for the ModelBase to be able to keep a list of validation rules without knowing their actual type. Then just change the type of the list and the property in ModelBase to IValidationRule.

(Note: You can use a private setter on the properties to make them read-only.)

public Interface IValidationRule {
   bool IsValid(object);
}

public class ValidationRule<T> : IValidationRule {

    public Func<T, bool> Rule { get; private set; }
    public string ErrorMessage { get; private set; }

    public ValidationRule(string errorMessage, Func<object, bool> rule) { 
        Rule = rule;
        ErrorMessage = errorMessage;
    }

    public bool IsValid(object obj) {
        return Rule((T)obj);
    }
}

Now, the type of the parameter in the lamda expression is the generic type, so you don't have to cast it:

ValidationRules.Add(
      new ValidationRule<Client>(
           "Client 'Name' is required.",
           c => !string.IsNullOrEmpty(c.Name)
      )
 );
Guffa