简单注射器和流畅验证的通用协方差

Voc*_*cte 2 c# contravariance fluentvalidation simple-injector

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

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

container.RegisterManyForOpenGeneric(typeof(IQueryValidationHandler<>), container.RegisterAll, _assemblies);
Run Code Online (Sandbox Code Playgroud)

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

public class QueryValidationHandler : IQueryValidationHandler<IQuery>{ }
Run Code Online (Sandbox Code Playgroud)

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

码:

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

    public QueryValidationHandler(IValidator<IQuery> validator)
    {
        _validator = validator;
    }
Run Code Online (Sandbox Code Playgroud)

注册:

container.RegisterManyForOpenGeneric(typeof (IValidator<>), container.RegisterAll, _assemblies);
Run Code Online (Sandbox Code Playgroud)

我收到以下错误:

QueryValidationHandler类型的构造函数包含IValidator <IQuery>类型的参数,其名称为"validator",未注册.请确保在容器中注册了IValidator <IQuery>,或者更改了QueryValidationHandler的构造函数.

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

更新

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

container.RegisterManyForOpenGeneric(typeof(IValidator<>), typeof(FindProductsForCompanyQuery).Assembly);
Run Code Online (Sandbox Code Playgroud)

对于以下实现:

public class FindProductsForCompanyQueryValidator : AbstractValidator<FindProductsForCompanyQuery>
{
    public FindProductsForCompanyQueryValidator()
    {
        RuleFor(q => q.CompanyId).NotNull();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Ste*_*ven 6

发生异常是因为您从未进行过注册IValidator<T>; 你只是为了注册IEnumerable<Validator<T>>.Simple Injector 将集合注册与"正常"注册区分开来.

由于您要求IValidator<IQuery>,简单注入器期望有注册IValidator<IQuery>,通常使用注册Register<IValidator<IQuery>, SomeImplementation>().但是,您注册了IValidator<T>(注意container.RegisterAll在其中RegisterManyForOpenGeneric)的集合,它允许Simple Injector注入集合.

这里真正的问题当然是:您需要什么类型的注册?应该采用什么样的映射IValidator<T>?你需要一个:

  • 一对一映射,每个封闭的通用版本只有一个实现IValidator<T>
  • 一对一或一对映射,对于每个封闭的通用版本,可能有一个实现(或没有)IValidator<T>
  • 一对多映射,每个封闭的通用版本有零个,一个或多个实现?

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

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

// Simple Injector v2.x
container.RegisterManyForOpenGeneric(typeof(IValidator<>), _assemblies);
Run Code Online (Sandbox Code Playgroud)

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<>));
Run Code Online (Sandbox Code Playgroud)

在这里,我们注册一个开放的通用EmptyValidator<T>作为后备注册,这意味着如果没有明确的注册,它将被选中.

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

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

// Simple Injector v2.x
container.RegisterManyForOpenGeneric(typeof(IValidator<>),
    container.RegisterAll, _assemblies);
Run Code Online (Sandbox Code Playgroud)

但是如果你注册一个集合,你需要注入一个枚举,或者你需要创建一个复合验证器.

注入IEnumerable的示例:

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

    public QueryValidationHandler(IEnumerable<IValidator<IQuery>> validators) {
        _validators = validators;
    }
}
Run Code Online (Sandbox Code Playgroud)

复合验证器非常有用,尤其是当您有多个类需要注入这些验证器时.在这种情况下,您不希望应用程序知道存在多个验证器(这是一个实现细节)的事实,就像循环所有验证器一样,也是一个实现细节; 它违反了依赖性倒置原则.此外,它会验证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;
    }
}
Run Code Online (Sandbox Code Playgroud)

有了这个,CompositeValidator<T>您将获得以下注册:

container.RegisterCollection(typeof(IValidator<>), _assemblies);
container.Register(typeof(IValidator<>), typeof(CompositeValidator<>),
    Lifestyle.Singleton);
Run Code Online (Sandbox Code Playgroud)

这是Simple Injector设计的众多优势之一,其中集合的注册与一对一注册分开.它使注册复合材料变得非常简单.对于没有这种清晰分离的容器来注册这样的复合很难,因为对于那些容器,没有对你的配置做任何特殊处理,注入到CompositeValidator<T>will中的一个验证器CompositeValidator<T>就是它自己,这最终会导致堆栈溢出例外.

您的查询处理程序现在可以再次依赖于IValidator<T>:

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

    public QueryValidationHandler(IValidator<IQuery> validator) {
        _validator = validator;
    }
}
Run Code Online (Sandbox Code Playgroud)

更新:

由于你有一个非通用的QueryValidationHandler应该能够验证任何查询,注入一个IValidator<IQuery>将无法工作,因为你的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您的消费者现在可以依赖非通用IValidator接口并获得Validator注入.此验证器将调用转发给IValidator<T>可能存在的任何实现.