2016-12-02 2 views
1

WebAPIプロジェクト内でFluentValidationを使用しています。私はStartup.csFluentValidationとActionFilterAttribute - 検証前のモデルを更新する

FluentValidationModelValidatorProvider.Configure(config);を追加することによって、グローバルに有効にするよ、私はそれが私のメソッド内で使用される前に、モデルを変更してカスタム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!"); 
} 

私はこれをチェックしましたPOSTを送信することにより、郵便配達をすると、データをhttp://localhost:63564/test/create/20をURLに:

Id:1 
Name:Test 

インサイドバリIdが値= 1を持っていますが、私のメソッド本体値= 20の内側。

私はその注文を変更し、バリデーター内にその値を更新したいと思います。

これは変更できますか?

同様のことがここで議論されました:Access route data in FluentValidation for WebApi 2と私は上記の解答の著者のコメントに基づいています。

答えて

1

はい変更できますが、汎用フィルタプロバイダを定義済みの注文を適用するものに置き換える必要があります。

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

それともIOrderedFilterAttributeによって公開されたOrderプロパティを設定します。あなたはあなたがそれらをしたいために、フィルタを追加することができますいずれか

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

は次のように解雇しました。コンフィグレーション/依存性注入、またはコンパイル時に知られていないその他の要因によって、順序を制御したい場合は、このメソッドを使用することをお勧めします。

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を有効にしています。私は 'FluentValidation'と' FluentValidation.WebApi'から2つのパッケージをインストールし、 '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

関連する問題