【问题标题】:Dependency-Injected Validation in Web APIWeb API 中的依赖注入验证
【发布时间】:2013-02-26 15:23:07
【问题描述】:

在 MVC 中,我可以创建一个可以接受依赖项的模型验证器。我通常为此使用 FluentValidation。例如,这使我可以检查帐户注册是否未使用电子邮件地址(注意:这是一个简化的示例!):

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 不存在此类集成。这里有 coupleattempts,但都没有解决依赖注入方面的问题,只能使用静态验证器。

之所以难,是因为 MVC 和 Web API 的 ModelValidatorProvider 和 ModelValidator 的实现方式不同。在 MVC 中,这些是按请求实例化的(因此注入上下文很容易)。在 Web API 中,它们是静态的,并且 ModelValidatorProvider 为每种类型维护一个 ModelValidators 缓存,以避免对每个请求进行不必要的反射查找。

我一直在尝试自己添加必要的集成,但一直是stuck trying to obtain the Dependency Scope。相反,我想我会退后一步,询问是否有任何其他解决方案 - 是否有人提出了执行模型验证的解决方案,其中可以注入依赖项。

我不想在控制器中执行验证(我使用ValidationActionFilter 将其分开),这意味着我无法从控制器的构造函数注入中获得任何帮助。

【问题讨论】:

  • 这是一个很好的问题,因为听起来您在提问之前确实做了研究。我不太确定在问这个问题时您是否真的在为能够注入依赖项而苦苦挣扎,或者问题是否是由于正在发生的缓存导致每次注入的任何依赖项都不会得到解决进行验证。这是我的团队遇到的情况,我在 FluentValidation 中打开了一个问题:github.com/JeremySkinner/FluentValidation/issues/108。您的以下解决方案可能对我们来说是一个可行的解决方案。
  • 如果你想使用它们,我已经把它们放在了 nuget 上

标签: c# dependency-injection asp.net-web-api fluentvalidation


【解决方案1】:

我能够注册,然后使用 GetDependencyScope() 扩展方法从请求中访问 Web API 依赖解析器。这允许在执行验证过滤器时访问模型验证器。

如果这不能解决您的依赖注入问题,请随时澄清。

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>();
}

【讨论】:

  • 不幸的是,在这个阶段已经调用了 Web API 验证,因此无法通过正常验证来执行此操作,而这正是我希望做的。但是,看起来确实有可能以这种方式进行第二轮验证,这有点困难,但总比没有好!我试试看。
  • 我很想听听您是如何解决这个问题的,因为我想做类似的事情,所以如果您有时间,请发布您的解决方案。
  • 周五我无法让它工作,主要是因为您无法从过滤器中正确访问模型。不过,今天尝试了更多与此相关的想法。
  • 我现在一切正常,并已发布有关如何作为单独答案的详细信息。需要做更多的工作才能使验证工作得井井有条,我会在周末尝试整齐地发布所有内容。由于您的回答使我走上了正确的道路,因此我已将赏金授予您。
  • 哇,没想到会这样。谢谢。我期待看到您计划发布的打包解决方案。
【解决方案2】:

我终于让它工作了,但这有点麻烦。如前所述,ModelValidatorProvider 将保留所有 Validator 的 Singleton 实例,因此这是完全不合适的。相反,我使用过滤器来运行我自己的验证,正如 Oppositional 所建议的那样。这个过滤器可以访问IDependencyScope,并且可以巧妙地实例化验证器。

在过滤器中,我通过ActionArguments,并通过验证。验证代码是从 DefaultBodyModelValidator 的 Web API 运行时源中复制出来的,经过修改以在 DependencyScope 中查找验证器。

最后,要使用ValidationActionFilter,您需要ensure that your filters are executed in a specific order.

我已在 github 上打包了我的解决方案,并在 nuget 上提供了一个版本。

【讨论】:

【解决方案3】:

当然不建议这样做,因为该类是内部的,但您可以在 WebApi 配置中删除 IModelValidatorCache 服务。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Services.Clear(Type.GetType("System.Web.Http.Validation.IModelValidatorCache, System.Web.Http"));
    }
}

【讨论】:

    【解决方案4】:

    我让 DI 在 WebApi 中使用 Fluent Validators 没有问题。我发现验证器经常被调用,而这些繁重的逻辑验证在模型验证器中没有位置。在我看来,模型验证器是用来检查数据形状的轻量级的。 Email 是否看起来像电子邮件,并提供了来电者 FirstNameLastNameMobileHomePhone

    此电子邮件是否可以注册之类的逻辑验证属于服务层,而不是控制器。我的实现也不共享隐式数据上下文,因为我认为这是一种反模式。

    我认为当前用于此的 NuGet 包具有 MVC3 依赖项,因此我最终只查看了 source directly 并创建了自己的 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" />
    

    【讨论】:

    • 谢谢。这个答案最终显示了我们需要加载哪些包,以及如何在没有疯狂工厂和代理类的情况下设置 MVC 和 WebAPI FluentValidation。 FluentValidation 文档中没有这些信息,但作者维护了所有这些包。
    【解决方案5】:

    我花了很多时间试图找到一个解决 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 并使用 kernel.Get&lt;IDbContext&gt;() 使 IKernel 构造函数通过 IDbContext

    【讨论】:

      【解决方案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 项目中使用它,没有任何问题。

      【讨论】:

      • 问题不在于在 Web API 中使用 FluentValidation,而在于将它与依赖注入结合使用,由于验证器是静态的,因此在标准实现中无法完成。
      • 对不起,我的错...我在 diff 上下文中阅读它。我不确定我的项目现在是否有任何使用 deps rt 的验证器,但我会检查并回来。从它的声音来看,我想我可能不得不删除我的答案......:|
      猜你喜欢
      • 2019-04-05
      • 1970-01-01
      • 2017-10-22
      • 1970-01-01
      • 1970-01-01
      • 2017-01-30
      • 1970-01-01
      • 1970-01-01
      • 2018-10-19
      相关资源
      最近更新 更多