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

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

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

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

Также можно повторить аналогичную реализацию для платформы Web-интерфейса 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; }
}

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

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

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

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

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

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

Ответы

Ответ 1

Думали ли вы о создании модели валидатора, используя 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#s10-new-support-for-validator-provider

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

Ответ 2

Update

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

Оригинальные

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

Выводит его в методе DataAnnotationsModelValidator 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
    };
  }
}

(скопировано и переформатировано из источника RTM MVC3)

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

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, поэтому он меньше проблем.

Update

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

  • Вывести новый класс из ValidatableObjectAdapter, я назвал его ValidatableObjectAdapterEx
  • из источника RTM MVCs v3, скопируйте частный метод 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 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)
  );

Ответ 3

В MVC 5.2 вы можете использовать steal @Andras answer и источник MVC и:

1. Вывести a 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. Применить hack Отметьте элегантный надрез после 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. Используйте свое воображение, чтобы злоупотреблять новым сервисом Locator, используя ctor injection через 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 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?