Подтверждение зависимостей в веб-интерфейсе

В 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 без каких-либо проблем.