使用 Owin 的 FluentValidation 忽略 Asp.Net WebApi2 中的 [FromUri] Dtos

FluentValidation with Owin ignores [FromUri] Dtos in Asp.Net WebApi2

我正在开发 ASP.NET WebApi2 (.NET 4.7.2),将 Owin 和 Autofac 作为 IOC 容器。在到达控制器之前,Dto 应使用 FluentValidation 进行验证。这是我的项目配置:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ...             
        var config = new HttpConfiguration();
        config.Filters.Add(new ValidateDtoStateFilter());
        ...
        FluentValidationModelValidatorProvider.Configure(config);
        ...
    }
}

验证 dto 状态过滤器:

public class ValidateDtoStateFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (!actionContext.ModelState.IsValid)
            {
                throw new ValidationException(actionContext.ModelState
                    .SelectMany(ms => ms.Value.Errors
                        .Select(er => new ValidationFailure(ms.Key.Split('.')[1].FromPascalToCamelCase(), er.ErrorMessage))));
            }
        }
    }

Autofac 生成器配置:

builder.RegisterAssemblyTypes(typeof(MyDtoValidator).Assembly)
                   .Where(t => t.Name.EndsWith("Validator"))
                   .AsImplementedInterfaces()
                   .InstancePerLifetimeScope();
builder.RegisterType<FluentValidationModelValidatorProvider>().As<ModelValidatorProvider>();
builder.RegisterType<AutofacValidatorFactory>().As<IValidatorFactory>().SingleInstance();

Autofac 验证器工厂:

public class AutofacValidatorFactory : ValidatorFactoryBase
{
    private readonly IComponentContext _context;

    public AutofacValidatorFactory(IComponentContext context)
    {
        _context = context;
    }

    public override IValidator CreateInstance(Type validatorType)
    {
        object instance;

        return _context.TryResolve(validatorType, out instance) ? instance as IValidator : null;
    }
}

它适用于 POST 或请求负载中 dto 出现 [FromBody] 的 PUT 端点。它 not[FromUri] dtos 一起工作,例如在 GET 请求中。验证器将由 Autofac 创建,但在 ValidateDtoStateFilterOnActionExecuting 中,无论提供的数据是否有效,actionContext.ModelState 始终为真。

我怎样才能实现 [FromUri] dto 也得到 FluentValidation 中间件的验证?

更新

如果我没有做任何更改,问题就不会再出现了。

我无法重现此行为。使用 Autofac 6.0.0FluentValidation 8.6.1 进行测试。但是,当模型为 null(没有传递查询字符串参数或请求正文为空)时,[FromUri][FromBody] dtos 都将 actionContext.ModelState.IsValid 设置为 true。在这种情况下,不会生成任何错误,因此验证通过。

This is actually by design. The purpose of FluentValidation is to validate properties on objects, which by definition requires a non-null instance in order to work. https://github.com/FluentValidation/FluentValidation/issues/486

我可以想出两种方法来解决这个限制。

  1. 在您的控制器中,只需测试 dto 是否为空:
[HttpGet]
public IHttpActionResult Get([FromUri] MyDto dto)
{
    if (dto == null) return BadRequest();
    ...
}
  1. 更新 ValidateDtoStateFilter class 以手动触发已定义验证器的空 dto 的验证:
public class ValidateDtoStateFilter : ActionFilterAttribute
{
    private readonly IDependencyResolver _resolver;

    public ValidateDtoStateFilter(IDependencyResolver resolver)
    {
        _resolver = resolver;
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var parameters = actionContext.ActionDescriptor.GetParameters();
        var nullDtosWithValidatorDefined = actionContext.ActionArguments
            .Where(argument => argument.Value == null)
            .Select(argument => new
            {
                argument.Key,
                // you may need to account for p.IsOptional and p.DefaultValue here
                Type = parameters.FirstOrDefault(p => p.ParameterName == argument.Key)?.ParameterType
            })
            .Where(argument => argument.Type != null)
            .Select(argument => new {
                argument.Key,
                Validator = (IValidator)_resolver.GetService(typeof(IValidator<>).MakeGenericType(argument.Type))
            })
            .Where(argument => argument.Validator != null)
            .Select(argument => argument.Key);
        foreach (var dto in nullDtosWithValidatorDefined)
        {
            actionContext.ModelState.AddModelError(dto, $"'{dto}' must not be null.");
        }

        if (!actionContext.ModelState.IsValid)
        {
            var errors = actionContext
                .ModelState
                .SelectMany(ms => ms.Value.Errors
                    .Select(er => new ValidationFailure(ms.Key, er.ErrorMessage))
                    );
            throw new ValidationException(errors);
        }
    }
}

// in the Startup.cs file the ValidateDtoStateFilter must be added after the DependencyResolver is set to Autofac:
config.DependencyResolver = new AutofacWebApiDependencyResolver(ConfigureAutofac());
config.Filters.Add(new ValidateDtoStateFilter(config.DependencyResolver));

虽然这可能无法解决您的具体问题,但如果您将 link 分享到您的项目或您的 Startup.cs 文件的片段会更容易,因为它可能与您的 api配置。