简单注入器 - 使用相同泛型类型的另一个依赖项注册装饰器

时间:2014-03-21 15:56:47

标签: c# .net generics dependency-injection simple-injector

我想我在Simple Injector的RegisterDecorator()中偶然发现了一个怪癖。它甚至出现在最近的2.5.0中。我有一种情况,我想装饰一个封闭的泛型类型,例如ICommandHandler<MessageCommand>,装饰器(通过构造函数注入)采用类型ICommandHandler<MessageCommand>的内部处理程序,但也是另一种类型的处理程序,说ICommandHandler<LogCommand>。尽管这些命令处理程序类型是不同的,但是当我在这样的装饰器类型上调用RegisterDecorator时,SimpleInjector似乎会混淆并引发异常:

  

ArgumentException:为了使容器能够使用MessageLogger作为装饰器,其构造函数必须包含ICommandHandler<MessageCommand>(或Func<ICommandHandler<MessageCommand>>)类型的单个参数 - 即正在实例的类型装饰。参数类型ICommandHandler<MessageCommand>在MessageLogger类的构造函数中定义多次。

...即使装饰者显然只有一个ICommandHandler<MessageCommand>参数。

以下是抛出异常的完整工作示例:

public interface ICommandHandler<T>
{
    void Execute(T command);
}

public class LogCommand
{
    public string LogMessage { get; set; }
    public DateTime Time { get; set; }
}

public class Logger : ICommandHandler<LogCommand>
{
    public void Execute(LogCommand command)
    {
        Debug.WriteLine(string.Format("Message \"{0}\" sent at {1}",
            command.LogMessage, command.Time));
    }
}


public class MessageCommand
{
    public string Message { get; set; }
}

public class MessageSender : ICommandHandler<MessageCommand>
{
    public void Execute(MessageCommand command)
    {
        Debug.WriteLine(command.Message);
    }
}

// message command handler decorator that logs about messages being sent
public class MessageLogger : ICommandHandler<MessageCommand>
{
    private ICommandHandler<MessageCommand> innerHandler;
    private ICommandHandler<LogCommand> logger;

    // notice these dependencies are two distinct closed generic types
    public MessageLogger(ICommandHandler<MessageCommand> innerHandler,
        ICommandHandler<LogCommand> logger)
    {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }

    public void Execute(MessageCommand command)
    {
        innerHandler.Execute(command);

        var logCommand = new LogCommand
            {
                LogMessage = command.Message,
                Time = DateTime.Now
            };
        logger.Execute(logCommand);
    }
}

// this works as intended, but is tedious in a real-world app
ICommandHandler<MessageCommand> ResolveManually()
{
    ICommandHandler<MessageCommand> sender = new MessageSender();
    ICommandHandler<LogCommand> logger = new Logger();

    ICommandHandler<MessageCommand> loggerSender =
        new MessageLogger(sender, logger);

    return loggerSender;
}

// this is what I want to work - seems simple?
ICommandHandler<MessageCommand> ResolveWithSimpleInjector()
{
    var container = new Container();

    container.Register<ICommandHandler<LogCommand>, Logger>();

    container.Register<ICommandHandler<MessageCommand>, MessageSender>();

    // this next line throws the exception
    container.RegisterDecorator(typeof(ICommandHandler<MessageCommand>),
        typeof(MessageLogger));

    return container.GetInstance<ICommandHandler<MessageCommand>>();
}

void Main()
{
    //ICommandHandler<MessageCommand> sender = ResolveManually();
    ICommandHandler<MessageCommand> sender = ResolveWithSimpleInjector();

    var command = new MessageCommand { Message = "Hello World!" };
    sender.Execute(command);
}

我找不到有关此情况的任何信息。这是一个错误,还是我错过了什么?

修改

我正在寻找一个关于SimpleInjector的开发人员的反馈,以确定是否存在这种限制的技术原因,或者它被忽略了......除非有人能说服我这个设计存在逻辑缺陷有一个很好的理由我不应该这样做,迄今为止没有任何答案能够做到。我非常感谢您的反馈。

在我看来,核心问题是RegisterDecorator()将两种不同的封闭泛型类型视为同一类型。基于其内部运作可能有技术原因,但可能不是吗?

4 个答案:

答案 0 :(得分:14)

