Skip to main content

Infrastructure soup

Five-Layer Brisket Chili
Five-Layer Brisket Chili

When it starts to get colder outside I start to think about soup. I love soup, especially chili. But I don't want any of that watery gunk that's just tomato soup with a few lonely beans floating in there somewhere. No sir! I want it thick and chunky. Load it up with ground meat, beans, onions, tomatoes, cheese, green peppers, jalepeños, pineapple–it's all good!

Just like with chili, we sometimes see code that feels kind of "thick and chunky." It's got validation, logging, exception handling, database communication, business logic, and so much more. But unlike chili, the result does not taste good.

We see this kind of bloated, muddled code all over the place, regardless of what language or framework is being used, and NServiceBus is no exception. Here's an example where someone has stuffed an NServiceBus message handler full to the breaking point:

public class UserCreator : IHandleMessages<CreateUser>
{
  MySessionProvider sessionProvider;
  ILog log = LogManager.GetLogger(typeof(UserCreator));

  public UserCreator(MySessionProvider sessionProvider)
  {
    this.sessionProvider = sessionProvider;
  }

  public async Task Handle(CreateUser message, IMessageHandlerContext context)
  {
    log.Info($"Starting message handler: {nameof(UserCreator)}");

    var stopwatch = Stopwatch.StartNew();

    var signature = context.MessageHeaders["MessageSignature.SHA256"];
    if(!Utilities.ValidateMessageSignature(message, signature))
    {
      throw new MessageSignatureValidationException(message);
    }

    using (var session = await sessionProvider.Open())
    {
      try
      {
        await session.Store(new User());
        await context.Publish(new UserCreated());

        await session.Commit();
      }
      catch(Exception)
      {
        await session.Rollback();
        throw;
      }
      finally
      {
        var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
        log.Info($"Message handler UserCreator complete: {elapsedMilliseconds}ms taken");
      }
    }
  }
}

Yikes! There must be a better way to go about this. Most frameworks have a way to counter this kind of bloat, pushing infrastructure concerns into separate components that can be reused.

Let's take a closer look at the problem and see what we can do about it.

Single responsiblity

That code had a lot going on. Let's break down everything happening in that block of code:

  1. A session provider is injected from a dependency injection container. This is used to create a database session object implementing a Unit of Work pattern.
  2. A logger and stopwatch are set up to log the beginning and end of the method execution, as well as how long the handler took to execute.
  3. An SHA256 signature is retrieved from a message header and validated.
  4. A user is created in the database, and an event is published to announce that action.

The 4th item, creating a user, is the whole point of this message handler, but it's buried in a jumble of infrastructure soup! It looks like somebody was trying to treat this code like chili and throw all the stuff into the message handler in the hopes that it would taste good.

But when it comes to code, I want it to be simple, bland, and easy to digest, like this:

public class BetterUserCreator : IHandleMessages<CreateUser>
{
  public async Task Handle(CreateUser message, IMessageHandlerContext context)
  {
    await context.Store(new User());
    await context.Publish(new UserCreated());
  }
}

This is the point of the single responsibility principle, making it so that code is much more understandable and easier to maintain. Any developer can look at this code and instantly see what's going on. Developers don't need a checklist each time they create a new message handler, and can focus only on the business task at hand.

So how do we get there?

Where infrastructure belongs

If this code were in an ASP.NET Core app, we could use a Filter to separate our infrastructure concerns. In classic ASP.NET MVC, it was called an Action Filter. In Express for Node.js, it's called middleware.

In NServiceBus systems, we can bundle infrastructure into pipeline behaviors for reuse throughout the system. Pipeline behaviors are deceptively simple yet extremely powerful.

Let's look at a simple behavior that implements the logging and stopwatch infrastructure concerns from the code snippet in the introduction:

public class LoggingBehavior : Behavior<IInvokeHandlerContext>
{
  ILog log = LogManager.GetLogger(typeof(LoggingBehavior));

  public override Task Invoke(IInvokeHandlerContext context, Func<Task> next)
  {

    log.Info($"Starting message handler: {context.MessageHandler.HandlerType.FullName}");

    var stopwatch = Stopwatch.StartNew();

    try
    {
      // Invokes the rest of the message handling pipeline
      return next();
    }
    finally
    {
      var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
      log.Info($"Message handler {context.MessageHandler.HandlerType.FullName} complete: {elapsedMilliseconds}ms taken");
    }
  }
}

A behavior will first identify a pipeline stage to act upon, in this case, IInvokeHandlerContext. Many different stages are available just on the incoming message pipeline:

  • ITransportReceiveContext: To control the very beginning and end of the message processing operation.
  • IIncomingPhysicalMessageContext: To act on the raw message body before it is deserialized. This is the ideal place to implement the SHA256 signature validation.
  • IIncomingLogicalMessageContext: To act after the message has been deserialized. This is the ideal place to implement the database session and unit of work requirements.
  • IInvokeHandlerContext: To act once per message handler, with information available about the handler, like the logging handler above.

The magic comes with the call to return next(); which instructs NServiceBus to call the remaining pipeline behaviors. At runtime, all the behaviors will be compiled down into a single method using compiled expression trees so it executes just as fast as if someone had taken the time to hand-craft a single, monolithic RunPipeline() method.

That's sufficient for simple behaviors, but behaviors are also able to share data through the context parameter, enabling you to do just about anything to modify and customize how NServiceBus handles messages.

Home grown

The flexibility offered by behaviors is exactly the reason why there are certain features that NServiceBus does not try to implement on purpose. This is because the complexity required to create a one-size-fits-all solution for everybody is immense compared to the simplicity of a few lines of infrastructure code in a behavior.

Consider the questions that would need to be answered before implementing a full-message encryption feature:

  • Which ciphers will be supported?
  • Where will keys be stored?
  • How will keys be loaded?
  • Will certificates be supported in addition to keys?
  • How many keys will be allowed at the same time?
  • How will keys be rotated given that in-flight messages may encrypted with an out-of-date key?

Trying to provide a universal solution that would cover all the answers to the above questions would result in a complex beast of an implementation. Luckily, developers can create their own custom encryption and decryption behaviors addressing only the specific data security policies of their own organization with just a few lines of code–a much more sustainable approach, all things considered.

Summary

Code and soup are not the same thing. All your code, and your message handlers in particular, should not be complex and flavorful like a five-layer brisket chili. Quite the opposite, message handler code should be as simple and bland as canned chicken broth…the low-sodium kind. After all, your message handler is but one raw ingredient. Combine it with a few infrastructure behaviors specifically tailored to your environment, plus all the other message handlers in your system–that's how you create a rich, flavorful meal.

If you're interested in the infrastructure bits I teased in this article, we have samples available showing how to implement all of them:

  • The Add handler timing pipeline sample shows how to implement the logging and timing, in the case of the sample, logging a warning when a handler takes longer than 500ms to process.
  • The Unit of work using the pipeline sample shows how to implement a database session and unit of work.
  • The Message signing using the pipeline sample shows how to implement cryptographic signing of outgoing messages with signature validation on incoming messages. The same pattern could be extended to implement symmetric encryption of the entire message body.

About the author: David Boike is a developer at Particular Software who is still learning when to stop adding spicy peppers to his chili, much to his wife's chagrin.

Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.
Need help getting started?