【问题标题】:Generic covariance with Simple Injector and Fluent Validation具有简单注入器和流利验证的通用协方差
【发布时间】:2015-02-14 09:35:27
【问题描述】:

我正在构建一个查询管道(使用 IQueryHandler 的装饰器模式),在该管道中,在实际执行查询之前,会处理许多横切关注点。其中一个问题是验证,为此我使用了 Fluent Validation Library。

Simple Injector 是首选的 IoC 容器,我使用它来注册每个 IQueryValidationHandler 使用通用逆变和以下注册:

container.RegisterManyForOpenGeneric(typeof(IQueryValidationHandler<>), container.RegisterAll, _assemblies);

这非常适合解决以下实现:

public class QueryValidationHandler : IQueryValidationHandler<IQuery>{ }

类中可用的查询是 IQuery 的具体类型。但是,当我尝试使用以下代码和注册将基于 IQuery 的 IValidator(通过构造函数注入)注入 QueryValidationHandler 时:

代码:

public class QueryValidationHandler : IQueryValidationHandler<IQuery>
{
    private IValidator< IQuery > _validator;

    public QueryValidationHandler(IValidator<IQuery> validator)
    {
        _validator = validator;
    }

注册

container.RegisterManyForOpenGeneric(typeof (IValidator<>), container.RegisterAll, _assemblies);

我收到以下错误:

QueryValidationHandler 类型的构造函数包含未注册的名为“validator”的 IValidator 类型参数。请确保在容器中注册了 IValidator,或者更改 QueryValidationHandler 的构造函数。

有没有办法为注入到 QueryValidationHandler 中的具体类型的 IQuery 获取 IValidator 的实现?

更新

在史蒂文的出色回答之后,我仍然陷入困境(同样的例外)。很可能是因为我试图将 Fluent Validators AbstractValidator 的实现注册为 IValidator。在以前的项目中,我能够进行以下注册:

container.RegisterManyForOpenGeneric(typeof(IValidator<>), typeof(FindProductsForCompanyQuery).Assembly);

对于以下实现:

public class FindProductsForCompanyQueryValidator : AbstractValidator<FindProductsForCompanyQuery>
{
    public FindProductsForCompanyQueryValidator()
    {
        RuleFor(q => q.CompanyId).NotNull();
    }
}

上面提到的QueryValidationHandler应该被注入到程序集中的所有AbstractValidator实现中,这样它就可以检查错误并在验证失败时抛出异常。

【问题讨论】:

    标签: c# fluentvalidation simple-injector contravariance


    【解决方案1】:

    出现异常是因为您从未注册过IValidator&lt;T&gt;;您只注册了IEnumerable&lt;Validator&lt;T&gt;&gt;。来自“正常”注册的简单注入器 differentiates registration of collections

    由于您请求IValidator&lt;IQuery&gt;,Simple Injector 预计会有IValidator&lt;IQuery&gt; 的注册,通常使用Register&lt;IValidator&lt;IQuery&gt;, SomeImplementation&gt;() 注册。但是,您注册了 IValidator&lt;T&gt; 的集合(注意 RegisterManyForOpenGeneric 中的 container.RegisterAll),这允许 Simple Injector 注入集合。

    这里真正的问题当然是:您需要什么类型的注册? IValidator&lt;T&gt;应该有什么样的映射?你需要一个:

    • 一对一映射,IValidator&lt;T&gt; 的每个封闭通用版本都有一个实现?
    • 一对零或一映射,IValidator&lt;T&gt; 的每个封闭通用版本可能有一个实现(或没有)?
    • 一对多映射,每个封闭的通用版本有零个、一个或多个实现?

    对于每种类型,您都需要进行不同的注册。对于一对一,您需要以下内容:

    // Simple Injector v3.x
    container.Register(typeof(IValidator<>), _assemblies);
    
    // Simple Injector v2.x
    container.RegisterManyForOpenGeneric(typeof(IValidator<>), _assemblies);
    

    Register 在传递程序集列表时的默认行为是在每个找到的实现上调用 Register(serviceType, ImplementationType)。由于 Simple Injector 不允许对同一服务类型进行多次注册(因为这就是 RegisterCollection 的用途),如果您(意外地)有第二个验证器用于相同的封闭泛型类型,注册将失败。这是一个非常有用的安全措施。

    但是,通过验证,通常只对您的一些查询进行验证。并非所有查询都需要有特定的验证器。使用一对一映射会非常烦人,因为您必须为每个根本没有任何验证的查询定义许多空验证器。对于这种情况,Simple Injector 允许您注册一个回退注册,以防万一没有注册:

    // Simple Injector v3.x
    container.Register(typeof(IValidator<>), _assemblies);
    container.RegisterConditional(typeof(IValidator<>), typeof(EmptyValidator<>),
        c => !c.Handled);
    
    // Simple Injector v2.x
    container.RegisterManyForOpenGeneric(typeof(IValidator<>), _assemblies);
    container.RegisterOpenGeneric(typeof(IValidator<>), typeof(EmptyValidator<>));
    

