As a heavy user of Martin for document database and Event Sourcing for the last couple of years, I've been intrigued by Wolverine which is also published by the JasperFx team.
Wolverine is described on the website as, "Next generation .NET Mediator and Message Bus". Based on this description, Wolverine could replace my preferred use of Mediatr or FastEndpoints for Mediator pattern and Rebus for Messaging.
The potential benefits I see to switching to use Wolverine is much cleaner integration with Marten, especially for Event Sourcing, and some built in patterns like inbox/outbox that are either missing or clunky implementations in the Rebus or NServiceBus.
This article started out as an overall exploration and impressions of Wolverine, but as I started converting one of my example projects from Rebus to Wolverine the plan changed. I found that Wolverine is probably too broad a subject for one article and discovered Wolverine.http, which is an unexpected feature. I'm going to dive into Wolverine.http and hopefully get writing some follow up articles that review the other capabilities.
To me the fun part of exploration is the unexpected. My exploration of Wolverine did not disappoint. As I rolled up my sleeves and started the work of upgrading my banking example from a few years ago and reading the Wolverine documentation, I discovered Wolverine.http.
My initial plan had been to convert the controllers to FastEndpoints as part of this migration. This plan suddenly seemed like it would be a missed opportunity to explore Wolverine.http. It took me a bit to come to the conclusion. I really like FastEndpoints. Once I jumped, I was glad I changed course.
What I found made me uncomfortable at times, but in the end I think I'm a convert. Here are some of my notes from the experience:
The inclusion of Wolverine.Http was unexpected
Using static methods to handle messages and http endpoints felt a bit wrong, but something I quickly gained an appreciation for
The little ceremony that Wolverine.http brings is welcome over Minimal API.
For reference, here is An example of a Wolverine.http endpoint that is using static method to handle the request.
public static class Endpoint
{
[Tags("Account")]
[WolverinePost("api/account/create")]
public static async Task<CreateAccountResponse> Post(CreateAccount command, IDocumentSession session)
{
var account = new AccountCreated
{
Owner = command.Owner,
AccountId = command.AccountId,
StartingBalance = command.StartingBalance,
};
session.Events.StartStream(account.AccountId, account);
await session.SaveChangesAsync();
return new CreateAccountResponse(command.AccountId);
}
}
There were a few aspects of Wolverine.http that are not deal breakers for me, but they do give me concern.
The ease of configuration and conventions obscure what is happening
There are not a ton of examples with guidance on how to use Wolverine
The main challenge I ran into when converting from Controllers to Wolverine.http was trying to maintain my contracts so that I did not break consumers of the API. I was able to get close, but in the end if this was a real world situation, I would have to implement breaking changes to my API. This is unfortunate, since it would mean converting an existing application to Wolverine.http would become a significantly larger and risky project. One that would likely get deprioritized by a product owner.
More from my notes:
"operationId":
/OperationGenerationMode:SingleClientFromOperationId
option to the NSwag options resolved the duplicate client, but resulted in some nasty method namesCreateAsync(CreateAccount body)
became POST_api_account_createAsync(CreateAccount body)
Implementing Swagger OpenAPI proved to be a challenge, particularly as I aimed to transition from controllers to Wolverine.Http. While the API functioned as intended, there were complications in how the OpenAPI.json
file was generated, leading to some heartache.
A major issue stemmed from the inclusion of "operationId"
in the OpenAPI JSON file, which caused NSwagCSharp to generate duplicate client classes, resulting in numerous compile errors. Resolving this involved adding the /OperationGenerationMode:SingleClientFromOperationId
option to the NSwag options, albeit at the cost of generating cumbersome method names like POST_api_account_createAsync(CreateAccount body)
.
Another significant hurdle arose from Wolverine.http's default settings for Accept
and Content-Type
headers, which conflicted with established conventions and consequently posed a challenge for consumers of my endpoints. Although Wolverine's conventions might be technically sound, this default behavior represented a breaking change for my API, highlighting the importance of being able to override such conventions.
I was unable to find anything in the documentation on Versioning APIs. Not having a versioning capability makes the breaking API changes more challenging since I would need to rebuild my entire API at the same time.
To illustrate some of my challenges, below is before and after snip for one of my endpoints in the OpenApi.json
file.
Generated from the controller based API endpoint
"/api/account/create": {
"post": {
"tags": [
"Account"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateAccount"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CreateAccount"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/CreateAccount"
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/CreateAccountResponse"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateAccountResponse"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/CreateAccountResponse"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/BadRequestResponse"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestResponse"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/BadRequestResponse"
}
}
}
}
}
}
},
Wolverine.http endpoint generated the following definition for the equivalent endpoint
"/api/account/create": {
"post": {
"tags": [
"Account"
],
"operationId": "POST_api_account_create",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateAccount"
}
}
},
"required": true
},
I am very impressed with Wolverine.http and I plan to watch it evolve closely. The reduced ceremony and integration with the overall Wolverine/Martin ecosystem is appealing. At this point in time, I will be very selective where I utilize Wolverine.http since it seems to be evolving rapidly right now and I think there are still some pointy bits.
I'm looking forward to seeing Wolverine.http evolve and excited to apply it to a real production project in the near future.