Unraveling the Magic in Wolverine

Just to pick a fight here, I think that folks who eschew all conventional approaches and insist on code being as explicit as possible end up writing very high ceremony code that’s completely unmaintainable in the end. I don’t see folks who insist on this style of “I never use frameworks” coding actually being all that effective in practice any time I’ve seen this other extreme. With all that being said, if you are using Wolverine you can choose to write very explicit code at any time and avoid using any of the built in conventions any time that’s necessary.

When you’re building or choosing an application framework, there’s a bit of tension between “magic” (convention over configuration) and explicitness in the code targeting that application framework. Speaking for myself, I lean very heavily toward low code ceremony tools that result in relatively uncluttered code with minimal boilerplate code for infrastructure. That bias admittedly leans toward conventional approaches, and Wolverine (and Marten to a much lesser degree) is chalk full of naming conventions.

Great! Except when it’s not. To make any kind of “magical” framework really work well for users, I think you need to:

  • First off, make the conventions be easy to understand and predictable (let’s call that a work in progress)
  • Document the conventions as well as you can
  • Hope that you don’t run into too many creative users that stretch the conventions farther than they were meant to — and relentlessly adapt as you inevitable run into those users
  • Provide the ability to bypass the conventions at will and write explicit code anytime the conventions don’t fit a use case — and that one’s a hard lesson learned from my experiences with FubuMVC/FubuTransportation back in the day:-(
  • Provide some some easily accessible mechanisms to unravel the magic and understand how the framework itself is calling into your code, routing requests, and even what middleware is being applied

For now, let’s focus on the last bullet point (while you mentally beat me up for the first). There is a Telehealth service application in the Wolverine codebase that has code for tracking and governing the workflow of “telehealth” appointments between patiens and various health professionals. Part of the system is a set of events and the following Marten aggregate for ProviderShift that models the activity and state of a health care provider (doctors, nurses, nurse practitioners, etc.) in a given day:

public class ProviderShift
{
    public Guid Id { get; set; }
    public int Version { get; set; }
    public Guid BoardId { get; private set; }
    public Guid ProviderId { get; init; }
    public ProviderStatus Status { get; private set; }
    public string Name { get; init; }
    public Guid? AppointmentId { get; set; }

    public static async Task<ProviderShift> Create(
        ProviderJoined joined,
        IQuerySession session)
    {
        var provider = await session
            .LoadAsync<Provider>(joined.ProviderId);

        return new ProviderShift
        {
            Name = $"{provider.FirstName} {provider.LastName}",
            Status = ProviderStatus.Ready,
            ProviderId = joined.ProviderId,
            BoardId = joined.BoardId
        };
    }

    public void Apply(ProviderReady ready)
    {
        AppointmentId = null;
        Status = ProviderStatus.Ready;
    }

    public void Apply(ProviderAssigned assigned)
    {
        Status = ProviderStatus.Assigned;
        AppointmentId = assigned.AppointmentId;
    }

    public void Apply(ProviderPaused paused)
    {
        Status = ProviderStatus.Paused;
        AppointmentId = null;
    }

    // This is kind of a catch all for any paperwork the
    // provider has to do after an appointment has ended
    // for the just concluded appointment
    public void Apply(ChartingStarted charting)
    {
        Status = ProviderStatus.Charting;
    }
}

The ProviderShift model above takes advantage of Marten’s “self-aggregate” functionality to teach Marten how to apply a stream of event data to update the current state of the model (the Apply() conventions are explained here).

So there’s a little bit of magic above, but let’s add some more before we get to the diagnostics. Now consider this HTTP endpoint from a Wolverine sample that’s using Marten‘s event store functionality within the sample “Telehealth” system for health providers to mark when they are done with their charting process (writing up their notes and follow up actions) after finishing an appointment:

    [WolverinePost("/shift/charting/complete")]
    [AggregateHandler]
    public (ChartingResponse, ChartingFinished) CompleteCharting(
        CompleteCharting charting,
        ProviderShift shift)
    {
        if (shift.Status != ProviderStatus.Charting)
        {
            throw new Exception("The shift is not currently charting");
        }
        
        return (
C// The HTTP response body
            new ChartingResponse(ProviderStatus.Paused),
            
            // An event to be appended to the ProviderShift aggregate event stream
            new ChartingFinished()
        );
    }

That HTTP endpoint uses Wolverine’s Aggregate Handler conventions as usage of the Decider Pattern to determine the event(s) that should be created for the given CompleteCharting and the current ProviderShift state of the provider shift referred to in the incoming command:

public record CompleteCharting(
    Guid ProviderShiftId,
    int Version
);

What’s it doing at runtime you ask? All told it’s:

  1. Deserializing the HTTP request body into the CompleteCharting command
  2. If we were applying any validation middleware, that might be happening next
  3. Loading the current state of the ProviderShift identified by the CompleteCharting command using Marten
  4. As it does #3, it’s opting into Marten’s optimistic concurrency checks for the provider shift event stream using the CompleteCharting.Version value
  5. Calling our actual endpoint method from up above
  6. Assuming there’s no validation exception, the `ChartingFinished` returned from our endpoint method is appended to the Marten event stream for the provider
  7. All pending Marten changes are persisted to the database
  8. The `ChartingResponse` object also returned from our endpoint method is serialized to the HTTP response stream

There’s a fair amount of infrastructure going on behind the scenes up above, but the goal of the Wolverine “Aggregate Handler” version of the “Decider pattern” is to allow our users to focus on writing the business logic for their application while letting Wolverine & Marten worry about all the important, but repetitive infrastructure code. Arguably, the result is that our users are mostly writing pure functions that are pretty easy to unit test.

Awesome! But there’s admittedly some room for confusion, especially for newer users. So let’s finally move on to Wolverine’s facilities to dispel the magic.

The Command Line is Sexy

No, seriously. Wolverine comes with a lot of diagnostic helpers that can be exposed from the command line of your application assuming that you’ve used Oakton for your command line runner as shown in the bottom of the Program file from the Telehealth sample:

// This is using the Oakton library for command running
await app.RunOaktonCommands(args);

First off, you can go check out everything that Wolverine is discovering or configuring from within your application with this command from the root folder of your main application project:

dotnet run -- describe

By itself, that’s going to tell you a lot about the static configuration of the application including all Wolverine HTTP endpoints with a textual display like this:

That tooling may help you right off the bat for troubleshooting handler discovery or message routing behavior in Wolverine applications, but let’s move on to understanding the actual logic of our CompleteCharting endpoint introduced earlier.

Wolverine has a significantly different runtime model than all the other HTTP endpoint models or message handling tools in .NET in that it uses runtime code generation to wrap its adapters around your code rather than forcing you to constrain your code for Wolverine. One of the upsides of all the gobbledy-gook I just spouted is that I can preview or write out Wolverine’s generated code by using the command line tooling like so:

dotnet run -- codegen write

Alright, start by preparing yourself to see some auto-generated code which inevitably means an eye sore. That command up above will write out the C# code around all the HTTP endpoints and Wolverine message handlers to the Internal/Generated/WolverineHandlers folder within your entry project. For HTTP endpoints, the generated file is named after the HTTP route, so in this case, we’re looking for the `POST_shift_charting_complete.cs` file, and here it is:

// <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
{
    public class POST_shift_charting_complete : Wolverine.Http.HttpHandler
    {
        private readonly Wolverine.Http.WolverineHttpOptions _wolverineHttpOptions;
        private readonly Wolverine.Runtime.IWolverineRuntime _wolverineRuntime;
        private readonly Wolverine.Marten.Publishing.OutboxedSessionFactory _outboxedSessionFactory;

        public POST_shift_charting_complete(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);
            var providerShiftEndpoint = new TeleHealth.WebApi.ProviderShiftEndpoint();
            // Reading the request body via JSON deserialization
            var (charting, jsonContinue) = await ReadJsonAsync<TeleHealth.WebApi.CompleteCharting>(httpContext);
            if (jsonContinue == Wolverine.HandlerContinuation.Stop) return;
            await using var documentSession = _outboxedSessionFactory.OpenSession(messageContext);
            var eventStore = documentSession.Events;
            
            // Loading Marten aggregate
            var eventStream = await eventStore.FetchForWriting<TeleHealth.Common.ProviderShift>(charting.ProviderShiftId, charting.Version, httpContext.RequestAborted).ConfigureAwait(false);

            
            // The actual HTTP request handler execution
            (var chartingResponse_response, var chartingFinished) = providerShiftEndpoint.CompleteCharting(charting, eventStream.Aggregate);

            eventStream.AppendOne(chartingFinished);
            await documentSession.SaveChangesAsync(httpContext.RequestAborted).ConfigureAwait(false);
            // Writing the response body to JSON because this was the first 'return variable' in the method signature
            await WriteJsonAsync(httpContext, chartingResponse_response);
        }

    }
}

It’s fugly code, but we’re trying to invest much more into adding explanatory comments into this generated code to try to explain *why* the code is generated around the signature of your inner message or HTTP handler. To go a little farther, the same codegen write command also wrote out the Marten code for the ProviderShift aggregate from up above as well (but that code is even uglier so I’m not showing it here).

Summary

Honestly, I’m out of time and need to leave to meet a friend for lunch, so let me leave you with:

Utilize dotnet run -- codegen write to understand how Wolverine is calling your code and how any middleware is being applied!

See the Wolverine docs on Code Generation for more help too. And don’t be afraid of magic as long as you’ve got the tools to understand it!

One thought on “Unraveling the Magic in Wolverine

  1. > `dotnet run — describe`

    Ooh, that’s nice 🙂

    Seriously though, I’m champing at the bit for a new project so I can use Wolverine!

Leave a comment