13

在 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;
    }
}

对于带有 FluentValidation 的 Web API,不存在这样的集成。对此进行了几次尝试但都没有解决依赖注入方面的问题,并且只使用静态验证器。

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

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

我不想在 Controller 中执行验证(我使用ValidationActionFilter来保持这个独立),这意味着我无法从控制器的构造函数注入中获得任何帮助。

4

6 回答 6

7

我能够注册,然后使用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>();
}
于 2013-03-01T07:43:33.700 回答
4

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

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

最后,要使用ValidationActionFilter,您需要确保过滤器以特定顺序执行。

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

于 2013-03-05T22:32:14.363 回答
1

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

像Can this email be registered 这样的逻辑验证属于服务层,而不是控制器。我的实现也不共享隐式数据上下文,因为我认为这是一种反模式。

我认为当前的 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" />
于 2014-05-15T05:48:06.973 回答
1

当然不建议这样做,因为该类是内部的,但您可以在 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"));
    }
}
于 2015-08-27T20:52:06.990 回答
0

我花了很多时间试图找到解决 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>()

于 2014-09-08T04:32:06.883 回答
-1

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

于 2014-05-12T17:54:58.037 回答