I've got this command bus implementation which is typically called like this:
bus.Invoke(new FocusCommand());
This works fine, but there's now a situation where the command to invoke is dynamic and it's throwing errors:
// Command to invoke is resolved at runtime.
Command parsedCommand = ParseCommand(input);
bus.Invoke(parsedCommand);
A simplified version of the implementation looks like this:
using System;
using System.Diagnostics;
public class Command
{
}
public class FocusCommand : Command
{
}
public interface ICommandHandler<TCommand> where TCommand : Command
{
void Handle(TCommand command);
}
public class FocusHandler : ICommandHandler<FocusCommand>
{
public void Handle(FocusCommand command)
{
Debug.WriteLine("FocusHandler was called.");
}
}
public class Bus
{
// Sends command to appropriate command handler.
public void Invoke<T>(T command) where T : Command
{
object handlerInstance = GetHandlerInstanceForCommand(command);
(handlerInstance as ICommandHandler<T>).Handle(command); // <--- Error is thrown here in 2nd case.
}
// Returns an instance of the corresponding handler for a command. In the real
// application, this gets the handler instance from the DI framework using
// `Provider.GetRequiredService`, which always returns an `object`.
private object GetHandlerInstanceForCommand<T>(T command) where T : Command
{
if (command.GetType() == typeof(FocusCommand))
return new FocusHandler();
else
throw new Exception();
}
}
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Debug.WriteLine("Application started");
Bus bus = new Bus();
bus.Invoke<FocusCommand>(new FocusCommand()); // Is called successfully.
Command parsedCommand = ParseCommand("FocusCommand");
bus.Invoke<Command>(parsedCommand); // Throws error.
}
// Returns different commands based on input.
private static Command ParseCommand(string input)
{
if (input == "FocusCommand")
return new FocusCommand();
else
throw new Exception();
}
}
When bus.Invoke<Command>(parsedCommand)
is called, casting handlerInstance as ICommandHandler<T>
within Bus.Invoke
fails.
How can Bus.Invoke
be changed to work with commands where the derived Command
type is unknown?
This has to do with covariance and contravariance.
The reason the cast fails is that, if you were allowed to do what you are trying to do, you would be able to pass an incompatible type to your handler. For example, your approach would allow this:
ICommandHandler<Command> focusHandler = new FocusHandler();
Command closeCommand = new CloseCommand();
focusHandler.Handle(closeCommand);
One way to deal with types that are not known at compile time is to use reflection. You mentioned Provider.GetRequiredService
so I'm going to assume that you are using Microsoft.Extensions.DependencyInjection. You can do the following:
Register your handlers like this:
services.AddTransient<ICommandHandler<FocusCommand>, FocusHandler>();
services.AddTransient<ICommandHandler<CloseCommand>, CloseHandler>();
Implement your Invoke
method like this:
public void Invoke(Command command)
{
Type commandType = command.GetType();
Type handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);
var handler = serviceProvider.GetService(handlerType);
var mi = handlerType.GetMethod("Handle", 0, new[] { commandType });
mi.Invoke(handler, new[] { command });
}