我不得不在代码库中做一些调查,看看会发生什么。你可以称之为Simple Injector实施中的一个小故障,但它的IMO是一个公平的权衡。 Simple Injector的装饰器子系统基于使用开放泛型类型和开放式通用装饰器的想法。它在装饰器注册时进行的检查是查看装饰器的构造函数是否只有一个装饰。使用必须应用装饰器的开放式通用抽象来完成此检查;在您的情况下ICommandHandler<T>。由于此时只有通用ICommandHandler<T>可用,因此两个构造函数参数与此类型匹配。

可以改进这些前置条件检查,但这实际上非常讨厌,而此功能的实用性非常有限。它受到限制,因为它仅对非通用装饰器有用。例如,看一下下面的装饰器:

public class GenericDecorator<TCommand> : ICommandHandler<TCommand> {
    public GenericDecorator(
        ICommandHandler<TCommand> decoratee,
        ICommandHandler<LoggingCommand> dependency)
    {
    }
}

这个装饰器是通用的,允许你将它应用于任何装饰器,这更有用。但是当你解决ICommandHandler<LoggingCommand>时会发生什么?这将导致循环依赖图和Simple Injector(显然)无法创建该图并将抛出异常。它必须抛出,因为在这种情况下装饰器将有两个ICommandHandler<LoggingCommand>参数。第一个将是decoratee,将注入你的Logger,第二个将是一个正常的依赖,并将注入一个GenericDecorator<LoggingCommand>,当然是递归的。

所以我认为问题出在你的设计中。一般来说,我建议不要使用其他命令处理程序组合命令处理程序。 ICommandHandler<T>应该是位于业务层之上的抽象,它定义了表示层如何与业务层进行通信。它不是业务层在内部使用的机制。如果您开始这样做,您的依赖配置会变得非常复杂。以下是使用DeadlockRetryCommandHandlerDecorator<T>TransactionCommandHandlerDecorator<T>的图表示例:

new DeadlockRetryCommandHandlerDecorator<MessageCommand>(
    new TransactionCommandHandlerDecorator<MessageCommand>(
        new MessageSender()))

在这种情况下,DeadlockRetryCommandHandlerDecorator<T>TransactionCommandHandlerDecorator<T>将应用于MessageSender命令处理程序。但是看看我们应用MessageLogger装饰器会发生什么:

new DeadlockRetryCommandHandlerDecorator<MessageCommand>(
    new TransactionCommandHandlerDecorator<MessageCommand>(
        new MessageLogger(
            new MessageSender(),
            new DeadlockRetryCommandHandlerDecorator<MessageLogger>(
                new TransactionCommandHandlerDecorator<MessageLogger>(
                    new Logger())))))

注意对象图中的第二个DeadlockRetryCommandHandlerDecorator<T>和第二个TransactionCommandHandlerDecorator<T>是怎样的。在事务中拥有事务并具有嵌套的死锁重试(在事务中)意味着什么。这可能会导致应用程序出现严重的可靠性问题(因为数据库死锁会导致您的operation to continue in a transaction-less connection)。

尽管可以以这样的方式创建装饰器,使得它们能够检测到它们是嵌套的,以便在它们嵌套的情况下使它们正常工作,这使得实现它们更加困难且更加脆弱。 IMO浪费你的时间。

因此,不是允许嵌套命令处理程序,而是让命令处理程序和命令处理程序装饰器依赖于其他抽象。在您的情况下,通过让装饰器使用某种类型的ILogger接口来更改装饰器,可以轻松解决问题:

public class MessageLogger : ICommandHandler<MessageCommand> {
    private ICommandHandler<MessageCommand> innerHandler;
    private ILogger logger;

    public MessageLogger(
        ICommandHandler<MessageCommand> innerHandler, ILogger logger) {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }

    public void Execute(MessageCommand command) {
        innerHandler.Execute(command);

        logger.Log(command.Message);
    }
}

如果表示层需要直接记录,您仍然可以实现ICommandHandler<LogCommand>实现,但在这种情况下,实现也可以仅依赖于ILogger

public class LogCommandHandler : ICommandHandler<LogCommand> {
    private ILogger logger;

    public LogCommandHandler(ILogger logger) {
        this.logger = logger;
    }

    public void Execute(LogCommand command) {
        logger(string.Format("Message \"{0}\" sent at {1}",
            command.LogMessage, DateTime.Now));
    }
}

答案 1 :(得分:1)

这是一个边缘情况,你可以争论任何一种方式,但事实是Simple Injector明确地不支持你想要做的事情。

