【问题标题】:FluentValidation and ActionFilterAttribute - update model before it is validatedFluentValidation 和 ActionFilterAttribute - 在验证之前更新模型
【发布时间】:2016-12-02 12:19:09
【问题描述】:

在我的 WebAPI 项目中,我正在使用 FluentValidation。我通过添加在全球范围内启用它 FluentValidationModelValidatorProvider.Configure(config); 在 Startup.cs 中

我添加了自定义ActionFolterAttribute,它在我的方法中使用它之前正在更改模型,但在测试之后我可以看到我的执行顺序不正确。

我希望在 FluentVatiodation 验证之前更改我的模型,但现在它会在 FluentVatiodation 验证我的模型后更新。
我需要这个才能访问 FluentVatiodation 验证数据中的一些路由数据

下面是我的自定义属性:

public class UpdateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ActionArguments.Any())
        {
            var args = actionContext.ActionArguments;

            var pId = args["productId"] as int?;
            var model = args["newAccount"] as TestBindingModel;

            if (pId.HasValue && model != null)
            {
                model.Id = pId.Value;
            }
        }
        base.OnActionExecuting(actionContext);
    }
}

我的带有验证器的模型:

[Validator(typeof(TestBindingModelValidator))]
public class TestBindingModel
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class TestBindingModelValidator : AbstractValidator<TestBindingModel>
{
    public TestBindingModelValidator()
    {
        RuleFor(u => u.Id)
            .Cascade(CascadeMode.StopOnFirstFailure)
            .NotEmpty().WithMessage("Id is required")
            .Must(BetweenOneAndTwo).WithMessage("Id is bad");
        RuleFor(u => u.Name)
            .Cascade(CascadeMode.StopOnFirstFailure)
            .NotEmpty().WithMessage("Name is required");
    }

    private bool BetweenOneAndTwo(TestBindingModel createAccountBindingModel, int id, PropertyValidatorContext context)
    {
        return id > 1;
    }
}

还有我的方法:

[AllowAnonymous]
[Route("create/{productId:int?}")]
[HttpPost]
[UpdateModelAttribute]
public async Task<IHttpActionResult> CreateAccount(TestBindingModel newAccount, int productId=100)
{
    if (!ModelState.IsValid)
    {
        return BadRequest("Invalid data");
    }
    Debug.WriteLine("{0} {1}", newAccount.Id, newAccount.Name);

    await Task.CompletedTask;
    return Ok("Works fine!");
}

我已经使用 PostMan 将 POST 发送到带有数据的 URL http://localhost:63564/test/create/20 进行了检查:

Id:1
Name:Test

在验证器 Id 内部的 value=1,但在我的方法主体内部 value=20。

我想更改该顺序并在我的验证器中更新该值。

这个可以改吗?

这里讨论了类似的事情:Access route data in FluentValidation for WebApi 2 和我基于问题作者评论的上述解决方案。

