[英]How to dispatch to generic handler from a non-generic method?
我有一個方法需要返工,特別是我需要刪除簽名中的通用參數。 該方法接收一個參數,該參數始終實現特定接口。
這是方法:
public void SendCommand<T>(T command) where T : ICommand
{
using (var scope = services.CreateScope())
{
var commandType = command.GetType();
var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);
var service = scope.ServiceProvider.GetService(handlerType);
(service as ICommandHandler<T>).Handle(command);
}
}
症結在於(service as ICommandHandler<T>).Handle(command)
行,它接收實現ICommand
的對象的類型參數。 根據參數的實際類型,檢索到的服務是不同的。
有什么方法可以刪除泛型參數,並使用參數的實際類型作為ICommandHandler<T>
行的泛型參數?
編輯:
這個返工可以解決問題,但它暴露了一個非常奇怪的,可能是錯誤的行為。
public void SendCommand(ICommand command)
{
using (var scope = services.CreateScope())
{
var commandType = command.GetType();
var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);
dynamic cmd = command;
dynamic service = scope.ServiceProvider.GetService(handlerType);
var method = handlerType.GetMethods().Single(s => s.Name == "Handle");
method.Invoke(service, new[] { command });
service.Handle(cmd);
}
}
從服務對象中提取Handle
方法並手動調用它就可以了。 但是使用service.Handle(cmd)
方法調用會引發異常(對象沒有Handle
的定義)。
這太奇怪了,因為提取方法確實有效。
任何人都可以闡明這種怪異現象嗎?
這里有幾個選項:
首先,如果保留泛型類型參數是一個選項,您可以將方法的復雜性降低到以下程度:
public void SendCommand<T>(T command) where T : ICommand
{
using (var scope = services.CreateScope())
{
var handler = scope.ServiceProvider
.GetRequiredService<ICommandHandler<T>>();
handler.Handle(command);
}
}
當然,這不是你的問題。 刪除泛型類型參數允許以更動態的方式分派命令,這在編譯時未知命令類型時很有用。 在這種情況下,您可以使用動態類型,如下所示:
public void SendCommand(ICommand command)
{
using (var scope = services.CreateScope())
{
var commandType = command.GetType();
var handlerType =
typeof(ICommandHandler<>).MakeGenericType(commandType);
dynamic handler = scope.ServiceProvider
.GetRequiredService(handlerType);
handler.Handle((dynamic)command);
}
}
這里注意兩件事:
dynamic
變量中。 因此,它的Handle
方法是一個動態調用,其中Handle
在運行時解析。ICommandHandler<{commandType}>
不包含Handle(ICommand)
方法,因此command
參數需要轉換為dynamic
的。 這指示 C# 綁定它應該查找任何名為Handle
method 的方法,其中一個參數與提供的運行時類型的command
相匹配。此選項效果很好,但這種“動態”方法有兩個缺點:
ICommandHandler<T>
接口的任何重構都不會引起注意。 這可能不是一個大問題,因為它可以很容易地進行單元測試。ICommandHandler<T>
實現的任何裝飾器都需要確保將其定義為公共類。 當類是內部類時, Handle
方法的動態調用將(奇怪地)失敗,因為 C# 綁定器不會發現ICommandHandler<T>
接口的Handle
方法是可公開訪問的。因此,除了使用動態之外,您還可以使用良好的舊泛型,類似於您的方法:
public void SendCommand(ICommand command)
{
using (var scope = services.CreateScope())
{
var commandType = command.GetType();
var handlerType =
typeof(ICommandHandler<>).MakeGenericType(commandType);
object handler = scope.ServiceProvider.GetRequiredService(handlerType);
var handleMethod = handlerType.GetMethods()
.Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));
handleMethod.Invoke(handler, new[] { command });
}
}
這可以防止以前方法出現的問題,因為這將繼續重構命令處理程序接口,並且即使處理程序是內部的,它也可以調用Handle
方法。
另一方面,它確實引入了一個新問題。 如果處理程序拋出異常,則對MethodBase.Invoke
的調用將導致將該異常包裝在InvocationException
中。 當消費層捕獲某些異常時,這可能會導致調用堆棧出現問題。 在那種情況下,應該首先解包異常,這意味着SendCommand
正在向其消費者泄露實現細節。
有幾種方法可以解決這個問題,例如:
public void SendCommand(ICommand command)
{
using (var scope = services.CreateScope())
{
var commandType = command.GetType();
var handlerType =
typeof(ICommandHandler<>).MakeGenericType(commandType);
object handler = scope.ServiceProvider.GetRequiredService(handlerType);
var handleMethod = handlerType.GetMethods()
.Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));
try
{
handleMethod.Invoke(handler, new[] { command });
}
catch (InvocationException ex)
{
throw ex.InnerException;
}
}
}
然而,這種方法的缺點是您丟失了原始異常的堆棧跟蹤,因為該異常被重新拋出(這通常不是一個好主意)。 因此,您可以執行以下操作:
public void SendCommand(ICommand command)
{
using (var scope = services.CreateScope())
{
var commandType = command.GetType();
var handlerType =
typeof(ICommandHandler<>).MakeGenericType(commandType);
object handler = scope.ServiceProvider.GetRequiredService(handlerType);
var handleMethod = handlerType.GetMethods()
.Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));
try
{
handleMethod.Invoke(handler, new[] { command });
}
catch (InvocationException ex)
{
ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
}
}
}
這利用了 .NET 4.5 的ExceptionDispatchInfo
,它在 .NET Core 1.0 及更高版本和 .NET Standard 1.0 下也可用。
作為最后一個選項,您還可以解析實現非泛型接口的包裝器類型,而不是解析ICommandHandler<T>
。 這使代碼類型安全,但確實會強制您注冊額外的通用包裝器類型。 過程如下:
public void SendCommand(ICommand command)
{
using (var scope = services.CreateScope())
{
var commandType = command.GetType();
var wrapperType =
typeof(CommandHandlerWrapper<>).MakeGenericType(commandType);
var wrapper = (ICommandHandlerWrapper)scope.ServiceProvider
.GetRequiredService(wrapperType);
wrapper.Handle(command);
}
}
public interface ICommandHandlerWrapper
{
void Handle(ICommand command);
}
public class CommandHandlerWrapper<T> : ICommandHandlerWrapper
where T : ICommand
{
private readonly ICommandHandler<T> handler;
public CommandHandlerWrapper(ICommandHandler<T> handler) =>
this.handler = handler;
public Handle(ICommand command) => this.handler.Handle((T)command);
}
// Extra registration
services.AddTransient(typeof(CommandHandlerWrapper<>));
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.