Customizing the Wolverine Code Generation Model

All of this sample code is checked in on GitHub at https://github.com/JasperFx/CritterStackSamples/tree/main/Reports.

When you develop with Wolverine as your application framework, Wolverine is really trying to push you toward using pure functions for your main message handler or HTTP endpoint methods. Recently, I was reviewing a large codebase using Wolverine and found several message handlers that had to use a simple interface like this one just get the next assigned identifier:

public interface IReportIdService
{
Task<int> GetNextReportId(CancellationToken cancellation);
}

In their command handlers, they were writing command handlers like this:

public static class StartReportEndpoint2
{
[WolverinePost("/report")]
public static async Task<ReportStarted> Handle(
// The command
StartReport command,
// Marten session
IDocumentSession session,
// The service just to fetch the next report id
IReportIdService idService,
CancellationToken cancellation
)
{
var id = await idService.GetNextReportId(cancellation);
session.Store(new Report{Id = id, Name = command.Name});
return new ReportStarted(command.Name, id);
}
}

(the real life handlers were a little more complicated than this because real code is almost always far more complex than the simplistic samples people like me use to demonstrate concepts)

Of course, the code above isn’t very difficult to understand conceptually and maybe it’s not worth the effort to write it any differently. But all the same, let me show you a Wolverine capability to customize the code generation to turn that handler method above into a synchronous pure function.

First off — and shockingly maybe for anybody who has seen me complain about these dad gum little things online — I want to introduce a little custom value type for the report id like this:

// You'd probably use something like Vogen
// on this too, but I didn't need that just
// for the demo here
public record ReportId(int Number);

Just so we can use this little type to identify our Report entities:

public class Report(ReportId Id)
{
public string Name { get; set; }
}

And next, what we want to get to is an HTTP endpoint signature that’s a pure function where the next ReportId is just poked in as a parameter argument:

public static class StartReportEndpoint
{
[WolverinePost("/report")]
public static (ReportStarted, IMartenOp) Handle(
// The command
StartReport command,
// The next report
ReportId id)
{
var op = MartenOps.Store(new Report(id) { Name = command.Name });
return (new ReportStarted(command.Name, id), op);
}
}

The MartenOps.Store() thing above is a built in “side effect” from Wolverine.Marten. Many of Wolverine’s earliest serious users were Functional Programming fans and they helped push Wolverine into a bit of an FP direction.

Alright, so the next step is to teach Wolverine how to generate a ReportId automatically and relay that to handler or endpoint methods that express a need for that through a method parameter. As an intermediate step, let’s do this simply and say that we’re just using a PostgreSQL Sequence for the number (I think my client’s implementation was something meaningful and more complicated than this, but just go with it please).

Knowing that our application has this sequence:

builder.Services.AddMarten(opts =>
{
// Set the connection string and database schema...
// Create a sequence to generate unique ids for documents
var sequence = new Sequence("report_sequence");
opts.Storage.ExtendedSchemaObjects.Add(sequence);
}).IntegrateWithWolverine();

Then we can build this little extension helper:

public static class DocumentSessionExtensions
{
public static async Task<ReportId> GetNextReportId(this IDocumentSession session, CancellationToken cancellation)
{
// This API was added in Marten 8.31 as I tried to write this blog post
var number = await session.NextSequenceValue("reports.report_sequence", cancellation);
return new ReportId(number);
}
}

And finally to connect the dots, we’re going to teach Wolverine how to resolve ReportId parameters with this extension of Wolverine’s code generation:

// Variable source is part of JasperFx's code generation
// subsystem. This just tells the code generation how
// to resolve code for a variable of type ReportId
internal class ReportIdSource : IVariableSource
{
public bool Matches(Type type)
{
return type == typeof(ReportId);
}
public Variable Create(Type type)
{
var methodCall = new MethodCall(typeof(DocumentSessionExtensions), nameof(DocumentSessionExtensions.GetNextReportId))
{
CommentText = "Creating a new ReportId"
};
// Little sleight of hand. The return variable here knows
// that the MethodCall creates it, so that gets woven into
// the generated code
return methodCall.ReturnVariable!;
}
}

And finally to register that with Wolverine at bootstrapping:

builder.Host.UseWolverine(opts =>
{
// Here's where we are adding the ReportId generation
opts.CodeGeneration.Sources.Add(new ReportIdSource());
});

Now, moving back to the StartReportEndpoint HTTP endpoint from up above, Wolverine is going to generate code around it like this:

// <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_report
[global::System.CodeDom.Compiler.GeneratedCode("JasperFx", "1.0.0")]
public sealed class POST_report : Wolverine.Http.HttpHandler
{
private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime;
private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;
public POST_report(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);
// Creating a new ReportId
var reportId = await DocumentSessionExtensions.GetNextReportId(documentSession, httpContext.RequestAborted).ConfigureAwait(false);
System.Diagnostics.Activity.Current?.SetTag("handler.type", "StartReportEndpoint");
// Reading the request body via JSON deserialization
var (command, jsonContinue) = await ReadJsonAsync<StartReport>(httpContext);
if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
// The actual HTTP request handler execution
(var reportStarted_response, var martenOp) = StartReportEndpoint.Handle(command, reportId);
if (martenOp != null)
{
// Placed by Wolverine's ISideEffect policy
martenOp.Execute(documentSession);
}
// Writing the response body to JSON because this was the first 'return variable' in the method signature
await WriteJsonAsync(httpContext, reportStarted_response);
}
}
// END: POST_report
}

And as usual, the generated code is an assault on the eyeballs, but if you squint and look for ReportId, you’ll see the generated code is executing our helper method to fetch the next report id value and pushing that into the call to our Handle() method.

Summary

I don’t know that this capability is something many teams would bother to employ, but it’s a possible way to simplify handler or endpoint code that hasn’t been previously documented very well. Wolverine itself uses this capability quite a bit for conventions.

What I think is more likely, I hope anyway, is that our continuing investment in AI Skills for the Critter Stack is that folks still get value out of these capabilities by either the AI skills recommending the usage upfront or helping people apply this later to continuously shrink the codebase and improve testability of the actual business logic and workflow code.

And because Wolverine has been such a busy project of the type that sometimes throws spaghetti up against the wall to see what sticks, there is another option for this kind of code generation customization you can see here in a sample that “pushes” DateTimeOffset.UtcNow into method parameters.

Lastly, we’re in the midst of an ongoing effort to improve the documentation across the JasperFx / Critter Stack family of projects and you can find more information about the code generation subsystem at https://shared-libs.jasperfx.net/codegen/.

Leave a comment