HomeNikola Knezevic

In this article

Banner

Messaging in .NET with Wolverine

30 Jan 2025
7 min

Stefan Djokic is launching his YouTube channel and building The CodeMan Community - your hub for .NET content, mini-courses, and expert advice!

The first 100 members get in for just $4/month! 🚀

Join now and grab his first ebook for free: https://www.skool.com/thecodeman

Sponsor Newsletter

Communication is crucial in modern applications, and .NET provides a wide range of libraries to utilize.

Typically, messaging libraries focus on either in-process or distributed messaging. MediatR is a popular choice for in-process messaging, while MassTransit is widely used for distributed messaging.

However, an interesting alternative is the Wolverine library, which supports both in-process and distributed messaging.

Wolverine

Wolverine is a powerful library for handling messages in .NET applications. Depending on your needs, it can function as a simple mediator or as a fully-featured asynchronous messaging framework.

Moreover, with the WolverineFx.Http library, the Wolverine pipeline can be used as an alternative ASP.NET Core Endpoint provider.

One of Wolverine's standout features is its built-in support for the transactional outbox, even for in-memory.

Interestingly, for cross-cutting concerns, Wolverine uses middleware that is directly integrated into the message handlers, resulting in a much more efficient runtime pipeline.

Let's jump right in and explore more about this amazing library!

Getting Started

To get started with Wolverine, you'll first need to install the necessary NuGet packages. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:

shell
dotnet add package WolverineFx

Depending on your message transport, you may need to install additional libraries and make slight adjustments only to the configuration.

Rules & Conventions

Wolverine leverages naming conventions to simplify its setup and configuration. This approach reduces boilerplate and streamlines the development process once you are used to it.

Having handlers that follow naming conventions ensures consistency. Naming convention:

  • Handler type names should end with either Handler or Consumer.
  • Handler method names should be Handle() or Consume().
csharp
public record CreateProductCommand(string Name, string Description, decimal Price);

public class CreateProductCommandHandler(IMessageBus bus, IApplicationDbContext dbContext)
{
    public async Task<Result<Guid>> Handle(
        CreateProductCommand request,
        CancellationToken cancellationToken)
    {
        var product = new Product(
            Guid.NewGuid(),
            DateTime.UtcNow,
            request.Name,
            request.Description,
            request.Price);

        dbContext.Products.Add(product);

        await dbContext.SaveChangesAsync(cancellationToken);

        var message = new ProductCreated(product.Id, product.Name, product.Description);
        await bus.PublishAsync(message);

        return Result.Success(product.Id);
    }
}

Messages and message handlers must be public types with public constructors. It's required due to Wolverine's code generation strategy.

The first argument of the handler method must be of the message type.

Wolverine assumes that the first argument of a handler method is the message type, while other arguments are inferred as services from the underlying IoC container. This method injection support helps minimize the boilerplate code typically required.

csharp
public class UpdateProductCommandHandler
{
    public async Task<Result> Handle(
        UpdateProductCommand request,
        IApplicationDbContext dbContext,
        CancellationToken cancellationToken)
    {
        var product = await dbContext.Products
            .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);

        if (product is null)
        {
            return Result.NotFound();
        }

        product.Update(request.Name, request.Description, request.Price);

        await dbContext.SaveChangesAsync(cancellationToken);

        return Result.Success();
    }
}

In-Memory Configuration

In-Memory Configuration couldn't be simpler, in Program.cs all you need to write is one line to register wolverine:

csharp
builder.Host.UseWolverine();

This adds Wolverine to your application's host, enabling message handling and command execution with minimal setup. For in-memory transport, this basic configuration will get you started.

In-Memory Messaging

In-memory messaging with Wolverine is lightweight, fast and efficient communication within the same application.

IMessageBus is a central interface to its messaging infrastructure. It enables communication across various parts of an application or between services in a distributed system.

One of the key methods of IMessageBus is InvokeAsync, which I am using to invoke commands and get results back from handlers:

csharp
app.MapPost("products", async (
    CreateProductRequest request,
    IMessageBus bus,
    CancellationToken cancellationToken) =>
{
    var command = request.Adapt<CreateProductCommand>();

    var response = await bus.InvokeAsync<Result<Guid>>(command, cancellationToken);

    return response.IsSuccess
        ? Results.Ok(response.Value)
        : Results.BadRequest();
}).WithTags(Tags.Products);

RabbitMQ Configuration

For this blog post, I'll be using RabbitMQ as my message transport.

Note: You can easily configure other transports as well, Wolverine supports several options, including Amazon SQS, Azure Service Bus, Kafka and more.

csharp
builder.Host.UseWolverine(options =>
{
    options.UseRabbitMq(new Uri("amqp://rabbitmq:5672"))
        .AutoProvision();
});

By using the UseRabbitMq method, you specify the address and port for RabbitMQ.

AutoProvision option ensures that the required exchanges and queues are automatically created. Without this, you would need to manually set up all the necessary components.

Distributed Messaging

As mentioned earlier, IMessageBus is responsible for sending and receiving messages. The only difference now is that you will need to add extra configuration for the message you want to publish using transport, like this:

csharp
builder.Host.UseWolverine(options =>
{
    options.UseRabbitMq(new Uri("amqp://rabbitmq:5672"))
        .AutoProvision();

    options.PublishMessage<ProductCreated>().ToRabbitQueue("product-queue");
});

Alternatively, you can automatically register all messages using the PublishAllMessages method:

csharp
builder.Host.UseWolverine(options =>
{
    options.UseRabbitMq(new Uri("amqp://rabbitmq:5672"))
        .AutoProvision();

    options.PublishAllMessages().ToRabbitQueue("product-queue");
});

Once the message is published, Wolverine’s message bus will route it to the appropriate consumer:

csharp
public class ProductCreatedConsumer
{
    public void Consume(ProductCreated message, ILogger<ProductCreatedConsumer> logger)
    {
        logger.LogInformation("Message received successfully: {Id} {Name} {Description}",
            message.Id,
            message.Name,
            message.Description);
    }
}

Additionally, Wolverine has built-in support for retries in case of failures during message processing. This is particularly important in distributed systems, where network or service failures can occur.

Outbox & Inbox Pattern

Wolverine can integrate with several database engines and persistence tools for durable messaging through the transactional inbox and outbox pattern.

To make the Wolverine outbox feature persist messages in the durable message storage, you need to explicitly make the outgoing subscriber endpoints be configured to be durable. Alternatively, you could globally make them durable with built in policy:

csharp
builder.Host.UseWolverine(options =>
{
    options.PublishAllMessages().ToPort(5555).UseDurableOutbox();

    // This forces every outgoing subscriber to use durable messaging
    options.Policies.UseDurableOutboxOnAllSendingEndpoints();
});

To enroll individual listening endpoints or all listening endpoints in the Wolverine inbox mechanics, use one of these options:

csharp
builder.Host.UseWolverine(options =>
{
    options.ListenAtPort(5555).UseDurableInbox();

    // This forces every listener endpoint to use durable storage
    options.Policies.UseDurableInboxOnAllListeners();
});

Conclusion

Wolverine is interesting and powerful library that bridges the gap between in-process and distributed messaging in .NET applications.

Its seamless integration, built-in transactional outbox making it a compelling alternative to MediatR and MassTransit.

By leveraging conventions, Wolverine simplifies setup and reduces boilerplate, allowing us to focus on building scalable and maintainable applications.

Whether you're looking for an efficient mediator or a fully-fledged messaging framework, Wolverine offers a flexible and performance-optimized solution worth exploring.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.