Skip to content

Wolverine as Mediator

TIP

All of the code on this page is from the InMemoryMediator sample project.

Recently there's been some renewed interest in the old Gof Mediator pattern as a way to isolate the actual functionality of web services and applications from the mechanics of HTTP request handling. In more concrete terms for .NET developers, a mediator tool allows you to keep MVC Core code ceremony out of your application business logic and service layer. It wasn't the original motivation of the project, but Wolverine can be used as a full-featured mediator tool.

Before you run off and use "Wolverine as MediatR", we think you can arrive at lower ceremony and simpler code in most cases by using WolverineFx.Http for your web services. If you really just like the approach of separating message handlers underneath ASP.Net Minimal API, there is also a set of helpers to more efficiently pipe Minimal API routes to Wolverine message handlers that are a bit more performance optimized than the typical usage of pulling IMessageBus out of the IoC container on every request. See Optimized Minimal API Integration for more information.

Mediator Only Wolverine

Wolverine was not originally conceived of as a "mediator" tool per se. Out of the box, Wolverine is optimized for asynchronous messaging that requires stateful background processing. If you are using Wolverine as "just" a mediator tool, all that background stuff for messaging is just unnecessary overhead, so let's tell Wolverine to turn all that stuff off so we can run more lightly:

cs
using var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.Services.AddMarten("some connection string")

            // This adds quite a bit of middleware for
            // Marten
            .IntegrateWithWolverine();

        // You want this maybe!
        opts.Policies.AutoApplyTransactions();

        // But wait! Optimize Wolverine for usage as *only*
        // a mediator
        opts.Durability.Mode = DurabilityMode.MediatorOnly;
    }).StartAsync();

snippet source | anchor

WARNING

Using the MediatorOnly mode completely disables all asynchronous messaging, including the local queueing as well

The MediatorOnly mode sharply reduces the overhead of using Wolverine you don't care about or need if Wolverine is only being used as a mediator tool.

Starting with Wolverine as Mediator

Let's jump into a sample project. Let's say that your system creates and tracks Items of some sort. One of the API requirements is to expose an HTTP endpoint that can accept an input that will create and persist a new Item, while also publishing an ItemCreated event message to any other system (or internal listener within the same system). For the technology stack, let's use:

First off, let's start a new project with the dotnet new webapi template. Next, we'll add some configuration to add in Wolverine, a small EF Core ItemDbContext service, and wire up our new service for Wolverine's outbox and EF Core middleware:

From there, we'll slightly modify the Program file generated by the webapi template to add Wolverine and opt into Wolverine's extended command line support:

cs
var builder = WebApplication.CreateBuilder(args);

// Using Weasel to make sure the items table exists
builder.Services.AddHostedService<DatabaseSchemaCreator>();

var connectionString = builder.Configuration.GetConnectionString("SqlServer");

builder.Host.UseWolverine(opts =>
{
    opts.PersistMessagesWithSqlServer(connectionString);

    // If you're also using EF Core, you may want this as well
    opts.UseEntityFrameworkCoreTransactions();
});

// Register the EF Core DbContext
builder.Services.AddDbContext<ItemsDbContext>(
    x => x.UseSqlServer(connectionString),

    // This is weirdly important! Using Singleton scoping
    // of the options allows Wolverine to significantly
    // optimize the runtime pipeline of the handlers that
    // use this DbContext type
    optionsLifetime: ServiceLifetime.Singleton);

snippet source | anchor

Now, let's add a Wolverine message handler that will:

  1. Handle a new CreateItemCommand message
  2. Create a new Item entity and persist that with a new ItemsDbContext custom EF Core DbContext
  3. Create and publish a new ItemCreated event message reflecting the new Item

Using idiomatic Wolverine, that handler looks like this:

