MVC HtmlHelper vs FluentValidation 3.1: проблемы с получением ModelMetadata IsRequired

Я создал HtmlHelper для метки, который ставит звезду после имени этой метки, если требуется соответствующее поле:

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
}

Если я использую DataAnnotations и шлепаю [Required] для свойства в моей ViewModel, metadata.IsRequired в моем личном LabelHelper будет равно True, и все будет работать, как задумано.

Однако, если я использую FluentValidation 3.1 и добавлю простое правило, подобное этому:

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

... в моих метаданных LabelHelper.IsRequired будет неправильно установлено значение false. (Валидатор работает, хотя: вы не можете отправить пустое поле, и оно должно быть похоже на адрес электронной почты).
Остальные метаданные выглядят корректно (например, metadata.DisplayName = "Email").
Теоретически FluentValidator накладывает атрибут RequiredAttribute на свойство, если используется Rule .NotNull().

Для справок: Моя ViewModel:

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

Мой контроллер:

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

Любая помощь приветствуется.

2 ответа

Решение

По умолчанию MVC использует атрибуты DataAnnotations для двух отдельных целей - метаданных и проверки.

Когда вы включаете FluentValidation в приложении MVC, FluentValidation подключается к инфраструктуре проверки, но не к метаданным - MVC будет продолжать использовать атрибуты для метаданных. Если вы хотите использовать FluentValidation для метаданных, а также для проверки, вам нужно написать собственную реализацию MVC ModelMetadataProvider, которая знает, как запрашивать классы валидатора - это не то, что FluentValidation поддерживает из коробки.

У меня есть пользовательский ModelMetadataProvider, который расширяет DataAnnotations по умолчанию, давая следующее:

  1. заполняет "DisplayName" из строки разделения имени свойства из Camel Case, если ни один не указан через DisplayAttribute.
  2. Если для ModelMetadata.IsRequired установлено значение false, он проверяет, существуют ли какие-либо текущие правила проверки (типа NotNull или NotEmpty).

Я определенно проверил исходный код, который подготовил Джереми, но я не был готов к полному пересмотру, поэтому я смешал и сопоставил, чтобы не потерять поведение по умолчанию. Вы можете найти это здесь

Вот код с некоторыми дополнительными преимуществами, взятыми из этого поста.

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;
    }
}
Другие вопросы по тегам