MVC HtmlHelper vs FluentValidation 3.1: Troubles getting ModelMetadata IsRequired

1.3k views Asked by At

I created a HtmlHelper for Label that puts a star after the name of that Label if associated field is required:

public static MvcHtmlString LabelForR<TModel, TValue>(
        this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
    return LabelHelper(
        html,
        ModelMetadata.FromLambdaExpression(expression, html.ViewData),
        ExpressionHelper.GetExpressionText(expression),
        null);
}

private static MvcHtmlString LabelHelper(HtmlHelper helper, ModelMetadata metadata, string htmlFieldName, string text)
{
    ... //check metadata.IsRequired here
    ... // if Required show the star
}

If I use DataAnnotations and slap [Required] on the property in my ViewModel, metadata.IsRequired in my private LabelHelper will be equal to True and everything will work as intended.

However, if I use FluentValidation 3.1 and add a simple rule like that:

public class CheckEmailViewModelValidator : AbstractValidator<CheckEmailViewModel>
{
    public CheckEmailViewModelValidator()
    {
        RuleFor(m => m.Email)
            .NotNull()
            .EmailAddress();
    }
}

... in my LabelHelper metadata.IsRequired will be incorrectly set to false. (The validator works though: you can't submit empty field and it needs to be an Email like).
The rest of the metadata looks correct (Ex: metadata.DisplayName = "Email").
In theory, FluentValidator slaps RequiredAttribute on property if Rule .NotNull() is used.

For references: My ViewModel:

[Validator(typeof(CheckEmailViewModelValidator))]
public class CheckEmailViewModel
{
    //[Required]
    [Display(Name = "Email")]
    public string Email { get; set; }
}

My Controller:

public class MemberController : Controller
{
    [HttpGet]
    public ActionResult CheckEmail()
    {
        var model = new CheckEmailViewModel();
        return View(model);
    }
}

Any help is appreciated.

2

There are 2 answers

0
Jeremy Skinner On BEST ANSWER

By default, MVC uses the DataAnnotations attributes for two separate purposes - metadata and validation.

When you enable FluentValidation in an MVC application, FluentValidation hooks into the validation infrastructure but not metadata - MVC will continue to use attributes for metadata. If you want to use FluentValidation for metadata as well as validation then you'd need to write a custom implementation of MVC's ModelMetadataProvider that knows how to interrogate the validator classes - this isn't something that FluentValidation supports out of the box.

2
cleftheris On

I have a custom ModelMetadataProvider that enhances the default DataAnnotations one giving the following:

  1. populates "DisplayName" from propertyname splitting string from Camel Case, if none is specified through DisplayAttribute.
  2. If the ModelMetadata.IsRequired is set to false it checks if there are any fluent validator rules present (of type NotNull or NotEmpty).

I definitely checked out the source code that Jeremy has prepared but I was not ready for a total overhaul so I mixed and matched in order not to lose the default behavior. You can find it here

Here is the code with some additional goodness taken from this post.

public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    readonly IValidatorFactory factory;
    public CustomModelMetadataProvider(IValidatorFactory factory) 
        : base() {
        this.factory = factory;
    }

    // Uppercase followed by lowercase but not on existing word boundary (eg. the start) 
    Regex _camelCaseRegex = new Regex(@"\B\p{Lu}\p{Ll}", RegexOptions.Compiled);
    // Creates a nice DisplayName from the model’s property name if one hasn't been specified 

    protected override ModelMetadata GetMetadataForProperty(
        Func<object> modelAccessor, 
        Type containerType,
        PropertyDescriptor propertyDescriptor) {

        ModelMetadata metadata = base.GetMetadataForProperty(modelAccessor, containerType, propertyDescriptor);
        metadata.IsRequired = metadata.IsRequired || IsNotEmpty(containerType, propertyDescriptor.Name);
        if (metadata.DisplayName == null)
            metadata.DisplayName = displayNameFromCamelCase(metadata.GetDisplayName());

        if (string.IsNullOrWhiteSpace(metadata.DisplayFormatString) && 
            (propertyDescriptor.PropertyType == typeof(DateTime) || propertyDescriptor.PropertyType == typeof(DateTime?))) {
            metadata.DisplayFormatString = "{0:d}";
        }

        return metadata;
    }

    string displayNameFromCamelCase(string name) {
        name = _camelCaseRegex.Replace(name, " $0");
        if (name.EndsWith(" Id"))
            name = name.Substring(0, name.Length - 3);
        return name;
    }

    bool IsNotEmpty(Type type, string name) {
        bool notEmpty = false;
        var validator = factory.GetValidator(type);

        if (validator == null)
            return false;

        IEnumerable<IPropertyValidator> validators = validator.CreateDescriptor().GetValidatorsForMember(name);

        notEmpty = validators.OfType<INotNullValidator>().Cast<IPropertyValidator>()
                             .Concat(validators.OfType<INotEmptyValidator>().Cast<IPropertyValidator>()).Count() > 0;
        return notEmpty;
    }
}