Asp.Net MVC3: установите пользовательский IServiceProvider в ValidationContext, чтобы валидаторы могли разрешать службы

Обновление 18 декабря 2012

Поскольку этот вопрос, кажется, получает довольно много точек зрения, я должен отметить, что принятый ответ - это не решение, которое я использовал, но оно предоставляет ссылки и ресурсы для построения решения, но, на мой взгляд, не идеальное решение, Мой ответ содержит замены для стандартных частей инфраструктуры MVC; и вы должны использовать их, только если вы уверены, что они все еще работают для будущих версий (некоторый закрытый код был вырван из официальных источников, потому что в базовых классах не было достаточной расширяемости).

Я могу подтвердить, однако, что эти два класса также работают для Asp.Net MVC 4, а также 3.

Также возможно повторить аналогичную реализацию для инфраструктуры веб-API Asp.Net, что я недавно сделал.

Конец обновления

У меня есть тип, который имеет много "стандартной" проверки (требуется и т. Д.), Но также немного настраиваемой проверки.

Часть этой проверки требует захвата служебного объекта и поиска метаданных более низкого уровня (то есть "под уровнем модели") с использованием одного из других свойств в качестве ключа. Затем метаданные определяют, требуется ли одно или несколько свойств, а также допустимые форматы этих свойств.

Чтобы быть более конкретным - тип является объектом Card Payment, упрощенным до двух рассматриваемых свойств следующим образом:

public class CardDetails
{
  public string CardTypeID { get; set; }
  public string CardNumber { get; set; }
}

У меня тогда есть сервис:

public interface ICardTypeService
{
  ICardType GetCardType(string cardTypeID);
}

ICardType затем содержит разные биты информации - вот два, которые имеют решающее значение:

public interface ICardType
{
  //different cards support one or more card lengths
  IEnumerable<int> CardNumberLengths { get; set; }
  //e.g. - implementation of the Luhn algorithm
  Func<string, bool> CardNumberVerifier { get; set; }
}

Все мои контроллеры имеют возможность разрешать ICardTypeService используя стандартный шаблон, т.е.

 var service = Resolve<ICardTypeService>();

(Хотя я должен отметить, что основа этого вызова является частной)

Что они получают за счет использования общего интерфейса

public interface IDependant
{
  IDependencyResolver Resolver { get; set; }
}

Мой фреймворк затем заботится о назначении наиболее специфического распознавателя зависимостей, доступного для экземпляра контроллера при его создании (либо другим распознавателем, либо фабрикой стандартных контроллеров MVC). Тот Resolve метод в последнем, но один блок кода является простой оберткой вокруг этого Resolver член.

Так что - если я могу взять выбранное ICardType для платежа, полученного из браузера, я могу затем выполнить первоначальные проверки длины номера карты и т. д. Вопрос заключается в том, как разрешить службу из-за переопределения IsValid(object, ValidationContext) переопределение ValidationAttribute?

Мне нужно пройти через преобразователь зависимостей текущего контроллера в контекст проверки. я вижу это ValidationContext оба инструмента IServiceProvider и имеет экземпляр IServiceContainer - так ясно, что я должен иметь возможность создать оболочку для моего распознавателя служб, которая также реализует одну из них (возможно, IServiceProvider).

Я уже отметил, что во всех местах, где ValidationContext создается инфраструктурой MVC, поставщику услуг всегда передается значение NULL.

Итак, в какой момент в конвейере MVC мне нужно переопределить поведение ядра и внедрить моего поставщика услуг?

Я должен добавить, что это не будет единственный сценарий, в котором мне нужно сделать что-то подобное - так что в идеале я хотел бы что-то, что я могу применить к конвейеру, чтобы все ValidationContext s настроены с текущим поставщиком услуг для текущего контроллера.

3 ответа

Решение

Задумывались ли вы о создании валидатора модели с использованием modelValidatorProvider вместо использования атрибутов валидации? Таким образом, вы не зависите от ValidationAttribute, но можете создать свою собственную реализацию проверки (это будет работать в дополнение к существующей проверке DataAnnotations).

http://msdn.microsoft.com/en-us/library/system.web.mvc.modelvalidatorprovider.aspx

http://dotnetslackers.com/articles/aspnet/Experience-ASP-NET-MVC-3-Beta-the-New-Dependency-Injection-Support-Part2.aspx

http://dotnetslackers.com/articles/aspnet/Customizing-ASP-NET-MVC-2-Metadata-and-Validation.aspx

