Initial Impressions of Wolverine

Initial Impressions of Wolverine
by Brad Jolicoeur
05/04/2024

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.

The Unexpected

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.

Observations

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

    • I see the value in reducing the ceremony code when leveraging Wolverine.http capabilities
    • I'm a big fan of FastEndpoints, but I have to admit that Wolverine wins the reduction of ceremony between the two.
  • Using static methods to handle messages and http endpoints felt a bit wrong, but something I quickly gained an appreciation for

    • Reduced the ceremony code significantly over creating handler of t style classes used by FastEndpoints
    • Should increase performance and use less resources by reducing allocations, which is a real benefit in the cloud world
    • I'm still concerned it will encourage less skilled engineers to create code that is challenging to test
      • This concern feels more like Neophobia after a little experience with Wolverine
    • Using method parameters to inject dependencies makes me feel uncomfortable
  • The little ceremony that Wolverine.http brings is welcome over Minimal API.

    • Minimal API quickly becomes disorganized and hard to read once you start building anything of complexity and is production ready.

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);
    }
}

Less Than Like

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

    • This can be similar to the RPC effect where you don't realize your not making a direct call
    • Conventions can speed up your work if you understand what they are and how they function
    • If you don't understand the conventions they can be maddening to debug or you create something that works by coincidence
    • It can also be challenging to get less experienced engineers to follow conventions.
      • Often they leave things out, because it's not apparent that it is necessary and/or they start compensating with their own creation
  • There are not a ton of examples with guidance on how to use Wolverine

    • It is still early days, so this is likely to change in the future
    • The JasperFX team has been super responsive to questions on Discord and you can buy a support plan so this compensates for some of the current lack of examples

Challenges

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:

  • Swagger OpenAPI was a challenge in implementing
    • I wanted to simulate converting from controllers to Wolverine.Http
    • The API worked as expected but there were changes in how the OpenAPI.json was generated that caused me some heartache
    • The biggest issue seemed to be from the inclusion of the "operationId":
      • This caused NSwagCSharp to generate duplicate client classes that resulted in lots of compile errors
      • Adding /OperationGenerationMode:SingleClientFromOperationId option to the NSwag options resolved the duplicate client, but resulted in some nasty method names
      • CreateAsync(CreateAccount body) became POST_api_account_createAsync(CreateAccount body)
    • The second biggest issue was that Wolverine.http defaults the Accept and Content-Type headers based on convention, which resulted in a breaking change for consumers of my endpoints
      • Granted, the Wolverine conventions are probably more correct, it is still a breaking change to my API if I can't override the convention
    • I didn't see anything in the documentation on Versioning API.
      • 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

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
},

Conclusions

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.

References