Single-responsibility, idempotent units of business logic with clear dependency contracts and easy testability.
Commands that return a result are Functions and those that don't are Actions.
Commands are basically POCOs that encapsulate all input needed to execute your business logic. Commands that implement IFunction<TResult>
return a result of type TResult
.
💪 Best Practice: Organize your Command's associated classes (results and handlers) as inner classes. This convention makes it easy to find and predict all the various classes.
public class AuthenticateUserCommand : IFunction<AuthenticateUserCommand.Result>
{
public int UserId { get; private set; }
public string Password { get; private set; }
public AuthenticateUserCommand(int userId, string password)
{
UserId = userid;
Password = password;
}
public class Result
{
public bool IsAuthenticated { get; set; }
}
...
}
The business logic is implemented in the Command's Handler, which implements IFunctionHandler<TCommand, TResult>
. Any dependencies the Handler needs must be injected into its constructor.
public class AuthenticateUserCommand : IFunction<AuthenticateUserCommand.Result>
{
...
public class Handler : IFunctionHandler<AuthenticateUserCommand, AuthenticateUserCommand.Result>
{
private readonly IUserService userService;
public Handler(IUserService userService)
{
this.userService = userService;
}
public AuthenticateUserCommand.Result Execute(AuthenticateUserCommand command)
{
var isAuthenticated =
userService.VerifyPassword(
command.UserId,
command.Password
);
return new Result {
IsAuthenticated = isAuthenticated
};
}
}
}
var command = new AuthenticateUserCommand(42, "P@ssw0rd");
var handler = new AuthenticateUserCommand.Handler(mockUserService);
var result = handler.Execute(command);
Assert.IsTrue(result.IsAuthenticated);
Commands that do not need to return a result should implement IAction
.
public class ResetPasswordCommand : IAction
{
public int UserId { get; set; }
public class Handler : IActionHandler<ResetPasswordCommand>
{
private readonly IUserService userService;
public Handler(IUserService userService)
{
this.userService = userService;
}
public void Execute(ResetPasswordAction action)
{
var user = userService.GetUser(action.UserId);
if (user == null) {
throw new Exception("user not found");
}
userService.ResetPassword(user);
}
}
}
The CommandRouter
is meant to help execute Commands without knowing at execution time exactly how to create the Handler and all its dependencies.
It abstracts CommandHandler location/creation/execution so that callers don't need to know about the Handlers. Since IFunction<TResult>
is generic, the CommandRouter
can infer Function result types from the Command's type.
var command =
new AuthenticateUserCommand {
UserId = UserId,
Password = Password
};
var result = commandRouter.ExecuteFunction(command);
Assert.True(result.IsAuthenticated);
Based on the classic PubSub pattern with some C# help.
public class UserAuthenticatedEvent : IEvent
{
public object EventSource { get; private set; }
public DateTime EventTime { get; private set; } = DateTime.Now;
public User User { get; private set; }
public UserAuthenticatedEvent(
object eventSource,
User user
)
{
EventSource = eventSource;
User = user;
}
}
// Define the handler
public class TrackAuthenticatedUsersEventHandler : IEventHandler<UserAuthenticatedEvent>
{
public void HandleEvent(UserAuthenticatedEvent e)
{
// Track the User
log.Info("User logged in: " + e.User.Name);
}
}
// Register the Handler type for a specific Event type
EventBus.Default.Register<UserAuthenticatedEvent, TrackAuthenticatedUsersEventHandler>();
EventBus.Default.Trigger(new UserAuthenticatedEvent(this, user));
You can register various types of Event Handlers for a given event.
The specified Action<TEvent>
is executed each time the event is triggered.
EventBus.Default.Register<UserAuthenticatedEvent>(e => log.Info("User authenticated: " + e.User));
The same handlerInstance
is used each time the event is triggered.
var handlerInstance = new TrackAuthenticatedUsersEventHandler();
EventBus.Default.Register<UserAuthenticatedEvent>(handlerInstance);
A new instance of TEventHandler
is created each time the event is triggered.
TEventHandler
must be an IEventHandler<TEvent>
and have a default constructor.
EventBus.Default.Register<UserAuthenticatedEvent, TrackAuthenticatedUsersEventHandler>();
The specified instance of IEventHandlerFactory
is used to create instances of IEventHandler<TEvent>
.
This is useful if your handlers need injected dependencies.
// Define your factory
public class TrackingEventHandlerFactory : IEventHandlerFactory
{
public IEnumerable<IEventHandler> GetHandlers<TEvent>() where TEvent : IEvent
{
if (typeof(TEvent) == typeof(UserAuthenticatedEvent))
{
return new [] { new TrackAuthenticatedUsersEventHandler(someDependency) };
}
else
{
throw new NotSupportedException();
}
}
}
// Register your factory
var factory = new TrackingEventHandlerFactory();
EventBus.Default.Register<UserAuthenticatedEvent>(trackingEventHandlerFactory);