2016-12-02 84 views
1

在我的WebAPI項目中我使用FluentValidation。我在全球範圍內啓用它通過增加 FluentValidationModelValidatorProvider.Configure(config);內Startup.csFluentValidation和ActionFilterAttribute - 在驗證之前更新模型

我自定義添加ActionFolterAttribute這是我的方法中使用之前被改變的模式,但經過測試,我可以看到我有執行的不好的秩序。

我希望我的模型在被FluentVatiodation驗證之前進行更改,但現在它在FluentVatiodation驗證我的模型後進行更新。
我需要這能夠訪問內部FluentVatiodation驗證數據的一些路線數據

下面是我的自定義屬性:

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

而且我的方法:

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

我與驗證模型:

[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!"); 
} 

我檢查了我們通過發送POST荷蘭國際集團郵差到URL http://localhost:63564/test/create/20與數據:

Id:1 
Name:Test 

內部驗證ID具有值= 1,但我的方法的身體值= 20的內部。

我想改變這個順序,並在我的驗證器中有更新的值。

這可以改變嗎?

類似的事情在這裏討論:Access route data in FluentValidation for WebApi 2和我基於上面的問題作者評論的解決方案。

回答

1

是的,它可以改變,但是你需要用一個強制定義的順序替換通用過濾器提供者。

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

您可以添加過濾器,你希望他們開槍這樣的順序:

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

或者設置由IOrderedFilterAttribute暴露O​​rder屬性。如果你想通過配置/依賴注入來控制排序,或者在編譯時不知道其他因素,你可能希望使用這種方法。

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

IOrderedFilterAttribute.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

/// <summary> 
    /// Order of execution for this filter 
    /// </summary> 
    public int Order { get; set; } 

    public BaseActionFilterAttribute() 
    { 
     Order = 0; 
    } 

    public BaseActionFilterAttribute(int order) 
    { 
     Order = order; 
    } 
} 

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); 
} 
+0

感謝您的回答。我在'Startup.cs'中啓用FluentValidation。我已經從nuget'FluentValidation'和'FluentValidation.WebApi'安裝了兩個軟件包,然後在名爲'Configuration'的方法內部的'Startup.cs'中添加了這個:'FluentValidationModelValidatorProvider.Configure(config);'作爲最後一行。 – Misiu

+1

我看過FluentValidation源代碼,它看起來代替了IBodyModelValidator(https://github.com/JeremySkinner/FluentValidation/blob/master/src/FluentValidation.WebApi/FluentValidationModelValidatorProvider.cs#L44),並且因爲驗證是第一個,之後,過濾器被執行,所以真正的問題是我可以在BodyModelValidator之前執行過濾器嗎? – Misiu

+1

啊,是的,你可以這樣做。內置註冊的問題是返回的「模型」錯誤不是很有用。我到達這個解決方案主要是因爲我想在我的驗證器中使用依賴注入,並控制錯誤返回和序列化的方式。我將以如何以有序方式註冊FluentValidationProvider來更新答案。 – Shibbz

相關問題