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



我正在构建一个查询管道(使用IQueryHandler的decorator模式),在实际执行查询之前,将在其中处理许多交叉问题。其中一个问题是验证,我使用Fluent验证库进行验证。

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类型的构造函数包含IValidator<名为"validator"但未注册的IQuery>。请确保IValidator<IQuery>已在容器中注册,或者更改QueryValidationHandler的构造函数。

有没有一种方法可以为注入QueryValidationHandler的具体类型的IQuery实现IValidator?

更新

在史蒂文的精彩回答之后,我仍然被卡住了(同样的例外)。很可能是因为我正在尝试注册Fluent Validators AbstractValidator<T>作为IV验证器<T>。在以前的项目中,我能够进行以下注册:

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

对于以下实现:

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

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

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

由于您请求了IValidator<IQuery>,Simple Injector希望有IValidator<IQuery>的注册,通常使用Register<IValidator<IQuery>, SomeImplementation>()进行注册。但是,您注册了IValidator<T>的集合(注意RegisterManyForOpenGeneric中的container.RegisterAll),这允许SimpleInjector注入集合。

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

  • 一对一映射,IValidator<T>的每个封闭通用版本恰好有一个实现
  • 一对零或一映射,其中IValidator<T>的每个封闭通用版本可能有一个实现(或没有)
  • 一对多映射,其中每个封闭的泛型版本有零个、一个或多个实现

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

// 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<T>注册为回退注册,这意味着在没有可用的显式注册的情况下,它将被拾取。

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

// 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<T>,您将获得以下注册:

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

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

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

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

更新:

由于您有一个非通用的QueryValidationHandler,它应该能够验证ANY查询,因此注入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);
    }
}

您的消费者现在可以依赖于非通用的IValidator接口并注入Validator。该验证器将把调用转发到可能存在的任何IValidator<T>实现。

最新更新