45

Since now I've used the excellent FluentValidation library to validate my model classes. In web applications I use it in conjunction with the jquery.validate plugin to perform client side validation as well. One drawback is that much of the validation logic is repeated on the client side and is no longer centralized at a single place.

For this reason I'm looking for an alternative. There are many examples out there showing the usage of data annotations to perform model validation. It looks very promising. One thing I couldn't find out is how to validate a property that depends on another property value.

Let's take for example the following model:

public class Event
{
    [Required]
    public DateTime? StartDate { get; set; }
    [Required]
    public DateTime? EndDate { get; set; }
}

I would like to ensure that EndDate is greater than StartDate. I could write a custom validation attribute extending ValidationAttribute in order to perform custom validation logic. Unfortunately I couldn't find a way to obtain the model instance:

public class CustomValidationAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        // value represents the property value on which this attribute is applied
        // but how to obtain the object instance to which this property belongs?
        return true;
    }
}

I found that the CustomValidationAttribute seems to do the job because it has this ValidationContext property that contains the object instance being validated. Unfortunately this attribute has been added only in .NET 4.0. So my question is: can I achieve the same functionality in .NET 3.5 SP1?


UPDATE:

It seems that FluentValidation already supports clientside validation and metadata in ASP.NET MVC 2.

Still it would be good to know though if data annotations could be used to validate dependent properties.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • have you or has anyone figured out a way to get dataannotations and FluentValidation working (for validation) together on the same class/model? if so that would be fantastic, i have a thread about this discussing with FV author Jeremy, you can view here: http://fluentvalidation.codeplex.com/Thread/View.aspx?ThreadId=212371 – Erx_VB.NExT.Coder May 14 '10 at 14:27

5 Answers5

29

MVC2 comes with a sample "PropertiesMustMatchAttribute" that shows how to get DataAnnotations to work for you and it should work in both .NET 3.5 and .NET 4.0. That sample code looks like this:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class PropertiesMustMatchAttribute : ValidationAttribute
{
    private const string _defaultErrorMessage = "'{0}' and '{1}' do not match.";

    private readonly object _typeId = new object();

    public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty)
        : base(_defaultErrorMessage)
    {
        OriginalProperty = originalProperty;
        ConfirmProperty = confirmProperty;
    }

    public string ConfirmProperty
    {
        get;
        private set;
    }

    public string OriginalProperty
    {
        get;
        private set;
    }

    public override object TypeId
    {
        get
        {
            return _typeId;
        }
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
            OriginalProperty, ConfirmProperty);
    }

    public override bool IsValid(object value)
    {
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
        object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
        object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
        return Object.Equals(originalValue, confirmValue);
    }
}

When you use that attribute, rather than put it on a property of your model class, you put it on the class itself:

