In this article, we’ll introduce the Wolverine library and its benefits for .NET developers. We’ll cover installation, setting up our first project, and core concepts around messaging. By the end, we’ll understand how to use Wolverine effectively in common practices and see real-world examples to help integrate it into our .NET applications.
Let’s get into it!
What is the Wolverine Library?
Wolverine is a framework for handling commands and asynchronous messaging in .NET. While other mature tools do similar things, Wolverine offers some unique benefits. It lets us write simple, clean code without forcing us to use complicated framework structures. Additionally, Wolverine is highly extensible, allowing developers to easily integrate custom functionalities and external infrastructure to meet specific application needs.
Getting Started with Wolverine
Let’s start building our first application using the Wolverine library. We implement a new API project using the command:
dotnet new webapi -n IntroductionToWolverineLibrary
This creates a new API project with the name IntroductionToWolverineLibrary
. Next, we install the Wolverine library:
dotnet add package WolverineFx
This installs the latest WolverineFx
library to our project. At the time of writing this article, the most recent version is WolverineFx 2.15.2
.
Following, in the Program.cs
file, we need to add:
builder.Host.UseWolverine();
This command makes Wolverine’s usages discoverable throughout our entire project. We are now ready to start building our app while exploiting the Wolverine library.
Wolverine Library as a Mediator
For demonstration purposes, we build the project as a book order management system. First, let’s create a simple object for a new book order:
public record Order(Guid OrderId, string CustomerName, int BookId);
Here we define a new record Order
, a special class type introduced in C# 9.0, designed for immutable data models. The object contains three fields. First, the OrderId
, a Guid
(Globally Unique Identifier) that identifies the order. Secondly, the CustomerName
, a string represents the name of the customer. Lastly, the BookId
, an int
that identifies the book being ordered.
InvokeAsync() To Send a Message
Now, let’s set up our first endpoint in the Program.cs
file. We’ll construct each of our endpoints using minimal API syntax:
app.MapPost("/order", async (Order newOrder, IMessageBus bus) => await bus.InvokeAsync(newOrder)) .WithName("NewBookOrder") .WithOpenApi();
Here, we define an HTTP POST
endpoint at the route /order
. We configure the endpoint to accept an Order
object and an IMessageBus
instance as parameters. Finally, the provided lambda function asynchronously executes the InvokeAsync()
method on the bus
, passing the newOrder
as an argument.
The call to WithName("NewBookOrder")
names this endpoint NewBookOrder
, which can be useful for routing or documentation purposes.
Now, let’s build the receiver method of the message:
public class OrderHandler { public static void Handle(Order? order) => Console.WriteLine($"New order received: {order}"); }
In a new class, we implement the method Handle
that takes an Order
object as a parameter and prints a message to the console, indicating that a new order has been received. By defining a method named Handle
that takes an Order
parameter, Wolverine recognizes this method as the handler for Order
messages. When a new order message is received, Wolverine routes it to this method for processing using the .NET local message bus.
When Wolverine integrates into a .NET Core
application, it registers the IMessageBus
service in the underlying system IoC container, enabling its injection into controller classes or Minimal API endpoints, as demonstrated above. Then, the IMessageBus.InvokeAsync(message)
method processes the provided message by determining the appropriate execution path for the message type and executing the corresponding Wolverine handler(s).
We don’t need to use interfaces to declare the intent of certain classes, such as IRequest
or IHandler
. Wolverine leverages Roslyn for Runtime Code Generation to minimize the code typically required to connect the correct handler to the appropriate command behind the scenes. Consequently, we can test the methods more easily. Instead of mocking an abstraction, we can directly test the method containing the business logic. Since the Handle
method is a pure function, we can invoke and test it without dependencies.
Use of InvokeAsync<T>() With Return Object
In case we want our handler to return an object, we need to use the IMessageBus.InvokeAsync<T>()
method:
app.MapPost("/orderReply", async (Order newOrder, IMessageBus bus) => await bus.InvokeAsync<string>(newOrder)) .WithName("NewBookOrderReply") .WithOpenApi();
By using the IMessageBus.InvokeAsync<T>(message)
overload, we expect the handler to return an object of type T
. In our example, we expect a simple string
response.
Now, let’s update the Handle
method to return a response:
public static string Handle(Order? order) => $"New order received: {order}";
This method returns a confirmation string that we received a new order. It’s worth mentioning that we can modify the Handle
method to return an object class or structure.
Using Queues with the Wolverine Library
The Wolverine library lets us use queues to send and receive our messages. In this way, we can communicate with different entities through a channel, like microservices or APIs.
Let’s set up the configuration to send messages to a specific queue:
builder.Host.UseWolverine( x => { x.PublishAllMessages().ToLocalQueue("book-ordering"); });
In the Program.cs
file, we configure the messages to be transmitted through a local queue with the name book-ordering
. We ensure that any message we send to Wolverine’s IMessageBus
will be processed according to Wolverine’s configured rules.
Similarly, let’s create a new BookReview
object:
public record BookReview(int BookId, string ReviewerName, int Rating);
This simple record holds information about a new book review. Now, let’s set up the new endpoint:
app.MapPost("/bookReview", async (BookReview review, IMessageBus bus) => { await bus.PublishAsync(review); return Results.Ok("Book review submitted successfully."); }) .WithName("BookReviewEndpoint") .WithOpenApi();
When we make a POST
request to this endpoint, the application asynchronously publishes this review using Wolverine’s IMessageBus
. After publishing the message, the endpoint returns an HTTP
response indicating the successful submission of the book review. This setup allows for decoupled handling of book reviews, leveraging Wolverine’s capabilities for message-based communication and processing within the .NET application architecture.
We publish this message to all queue subscribers. This is a common best practice when we want to raise event-like messages. This is especially necessary in an architecture where we want to process messages asynchronously and independently from the transmitter.
Now, let’s proceed with the handler implementation:
public class BookReviewHandler { public static void Handle(BookReview? review) => Console.WriteLine($"New book review received: {review}"); }
This handler is invoked after the message transmission since the method’s name is Handle
and its argument is BookReview
type. In this method, we similarly process the BookReview
by generating a string that acknowledges receipt of the review.
Let’s check the logs in the console:
Successfully processed message IntroductionToWolverineLibrary.Models.BookReview#01909954-3efa-401e-b0ac-f93465bbadc4 from local://local-queue/
Wolverine produces a log when it successfully processes a message. As we can see here, the log indicates that Wolverine has successfully processed a message of type BookReview
from the IntroductionToWolverineLibrary.Models
namespace. The message source is the local://local-queue/
, indicating also that it is handled from a local queue within the application.
External Queues For Handling Messages
Wolverine provides the capability to use external queue providers to handle messaging. For example, we can integrate with popular messaging systems such as RabbitMQ, Apache Kafka, Azure Service Bus, and Amazon SQS. Subsequently, this allows Wolverine to leverage these platforms’ robust and scalable messaging infrastructures, enabling seamless asynchronous communication and distributed processing within our applications.
By utilizing Wolverine to both publish and process messages through an external infrastructure, we can handle complex messaging patterns, ensure reliable message delivery, and scale our messaging architecture to meet the demands of high-traffic environments.
Conclusion
In this article, we introduced the Wolverine library for handling messaging within our .NET application. We demonstrated, through an API how to set up endpoints, configure queues for message transmission, and implement handlers for asynchronous message processing.