cs
public class ItemHandler
{
    // This attribute applies Wolverine's EF Core transactional
    // middleware
    [Transactional]
    public static ItemCreated Handle(
        // This would be the message
        CreateItemCommand command,

        // Any other arguments are assumed
        // to be service dependencies
        ItemsDbContext db)
    {
        // Create a new Item entity
        var item = new Item
        {
            Name = command.Name
        };

        // Add the item to the current
        // DbContext unit of work
        db.Items.Add(item);

        // This event being returned
        // by the handler will be automatically sent
        // out as a "cascading" message
        return new ItemCreated
        {
            Id = item.Id
        };
    }
}

snippet source | anchor

Note, as long as this handler class is public and in the main application assembly, Wolverine is going to find it and wire it up inside its execution pipeline. There's no explicit code or funky IoC registration necessary.

Now, moving up to the API layer, we can add a new HTTP endpoint to delegate to Wolverine as a mediator with:

cs
app.MapPost("/items/create", (CreateItemCommand cmd, IMessageBus bus) => bus.InvokeAsync(cmd));

snippet source | anchor

There isn't much to this code -- and that's the entire point! When Wolverine registers itself into a .NET Core application, it adds the IMessageBus service to the underlying system IoC container so it can be injected into controller classes or Minimal API endpoint as shown above.The IMessageBus.InvokeAsync(message) method takes the message passed in, finds the correct execution path for the message type, and executes the correct Wolverine handler(s) as well as any of the registered Wolverine middleware.

TIP

This execution happens inline, but will use the "Retry" or "Retry with Cooldown" error handling capabilities. See Wolverine's error handling for more information.

See also:

  • Cascading messages from actions for a better explanation of how the ItemCreated event message is automatically published if the handler success.
  • Messages for the details of messages themselves including versioning, serialization, and forwarding.
  • Message handlers for the details of how to write Wolverine message handlers and how they are discovered

As a contrast, here's what the same functionality looks like if you write all the functionality out explicitly in a controller action:

cs
// This controller does all the transactional work and business
// logic all by itself
public class DoItAllMyselfItemController : ControllerBase
{
    [HttpPost("/items/create3")]
    public async Task Create(
        [FromBody] CreateItemCommand command,
        [FromServices] IDbContextOutbox<ItemsDbContext> outbox)
    {
        // Create a new Item entity
        var item = new Item
        {
            Name = command.Name
        };

        // Add the item to the current
        // DbContext unit of work
        outbox.DbContext.Items.Add(item);

        // Publish an event to anyone
        // who cares that a new Item has
        // been created
        var @event = new ItemCreated
        {
            Id = item.Id
        };

        // Because the message context is enlisted in an
        // "outbox" transaction, these outgoing messages are
        // held until the ongoing transaction completes
        await outbox.SendAsync(@event);

        // Commit the unit of work. This will persist
        // both the Item entity we created above, and
        // also a Wolverine Envelope for the outgoing
        // ItemCreated message
        await outbox.SaveChangesAndFlushMessagesAsync();
    }
}

snippet source | anchor

So one, there's just more going on in the /items/create HTTP endpoint defined above because you're needing to do a little bit of additional work that Wolverine can do for you inside of its execution pipeline (the outbox mechanics, the cascading message getting published, transaction management). Also though, you're now mixing up MVC controller stuff like the [HttpPost] attribute to control the Url for the endpoint and the service application code that exercises the data and domain model layers.

Getting a Response

The controller methods above would both return an empty response body and the default 200 OK status code. But what if you want to return some kind of response body that gave the client of the web service some kind of contextual information about the newly created Item.

To that end, let's write a different controller action that will relay the body of the ItemCreated output of the message handler to the HTTP response body (and assume we'll use JSON because that makes the example code simpler):

cs
app.MapPost("/items/create2", (CreateItemCommand cmd, IMessageBus bus) => bus.InvokeAsync<ItemCreated>(cmd));

snippet source | anchor

Using the IMessageBus.Invoke<T>(message) overload, the returned ItemCreated response of the message handler is returned from the Invoke() message. To be perfectly clear, this only works if the message handler method returns a cascading message of the exact same type of the designated T parameter.

Released under the MIT License.