2016-12-02 3 views
1

In meinem WebAPI-Projekt verwende ich FluentValidation. Ich ermögliche es global durch FluentValidationModelValidatorProvider.Configure(config); innerhalb Startup.cs ZugabeFluentValidation und ActionFilterAttribute - Aktualisieren des Modells, bevor es validiert wird

Ich habe benutzerdefinierte ActionFolterAttribute hinzugefügt, das Modell zu ändern, bevor es in meinem Verfahren verwendet wird, aber nach dem Test kann ich sehen, dass ich schlechte Reihenfolge der Ausführung habe.

Ich möchte, dass mein Modell geändert wird, bevor es von FluentVatiodation validiert wird, aber im Moment wird es aktualisiert, nachdem FluentVatiodation mein Modell validiert hat.

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

Mein Modell mit Validator:

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

Und meine Methode einig Routendaten für den Zugriff innerhalb FluentVatiodation Validierungsdaten

Unten ist mein benutzerdefiniertes Attribut
Ich brauche diese in der Lage sein:

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

Ich habe diese uns überprüft PostMan ing per Post versenden http://localhost:63564/test/create/20 mit Daten URL:

Id:1 
Name:Test 

Innen Validator Id Wert = 1, aber in meinem Verfahren Körper-Wert = 20.

Ich möchte diese Reihenfolge ändern und diesen aktualisierten Wert in meinem Validator haben.

Kann dies geändert werden?

Ähnliche Sache wurde hier diskutiert: Access route data in FluentValidation for WebApi 2 und ich basierend auf Lösung auf Frage Autor Kommentar.

Antwort

1

Ja, es kann geändert werden, jedoch müssen Sie den generischen Filteranbieter durch einen ersetzen, der eine definierte Reihenfolge erzwingt.

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

Sie können entweder fügen Sie die Filter in der Reihenfolge, die Sie wie folgt gebrannt werden soll:

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

oder die Bestellung Eigentum von IOrderedFilterAttribute ausgesetzt eingestellt. Möglicherweise möchten Sie diese Methode verwenden, wenn Sie die Reihenfolge über config/dependency injection oder einen anderen Faktor steuern möchten, der bei der Kompilierung nicht bekannt ist.

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

Und das verwenden, werden Sie ein paar unterstützenden Klassen benötigen.

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 - dies tatsächlich einen Fehler zurück Modell:

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

Danke für Antwort. Ich aktiviere FluentValidation in 'Startup.cs'. Ich habe zwei Pakete von nuget 'FluentValidation' und' FluentValidation.WebApi' installiert und dann innerhalb 'Startup.cs' innerhalb der Methode namens' Configuration'. Ich füge folgendes hinzu: 'FluentValidationModelValidatorProvider.Configure (config);' als letzte Zeile. – Misiu

+1

Ich habe FluentValidation Quelle angeschaut und es sieht aus, dass es IBodyModelValidator ersetzt (https://github.com/JeremySkinner/FluentValidation/blob/master/src/FluentValidation.WebApi/FluentValidationModelValidatorProvider.cs#L44) und wegen dieser Validierung ist das erste und danach werden Filter ausgeführt, also ist die Frage wirklich, kann ich Filter vor BodyModelValidator ausführen? – Misiu

+1

Ahh ja du kannst es so machen. Das Problem mit der eingebauten Registrierung ist, dass die zurückgegebenen "Modelle" für Fehler nicht sehr brauchbar sind. Ich kam hauptsächlich deshalb zu dieser Lösung, weil ich die Abhängigkeitsinjektion in meinen Validatoren verwenden und auch steuern wollte, wie Fehler zurückgegeben und serialisiert wurden. Ich werde die Antwort aktualisieren, um den FluentValidationProvider geordnet zu registrieren. – Shibbz

Verwandte Themen