I have a method that needs a rework, specifically I need to remove the generic parameter in the signature. The method receives a single parameter, which always implements a specific interface.
This is the 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);
}
}
The sticking point is the (service as ICommandHandler<T>).Handle(command)
, line, which receives a type parameter of an object that implements ICommand
. Depending on the parameter actual type, the service retrieved is different.
Is there any way to remove the generic parameter, and use the actual type of the parameter as the generic parameter of the ICommandHandler<T>
line?
EDIT:
This rework does the trick, but it exposes a pretty weird, perhaps buggy, behavior.
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);
}
}
Extracting the Handle
method from the service object and invoking it manually does the trick. But using the service.Handle(cmd)
method call throws an exception (object does not have a definition for Handle
).
This is weird as hell, because extracting the method does work.
Anyone can shed light on this weirdness?
There are a few options here:
First of all, if keeping the generic type argument is an option, you can reduce the method's complexity to the following:
public void SendCommand<T>(T command) where T : ICommand
{
using (var scope = services.CreateScope())
{
var handler = scope.ServiceProvider
.GetRequiredService<ICommandHandler<T>>();
handler.Handle(command);
}
}
This is, of course, not what your question is about. Removing the generic type argument allows a more dynamic way of dispatching commands, which is useful when command types are not known at compile time. In that case you can use dynamic typing, as follows:
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);
}
}
Notice two things here:
dynamic
variable. Its Handle
method is, therefore, a dynamic invocation where Handle
is resolved at runtime.ICommandHandler<{commandType}>
does not contain a Handle(ICommand)
method, the command
argument needs to be cast to a dynamic
. This instructs the C# binding that it should look for any method named Handle
method with one single argument that matches the supplied runtime type of command
.This option works pretty well, but there are two downsides to this 'dynamic' approach:
ICommandHandler<T>
interface get unnoticed. This is probably not a huge problem, as it can easily be unit tested.ICommandHandler<T>
implementation needs to ensure that it is defined as a public class. The dynamic invocation of the Handle
method will (weirdly) fail when the class is internal, as the C# binder won't spot that the Handle
method of the ICommandHandler<T>
interface is publicly accessible.So instead of using dynamic, you can also use good old generics, similar to your approach:
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 });
}
}
This prevents the problems with the previous approach, as this will surfive refactoring of the command handler interface, and it can invoke the Handle
method even if the handler is internal.
On the other hand, it does introduce a new problem. In case a handler throws an exception, the call to MethodBase.Invoke
will cause that exception to be wrapped in an InvocationException
. This can cause trouble up the call stack, when the consuming layer catches certain exceptions. In that case the exception should first be unwrapped, which means SendCommand
is leaking implementation details to its consumers.
There are several ways to fix this, for instance:
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;
}
}
}
Downside of this approach, however, is that you lose the stack trace of the original exception, as this exception is rethrown (which is typically not a good idea). So instead, you can do the following:
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();
}
}
}
This makes use of .NET 4.5's ExceptionDispatchInfo
which is also available under .NET Core 1.0 and up and .NET Standard 1.0.
As a last option, you can also, instead of resolving ICommandHandler<T>
, resolve a wrapper type that implements a non-generic interface. This makes the code types safe, but does force you to register the extra generic wrapper type. This goes as follows:
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<>));
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.