通常需要装饰器在特定抽象的所有(或某些)上应用公共逻辑,在您的示例中为ICommandHandler。换句话说,MessageLogger旨在装饰ICommandHandler,因为它是ICommandHandler的装饰者,它只需要一个ICommandHandler它的构造函数。此外,允许这样的事情需要大量的可怕的循环检查,最好通过更清洁的设计来避免!

因此,您通常会定义一个装饰器,它具有与装饰类型相同的接口(和通用参数)

public class MessageLogger<TCommand> : ICommandHandler<TCommand>
    where TCommand : <some criteria e.g. MessageCommand>
{
    //....
}

我能想到的第一个解决问题的方法是创建一个中介来消除直接依赖:

public class LoggerMediator
{
    private readonly ICommandHandler<LogCommand> logger;

    public LoggerMediator(ICommandHandler<LogCommand> logger)
    {
        this.logger = logger;
    }

    public void Execute(LogCommand command)
    {
        this.logger.Execute(command);
    }
}

并更改您的MessageLogger以使用调解员。

public class MessageLogger<TCommand> : ICommandHandler<TCommand>
    where TCommand : MessageCommand
{
    private ICommandHandler<TCommand> innerHandler;
    private LoggerMediator logger;

    public MessageLogger(
        ICommandHandler<TCommand> innerHandler,
        LoggerMediator logger)
    {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }

    public void Execute(TCommand command)
    {
        innerHandler.Execute(command);

        var logCommand = new LogCommand
        {
            LogMessage = command.Message,
            Time = DateTime.Now
        };
        logger.Execute(logCommand);
    }
}

顺便说一下,你可以像这样简化你的注册

var container = new Container();
container.RegisterManyForOpenGeneric(
    typeof(ICommandHandler<>), 
    typeof(ICommandHandler<>).Assembly);
container.Register<LoggerMediator>();
container.RegisterDecorator(typeof(ICommandHandler<>), typeof(MessageLogger<>));
container.Verify();

<强>更新

在这里查看我的代码库,我发现我有类似的要求,我用一个额外的类解决了它 - 一个通用的命令介体:

public class CommandHandlerMediator<TCommand>
{
    private readonly ICommandHandler<TCommand> handler;

    public CommandHandlerMediator(ICommandHandler<TCommand> handler)
    {
        this.handler = handler;
    }

    public void Execute(TCommand command)
    {
        this.handler.Execute(command);
    }
}

这样注册:

container.RegisterOpenGeneric(
    typeof(CommandHandlerMediator<>), 
    typeof(CommandHandlerMediator<>));

并像这样引用:

public class MessageLogger<TCommand> : ICommandHandler<TCommand>
    where TCommand : <some criteria e.g. MessageCommand>
{
    private ICommandHandler<TCommand> decorated;
    private CommandHandlerMediator<LogCommand> logger;

    public MessageLogger(
        ICommandHandler<TCommand> decorated,
        CommandHandlerMediator<LogCommand> logger)
    {
        this.innerHandler = innerHandler;
        this.logger = logger;
    }

    //....

}

一个新课程,您为所有处理程序排序。

答案 2 :(得分:0)

您可以将装饰器更改为

public MessageLogger(ICommandHandler<MessageCommand> innerHandler)
{
    this.innerHandler = innerHandler;
}

将必要的ctor签名与&#34; ICommandHandler(或Func&gt;)&#34;类型的单个参数进行匹配。并将logger注入属性而不是ctor参数。我没有使用简单注入器,但是看看你的异常消息,它是最明显的解决方案,因为装饰器构造函数签名限制。

答案 3 :(得分:0)

你的解决方案似乎有点尴尬,因为它是装饰器和构造函数注入/组合(某些东西)的组合。虽然它并不完全是你的问题的答案,但它可能会解决你的问题(我会以更好的方式说):

public class LoggingHandler : ICommandHandler<MessageCommand>
{
    private ICommandHandler<MessageCommand> innerHandler;

    public LoggingHandler(ICommandHandler<MessageCommand> innerHandler)
    {
        this.innerHandler = innerHandler;
    }

    public void Execute(MessageCommand command)
    {
        innerHandler.Execute(command);

        Debug.WriteLine(string.Format("Message \"{0}\" sent at {1}",
        command.Message, DateTime.Now));
    }
}

我没有看到LogMessage需要单独的CommandHandler。您只需登录装饰实际命令处理程序的对象即可。它的目的是什么呢?

使用这种方法,你有一个纯粹的装饰,这是一个更好的解决方案IMO,因为它节省了两个额外的类。