На MVC 5.2 вы можете использовать ответ украсть @ Андрас и источник MVC и:

1. Получите DataAnnotationsModelValidatorEx от DataAnnotationsModelValidator

namespace System.Web.Mvc
{
    // From https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/DataAnnotationsModelValidator.cs
    // commit 5fa60ca38b58, Apr 02, 2015
    // Only diff is adding of secton guarded by THERE_IS_A_BETTER_EXTENSION_POINT
    public class DataAnnotationsModelValidatorEx : DataAnnotationsModelValidator
    {
        readonly bool _shouldHotwireValidationContextServiceProviderToDependencyResolver;

        public DataAnnotationsModelValidatorEx(
            ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute,
            bool shouldHotwireValidationContextServiceProviderToDependencyResolver=false)
            : base(metadata, context, attribute)
        {
           _shouldHotwireValidationContextServiceProviderToDependencyResolver =
                shouldHotwireValidationContextServiceProviderToDependencyResolver;
        }
    }
}

2. Клонировать базовый импл public override IEnumerable<ModelValidationResult> Validate(object container)

3. Применить взломать Render элегантный разрез после Validate создает контекст: -

public override IEnumerable Validate(object container) { // Per the WCF RIA Services team, instance can never be null (if you have // no parent, you pass yourself for the "instance" parameter). string memberName = Metadata.PropertyName ?? Metadata.ModelType.Name; ValidationContext context = new ValidationContext(container ?? Metadata.Model) { DisplayName = Metadata.GetDisplayName(), MemberName = memberName };

#if !THERE_IS_A_BETTER_EXTENSION_POINT
   if(_shouldHotwireValidationContextServiceProviderToDependencyResolver 
       && Attribute.RequiresValidationContext)
       context.InitializeServiceProvider(DependencyResolver.Current.GetService);
#endif
   ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);
    if (result != ValidationResult.Success)
    {
        // ModelValidationResult.MemberName is used by invoking validators (such as ModelValidator) to
        // construct the ModelKey for ModelStateDictionary. When validating at type level we want to append the
        // returned MemberNames if specified (e.g. person.Address.FirstName). For property validation, the
        // ModelKey can be constructed using the ModelMetadata and we should ignore MemberName (we don't want
        // (person.Name.Name). However the invoking validator does not have a way to distinguish between these two
        // cases. Consequently we'll only set MemberName if this validation returns a MemberName that is different
        // from the property being validated.

       string errorMemberName = result.MemberNames.FirstOrDefault();
        if (String.Equals(errorMemberName, memberName, StringComparison.Ordinal))
        {
            errorMemberName = null;
        }

       var validationResult = new ModelValidationResult
        {
            Message = result.ErrorMessage,
            MemberName = errorMemberName
        };

       return new ModelValidationResult[] { validationResult };
    }

   return Enumerable.Empty<ModelValidationResult>();
}

4. Расскажите MVC о новом DataAnnotationsModelValidatorProvider в городе

после того, как ваш Global.asax делает DependencyResolver.SetResolver(new AutofacDependencyResolver(container)): -

DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(
    typeof(ValidatorServiceAttribute),
    (metadata, context, attribute) => new DataAnnotationsModelValidatorEx(metadata, context, attribute, true));

5. Используйте свое воображение, чтобы злоупотреблять вашим новым сервисным локатором, используя инъекцию ctor через GetService в вашем ValidationAttribute, например:

public class ValidatorServiceAttribute : ValidationAttribute
{
    readonly Type _serviceType;

    public ValidatorServiceAttribute(Type serviceType)
    {
        _serviceType = serviceType;
    }

    protected override ValidationResult IsValid(
        object value, 
        ValidationContext validationContext)
    {
        var validator = CreateValidatorService(validationContext);
        var instance = validationContext.ObjectInstance;
        var resultOrValidationResultEmpty = validator.Validate(instance, value);
        if (resultOrValidationResultEmpty == ValidationResult.Success)
            return resultOrValidationResultEmpty;
        if (resultOrValidationResultEmpty.ErrorMessage == string.Empty)
            return new ValidationResult(ErrorMessage);
        return resultOrValidationResultEmpty;
    }

    IModelValidator CreateValidatorService(ValidationContext validationContext)
    {
        return (IModelValidator)validationContext.GetService(_serviceType);
    }
}

Позволяет вам ударить по вашей модели: -

class MyModel 
{
    ...
    [Required, StringLength(42)]
    [ValidatorService(typeof(MyDiDependentValidator), 
        ErrorMessage = "It's simply unacceptable")]
    public string MyProperty { get; set; }
    ....
}

