Skip to content

Custom Middleware

While reviewing a very large system that used asynchronous messaging I noticed a common pattern in many of the message handlers:

  1. Attempt to load account data referenced by the incoming command
  2. If the account didn't exist, log that the account referenced by the command didn't exist and stop the processing

Like this code:

cs
public static async Task Handle(DebitAccount command, IDocumentSession session, ILogger logger)
{
    // Try to find a matching account for the incoming command
    var account = await session.LoadAsync<Account>(command.AccountId);
    if (account == null)
    {
        logger.LogInformation("Referenced account {AccountId} does not exist", command.AccountId);
        return;
    }

    // do the real processing
}

snippet source | anchor

That added up to a lot of repetitive code, and it'd be nice if we introduced some kind of middleware to eliminate the duplication -- so let's do just that!

Using Wolverine's conventional middleware approach strategy, we'll start by lifting a common interface for command message types that reference an Account like so:

cs
public interface IAccountCommand
{
    Guid AccountId { get; }
}

snippet source | anchor

So a command message might look like this:

cs
public record CreditAccount(Guid AccountId, decimal Amount) : IAccountCommand;

snippet source | anchor

Skipping ahead a little bit, if we had a handler for the CreditAccount command type above that was counting on some kind of middleware to just "push" the matching Account data in, the handler might just be this:

cs
public static class CreditAccountHandler
{
    public static void Handle(
        CreditAccount command,

        // Wouldn't it be nice to just have Wolverine "push"
        // the right account into this method?
        Account account,

        // Using Marten for persistence here
        IDocumentSession session)
    {
        account.Balance += command.Amount;

        // Just mark this account as needing to be updated
        // in the database
        session.Store(account);
    }
}

snippet source | anchor

You'll notice at this point that the message handler is synchronous because it's no longer doing any calls to the database. Besides removing some repetitive code, this appproach arguably makes the Wolverine message handler methods easier to unit test now that you can happily "push" in system state rather than fool around with stubs or mocks.

Next, let's build the actual middleware that will attempt to load an Account matching a command's AccountId, then determine if the message handling should continue or be aborted. Here's sample code to do exactly that:

cs
// This is *a* way to build middleware in Wolverine by basically just
// writing functions/methods. There's a naming convention that
// looks for Before/BeforeAsync or After/AfterAsync
public static class AccountLookupMiddleware
{
    // The message *has* to be first in the parameter list
    // Before or BeforeAsync tells Wolverine this method should be called before the actual action
    public static async Task<(HandlerContinuation, Account?, OutgoingMessages)> LoadAsync(
        IAccountCommand command,
        ILogger logger,

        // This app is using Marten for persistence
        IDocumentSession session,

        CancellationToken cancellation)
    {
        var messages = new OutgoingMessages();
        var account = await session.LoadAsync<Account>(command.AccountId, cancellation);
        if (account == null)
        {
            logger.LogInformation("Unable to find an account for {AccountId}, aborting the requested operation", command.AccountId);

            messages.RespondToSender(new InvalidAccount(command.AccountId));
            return (HandlerContinuation.Stop, null, messages);
        }

        // messages would be empty here
        return (HandlerContinuation.Continue, account, messages);
    }
}

snippet source | anchor

Now, some notes about the code above:

  • Wolverine has a convention that generates a call to the middleware's LoadAsync() method before the actual message handler method (CreditAccountHandler.Handle())
  • The ILogger would be the ILogger<T> for the message type that is currently being handled. So in the case of the CreditAccount, the logger would be ILogger<CreditAccount>
  • Wolverine can wire up the Account object returned from the middleware method to the actual Handle() method's Account argument
  • By returning HandleContinuation from the LoadAsync() method, we can conditionally tell Wolverine to abort the message processing

Lastly, let's apply the newly built middleware to only the message handlers that work against some kind of IAccountCommand message:

cs
builder.Host.UseWolverine(opts =>
{
    // This middleware should be applied to all handlers where the
    // command type implements the IAccountCommand interface that is the
    // "detected" message type of the middleware
    opts.Policies.ForMessagesOfType<IAccountCommand>().AddMiddleware(typeof(AccountLookupMiddleware));

    opts.UseFluentValidation();

    // Explicit routing for the AccountUpdated
    // message handling. This has precedence over conventional routing
    opts.PublishMessage<AccountUpdated>()
        .ToLocalQueue("signalr")

        // Throw the message away if it's not successfully
        // delivered within 10 seconds
        .DeliverWithin(10.Seconds())

        // Not durable
        .BufferedInMemory();
});

snippet source | anchor

Released under the MIT License.