【问题讨论】:

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


    【解决方案1】:

    是的,它可以更改,但是您需要将通用过滤器提供程序替换为强制执行已定义顺序的提供程序。

    webApiConfiguration.Services.Replace(typeof(System.Web.Http.Filters.IFilterProvider), new OrderedFilterProvider());
    

    您可以按您希望它们触发的顺序添加过滤器,如下所示:

    webApiConfiguration.Filters.Add(new UpdateModelAttribute());
    webApiConfiguration.Filters.Add(new ValidationActionFilter());
    

    或者设置 IOrderedFilterAttribute 暴露的 Order 属性。如果您想通过配置/依赖注入或其他编译时未知的因素来控制排序,您可能希望使用此方法。

    OrderedFilterProvider.cs

    /// <summary>
    /// Combines Action Filters from multiple sources
    /// </summary>
    public class OrderedFilterProvider : IFilterProvider
    {
        private List<IFilterProvider> _filterProviders;
    
        /// <summary>
        /// Constructor using default filter providers
        /// </summary>
        public OrderedFilterProvider()
        {
            _filterProviders = new List<IFilterProvider>();
            _filterProviders.Add(new ConfigurationFilterProvider());
            _filterProviders.Add(new ActionDescriptorFilterProvider());
        }
    
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="innerProviders">The inner providers.</param>
        public OrderedFilterProvider(IEnumerable<IFilterProvider> innerProviders)
        {
            _filterProviders = innerProviders.ToList();
        }
    
        /// <summary>
        /// Returns all appropriate Filters for the specified action, sorted by their Order property if they have one
        /// </summary>
        public IEnumerable<FilterInfo> GetFilters(HttpConfiguration configuration, HttpActionDescriptor actionDescriptor)
        {
            if (configuration == null) { throw new ArgumentNullException("configuration"); }
            if (actionDescriptor == null) { throw new ArgumentNullException("actionDescriptor"); }
    
            List<OrderedFilterInfo> filters = new List<OrderedFilterInfo>();
    
            foreach (IFilterProvider fp in _filterProviders)
            {
                filters.AddRange(
                    fp.GetFilters(configuration, actionDescriptor)
                        .Select(fi => new OrderedFilterInfo(fi.Instance, fi.Scope)));
            }
    
            var orderedFilters = filters.OrderBy(i => i).Select(i => i.ConvertToFilterInfo());
            return orderedFilters;
        }
    }
    

    要使用它,您需要一些支持类。

    OrderedFilterInfo.cs

    /// <summary>
    /// Our version of FilterInfo, with the ability to sort by an Order attribute.  This cannot simply inherit from
    /// FilterInfo in the Web API class because it's sealed :(
    /// </summary>
    public class OrderedFilterInfo : IComparable
    {
        public OrderedFilterInfo(IFilter instance, FilterScope scope)
        {
            if (instance == null) { throw new ArgumentNullException("instance"); }
    
            Instance = instance;
            Scope = scope;
        }
    
        /// <summary>
        /// Filter this instance is about
        /// </summary>
        public IFilter Instance { get; private set; }
    
        /// <summary>
        /// Scope of this filter
        /// </summary>
        public FilterScope Scope { get; private set; }
    
        /// <summary>
        /// Allows controlled ordering of filters
        /// </summary>
        public int CompareTo(object obj)
        {
            if (obj is OrderedFilterInfo)
            {
                var otherfilterInfo = obj as OrderedFilterInfo;
    
                // Global filters should be executed before Controller and Action Filters.  We don't strictly have to 
                // do this, since it's done again in the framework, but it's a little more consistent for testing!
                if (this.Scope == FilterScope.Global && otherfilterInfo.Scope != FilterScope.Global)
                {
                    return -10;
                }
                else if (this.Scope != FilterScope.Global && otherfilterInfo.Scope == FilterScope.Global)
                {
                    return 10;
                }
    
                IOrderedFilterAttribute thisAttribute = this.Instance as IOrderedFilterAttribute;
                IOrderedFilterAttribute otherAttribute = otherfilterInfo.Instance as IOrderedFilterAttribute;
                IFilter thisNonOrderedAttribute = this.Instance as IFilter;
                IFilter otherNonOrderedAttribute = otherfilterInfo.Instance as IFilter;
    
                if (thisAttribute != null && otherAttribute != null)
                {
                    int value = thisAttribute.Order.CompareTo(otherAttribute.Order);
                    if (value == 0)
                    {
                        // If they both have the same order, sort by name instead
                        value = thisAttribute.GetType().FullName.CompareTo(otherAttribute.GetType().FullName);
                    }
    
                    return value;
                }
                else if (thisNonOrderedAttribute != null && otherAttribute != null)
                {
                    return 1;
                }
                else if (thisAttribute != null && otherNonOrderedAttribute != null)
                {
                    return -1;
                }
                {
                    return thisNonOrderedAttribute.GetType().FullName.CompareTo(otherNonOrderedAttribute.GetType().FullName);
                }
            }
            else
            {
                throw new ArgumentException("Object is of the wrong type");
            }
        }
    
        /// <summary>
        /// Converts this to a FilterInfo (because FilterInfo is sealed, and we can't extend it. /sigh
        /// </summary>
        /// <returns></returns>
        public FilterInfo ConvertToFilterInfo()
        {
            return new FilterInfo(Instance, Scope);
        }
    }
    

    IOorderedFilterAttribute.cs:

    /// <summary>
    /// Allows ordering of filter attributes
    /// </summary>
    public interface IOrderedFilterAttribute
    {
        /// <summary>
        /// Order of execution for this filter
        /// </summary>
        int Order { get; set; }
    }
    

    BaseActionFilterAttribute.cs

    public abstract class BaseActionFilterAttribute : ActionFilterAttribute, IOrderedFilterAttribute
    {
        /// <summary>
        /// Order of execution for this filter
        /// </summary>
        public int Order { get; set; }
    
        public BaseActionFilterAttribute()
        {
            Order = 0;
        }
    
        public BaseActionFilterAttribute(int order)
        {
            Order = order;
        }
    }
    

    FluentValidationActionFilter.cs

    /// <summary>
    /// A Filter which can be applied to Web API controllers or actions which runs any FluentValidation Validators
    /// registered in the DependencyResolver to be run.  It's not currently possible to perform this validation in the
    /// standard Web API validation location, since this doesn't provide any way of instantiating Validators on a
    /// per-request basis, preventing injection of Unit of Work or DbContexts, for example.    ///
    /// </summary>
    public class FluentValidationActionFilter : BaseActionFilterAttribute
    {
        private static readonly List<HttpMethod> AllowedHttpMethods = new List<HttpMethod> { HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete };
    
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="order">Order to run this filter</param>
        public FluentValidationActionFilter(int order = 1)
            : base(order)
        { }
    
        /// <summary>
        /// Pick out validation errors and turn these into a suitable exception structure
        /// </summary>
        /// <param name="actionContext">Action Context</param>
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            ModelStateDictionary modelState = actionContext.ModelState;
    
            // Only perform the FluentValidation if we've not already failed validation earlier on
            if (modelState.IsValid && AllowedHttpMethods.Contains(actionContext.Request.Method))
            {
                IDependencyScope scope = actionContext.Request.GetDependencyScope();
                var mvp = scope.GetService(typeof(IFluentValidatorProvider)) as IFluentValidatorProvider;
    
                if (mvp != null)
                {
                    ModelMetadataProvider metadataProvider = actionContext.GetMetadataProvider();
    
                    foreach (KeyValuePair<string, object> argument in actionContext.ActionArguments)
                    {
                        if (argument.Value != null && !argument.Value.GetType().IsSimpleType())
                        {
                            ModelMetadata metadata = metadataProvider.GetMetadataForType(
                                    () => argument.Value,
                                    argument.Value.GetType()
                                );
    
                            var validationContext = new InternalValidationContext
                            {
                                MetadataProvider = metadataProvider,
                                ActionContext = actionContext,
                                ModelState = actionContext.ModelState,
                                Visited = new HashSet<object>(),
                                KeyBuilders = new Stack<IKeyBuilder>(),
                                RootPrefix = String.Empty,
                                Provider = mvp,
                                Scope = scope
                            };
    
                            ValidateNodeAndChildren(metadata, validationContext, null);
                        }
                    }
                }
            }
        }
    
        /// <summary>
        /// Validates a single node (not including children)
        /// </summary>
        /// <param name="metadata">Model Metadata</param>
        /// <param name="validationContext">Validation Context</param>
        /// <param name="container">The container.</param>
        /// <returns>True if validation passes successfully</returns>
        private static bool ShallowValidate(ModelMetadata metadata, InternalValidationContext validationContext, object container)
        {
            bool isValid = true;
    
            // Use the DependencyResolver to find any validators appropriate for this type
            IEnumerable<IValidator> validators = validationContext.Provider.GetValidators(metadata.ModelType, validationContext.Scope);
    
            foreach (IValidator validator in validators)
            {
                IValidatorSelector selector = new DefaultValidatorSelector();
                var context = new ValidationContext(metadata.Model, new PropertyChain(), selector);
    
                ValidationResult result = validator.Validate(context);
    
                foreach (var error in result.Errors)
                {
                    if (!validationContext.ModelState.ContainsKey(error.PropertyName))
                    {
                        validationContext.ModelState.Add(error.PropertyName, new ModelState
                        {
                            Value = new ValueProviderResult(error.AttemptedValue, error.AttemptedValue?.ToString(), CultureInfo.CurrentCulture)
                        });
                    }
    
                    validationContext.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
                    isValid = false;
                }
            }
            return isValid;
        }
    
        #region Copied from DefaultBodyModelValidator in Web API Source
    
        private bool ValidateElements(IEnumerable model, InternalValidationContext validationContext)
        {
            bool isValid = true;
            Type elementType = GetElementType(model.GetType());
            ModelMetadata elementMetadata = validationContext.MetadataProvider.GetMetadataForType(null, elementType);
    
            var elementScope = new ElementScope { Index = 0 };
            validationContext.KeyBuilders.Push(elementScope);
            foreach (object element in model)
            {
                elementMetadata.Model = element;
                if (!ValidateNodeAndChildren(elementMetadata, validationContext, model))
                {
                    isValid = false;
                }
                elementScope.Index++;
            }
            validationContext.KeyBuilders.Pop();
            return isValid;
        }
    
        private bool ValidateNodeAndChildren(ModelMetadata metadata, InternalValidationContext validationContext, object container)
        {
            bool isValid = true;
    
            object model = metadata.Model;
    
            // Optimization: we don't need to recursively traverse the graph for null and primitive types
            if (model != null && model.GetType().IsSimpleType())
            {
                return ShallowValidate(metadata, validationContext, container);
            }
    
            // Check to avoid infinite recursion. This can happen with cycles in an object graph.
            if (validationContext.Visited.Contains(model))
            {
                return true;
            }
            validationContext.Visited.Add(model);
    
            // Validate the children first - depth-first traversal
            var enumerableModel = model as IEnumerable;
            if (enumerableModel == null)
            {
                isValid = ValidateProperties(metadata, validationContext);
            }
            else
            {
                isValid = ValidateElements(enumerableModel, validationContext);
            }
    
            if (isValid && metadata.Model != null)
            {
                // Don't bother to validate this node if children failed.
                isValid = ShallowValidate(metadata, validationContext, container);
            }
    
            // Pop the object so that it can be validated again in a different path
            validationContext.Visited.Remove(model);
    
    
            return isValid;
        }
    
        private bool ValidateProperties(ModelMetadata metadata, InternalValidationContext validationContext)
        {
            bool isValid = true;
            var propertyScope = new PropertyScope();
            validationContext.KeyBuilders.Push(propertyScope);
            foreach (ModelMetadata childMetadata in validationContext.MetadataProvider.GetMetadataForProperties(
                metadata.Model, GetRealModelType(metadata)))
            {
                propertyScope.PropertyName = childMetadata.PropertyName;
                if (!ValidateNodeAndChildren(childMetadata, validationContext, metadata.Model))
                {
                    isValid = false;
                }
            }
            validationContext.KeyBuilders.Pop();
            return isValid;
        }
    
        #endregion Copied from DefaultBodyModelValidator in Web API Source
    
        #region Inaccessible Helper Methods from the Web API source needed by the other code here
    
        private interface IKeyBuilder
        {
            string AppendTo(string prefix);
        }
    
        private static string CreateIndexModelName(string parentName, int index) => CreateIndexModelName(parentName, index.ToString(CultureInfo.InvariantCulture));
    
        private static string CreateIndexModelName(string parentName, string index) => (parentName.Length == 0) ? $"[{index}]" : $"{parentName}[{index}]";
        private static string CreatePropertyModelName(string prefix, string propertyName)
        {
            if (String.IsNullOrEmpty(prefix))
            {
                return propertyName ?? String.Empty;
            }
            else if (String.IsNullOrEmpty(propertyName))
            {
                return prefix ?? String.Empty;
            }
            else
            {
                return prefix + "." + propertyName;
            }
        }
        private static Type GetElementType(Type type)
        {
            Contract.Assert(typeof(IEnumerable).IsAssignableFrom(type));
            if (type.IsArray)
            {
                return type.GetElementType();
            }
            foreach (Type implementedInterface in type.GetInterfaces())
            {
                if (implementedInterface.IsGenericType && implementedInterface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                {
                    return implementedInterface.GetGenericArguments()[0];
                }
            }
            return typeof(object);
        }
        private Type GetRealModelType(ModelMetadata metadata)
        {
            Type realModelType = metadata.ModelType;
            // Don't call GetType() if the model is Nullable<T>, because it will
            // turn Nullable<T> into T for non-null values
            if (metadata.Model != null && !metadata.ModelType.IsNullableValueType())
            {
                realModelType = metadata.Model.GetType();
            }
            return realModelType;
        }
        private class ElementScope : IKeyBuilder
        {
            public int Index { get; set; }
            public string AppendTo(string prefix) => CreateIndexModelName(prefix, Index);
        }
        private class PropertyScope : IKeyBuilder
        {
            public string PropertyName { get; set; }
            public string AppendTo(string prefix) => CreatePropertyModelName(prefix, PropertyName);
        }
        #endregion Inaccessible Helper Methods from the Web API source needed by the other code here
        private class InternalValidationContext
        {
            public HttpActionContext ActionContext { get; set; }
            public Stack<IKeyBuilder> KeyBuilders { get; set; }
            public ModelMetadataProvider MetadataProvider { get; set; }
            public ModelStateDictionary ModelState { get; set; }
            public IFluentValidatorProvider Provider { get; set; }
            public string RootPrefix { get; set; }
            public IDependencyScope Scope { get; set; }
            public HashSet<object> Visited { get; set; }
        }
    

    }

    ValidationActionFilter.cs - 这实际上返回了一个错误模型:

    public class ValidationActionFilter : BaseActionFilterAttribute
    {
        // This must run AFTER the FluentValidation filter, which runs as 0
        public ValidationActionFilter() : base(1000) { }
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var modelState = actionContext.ModelState;
    
            if (modelState.IsValid) return;
    
            var errors = new ErrorModel();
            foreach (KeyValuePair<string, ModelState> item in actionContext.ModelState)
            {
                errors.ModelErrors.AddRange(item.Value.Errors.Select(e => new ModelPropertyError
                {
                    PropertyName = item.Key,
                    ErrorMessage = e.ErrorMessage
                }));
            }
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
        }
    }
    

    IFluentValidatorProvider.cs

    /// <summary>
    /// Provides FluentValidation validators for a type
    /// </summary>
    public interface IFluentValidatorProvider
    {
        /// <summary>
        /// Provides any FluentValidation Validators appropriate for validating the specified type.  These will have
        /// been created within the specified Dependency Scope
        /// </summary>
        /// <param name="type">Model type to find validators for</param>
        /// <param name="scope">Scope to create validators from</param>
        /// <returns></returns>
        IEnumerable<IValidator> GetValidators(Type type, IDependencyScope scope);
    }
    

    【讨论】:

    • 感谢您的回答。我在Startup.cs 中启用了 FluentValidation。我已经从 nuget FluentValidationFluentValidation.WebApi 安装了两个包,然后在 Startup.cs 内部方法称为 Configuration 我添加了这个:FluentValidationModelValidatorProvider.Configure(config); 作为最后一行。
    • 我查看了 FluentValidation 源代码,它看起来正在替换 IBodyModelValidator (github.com/JeremySkinner/FluentValidation/blob/master/src/…),因为验证是首先执行过滤器之后,所以真正的问题是我可以执行过滤器在 BodyModelValidator 之前?
    • 啊,是的,你可以这样做。内置注册的问题是返回的错误“模型”不是很有用。我得到这个解决方案主要是因为我想在我的验证器中使用依赖注入,并且还控制错误返回和序列化的方式。我将用如何按顺序注册 FluentValidationProvider 来更新答案。
    • @Shibbz “啊,是的,你可以这样做。”我很好奇您是如何以有序的方式注册 FluentValidationProvider 的(因为通过 FluentValidation.WebApi 中的 FluentValidationModelValidatorProvider 注册它,所以总是首先执行验证)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-15
    • 1970-01-01
    相关资源
    最近更新 更多