[PropertiesMustMatch("NewPassword", "ConfirmPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public class ChangePasswordModel
{
    public string NewPassword { get; set; }
    public string ConfirmPassword { get; set; }
}

When "IsValid" gets called on your custom attribute, the whole model instance is passed to it so you can get the dependent property values that way. You could easily follow this pattern to create a date comparison attribute, or even a more general comparison attribute.

Brad Wilson has a good example on his blog showing how to add the client-side portion of the validation as well, though I'm not sure if that example will work in both .NET 3.5 and .NET 4.0.

Travis Illig
  • 23,195
  • 2
  • 62
  • 85
  • 2
    i've tried this but i can never get the validation error to display on my aspx pages/views. i've tried calling validationmessagefor using an empty string, also tried using validation summary and it's not displaying there (as it does in the propertiesmustmatch example) – Erx_VB.NExT.Coder May 14 '10 at 14:21
  • 1
    I wasted a good few hours trying to get this working and thinking that my code was wrong, until I finally realised I simply wasn't testing it properly when I saw this post: http://stackoverflow.com/questions/3586324/custom-validation-attribute-is-not-called-asp-net-mvc (basically the field/property-level validation fires first, so you need those to fully validate before it will fire your class-level attribute isvalid() method). – jimasp Nov 08 '11 at 14:42
  • I later found a better post on the class validation after field validation problem here: http://stackoverflow.com/questions/3099397/property-level-validation-errors-hinder-the-validation-of-class-level-validation – jimasp Nov 08 '11 at 15:05
15

I had this very problem and recently open sourced my solution: http://foolproof.codeplex.com/

Foolproof's solution to the example above would be:

public class Event
{
    [Required]
    public DateTime? StartDate { get; set; }

    [Required]
    [GreaterThan("StartDate")]
    public DateTime? EndDate { get; set; }
}
Nick Riggs
  • 1,267
  • 6
  • 7
7

Instead of the PropertiesMustMatch the CompareAttribute that can be used in MVC3. According to this link http://devtrends.co.uk/blog/the-complete-guide-to-validation-in-asp.net-mvc-3-part-1:

public class RegisterModel
{
    // skipped

    [Required]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }                       

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation do not match.")]
    public string ConfirmPassword { get; set; }
}

CompareAttribute is a new, very useful validator that is not actually part of System.ComponentModel.DataAnnotations, but has been added to the System.Web.Mvc DLL by the team. Whilst not particularly well named (the only comparison it makes is to check for equality, so perhaps EqualTo would be more obvious), it is easy to see from the usage that this validator checks that the value of one property equals the value of another property. You can see from the code, that the attribute takes in a string property which is the name of the other property that you are comparing. The classic usage of this type of validator is what we are using it for here: password confirmation.

orcy
  • 1,304
  • 14
  • 29
4

It took a little while since your question was asked, but if you still like metadata (at least sometimes), below there is yet another alternative solution, which allows you provide various logical expressions to the attributes:

[Required]
public DateTime? StartDate { get; set; }    
[Required]
[AssertThat("StartDate != null && EndDate > StartDate")]
public DateTime? EndDate { get; set; }

It works for server as well as for client side. More details can be found here.

jwaliszko
  • 16,942
  • 22
  • 92
  • 158
3

Because the methods of the DataAnnotations of .NET 3.5 don't allow you to supply the actual object validated or a validation context, you will have to do a bit of trickery to accomplish this. I must admit I'm not familiar with ASP.NET MVC, so I can't say how to do this exactly in conjunction with MCV, but you can try using a thread-static value to pass the argument itself. Here is an example with something that might work.

First create some sort of 'object scope' that allows you to pass objects around without having to pass them through the call stack:

public sealed class ContextScope : IDisposable 
{
    [ThreadStatic]
    private static object currentContext;

    public ContextScope(object context)
    {
        currentContext = context;
    }

    public static object CurrentContext
    {
        get { return context; }
    }

    public void Dispose()
    {
        currentContext = null;
    }
}

Next, create your validator to use the ContextScope:

public class CustomValidationAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
         Event e = (Event)ObjectContext.CurrentContext;

         // validate event here.
    }
}

And last but not least, ensure that the object is past around through the ContextScope:

Event eventToValidate = [....];
using (var scope new ContextScope(eventToValidate))
{
    DataAnnotations.Validator.Validate(eventToValidate);
}

Is this useful?

Steven
  • 166,672
  • 24
  • 332
  • 435
  • Steven, this looks nice. The only thing that I would modify is to store the current context into the HttpContext instead of using `ThreadStatic`. I would simply avoid it in an ASP.NET application. – Darin Dimitrov Feb 17 '10 at 20:03
  • Can you explain why you believe we should avoid this in an ASP.NET application. I use this construct in my own production apps, so I am very interested why this is bad. – Steven Feb 17 '10 at 22:06
  • 4
    There are many articles on internet why this is bad. Here's one: http://www.hanselman.com/blog/ATaleOfTwoTechniquesTheThreadStaticAttributeAndSystemWebHttpContextCurrentItems.aspx The problem with ThreadStatic is that in ASP.NET you don't have control over the thread life and as threads are reused there are circumstances where the variable might get modified. Things get even uglier if you use asynchronous pages and controllers. For example a request might start on a thread and finish on a different thread. So in ASP.NET the **only** way to have a true per-request storage is the HttpContext. – Darin Dimitrov Feb 17 '10 at 22:37
  • 1
    Yes, I guessed this was where you're going at. You're absolutely right about asynchronous pages and controllers. However, I'm not worried about using th ThreadStatic in this scenario, because of the 'context' in which it is used. The ContextScope is used in a single method call, probably somewhere in your business layer. In that case, there is no way for ASP.NET to rip your method call to pieces (switch threads while you’re executing that particular method). Of course, you should not store a ContextScope as private member of a Page, that indeed would be asking for trouble. ... – Steven Feb 18 '10 at 08:01
  • So in that case, yes you have to be careful for abuse, and I agree that you should not use it in your ASP.NET web application *project*, but you can happily use it in the business layer of your web app. But when you’re still worried, that developers on your team might abuse this construct, simply change the occurrences of “currentContext =” to “HttpContext.Current.Items[typeof(ContextScope)] =”. – Steven Feb 18 '10 at 08:02
  • I found a more accurate blog that describes what's happening with HttpContext and ThreadStatic in ASP.NET: http://piers7.blogspot.com/2005/11/threadstatic-callcontext-and_02.html. – Steven Feb 18 '10 at 08:32