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:
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().
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.
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:
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:
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.
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:
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:
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:
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:
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:
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 CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!