Подтверждение зависимостей в веб-интерфейсе
В MVC я могу создать Model Validator, который может принимать зависимости. Для этого я обычно использую FluentValidation. Это позволяет мне, например, проверить регистрацию учетной записи, что адрес электронной почты не был использован (NB: Это упрощенный пример!):
public class RegisterModelValidator : AbstractValidator<RegisterModel> {
private readonly MyContext _context;
public RegisterModelValidator(MyContext context) {
_context = context;
}
public override ValidationResult Validate(ValidationContext<RegisterModel> context) {
var result = base.Validate(context);
if (context.Accounts.Any(acc => acc.Email == context.InstanceToValidate.Email)){
result.Errors.Add(new ValidationFailure("Email", "Email has been used"));
}
return result;
}
}
Такая интеграция не существует для веб-API с FluentValidation. Там была пара попытки в этом, но не были решены зависимость Инъекционный аспект и работает только со статическими валидаторами.
Причина, по которой это сложно, объясняется различиями в реализации ModelValidatorProvider и ModelValidator между MVC и Web API. В MVC они создаются для каждого запроса (следовательно, инъекция контекста проста). В Web API они являются статическими, а ModelValidatorProvider поддерживает кэш ModelValidators для каждого типа, чтобы избежать ненужных поисков отражения при каждом запросе.
Я пытался добавить необходимую интеграцию самостоятельно, но застрял, пытаясь получить область зависимостей. Вместо этого мне показалось, что я вернусь и спрошу, есть ли какие-либо другие решения проблемы - если кто-нибудь придумал решение для проверки модели, где могут быть введены зависимости.
Я НЕ хочу выполнять проверку в контроллере (я использую ValidationActionFilter, чтобы сохранить это отдельно), что означает, что я не могу получить какую-либо помощь от инжектор конструктора контроллера.
Ответы
Ответ 1
Наконец-то я получил это, чтобы работать, но это немного ловушка. Как уже упоминалось ранее, ModelValidatorProvider будет хранить экземпляры Singleton всех валидаторов, поэтому это было совершенно непригодно. Вместо этого я использую фильтр для собственной проверки, как это было предложено Oppositional. Этот фильтр имеет доступ к IDependencyScope
и может аккумулировать проверки достоверности.
Внутри фильтра я прохожу через ActionArguments
и передаю их через проверку. Код проверки был скопирован из источника времени выполнения Web API для DefaultBodyModelValidator
, изменен для поиска Validator в DependencyScope
.
Наконец, чтобы сделать эту работу с ValidationActionFilter
, вам нужно убедиться, что ваши фильтры выполнены в определенном порядке.
Я упаковал свое решение на github с версией, доступной в nuget.
Ответ 2
Я смог зарегистрироваться, а затем получить доступ к преобразователю зависимостей Web API из запроса с помощью метода расширения GetDependencyScope(). Это позволяет получить доступ к валидатору модели при выполнении фильтра проверки.
Пожалуйста, не стесняйтесь прояснить, не разрешает ли это проблемы, связанные с впрыском зависимостей.
Конфигурация веб-API (с использованием Unity в качестве контейнера IoC):
public static void Register(HttpConfiguration config)
{
config.DependencyResolver = new UnityDependencyResolver(
new UnityContainer()
.RegisterInstance<MyContext>(new MyContext())
.RegisterType<AccountValidator>()
.RegisterType<Controllers.AccountsController>()
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
Фильтр действия проверки:
public class ModelValidationFilterAttribute : ActionFilterAttribute
{
public ModelValidationFilterAttribute() : base()
{
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
var scope = actionContext.Request.GetDependencyScope();
if (scope != null)
{
var validator = scope.GetService(typeof(AccountValidator)) as AccountValidator;
// validate request using validator here...
}
base.OnActionExecuting(actionContext);
}
}
Валидатор модели:
public class AccountValidator : AbstractValidator<Account>
{
private readonly MyContext _context;
public AccountValidator(MyContext context) : base()
{
_context = context;
}
public override ValidationResult Validate(ValidationContext<Account> context)
{
var result = base.Validate(context);
var resource = context.InstanceToValidate;
if (_context.Accounts.Any(account => String.Equals(account.EmailAddress, resource.EmailAddress)))
{
result.Errors.Add(
new ValidationFailure("EmailAddress", String.Format("An account with an email address of '{0}' already exists.", resource.EmailAddress))
);
}
return result;
}
}
Метод действия контроллера API:
[HttpPost(), ModelValidationFilter()]
public HttpResponseMessage Post(Account account)
{
var scope = this.Request.GetDependencyScope();
if(scope != null)
{
var accountContext = scope.GetService(typeof(MyContext)) as MyContext;
accountContext.Accounts.Add(account);
}
return this.Request.CreateResponse(HttpStatusCode.Created);
}
Модель (пример):
public class Account
{
public Account()
{
}
public string FirstName
{
get;
set;
}
public string LastName
{
get;
set;
}
public string EmailAddress
{
get;
set;
}
}
public class MyContext
{
public MyContext()
{
}
public List<Account> Accounts
{
get
{
return _accounts;
}
}
private readonly List<Account> _accounts = new List<Account>();
}
Ответ 3
У меня есть DI, работающий с Fluent Validator в WebApi, без проблем. Я обнаружил, что валидаторы вызываются много, и подобные проверки в тяжелой логике не имеют места в валидаторе модели. Модельные валидаторы, на мой взгляд, предназначены для облегчения проверки формы данных. Отображает ли Email
сообщение электронной почты и предоставляет ли вызывающий код FirstName
, LastName
и Mobile
или HomePhone
?
Подтверждение логики, например, можно зарегистрировать это электронное письмо, относится к уровню обслуживания, а не к контроллеру. Мои реализации также не разделяют неявный контекст данных, так как я думаю, что это анти-шаблон.
Я думаю, что текущий пакет NuGet для этого имеет зависимость от MVC3, поэтому я просто посмотрел на источник и создал свой собственный NinjectFluentValidatorFactory
.
В App_Start/NinjectWebCommon.cs
мы имеем следующее.
/// <summary>
/// Set up Fluent Validation for WebApi.
/// </summary>
private static void FluentValidationSetup(IKernel kernel)
{
var ninjectValidatorFactory
= new NinjectFluentValidatorFactory(kernel);
// Configure MVC
FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure(
provider => provider.ValidatorFactory = ninjectValidatorFactory);
// Configure WebApi
FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure(
System.Web.Http.GlobalConfiguration.Configuration,
provider => provider.ValidatorFactory = ninjectValidatorFactory);
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
}
Я считаю, что только другие необходимые пакеты для вышеперечисленного:
<package id="FluentValidation" version="5.1.0.0" targetFramework="net451" />
<package id="FluentValidation.MVC5" version="5.1.0.0" targetFramework="net451" />
<package id="FluentValidation.WebApi" version="5.1.0.0" targetFramework="net451" />
<package id="Ninject" version="3.2.0.0" targetFramework="net451" />
<package id="Ninject.MVC3" version="3.2.0.0" targetFramework="net451" />
<package id="Ninject.Web.Common" version="3.2.0.0" targetFramework="net451" />
Ответ 4
Я потратил много времени, пытаясь найти хороший способ обойти тот факт, что WebApi ModelValidatorProvider хранит валидаторы как одиночные. Я не хотел, чтобы я помещал вещи фильтрами проверки, поэтому в итоге я ввел IKernel в валидатор и использовал это для получения контекста.
public class RequestValidator : AbstractValidator<RequestViewModel>{
public readonly IDbContext context;
public RequestValidator(IKernel kernel) {
this.context = kernel.Get<IDbContext>();
RuleFor(r => r.Data).SetValidator(new DataMustHaveValidPartner(kernel)).When(r => r.RequestableKey == "join");
}
}
Кажется, что это работает, хотя валидатор хранится как одиночный. Если вы также хотите иметь возможность вызвать его с помощью контекста, вы можете просто создать второй конструктор, который принимает IDbContext
и сделать конструктор IKernel
pass IDbContext
, используя kernel.Get<IDbContext>()
Ответ 5
Это, конечно, не рекомендуется, поскольку класс является внутренним, но вы можете удалить службы IModelValidatorCache в своей конфигурации WebApi.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Services.Clear(Type.GetType("System.Web.Http.Validation.IModelValidatorCache, System.Web.Http"));
}
}
Ответ 6
FluentValidation уже некоторое время поддерживала WebApi (не уверен, что до этого заданы ваши вопросы): https://fluentvalidation.codeplex.com/discussions/533373
Цитата из потока:
{
GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Validation.ModelValidatorProvider),
new WebApiFluentValidationModelValidatorProvider()
{
AddImplicitRequiredValidator = false //we need this otherwise it invalidates all not passed fields(through json). btw do it if you need
});
FluentValidation.ValidatorOptions.ResourceProviderType = typeof(FluentValidationMessages); // if you have any related resource file (resx)
FluentValidation.ValidatorOptions.CascadeMode = FluentValidation.CascadeMode.Continue; //if you need!
Я использовал его в проекте WebApi2 без каких-либо проблем.