views:

2375

answers:

4

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.

+2  A: 

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
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
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
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
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
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
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
+6  A: 

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
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
+2  A: 

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  A: 

But since PropertiesMustMatch is being applied to the Class, if there is an error it will not appear next to the confirm Password field. It will appear at the top of the page. How should the code be changed to get the message to appear next to the confirm password field?

Ninad
Getting on that now, let's see if I can find something.
JP Araujo