какие провода это к:

public class MyDiDependentValidator : Validator<MyModel>
{
    readonly IUnitOfWork _iLoveWrappingStuff;

    public MyDiDependentValidator(IUnitOfWork iLoveWrappingStuff)
    {
        _iLoveWrappingStuff = iLoveWrappingStuff;
    }

    protected override bool IsValid(MyModel instance, object value)
    {
        var attempted = (string)value;
        return _iLoveWrappingStuff.SaysCanHazCheez(instance, attempted);
    }
}

Предыдущие два связаны:

interface IModelValidator
{
    ValidationResult Validate(object instance, object value);
}

public abstract class Validator<T> : IModelValidator
{
    protected virtual bool IsValid(T instance, object value)
    {
        throw new NotImplementedException(
            "TODO: implement bool IsValid(T instance, object value)" +
            " or ValidationResult Validate(T instance, object value)");
    }

    protected virtual ValidationResult Validate(T instance, object value)
    {
        return IsValid(instance, value) 
            ? ValidationResult.Success 
            : new ValidationResult("");
    }

    ValidationResult IModelValidator.Validate(object instance, object value)
    {
        return Validate((T)instance, value);
    }
}

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

Обновить

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

оригинал

Так как я нацеливаюсь ValidationAttributeна основе проверки в тот момент, когда я исследовал, где MVC создает ValidationContext что кормят GetValidationResult метод этого класса.

Оказывается, это в DataAnnotationsModelValidator"s Validate метод:

public override IEnumerable<ModelValidationResult> Validate(object container) {
  // Per the WCF RIA Services team, instance can never be null (if you have
  // no parent, you pass yourself for the "instance" parameter).
  ValidationContext context = new ValidationContext(
    container ?? Metadata.Model, null, null);
  context.DisplayName = Metadata.GetDisplayName();

  ValidationResult result = 
    Attribute.GetValidationResult(Metadata.Model, context);

  if (result != ValidationResult.Success) {
    yield return new ModelValidationResult {
      Message = result.ErrorMessage
    };
  }
}

(Скопировано и переформатировано из исходного кода MVC3 RTM)

Так что я решил, что некоторые возможности расширения будут в порядке:

public class DataAnnotationsModelValidatorEx : DataAnnotationsModelValidator
{
  public DataAnnotationsModelValidatorEx(
    ModelMetadata metadata, 
    ControllerContext context, 
    ValidationAttribute attribute)
    : base(metadata, context, attribute)
  {
  }

  public override IEnumerable<ModelValidationResult> Validate(object container)
  {
    ValidationContext context = CreateValidationContext(container);

    ValidationResult result = 
      Attribute.GetValidationResult(Metadata.Model, context);

    if (result != ValidationResult.Success)
    {
      yield return new ModelValidationResult
      {
        Message = result.ErrorMessage
      };
    }
  }

  // begin Extensibility

  protected virtual ValidationContext CreateValidationContext(object container)
  {
    IServiceProvider serviceProvider = CreateServiceProvider(container);
    //TODO: add virtual method perhaps for the third parameter?
    ValidationContext context = new ValidationContext(
      container ?? Metadata.Model, 
      serviceProvider, 
      null);
    context.DisplayName = Metadata.GetDisplayName();
    return context;
  }

  protected virtual IServiceProvider CreateServiceProvider(object container)
  {
    IServiceProvider serviceProvider = null;

    IDependant dependantController = 
      ControllerContext.Controller as IDependant;

    if (dependantController != null && dependantController.Resolver != null)
      serviceProvider = new ResolverServiceProviderWrapper
                        (dependantController.Resolver);
    else
      serviceProvider = ControllerContext.Controller as IServiceProvider;
    return serviceProvider;
  }
}

Поэтому я сначала проверяю IDependant интерфейс от контроллера, в этом случае я создаю экземпляр класса-оболочки, который действует как адаптер между моим IDependencyResolver интерфейс и System.IServiceProvider,

Я думал, что я также буду обрабатывать случаи, когда сам контроллер является IServiceProvider тоже (не то, что применимо в моем случае - но это более общее решение).

Тогда я делаю DataAnnotationsModelValidatorProvider используйте этот валидатор по умолчанию вместо оригинала:

//register the new factory over the top of the standard one.
DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(
  (metadata, context, attribute) => 
    new DataAnnotationsModelValidatorEx(metadata, context, attribute));

