Simple Injector – 使用相同generics类型的另一个依赖项注册装饰器

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

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

…即使装饰器显然只有一个ICommandHandler参数。

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

 public interface ICommandHandler { void Execute(T command); } public class LogCommand { public string LogMessage { get; set; } public DateTime Time { get; set; } } public class Logger : ICommandHandler { 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 { public void Execute(MessageCommand command) { Debug.WriteLine(command.Message); } } // message command handler decorator that logs about messages being sent public class MessageLogger : ICommandHandler { private ICommandHandler innerHandler; private ICommandHandler logger; // notice these dependencies are two distinct closed generic types public MessageLogger(ICommandHandler innerHandler, ICommandHandler 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 ResolveManually() { ICommandHandler sender = new MessageSender(); ICommandHandler logger = new Logger(); ICommandHandler loggerSender = new MessageLogger(sender, logger); return loggerSender; } // this is what I want to work - seems simple? ICommandHandler ResolveWithSimpleInjector() { var container = new Container(); container.Register<ICommandHandler, Logger>(); container.Register<ICommandHandler, MessageSender>(); // this next line throws the exception container.RegisterDecorator(typeof(ICommandHandler), typeof(MessageLogger)); return container.GetInstance<ICommandHandler>(); } void Main() { //ICommandHandler sender = ResolveManually(); ICommandHandler sender = ResolveWithSimpleInjector(); var command = new MessageCommand { Message = "Hello World!" }; sender.Execute(command); } 

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

编辑

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

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

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

可以改进这些前置条件检查,但这实际上非常讨厌,而这个function的用处非常有限。 它是有限的,因为它只对非通用装饰器有用。 例如,看一下下面的装饰器:

 public class GenericDecorator : ICommandHandler { public GenericDecorator( ICommandHandler decoratee, ICommandHandler dependency) { } } 

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

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

 new DeadlockRetryCommandHandlerDecorator( new TransactionCommandHandlerDecorator( new MessageSender())) 

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

 new DeadlockRetryCommandHandlerDecorator( new TransactionCommandHandlerDecorator( new MessageLogger( new MessageSender(), new DeadlockRetryCommandHandlerDecorator( new TransactionCommandHandlerDecorator( new Logger()))))) 

注意对象图中有第二个DeadlockRetryCommandHandlerDecorator和第二个TransactionCommandHandlerDecorator 。 在事务中拥有事务并具有嵌套的死锁重试(在事务中)意味着什么。 这可能会导致应用程序出现严重的可靠性问题(因为数据库死锁会导致您的操作在无事务连接中继续 )。

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

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

 public class MessageLogger : ICommandHandler { private ICommandHandler innerHandler; private ILogger logger; public MessageLogger( ICommandHandler innerHandler, ILogger logger) { this.innerHandler = innerHandler; this.logger = logger; } public void Execute(MessageCommand command) { innerHandler.Execute(command); logger.Log(command.Message); } } 

如果表示层需要直接记录,您仍然可以拥有ICommandHandler实现,但在这种情况下,实现也可以简单地依赖于ILogger

 public class LogCommandHandler : ICommandHandler { 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)); } } 

这是一个边缘情况,您可能会以任何方式争论,但事实是Simple Injector明确地不支持您尝试做的事情。

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

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

 public class MessageLogger : ICommandHandler where TCommand :  { //.... } 

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

 public class LoggerMediator { private readonly ICommandHandler logger; public LoggerMediator(ICommandHandler logger) { this.logger = logger; } public void Execute(LogCommand command) { this.logger.Execute(command); } } 

并更改您的MessageLogger以使用中介。

 public class MessageLogger : ICommandHandler where TCommand : MessageCommand { private ICommandHandler innerHandler; private LoggerMediator logger; public MessageLogger( ICommandHandler 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(); container.RegisterDecorator(typeof(ICommandHandler<>), typeof(MessageLogger<>)); container.Verify(); 

UPDATE

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

 public class CommandHandlerMediator { private readonly ICommandHandler handler; public CommandHandlerMediator(ICommandHandler handler) { this.handler = handler; } public void Execute(TCommand command) { this.handler.Execute(command); } } 

注册如下:

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

并引用如下:

 public class MessageLogger : ICommandHandler where TCommand :  { private ICommandHandler decorated; private CommandHandlerMediator logger; public MessageLogger( ICommandHandler decorated, CommandHandlerMediator logger) { this.innerHandler = innerHandler; this.logger = logger; } //.... } 

一个新的类,你为所有的处理程序排序。

您可以将装饰器更改为

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

将必要的ctor签名与“ICommandHandler(或Func>)类型的单个参数”进行匹配。 并将logger作为属性而不是ctor参数注入。 我没有使用简单注入器,但查看您的exception消息,这是最明显的解决方案,因为装饰器构造函数签名限制。

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

 public class LoggingHandler : ICommandHandler { private ICommandHandler innerHandler; public LoggingHandler(ICommandHandler 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,因为它节省了两个额外的类。