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
?