Внедрение зависимостей в веб-API
В MVC я могу создать модель валидатора, которая может принимать зависимости. Я обычно использую 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;
}
}
Для Web API с FluentValidation такой интеграции не существует. Было несколько попыток сделать это, но ни один из них не затрагивал аспект внедрения зависимостей и работал только со статическими валидаторами.
Причина, по которой это сложно, заключается в разнице в реализации Model ValidatorProvider и Model Validator между MVC и Web API. В MVC они создаются для каждого запроса (следовательно, введение контекста легко). В веб-API они являются статическими, и Model ValidatorProvider поддерживает кэш ModelValidators для каждого типа, чтобы избежать ненужных поисков отражения при каждом запросе.
Я сам пытался добавить необходимую интеграцию, но застрял, пытаясь получить область зависимостей. Вместо этого я решил отступить и спросить, есть ли какие-либо другие решения проблемы - если кто-нибудь придумал решение для проверки модели, в которой могут быть введены зависимости.
Я НЕ хочу выполнять проверку в контроллере (я использую ValidationActionFilter, чтобы сохранить это отдельно), что означает, что я не могу получить какую-либо помощь от внедрения конструктора контроллера.
6 ответов
Я наконец-то заставил это сработать, но это немного не так. Как упоминалось ранее, ModelValidatorProvider будет хранить экземпляры Singleton всех валидаторов, так что это было совершенно непригодно. Вместо этого я использую Фильтр для запуска моей собственной проверки, как это предложено Oppositional. Этот фильтр имеет доступ к IDependencyScope
и может аккуратно создавать экземпляры валидаторов.
Внутри фильтра я прохожу ActionArguments
и передать их через проверку. Код проверки был скопирован из источника времени выполнения Web API для DefaultBodyModelValidator
, модифицированный для поиска валидатора в DependencyScope
,
Наконец, чтобы сделать эту работу с ValidationActionFilter
Вы должны убедиться, что ваши фильтры выполняются в определенном порядке.
Я упаковал свое решение на github с версией, доступной на nuget.
Я смог зарегистрироваться, а затем получить доступ к определителю зависимостей Web API из запроса, используя метод расширения GetDependencyScope(). Это позволяет получить доступ к валидатору модели при выполнении фильтра валидации.
Пожалуйста, не стесняйтесь уточнить, если это не решит ваши проблемы внедрения зависимости.
Конфигурация Web 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>();
}
Это, конечно, не рекомендуется, поскольку класс является внутренним, но вы можете удалить службы 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"));
}
}
У меня DI работает с Fluent Validators в 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" />
Я потратил много времени, пытаясь найти хороший способ обойти тот факт, что 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
проход конструктора IDbContext
с помощью kernel.Get<IDbContext>()
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 без каких-либо проблем.