Внедрение зависимостей в веб-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 без каких-либо проблем.

Другие вопросы по тегам