Customized DataAnnotationsModelMetadataProvider no

2019-02-27 07:26发布

问题:

I have many properties that require 1 or more validation attributes, like the following:

public class TestModel
{
    [Some]
    [StringLength(6)]
    [CustomRequired] // more attributes...
    public string Truck { get; set; }
}

Please note all the above annotations work.

I do not want to write that all the time because whenever Some is applied, all other attributes are also applied to the property. I want to be able to do this:

public class TestModel
{
    [Some]
    public string Truck { get; set; }
}

Now this is doable by inheriting; therefore, I wrote a custom DataAnnotationsModelMetadataProvider and overrode the CreateMetadata. This looks for anything that is decorated with Some and then adds more attributes to it:

public class TruckNumberMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var attributeList = attributes.ToList();
        if (attributeList.OfType<SomeAttribute>().Any())
        {
            attributeList.Add(new StringLengthAttribute(6));
            attributeList.Add(new CustomRequiredAttribute());
        }

        return base.CreateMetadata(attributeList, containerType, modelAccessor, modelType, propertyName);
    }
}

These are the attributes in case it helps:

public class CustomRequiredAttribute : RequiredAttribute
{
    public CustomRequiredAttribute()
    {
        this.ErrorMessage = "Required";
    }
}
public class SomeAttribute : RegularExpressionAttribute
{
    public SomeAttribute()
        : base(@"^[1-9]\d{0,5}$")
    {
    }
}

Usage

@Html.TextBoxFor(x => x.Truck)

HTML Rendered:

<input name="Truck" id="Truck" type="text" value="" 
    data-val-required="The Truck field is required." 
    data-val-regex-pattern="^[1-9]\d{0,5}$" 
    data-val-regex="The field Truck must match the regular expression '^[1-9]\d{0,5}$'." 
    data-val="true">
</input>

Problems/Questions

  1. The CustomRequired was applied. But why does it pick up the message from the base class RequiredAttribute if I am using CustomRequired. The data-val-required should just say Required.
  2. The StringLenth of 6 chars is not applied. There is no sign of anything rendered for StringLength, why?

回答1:

What your custom DataAnnotationsModelMetadataProvider is doing is creating/modifying the ModelMetada associated with your property.

If you inspect the ModelMetadata class you will note that it contains properties such as string DisplayName and string DisplayFormatString which are set based on the application of the [Display] and [DisplayFormat] attributes. It also contains a bool IsRequired attribute that determines if the value of a property is required (a bit more on that later).

It does not contain anything relating to a regular expression or the maximum length, or indeed anything related to validation (except the IsRequired property and the ModelType which is used to validate that that the value can be converted to the type).

It is the HtmlHelper methods that are responsible for generating the html that is passed to the view. To generate the data-val-* attributes, your TextBoxFor() method internally calls the GetUnobtrusiveValidationAttributes() method of the HtmlHelper class which in turn calls methods in the DataAnnotationsModelValidatorProvider class which ultimately generate a Dictionary of the data-val attribute names and values used to generate the html.

I'll leave it to you to explore the source code if you want more detail (refer links below) but to summarize, it gets a collection of all attributes applied to your Truck property that inherit from ValidationAttribute to build the dictionary. In your case the only ValidationAttribute is [Some] which derives from RegularExpressionAttribute so the data-val-regex and data-val-regex-pattern attributes are added.

But because you have added your CustomRequiredAttribute in the TruckNumberMetadataProvider, the IsRequired property of the ModelMetadata has been set to true. If you insect the GetValidators() of DataAnnotationsModelValidatorProvider you will see that a RequiredAttribute is automatically added to the collection of attributes because you have not applied one to the property. The relevant snippet of code is

if (AddImplicitRequiredAttributeForValueTypes && metadata.IsRequired && !attributes.Any(a => a is RequiredAttribute))
{
    attributes = attributes.Concat(new[] { new RequiredAttribute() });
}

which results in the data-val-required attribute being added to the html (and it uses the default message because it knows nothing about your CustomRequiredAttribute)

Source code files to get you started if you want to understand the internal workings

  1. HtmlHelper.cs - refer GetUnobtrusiveValidationAttributes() methods at line 413
  2. ModelValidatorProviders.cs - gets the various ValidatorProviders used for validation
  3. DataAnnotationsModelValidatorProvider.cs - the ValidatorProvider for ValidationAttributes

One possible solution if you really want to use just one single ValidationAttribute would be to have it implement IClientValidatable and in the GetClientValidationRules() method, add rules, for example

var rule = new ModelClientValidationRule
{
    ValidationType = "required",
    ErrorMessage = "Required"
}

which will be read by the ClientDataTypeModelValidatorProvider (and delete your TruckNumberMetadataProvider class). However that will create a maintenance nightmare so I recommend you just add the 3 validation attributes to your property