ASP.NET Core MVC: расширяем возможности работы RequiredAttribute

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

public class ReferenceDataViewModel
{
    public int? Id { get; set; }
    public string? Name { get; set; }
}

Это можно использовать на других моделях представления, например:

public class MyEditViewModel
{
    // Optional: this property can be null or this property's Id value can be null
    public ReferenceViewModel? OptionalRef { get; set; }
    
    // Required: this property should not be null nor should this property's Id value
    [Required]
    public ReferenceViewModel? RequiredRef { get; set; }
}

С RequiredRef Само свойство не должно быть null (которое обрабатывается RequiredAttribute), но также Id не должно быть нулевым.

Я знаю, что могу добиться этого, создав свой собственный ValidationAttribute (см. ниже), но для единообразия я бы хотел использовать встроенный RequiredAttribute.

public class ExtendedRequiredAttribute : RequiredAttribute
{
    public ExtendedRequiredAttribute() { }

    public ExtendedRequiredAttribute(RequiredAttribute attributeToCopy)
    {
        AllowEmptyStrings = attributeToCopy.AllowEmptyStrings;

        if (attributeToCopy.ErrorMessage != null)
        {
            ErrorMessage = attributeToCopy.ErrorMessage;
        }
   
        if (attributeToCopy.ErrorMessageResourceType != null)
        {
            ErrorMessageResourceName = attributeToCopy.ErrorMessageResourceName;
            ErrorMessageResourceType = attributeToCopy.ErrorMessageResourceType;
        }
    }

    public override bool IsValid(object? value)
    {
        if (value is ReferenceDataViewModel entityReference)
        {
            return entityReference?.Id != null;
        }

        return base.IsValid(value);
    }
}

Я пробовал использовать AttributeAdapter (который, кажется, работает в ASP.NET MVC), но это не помогает. Такое ощущение, что я что-то упускаю:

public class ExtendedRequiredAttributeAdapter : AttributeAdapterBase<RequiredAttribute>
{
    public ExtendedRequiredAttributeAdapter(ExtendedRequiredAttribute attribute, IStringLocalizer? stringLocalizer)
        : base(attribute, stringLocalizer)
    { }

    public override void AddValidation(ClientModelValidationContext context)
    {
        MergeAttribute(context.Attributes, "data-val",          "true");
        MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context));
    }

    public override string GetErrorMessage(ModelValidationContextBase validationContext)
    {
        ArgumentNullException.ThrowIfNull(validationContext);
        return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
    }
}

public class CustomValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
    private readonly IValidationAttributeAdapterProvider _innerProvider = new ValidationAttributeAdapterProvider();

    public IAttributeAdapter? GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer? stringLocalizer)
    {
        var type = attribute.GetType();

        if (type == typeof(RequiredAttribute))
        {
            var requiredAttribute = (RequiredAttribute)attribute;
            var extendedRequiredAttrib = new ExtendedRequiredAttribute(requiredAttribute);
            return new ExtendedRequiredAttributeAdapter(extendedRequiredAttrib, stringLocalizer);
        }

        return _innerProvider.GetAttributeAdapter(attribute, stringLocalizer);
    }
}

Это регистрируется при запуске:

services.AddSingleton<IValidationAttributeAdapterProvider, CustomValidationAttributeAdapterProvider>();

Используйте RequiredAttribute также в ReferenceDataViewModel.Id?

Ralf 09.04.2024 11:21

К сожалению, это не работает, поскольку это требуется не во всех случаях использования. (См. свойство «OptionalRef» в MyEditViewModel)

RikRak 09.04.2024 11:34

Вы хотите сказать, что дляOptionalRef это нормально, что, когда он сам не равен нулю, идентификатор может быть там нулевым? Странная концепция.

Ralf 09.04.2024 11:36

Странно? Не совсем. Когда форма отправляется в MVC, она может содержать пустые значения идентификаторов. Они преобразуются в нулевые, поэтому MyEditViewModel может содержать ненулевое значение дляOptionalRef, но иметь нулевое значение дляOptionalRef.Id.

RikRak 09.04.2024 11:44

Как насчет создания модели с двумя представлениями: одна для нулевого значения, другая для ненулевого? Боюсь, это будет яснее...

Tiny Wang 10.04.2024 03:55

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

RikRak 10.04.2024 10:51

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

Tiny Wang 11.04.2024 03:25
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
64
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я решил эту проблему, выбрав немного другой путь. По сути, я использовал инфраструктуру проверки ядра MVC, создав IModelValidator, а затем задействовав его через IModelValidatorProvider, а затем зарегистрировав этого провайдера в MvcOptions. Вот код:

public class ReferenceDataViewModelRequiredValidator : IModelValidator
{
    public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
    {
        var model = context.Model as ReferenceDataViewModel;
        if (model == null || model.Id.HasValue == false)
        {
            var message = GetErrorMessage(context.ModelMetadata);
            yield return new ModelValidationResult("", message);
        }
    }

    private string GetErrorMessage(ModelMetadata metadata)
    {
        string? message = null;

        var parent    = metadata.ContainerType;
        var modelName = metadata.PropertyName;
        if (parent != null && !string.IsNullOrWhiteSpace(modelName))
        {
            var modelProperty  = parent.GetProperty(modelName);
            var requiredAttrib = modelProperty?.GetCustomAttribute<RequiredAttribute>();
            if (requiredAttrib != null)
            {
                message = requiredAttrib.ErrorMessage;
                if (string.IsNullOrWhiteSpace(message))
                {
                    message = requiredAttrib.FormatErrorMessage(modelName);
                }
            }
        }

        if (string.IsNullOrWhiteSpace(message))
        {
            message = $"{metadata.GetDisplayName()} is required.";
        }
        return message;
    }

}

Это можно интегрировать в проверку MVC через ModelValidatorProvider:

public class MyModelValidatorProvider : IModelValidatorProvider
{
    public void CreateValidators(ModelValidatorProviderContext context)
    {
        if (HasRequiredAttribute(context.ModelMetadata))
        {
            if (context.ModelMetadata.ModelType == typeof(ReferenceDataViewModel))
            {
                var validatorItem = new ValidatorItem()
                {
                    IsReusable = true,
                    Validator  = new ReferenceDataViewModelRequiredValidator()
                };
                // put the validator in early
                context.Results.Insert(0, validatorItem);
            }

    }
    /// <summary>
    /// Determines if the property being validated has a [Required] attribute
    /// </summary>
    /// <remarks>
    /// There is an IsRequired property on the <see cref = "ModelMetadata"/>, but always seems to be "true".
    /// Not sure why.  
    /// </remarks>
    private static bool HasRequiredAttribute(ModelMetadata metadata)
    {
        if (metadata.ContainerType != null && !string.IsNullOrWhiteSpace(metadata.PropertyName))
        {
            var property = metadata.ContainerType.GetProperty(metadata.PropertyName);
            return property?.HasAttribute<RequiredAttribute>() ?? false;
        }
        return false;
    }
}

ModelValidatorProvider необходимо зарегистрировать при запуске:

builder
    .AddControllersWithViews(opts =>
    {
        opt.ModelValidatorProviders.Add(new MyModelValidatorProvider())
    });

Другие вопросы по теме