
One of the things I’m wrestling with right now is frankly how to sell Wolverine as a server side toolset. Yes, it’s technically a message library like MassTransit or NServiceBus. It can also be used as “just” a mediator tool like MediatR. With Wolverine.HTTP, it’s even an alternative HTTP endpoint framework that’s technically an alternative to FastEndpoints, MVC Core, or Minimal API. You’ve got to categorize Wolverine somehow, and we humans naturally understand something new by comparing it to some older thing we’re already familiar with. In the case of Wolverine, it’s drastically selling the toolset short by comparing it to any of the older application frameworks I rattled off above because Wolverine fundamentally does much more to remove code ceremony, improve testability throughout your codebase, and generally just let you focus more on core application functionality than older application frameworks.
This post was triggered by a conversation I had with a friend last week who told me he was happy with his current toolset for HTTP API creation and couldn’t imagine how Wolverine’s HTTP endpoint model could possibly reduce his efforts. Challenge accepted!
For just this moment, consider a simplistic HTTP service that works on this little entity:
public record Counter(Guid Id, int Count);
Now, let’s build an HTTP endpoint that will:
- Receive route arguments for the
Counter.Idand the current tenant id because of course let’s say that we’re using multi-tenancy with a separate database per tenant - Try to look up the existing
Counterentity by its id from the right tenant database - If the entity doesn’t exist, return a status code 404 and get out of there
- If the entity does exist, increment the
Countproperty and save the entity to the database and return a status code 204 for a successful request with an empty body
Just to make it easier on me because I already had this example code, we’re going to use Marten for persistence which happens to have much stronger multi-tenancy built in than EF Core. Knowing all that, here’s a sample MVC Core controller to implement the functionality I described above:
public class CounterController : ControllerBase
{
[HttpPost("/api/tenants/{tenant}/counters/{id}")]
[ProducesResponseType(204)] // empty response
[ProducesResponseType(404)]
public async Task<IResult> Increment(
Guid id,
string tenant,
[FromServices] IDocumentStore store)
{
// Open a Marten session for the right tenant database
await using var session = store.LightweightSession(tenant);
var counter = await session.LoadAsync<Counter>(id, HttpContext.RequestAborted);
if (counter == null)
{
return Results.NotFound();
}
else
{
counter = counter with { Count = counter.Count + 1 };
await session.SaveChangesAsync(HttpContext.RequestAborted);
return Results.Empty;
}
}
}
I’m completely open to recreating the multi-tenancy support from the Marten + Wolverine combo for EF Core and SQL Server through Wolverine, but I’m shamelessly waiting until another company is willing to engage with JasperFx Software to deliver that.
Alright, now let’s switch over to using Wolverine.HTTP with its WolverineFx.Http.Marten add on Nuget setup. Let’s drink some Wolverine koolaid and write a functionally identical endpoint the Wolverine way:
You need Wolverine 2.7.0 for this by the way!
[WolverinePost("/api/tenants/{tenant}/counters/{id}")]
public static IMartenOp Increment([Document(Required = true)] Counter counter)
{
counter = counter with { Count = counter.Count + 1 };
return MartenOps.Store(counter);
}
Seriously, this is the same functionality and even the same generated OpenAPI documentation. Some things to note:
- Wolverine is able to derive much more of the OpenAPI documentation from the type signatures and from policies applied to the endpoint method, like…
- The usage of the
Document(Required = true)tells Wolverine that it will be trying to load a document of typeCounterfrom Marten, and by default it’s going to do that through a route argument named “id”. TheRequiredproperty tells Wolverine to return a 404 NotFound status code automatically if theCounterdocument doesn’t exist. This attribute usage also applies some OpenAPI smarts to tag the route as potentially returning a 404 - The return value of the method is an
IMartenOp“side effect” just saying “go save this document”, which Wolverine will do as part of this endpoint execution. Using the side effect makes this method a nice, simple pure function that’s completely synchronous. No wrestling withasync Task,await, or schlepping aroundCancellationTokenevery which way - Because Wolverine can see there will not be any kind of response body, it’s going to use a 204 status code to denote the empty body and tag the OpenAPI with that as well.
- There is absolutely zero Reflection happening at runtime because Wolverine is generating and compiling code at runtime (or ahead of time for faster cold starts) that “bakes” in all of this knowledge for fast execution
- Wolverine + Marten has a far more robust support for multi-tenancy all the way through the technology stack than any other application framework I know of in .NET (web frameworks, mediators, or messaging libraries), and you can see that evident in the code above where Marten & Wolverine would already know how to detect tenant usage in an HTTP request and do all the wiring for you all the way through the stack so you can focus on just writing business functionality.
To make this all more concrete, here’s the generated code:
// <auto-generated/>
#pragma warning disable
using Microsoft.AspNetCore.Routing;
using System;
using System.Linq;
using Wolverine.Http;
using Wolverine.Marten.Publishing;
using Wolverine.Runtime;
namespace Internal.Generated.WolverineHandlers
{
// START: POST_api_tenants_tenant_counters_id_inc2
public class POST_api_tenants_tenant_counters_id_inc2 : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime;
private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;
public POST_api_tenants_tenant_counters_id_inc2(Wolverine.Http.WolverineHttpOptions wolverineHttpOptions, Wolverine.Runtime.IWolverineRuntime wolverineRuntime, Wolverine.Marten.Publishing.OutboxedSessionFactory outboxedSessionFactory) : base(wolverineHttpOptions)
{
_wolverineHttpOptions = wolverineHttpOptions;
_wolverineRuntime = wolverineRuntime;
_outboxedSessionFactory = outboxedSessionFactory;
}
public override async System.Threading.Tasks.Task Handle(Microsoft.AspNetCore.Http.HttpContext httpContext)
{
var messageContext = new Wolverine.Runtime.MessageContext(_wolverineRuntime);
// Building the Marten session
await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext);
if (!System.Guid.TryParse((string)httpContext.GetRouteValue("id"), out var id))
{
httpContext.Response.StatusCode = 404;
return;
}
var counter = await documentSession.LoadAsync<Wolverine.Http.Tests.Bugs.Counter>(id, httpContext.RequestAborted).ConfigureAwait(false);
// 404 if this required object is null
if (counter == null)
{
httpContext.Response.StatusCode = 404;
return;
}
// The actual HTTP request handler execution
var martenOp = Wolverine.Http.Tests.Bugs.CounterEndpoint.Increment(counter);
if (martenOp != null)
{
// Placed by Wolverine's ISideEffect policy
martenOp.Execute(documentSession);
}
// Commit any outstanding Marten changes
await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);
// Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536
await messageContext.FlushOutgoingMessagesAsync().ConfigureAwait(false);
// Wolverine automatically sets the status code to 204 for empty responses
if (!httpContext.Response.HasStarted) httpContext.Response.StatusCode = 204;
}
}
// END: POST_api_tenants_tenant_counters_id_inc2
}
Summary
Wolverine isn’t “just another messaging library / mediator / HTTP endpoint alternative.” Rather, Wolverine is a completely different animal that while fulfilling those application framework roles for server side .NET, potentially does a helluva lot more than older frameworks to help you write systems that are maintainable, testable, and resilient. And do all of that with a lot less of the typical “Clean/Onion/Hexagonal Architecture” cruft that shines in software conference talks and YouTube videos but helps lead teams into a morass of unmaintainable code in larger systems in the real world.
But yes, the Wolverine community needs to find a better way to communicate how Wolverine adds value above and beyond the more traditional server side application frameworks in .NET. I’m completely open to suggestions — and fully aware that some folks won’t like the “magic” in the “drank all the Wolverine Koolaid” approach I used.
You can of course use Wolverine with 100% explicit code and none of the magic.
Great innovation but it feels like there’s too much magic happenning and taking too much control away from the developer. Someone not already familiar with the framework cannot easily understand what in the wold is going on logically just by looking at the code. Having to read documentation and integrate conventions/principles (basically learning how some abstraction works) is the worst part of my day as a developer, and I’d happily pass on anything that requires me to do so, even if it makes me write fewer LOC in the future. Keep up the great work nonetheless.
With all due respect, I’ve heard that criticism of this kind of approach hundreds of times and I’m not really buying into it. There’s a learning curve to everything you do in code, and you always have to know a little bit about how anything you do works underneath your own code. To be 100% clear, Wolverine will happily allow you to write the most explicit code possible when and if you want to do that, so you get the best of both worlds.
The one thing that Wolverine does with its “magic” that I think is unique is its ability to just show you the generated code *with annotated comments* to explain exactly what it’s doing.
It’s not just fewer lines of code either, it’s how Wolverine allows you to express code in a more testable way and how much easier it’s going to be to iterate in code with Wolverine’s much lower ceremony approach that’s going to help folks be more successful.
Last thing I’ll say is that folks are already succeeding with Wolverine using the approach I showed in this blog post.
How rbac can be added to document resolution? I mean, If I need to check that claims principal can have access to that tenant and that counter, probably I need to check some other tables to do so – but how to tell Marten that I need to make those checks?
Just code. I think that’s going to be a short follow up