    我们在这里注册一个开放的通用EmptyValidator&lt;T&gt; 作为后备注册,这意味着如果没有可用的显式注册,它将被拾取。

    第三种情况,一对多,是你已经在使用的:

    // Simple Injector v3.x
    container.RegisterCollection(typeof(IValidator<>), _assemblies);
    
    // Simple Injector v2.x
    container.RegisterManyForOpenGeneric(typeof(IValidator<>),
        container.RegisterAll, _assemblies);
    

    但是如果你注册一个集合,你要么需要注入一个可枚举,要么你需要创建一个复合验证器。

    注入 IEnumerable 的示例:

    public class QueryValidationHandler : IQueryValidationHandler<IQuery>
    {
        private IEnumerable<IValidator<IQuery>> _validators;
    
        public QueryValidationHandler(IEnumerable<IValidator<IQuery>> validators) {
            _validators = validators;
        }
    }
    

    复合验证器非常有用,尤其是当您有多个需要注入这些验证器的类时。在这种情况下,您不希望应用程序知道存在多个验证器这一事实(这是一个实现细节),就像遍历所有验证器也是一个实现细节一样;它违反了依赖倒置原则。此外,它验证 DRY,因为您正在复制代码。总是坏事。

    所以你可以做的是创建一个复合:

    public class CompositeValidator<T> : IValidator<T>
    {
        private IEnumerable<IValidator<T>> _validators;
        public CompositeValidator(IEnumerable<IValidator<T>> validators) {
            _validators = validators;
        }
    
        public IEnumerable<ValidationError> Validate(T instance) {
            return
                from validator in _validators
                from error in validator.Validate(instance)
                select error;
        }
    }
    

    使用此CompositeValidator&lt;T&gt;,您将获得以下注册:

    container.RegisterCollection(typeof(IValidator<>), _assemblies);
    container.Register(typeof(IValidator<>), typeof(CompositeValidator<>),
        Lifestyle.Singleton);
    

    这是 Simple Injector 设计的众多优势之一,其中集合的注册与一对一的注册是分开的。它使注册复合材料变得非常简单。对于没有这种明确分离的容器,注册这样的组合要困难得多,因为对于这些容器,如果不对您的配置做任何特殊的事情,注入CompositeValidator&lt;T&gt; 的验证器之一就是CompositeValidator&lt;T&gt; 本身,它将最终导致堆栈溢出异常。

    您的查询处理程序现在可以再次简单地依赖于IValidator&lt;T&gt;

    public class QueryValidationHandler : IQueryValidationHandler<IQuery>
    {
        private IValidator<IQuery> _validator;
    
        public QueryValidationHandler(IValidator<IQuery> validator) {
            _validator = validator;
        }
    }
    

    更新:

    由于您有一个应该能够验证任何查询的非泛型 QueryValidationHandler,因此注入 IValidator&lt;IQuery&gt; 将不起作用,因为您的 DI 库不知道它需要注入什么验证器实现。相反,您需要使用可以将验证委托给真正的验证器的中介。例如:

    sealed class Validator : IValidator // note: non-generic interface
    {
        private readonly Container container;
    
        public Validator(Container container) {
            this.container = container;
        }
    
        [DebuggerStepThrough]
        public IEnumerable<ValidationError> Validate(object instance) {
            var validators = container.GetAllInstances(
                typeof(IValidator<>).MakeGenericType(instance.GetType()));
    
            return
                from validator in validators
                from error in Validate(validator, instance)
                select error;
        }
    
        private static IEnumerable<ValidationError> Validate(
            dynamic validator, dynamic instance) {
            return validator.Validate(instance);
        }
    }
    

    您的消费者现在可以依赖非通用的IValidator 接口并注入Validator。此验证器会将调用转发到可能存在的任何 IValidator&lt;T&gt; 实现。

    【讨论】:

    • 史蒂文,谢谢你的回答。它清楚地说明了几件事。不幸的是,我仍然得到相同的结果。我正在尝试将 Fluent Validation 的 IValidator 与 AbstractValidator 实现注册。我认为这就是它失败的地方。
    • @Vocte:在那种情况下,我不确定你到底在做什么。您能否通过更多注册来更新您的问题并解释这个QueryValidationHandler 的实际使用方式?
    • 我已经更新了我的答案。如果我需要进一步澄清,请告诉我:-)
    • @Vocte:我开始明白你在做什么。依赖项确实是IValidator&lt;IQuery&gt;。简单注入器不可能知道它应该注入哪个已注册的实现(因为可能所有验证器都可以注入)。因此,您需要注入一个可以将经过验证的实例分派给真正的验证器的中介器。该调解员必须明确注册。
    • @Vocte 有关此类中介的示例,请查看 this blog post 中显示的 QueryProcessor 类
    猜你喜欢
    • 1970-01-01
    • 2011-10-12
    • 2023-04-02
    • 2015-05-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    相关资源
    最近更新 更多