C# FluentValidation和ActionFilterAttribute-在验证之前更新模型

C# FluentValidation和ActionFilterAttribute-在验证之前更新模型,c#,asp.net-web-api,fluentvalidation,C#,Asp.net Web Api,Fluentvalidation,在我的WebAPI项目中,我正在使用FluentValidation。我通过添加 FluentValidationModelValidatorProvider.Configure(配置)内部Startup.cs 我已经添加了自定义的actionfolteratAttribute,它在我的方法中使用之前正在改变模型,但是在测试之后,我可以看到我的执行顺序不好 我希望在FluentVatiodation验证我的模型之前对其进行更改,但现在它会在FluentVatiodation验证我的模型之后进行更

在我的WebAPI项目中,我正在使用FluentValidation。我通过添加
FluentValidationModelValidatorProvider.Configure(配置)内部Startup.cs

我已经添加了自定义的
actionfolteratAttribute
,它在我的方法中使用之前正在改变模型,但是在测试之后,我可以看到我的执行顺序不好

我希望在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;
    }
}
内部验证程序Id的值为1,但在我的方法体中的值为20

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

这能改变吗


这里也讨论了类似的事情:我根据问题作者的评论提出了上述解决方案。

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

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;
    }
}
/// <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);
}
//
///组合来自多个源的操作筛选器
/// 
公共类OrderedFilterProvider:IFilterProvider
{
私有列表过滤器提供程序;
/// 
///使用默认筛选器提供程序的构造函数
/// 
公共OrderedFilterProvider()
{
_filterProviders=新列表();
_添加(新配置FilterProvider());
_添加(新的ActionDescriptorFilterProvider());
}
/// 
///建造师
/// 
///内部提供者。
公共OrderedFilterProvider(IEnumerable InnerProvider)
{
_filterProviders=innerProviders.ToList();
}
/// 
///返回指定操作的所有适当筛选器,如果有,则按其Order属性排序
/// 
公共IEnumerable GetFilters(HttpConfiguration配置,HttpActionDescriptor actionDescriptor)
{
如果(配置==null){抛出新的ArgumentNullException(“配置”);}
如果(actionDescriptor==null){抛出新的ArgumentNullException(“actionDescriptor”);}
列表过滤器=新列表();
foreach(iFilterProviders中的IFilterProvider)
{
filters.AddRange(
fp.GetFilters(配置,actionDescriptor)
.Select(fi=>neworderedfilterinfo(fi.Instance,fi.Scope));
}
var orderedFilters=filters.OrderBy(i=>i).Select(i=>i.ConvertToFilterInfo());
返回订单过滤器;
}
}
要使用它,您需要一些支持类

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);
    }
}
//
///我们的FilterInfo版本,具有按顺序属性排序的功能。这不能简单地从中继承
///Web API类中的FilterInfo,因为它是密封的:(
/// 
公共类OrderedFilterInfo:IComparable
{
public OrderedFilterInfo(IFilter实例,FilterScope作用域)
{
如果(instance==null){抛出新的ArgumentNullException(“instance”);}
实例=实例;
范围=范围;
}
/// 
///筛选此实例是关于
/// 
公共IFilter实例{get;private set;}
/// 
///此筛选器的范围
/// 
公共筛选器作用域{get;private set;}
/// 
///允许对过滤器进行受控排序
/// 
公共整数比较(对象对象对象)
{
如果(obj是OrderedFilterInfo)
{
var otherfilterInfo=obj作为OrderedFilterInfo;
//全局筛选器应该在控制器和操作筛选器之前执行。严格来说,我们不必这样做
//这样做,因为它是在框架中再次完成的,但是它对于测试来说更加一致!
if(this.Scope==FilterScope.Global&&otherfilterInfo.Scope!=FilterScope.Global)
{
返回-10;
}
else if(this.Scope!=FilterScope.Global&&otherfilterInfo.Scope==FilterScope.Global)
{
返回10;
}
IOrderedFilterAttribute thisAttribute=此实例作为IOrderedFilterAttribute;
IOrderedFilterAttribute otherAttribute=otherfilterInfo.Instance作为IOrderedFilterAttribute;
IFilter thisNonOrderedAttribute=this.Instance作为IFilter;
IFilter otherNonOrderedAttribute=otherfilterInfo.Instance作为IFilter;
if(thisAttribute!=null&&otherAttribute!=null)
{
int value=thisAttribute.Order.CompareTo(otherAttribute.Order);
如果(值==0)
{
//如果它们的顺序相同,则改为按名称排序
value=thisAttribute.GetType().FullName.CompareTo(otherAttribute.GetType().FullName);
}
返回值;
}
else if(thisNonOrderedAttribute!=null&&otherAttribute!=null)
{
返回1;
}
else if(thisAttribute!=null&&otherNonOrderedAttribute!=null)
{
返回-1;
}
{
返回此NonOrderedAttribute.GetType().FullName.CompareTo(其他NonOrderedAttribute.GetType().FullName);
}
}
其他的
{
抛出新ArgumentException(“对象类型错误”);
}
}
/// 
///将其转换为FilterInfo(因为FilterInfo是密封的,我们无法扩展它。/sigh
/// 
/// 
公共过滤器信息转换
/// <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);
    }
}
/// <summary>
/// Allows ordering of filter attributes
/// </summary>
public interface IOrderedFilterAttribute
{
    /// <summary>
    /// Order of execution for this filter
    /// </summary>
    int Order { get; set; }
}
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;
    }
}
/// <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; }
    }
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);
    }
}
/// <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);
}