Trying to use Command/Handler pattern and Aspect Oriented Programming with Simple Injector.
I have my command and handler classes.
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
public class MakeCoffeeCommand
{
public string Flavor { get; set; }
}
internal class MakeCoffeeCommandHandler : ICommandHandler<MakeCofeeCommand>
{
public void Handle(MakeCoffeeCommand command)
{
...
}
}
public class MakeCakeCommand
{
public float OvenTemperature { get; set; }
}
internal class MakeCakeCommandHandler : ICommandHandler<MakeCakeCommand>
{
public void Handle(MakeCakeCommand command)
{
...
}
}
So far, I can inject implementation through this single rule.
//this rule automagically ignores possibly existing decorators! :-)
c.Register(typeof(ICommandHandler<>), typeof(ICommandHandler<>).Assembly);
Then, I would like to register decorators aimed at validating the command instance. My decorator implementations depend on the concrete command types. I created an interface all the decorators have to inherit from.
public interface ICommandHandlerValidator<TCommand> : ICommandHandler<TCommand>
{
}
Then, the concrete decorators.
internal class ValidatorMakeCoffeeCommandDecorator
: ICommandHandlerValidator<MakeCoffeeCommand>
{
private readonly ICommandHandler<MakeCoffeeCommand> decoratee;
public ValidatorMakeCoffeeCommandDecorator(ICommandHandler<MakeCoffeeCommand> decoratee)
{
this.decoratee = decoratee;
}
public void Handle(MakeCoffeeCommand command)
{
...
}
}
internal class ValidatorMakeCakeCommandDecorator
: ICommandHandlerValidator<MakeCakeCommand>
{
private readonly ICommandHandler<MakeCakeCommand> decoratee;
public ValidatorMakeCakeCommandDecorator(ICommandHandler<MakeCakeCommand> decoratee)
{
this.decoratee = decoratee;
}
public void Handle(MakeCakeCommand command)
{
...
}
}
I'm trying to register these validators with a single line, as in the previous case.
c.RegisterDecorator(typeof(ICommandHandler<>), typeof(ICommandHandlerValidator<>));
But I get this error.
The given type
ICommandHandlerValidator<TCommand>
is not a concrete type. Please use one of the other overloads to register this type.
Note: I have to implement multiple decorators for all cross-cutting concerns. Some of them depend on the concrete command (i.e. authorization, full-text indexing), some do not, and have the same implementation shared by all the commands (i.e. logging, performance analysis).
It this a correct approach?
This answer might be a bit opinionated, but to me, this isn't the right approach. Your ICommandHandlerValidator<T>
interface serves no function, and your decorators can as easily derive directly from ICommandHandler<T>
.
On top of that, you are somewhat 'abusing' decorators to implement very specific logic, while decorators are best suited to implement very generic cross-cutting concerns.
Although you might argue that validation is very generic, your implementations aren't generic at all, since each decorator has logic that is specific to a single handler implementation. This leads to the situation that you get many decorators, and need to batch-register them.
What I typically like to do is to take a step back and look at the design. In your architecture you determined that business logic that mutates state is a certain artifact that deserves its own abstraction. You call this abstraction ICommandHandler<T>
. Not only does this allow you to clearly distinguish these particular type of components from other components in the system, it allows you to batch register them and apply cross-cutting concerns very effectively.
While looking at your code, however, it seems to me that logic that validates commands before they are executed by their command handler has importance of its own in your application. That means it deserves an abstraction of its own. For instance, you can call it ICommandValidator<T>
:
public interface ICommandValidator<TCommand>
{
IEnumerable<ValidationResult> Validate(TCommand command);
}
Note that this interface has no relationship with ICommandHandler<TCommand>
. Validation components are a different artifact. The ICommandValidator<T>
interface returns validation results, which can be practical for implementations. You might want to play with the best design for this validator.
Using this interface you can now define specific validators:
public class MakeCoffeeValidator : ICommandValidator<MakeCoffeeCommand> { ... }
public class MakeCakeValidator : ICommandValidator<MakeCakeCommand> { ... }
Besides the visibility of these validators in your design, this separate interface allows your validators to be batch-registered:
c.Collection.Register(typeof(ICommandValidator<>),
typeof(MakeCakeValidator).Assembly);
Here, the validators are registered as collection, assuming that there might be zero or multiple validator implementations for a single command. If there is always exactly one implementation (as you'll see with command handler implementations), you should call c.Register
instead.
This by itself, however, doesn't do much, because those validators will not get executed by themselves. For this you will need to write a generic piece of cross-cutting code that can be applied to all command handlers in the system. In other words, you need to write a decorator:
public class ValidatingCommandHandlerDecorator<T> : ICommandHandler<T>
{
private readonly ICommandHandler<T> decoratee;
private readonly IEnumerable<ICommandValidator<T>> validators;
public ValidatingCommandHandlerDecorator(
IEnumerable<ICommandValidator<T>> validators,
ICommandHandler<T> decoratee)
{
this.validators = validators;
this.decoratee = decoratee;
}
public void Handle(T command)
{
var validationResults = (
from validator in this.validators
from result in validator.Validate(command)
select result)
.ToArray();
if (validationResults.Any())
{
throw new ValidationException(validationResults);
}
this.decoratee.Handle(command);
}
}
This decorator can be registered in a way you are already familair with:
c.RegisterDecorator(
typeof(ICommandHandler<>),
typeof(ValidatingCommandHandlerDecorator<>));
Although you could try to batch-register this decorator, together with all decorators in the system, that would typically not work out great. This is because the order in which you execute cross-cutting concerns is of vital importance. For instance, when you implement a deadlock retry decorator and a transaction decorator, you wish to have the deadlock decorator to wrap the transaction decorator, otherwise you might end up retrying a deadlocked operation outside the context of a transaction (because of the way SqlTransaction
and SQL server work). Likewise, you wish to operate writing the audit trail inside a transaction. Otherwise you could end up missing an audit trail for a succeeded operation.