33

Apparently it is possible to dynamically attach DataAnnotation attributes to object properties at runtime and as such achieve dynamic validation.

Can someone provide code sample on this?

Ronald Wildenberg
  • 31,634
  • 14
  • 90
  • 133
mare
  • 13,033
  • 24
  • 102
  • 191

4 Answers4

41

MVC has a hook to provide your own ModelValidatorProvider. By default MVC 2 uses a sub class of ModelValidatorProvider called DataAnnotationsModelValidatorProvider that is able to use System.DataAnnotations.ComponentModel.ValidationAttribute attributes for validation.

The DataAnnotationsModelValidatorProvider uses reflection to find all the ValidationAttributes and simply loops through the collection to validate your models. All you need to do is override a method called GetValidators and inject your own attributes from whichever source you choose. I use this technique to do convention validations, the properties with DataType.Email attribute always gets passed through a regex, and use this technique to pull information from the database to apply more restrictive validations for "non-power" users.

The following example simply says "always make any FirstName properties required":

 public class CustomMetadataValidationProvider : DataAnnotationsModelValidatorProvider
 {
    protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes)
    {
        //go to db if you want
        //var repository = ((MyBaseController) context.Controller).RepositorySomething;

        //find user if you need it
        var user = context.HttpContext.User;

        if (!string.IsNullOrWhiteSpace(metadata.PropertyName) && metadata.PropertyName == "FirstName")
            attributes = new List<Attribute>() {new RequiredAttribute()};

        return base.GetValidators(metadata, context, attributes);
    }
}

All you have to do is register the provider in your Global.asax.cs file:

    protected void Application_Start()
    {
        ModelValidatorProviders.Providers.Add(new CustomMetadataValidationProvider());

        AreaRegistration.RegisterAllAreas();

        RegisterRoutes(RouteTable.Routes);
    }

The end result:

end result

with this model:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime Birthday { get; set; }
}
John Farrell
  • 24,673
  • 10
  • 77
  • 110
  • One problem with the example code in this answer is attributes can be processed twice: once by your subclass of `DataAnnotationsModelValidatorProvider` and once by the pre-existing one. This can lead to errors. I think it's better to only pass attributes that you're adding into the call to `base.GetValidators` to avoid this. – Sam Jul 03 '14 at 05:10
  • 1
    Also, in ASP.NET MVC 4 (and possibly others), `base.GetValidators` automatically adds `RequiredAttribute` when `metadata.IsRequired` is `true`. One way to circumvent this is to set `metadata.IsRequired` to `false` before calling `base.GetValidators`. You can set it back to the original value after the call. – Sam Jul 03 '14 at 05:12
  • I do not want to depend upon data annotation , we want to make it more generic validation please tell me is this approach useful? – Neo Mar 19 '15 at 06:48
  • I am getting compilation error `cannot convert from 'DynValidationMVC.CustomMetadataValidationProvider' to 'System.Web.Mvc.ModelValidatorProvider'` when adding in Global.asax file. – Mahesh Kudikala May 29 '19 at 13:07
  • Sorry my bad, I have added different namespace. After adding `System.Web.Mvc;`, it compiled. :-) – Mahesh Kudikala May 29 '19 at 13:11
8

In your global.asax you have to clear the ModelValidatorProviders before adding the new one. Otherwise it will add every annotation two times which will give you a "Validation type names in unobtrusive client validation rules must be unique."-error.

protected void Application_Start()
{
    ModelValidatorProviders.Providers.Clear();
    ModelValidatorProviders.Providers.Add(new CustomMetadataValidationProvider());

    AreaRegistration.RegisterAllAreas();

    RegisterRoutes(RouteTable.Routes);
}
Frank Horemans
  • 396
  • 3
  • 8
0

The approach of using a custom MetadataValidationProvider with an overridden GetValidators has a few weaknesses:

  • Some attributes such as DisplayAttribute aren't related to validation, so adding them at the validation stage doesn't work.
  • It may not be future-proof; a framework update could cause it to stop working.

If you want your dynamically-applied data annotations to work consistently, you can subclass DataAnnotationsModelMetadataProvider and DataAnnotationsModelValidatorProvider. After doing this, replace the framework's ones via ModelMetadataProviders.Current and ModelValidatorProviders.Providers at application start-up. (You could do it in Application_Start.)

When you subclass the built-in providers, a systematic and hopefully future-proof way to apply your own attributes is to override GetTypeDescriptor. I've done this successfully, but it involved creating an implementation of ICustomTypeDescriptor and PropertyDescriptor, which required a lot of code and time.

Sam
  • 40,644
  • 36
  • 176
  • 219
-1

I don't think you can add attributes to members at runtime, but you could probably use a custom metadata provider to handle this for you.

You should check out this blog post.

Matthew Abbott
  • 60,571
  • 9
  • 104
  • 129
  • 3
    Wasn't me who downvoted, but I'd say this doesn't pass the link-only answer sniff test were the link to die (as is currently the case btw). This is a signpost, perhaps a good one, but the users will still need to go off site to implement. Consider providing more inline detail or converting to a comment, or accepting the errant vote here and there. – KyleMit Sep 27 '16 at 23:45
  • 3
    Which of course is EXACTLY what happened, as that link is dead now. – StevoInco Mar 22 '17 at 15:26