Теперь "нормально" ValidationAttributeвалидаторы, могут разрешать услуги:

public class ExampleAttribute : ValidationAttribute
{
  protected override ValidationResult 
    IsValid(object value, ValidationContext validationContext)
  {
    ICardTypeService service = 
      (ICardTypeService)validationContext.GetService(typeof(ICardTypeService));
  }
}

Это все еще оставляет прямой ModelValidatorнуждаются в повторной реализации для поддержки той же техники - хотя они уже имеют доступ к ControllerContextтак что это не проблема.

Обновить

Аналогичное должно быть сделано, если вы хотите IValidatableObject- реализующие типы, чтобы иметь возможность разрешать службы во время реализации Validate без необходимости извлекать собственные адаптеры для каждого типа.

  • Получите новый класс от ValidatableObjectAdapterЯ назвал это ValidatableObjectAdapterEx
  • из исходного кода MVC v3 RTM скопируйте Validate а также ConvertResults приватный метод этого класса.
  • Настройте первый метод для удаления ссылок на внутренние ресурсы MVC и
  • изменить как ValidationContext построен

Обновление (в ответ на комментарий ниже)

Вот код для ValidatableObjectAdapterEx - и я укажу, надеюсь, более четко, что IDependant а также ResolverServiceProviderWrapper здесь и раньше используются типы, которые применимы только к моей среде - однако, если вы используете глобальный, статически доступный DI-контейнер, то повторная реализация этих двух классов должна быть тривиальной. CreateServiceProvider методы соответственно.

public class ValidatableObjectAdapterEx : ValidatableObjectAdapter
{
  public ValidatableObjectAdapterEx(ModelMetadata metadata, 
                                    ControllerContext context)
   : base(metadata, context) { }

  public override IEnumerable<ModelValidationResult> Validate(object container)
  {
    object model = base.Metadata.Model;
    if (model != null)
    {
      IValidatableObject instance = model as IValidatableObject;
      if (instance == null)
      {
        //the base implementation will throw an exception after 
        //doing the same check - so let's retain that behaviour
        return base.Validate(container);
      }
      /* replacement for the core functionality */
      ValidationContext validationContext = CreateValidationContext(instance);
      return this.ConvertResults(instance.Validate(validationContext));
    }
    else
      return base.Validate(container);  /*base returns an empty set 
                                          of values for null. */
  }

  /// <summary>
  /// Called by the Validate method to create the ValidationContext
  /// </summary>
  /// <param name="instance"></param>
  /// <returns></returns>
  protected virtual ValidationContext CreateValidationContext(object instance)
  {
    IServiceProvider serviceProvider = CreateServiceProvider(instance);
    //TODO: add virtual method perhaps for the third parameter?
    ValidationContext context = new ValidationContext(
      instance ?? Metadata.Model,
      serviceProvider,
      null);
    return context;
  }

  /// <summary>
  /// Called by the CreateValidationContext method to create an IServiceProvider
  /// instance to be passed to the ValidationContext.
  /// </summary>
  /// <param name="container"></param>
  /// <returns></returns>
  protected virtual IServiceProvider CreateServiceProvider(object container)
  {
    IServiceProvider serviceProvider = null;

    IDependant dependantController = ControllerContext.Controller as IDependant;

    if (dependantController != null && dependantController.Resolver != null)
    {
      serviceProvider = 
        new ResolverServiceProviderWrapper(dependantController.Resolver);
    }
    else
      serviceProvider = ControllerContext.Controller as IServiceProvider;

    return serviceProvider;
  }

  //ripped from v3 RTM source
  private IEnumerable<ModelValidationResult> ConvertResults(
    IEnumerable<ValidationResult> results)
  {
    foreach (ValidationResult result in results)
    {
      if (result != ValidationResult.Success)
      {
        if (result.MemberNames == null || !result.MemberNames.Any())
        {
          yield return new ModelValidationResult { Message = result.ErrorMessage };
        }
        else
        {
          foreach (string memberName in result.MemberNames)
          {
            yield return new ModelValidationResult 
             { Message = result.ErrorMessage, MemberName = memberName };
          }
        }
      }
    }
  }
}

Код окончания

С этим классом на месте, вы можете зарегистрировать его как адаптер по умолчанию для IValidatableObject экземпляры со строкой:

DataAnnotationsModelValidatorProvider.
  RegisterDefaultValidatableObjectAdapterFactory(
    (metadata, context) => new ValidatableObjectAdapterEx(metadata, context)
  );
Другие вопросы по тегам