try-catch-FAIL

Failure is inevitable

NAVIGATION - SEARCH

Heavy controller actions? The Message Bus Pattern to the rescue!

I recently derailed (badly) while working on a simple requirement for RageFeed: new user registration.  We decided to use the baked-in ASP.NET membership and profile providers, and I found myself adding a lot of code to facilitate the registration workflow.  This was a problem in and of itself, but I realized I had a bigger problem when I stopped and looked at the tremendous number of test cases for my controller.  I had drifted so slowly into a bad design that I didn’t even realize it was happening until it was too late.  Fortunately, all was not lost.  A design pattern from the distributed computing domain came to save the day.

RageFeed, the super-secret project that I have been only murmuring about from time to time for the last couple of months, is trudging along slowly.  Most of the infrastructure is in place and working, so work has turned towards implementing real, honest-to-goodness features.  The first thing I tried to tackle was new user registration; after all, what good are features without users to (ab)use them?  Our team decided that we wanted users to be able to get in and “raging” as quickly as possible.  The workflow would be like this:

  • User registers with their E-mail address and is given immediate (restricted) access.
  • A verification E-mail is sent to the user.
  • The user clicks a verification link that redirects to the system.
  • The system prompts the user to create a password.
  • The user is given full access to the system.

Knowing that the baked-in ASP.NET membership and profile providers were fairly rigid and not really all that well matched to ASP.NET MVC, we still naively assumed that we would be able to bend them to meet this non-standard workflow.  That was certainly a mistake in hindsight (which I’ll probably blog about later), but it led a bigger problem: bad code. In my push to “get the frickin’ users in the system”, I had lowered my guard and tried to brute force my way through the story.  I knew things were starting to go awry when the unit tests for the signup controller hit a dozen.  Sure, the user registration workflow is a bit complicated, but most of the complexity is business logic, not something that belongs in the controller. 

If I wasn’t so focused on “get it done, now”, I think I would have realized my approach was flawed from the beginning.  Some of the tests were difficult to write and required setting up expectations on multiple services.  This testing friction is usually a good sign that you need to rethink your approach.  In fact, I’d argue that a controller with more than a couple service dependencies needs to be redesigned anyway.  Coding to interfaces and using dependency injection does eliminate your coupling to concrete types, but the interfaces themselves still introduce coupling, and coupling should be kept to a minimum.

The Problem

Let’s look at the constructor for my original implementation of the SignupController:

private readonly IRageFeedMembershipProvider _membershipProvider;
private readonly IEmailDispatcher _emailDispatcher;
private readonly IProfileService _profileService;
private readonly IMembershipService _membershipService;

public SignupController(IRageFeedMembershipProvider membershipProvider, 
    IEmailDispatcher emailDispatcher, 
    IProfileService profileService, 
    IMembershipService membershipService)
{
    _membershipProvider = membershipProvider;
    _emailDispatcher = emailDispatcher;
    _profileService = profileService;
    _membershipService = membershipService;
}

Yep, it depends on *four* separate services.  At a minimum, this complexity could be hidden behind a facade, but there’s a deeper problem.  You can see it when you look at the Activate action:

public ViewResult Activate(string username, string activationToken)
{
    var profile = _profileService.GetForUser(username);

    if (profile == null || profile.ActivationToken != activationToken)
    {
        return View("ActivationFailed");
    }

    var user = _membershipService.GetUser(username);
    user.IsApproved = true;
    _membershipService.UpdateUser(user);

    return View();
}

See that?  It’s subtle, but it’s there, hiding in plain sight: business logic.  The controller now knows the rules for how a user can be activated (their activation token must match the previously stored activation token) and how to activate them (setting the IsApproved flag to true).  As I was TDDing this method, I created three separate test cases: one to test for a non-existent user (null profile), another for an invalid activation token, and finally one to verify that the user is correctly activated.  Each required at least some setup on the mock services.  This same sort of business logic leak occurred in some of the other signup-related actions, leading to a large test fixture and code that made me cringe. 

The Solution

Instead of containing business logic, my controller action should do nothing more than get data from (or provide data to) the views, and pass it off to the business layer of the system for further processing.  The controller should handle things like user-input validation (via model binding) and determining which view to render, but it shouldn’t make business decisions.  Thinking in very generic terms, the controller should publish a command to the business layer, and something in the business layer that subscribes to the command should process it, returning only the information the controller needs to decide which action result to return.  This is about as loosely coupled as you can realistically get: the controller shouldn’t know (or care) who is processing the command, and the class processing the command shouldn’t care about where the command comes from.

There’s a Pattern For That: the Message Bus. While this pattern has historically been used for communication between applications in a potentially distributed environment, it has gained ground recently as a means of decoupling components within the same physical application.  The MVCContrib project includes a message bus, and numerous blog posts of late have presented various incarnations of this pattern. 

Ignoring how RageFeed’s message bus works, take a look at the revised implementation of the Activate action now:

public ViewResult Activate(string username, string activationToken)
{
    var command = new ActivateUserCommand {Username = username, ActivationToken = activationToken};

    var result = _bus.Send(command);

    if (!result.Succeeded)
    {
        return View("ActivationFailed");
    }

    return View();
}

What was gained?

The action is simpler now, and more importantly, it is completely devoid of business logic.  It just creates the command, sends it, gets the response, and decides which view to render.  No longer is the controller coupled to multiple services, instead it has just a single dependency on the message bus, and that’s it.  The corresponding tests are also simpler: they just need to verify that the expected command was sent, and that the correct view was rendered for both success and failure.  They don’t need to setup expectations on anything except the service bus.  Note that I really didn’t remove any business logic from the system, I just (correctly) encapsulated it in the business layer, leaving the controller free to focus on user interface-related concerns. 

So how does it work?

Good question!  I’ll dive into the details of how the bus is implemented and how the command processor for the ActivateUserCommand is implemented in the next post!  In the meantime, let me know what you think in the comments.

About Matt Honeycutt...

Matt Honeycutt is a software architect specializing in ASP.NET web applications, particularly ASP.NET MVC. He has over a decade of experience in building (and testing!) web applications. He’s an avid practitioner of Test-Driven Development, creating both the SpecsFor and SpecsFor.Mvc frameworks.

He's also an author for Pluralsight, where he publishes courses on everything from web applications to testing!

blog comments powered by Disqus