03 September 2017
This is the second post of my two-part series about designing applications using vertical slices and how the open-source library MediatR can facilitate this design. In the first post we talked about the vertical slices design and how it addresses some of the problems in traditional database table-centric designs. In this post we will see how we can implement a vertical slices design using the open source library MediatR. Put on your programmer hat as we’ll dive into some code!
MediatR allows coding around the pattern of request > processing > response. The request is something that might come in through HTTP, such as a page navigation via GET or a form submission via POST. Some processing will occur, either to retrieve some data or to mutate the state of the system. There may or may not be a response that is returned to the client.
MediatR provides the IRequest
interface, which should be implemented by a class that represents an incoming request. In a typical MVC controller, this request class would be the same as the class that gets passed as a parameter in a GET request. If there is a response, the request should implement the IRequest<TResponse>
interface instead, where TResponse
is the type of response.
MediatR also provides the IRequestHandler<TRequest>
interface. This interface should be implemented by the class who handles the request. For requests that return a response, the IRequestHandler<TRequest, TResponse>
should be implemented instead. There are also corresponding IAsyncRequestHandler
interfaces for supporting asynchronous operations.
MediatR can be installed via NuGet. Just look for MediatR in the Manage NuGet Packages page or install it through the package manager console:
Install-Package MediatR
I will demonstrate the usage of the request, response, and handler classes by refactoring the login code that comes with the default ASP.NET MVC project template.
Here is the default GET login action:
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
And here is the default POST login action:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
The first action corresponds to a query, and the second action corresponds to a command. We are going to have two sets of request-response-handlers: one for the query, and one for the command. This is typical when using the MediatR library. Each action corresponds to a single set of request-response-handler classes. The response class is optional and is only needed for operations that return a response.
Let’s create the first set corresponding to the query first. The request would be a class that has a single property of return url:
public class Query : IRequest<Command>
{
public string ReturnUrl { get; set; }
}
We are implementing the IRequest
interface because this is a request object. We are also implementing the generic version of the interface because this returns a value of type Command
. The command class corresponds to the LoginViewModel
and looks like the following:
public class Command : IRequest<SignInStatus>
{
public string Email { get; set; }
public string Password { get; set; }
public bool RememberMe { get; set; }
public string ReturnUrl { get; set; }
}
This Command
class is the response to our query. It is also a request - but we will take a look at that role in more detail later. For now, let’s look at the query handler class:
public class QueryHandler : IRequestHandler<Query, Command>
{
public Command Handle(Query message)
{
return new Command
{
ReturnUrl = message.ReturnUrl
};
}
}
Because the query request returns a value, the interface we implemented for the handler class is the one that takes in two generic type arguments, one for the request (Query
) and one for the response (Command
). The Handle
method, in turn, takes in a parameter of type Query
and returns a Command
.
More generally, the Handle
method takes in a parameter of type TRequest
and returns a type of TResponse
. For requests that don’t have a response, it returns void
. The async versions of a handler returns a type of Task<TResponse>
for requests that have responses or Task
for requests that don’t.
That completes the first set of request-response-handler classes. The request is an object of type Query
, the response is an object of type Command
, and the handler’s job is to produce a response based on the request.
As mentioned earlier, the Command
object corresponds to the LoginViewModel
. It would be the model for the strongly-typed login view and its properties will be bound to the page controls. It is also the parameter type that will get posted through a form submission.
The command object is also a request. In this case, it would return an object of type SignInStatus
. The handler should implement the interface that takes two generic type parameters that correspond to the Command
request and SignInStatus
response:
public class CommandHandler : IAsyncRequestHandler<Command, SignInStatus>
{
private readonly ApplicationSignInManager _signInManager;
public CommandHandler(ApplicationSignInManager signInManager)
{
_signInManager = signInManager;
}
public async Task<SignInStatus> Handle(Command message)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to shouldLockout: true
return await _signInManager.PasswordSignInAsync(message.Email, message.Password, message.RememberMe, shouldLockout: false);
}
}
Note that I used the async version of the request handler interface, IAsyncRequestHandler
, to support the async workflow. Other than that, and the fact that I have used dependency injection to inject an object of type ApplicationSignInManager
, the handler class is straightforward and the implementation of the Handle
method is exactly the same as what is in the controller.
Now we have completed all the classes that are needed to handle the GET flow and the POST flow. For the GET flow, the request was of type Query
and the response was of type Command
. For the POST flow, the request was of type Command
and the response was of type SignInStatus
. There are also handler classes for both flows whose job is to produce a response object based on the request object.
The controller actions would now look like this:
private readonly IMediator _mediator;
public AccountController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
[AllowAnonymous]
public async Task<ActionResult> Login(Query query)
{
var command = await _mediator.Send(query);
return View(command);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(Command command)
{
if (!ModelState.IsValid)
{
return View(command);
}
var signInStatus = await _mediator.Send(command);
switch (signInStatus)
{
case SignInStatus.Success:
return RedirectToLocal(command.ReturnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = command.ReturnUrl, RememberMe = command.RememberMe });
case SignInStatus.Failure:
default:
ModelState.AddModelError("", "Invalid login attempt.");
return View(command);
}
}
So now we see that the business logic of implementing the GET and POST workflows have almost completely gone out of the controller and into the handler classes. We also used dependency injection to inject an object of type IMediator
in to the controller. There is some configuration needed to be able to inject the mediator object and also to tie together the appropriate request-response-handler classes. Check out MediatR’s wiki page for samples on how to set it up using your preferred IoC container.
For each controller action, there would be a corresponding set of request-response-handler classes, with the response class being optional. This set represents the vertical slice of the application.
One vertical slice is isolated from other vertical slices. In the same way, a set of request-response-handler classes only correspond to one and only one controller action. What’s injected into the handlers are objects that handle cross-cutting concerns. Examples of which are the ApplicationSignInManager
class we saw above, a class derived from DbContext
, a logging class, or any class that handles domain-agnostic features, such as sending email or doing file storage.
What are NOT cross-cutting concerns and therefore shouldn’t be injected into handlers are traditional service and repository classes. In fact, when using the MediatR library and subscribing to the request-response-handler pattern, there is no need for service and repository classes to exist in the beginning, or even to exist at all. Classes that hold common logic across handlers, other than the common domain-agnostic ones mentioned above, should only be created once the situation calls for them, and not before.
Changes that are relevant to one specific workflow only occur in the corresponding handler class. When making changes to any handler class, we are confident that we are not breaking other parts of the system, since we know that the handler class is not being used anywhere else. This is in stark contrast with a database-table centric design where a change in a repository method or service method implementation may affect other workflows.
Conclusion In this post we saw how to apply the vertical slices design using the MediatR library. Designing using vertical slices is, in my opinion, a better approach compared to a database table-centric design using layers. Since a vertical slice is isolated from other slices, we can make changes to its implementation details without having to worry about other slices. In addition, there is no need for service or repository layers anymore, which reduces the amount of [unnecessary] code that has to be written.
If you want to know more about this topic, please check out Jimmy Bogard’s video SOLID Architecture in Slices not Layers.