Jasper’s Efficient and Flexible Roslyn-Powered Execution Pipeline

You’ll need to pull the very latest code from the sample application linked to in this post because of course I found some bugs by dog-fooding while writing this:/

This is an update to a post from a couple years ago called Jasper’s Roslyn-Powered “Special Sauce.” You can also find more information about Lamar’s code generation and runtime compilation support from my Dynamic Runtime Code with Roslyn talk at London NDC 2019.

The more or less accepted understanding of a “framework” as opposed to a “library” is that a framework calls your application code (the Hollywood Principle). To be useful, frameworks should be dealing with cross cutting infrastructure concerns like security, data marshaling, or error handling. At some point, frameworks have to have some way to call your application code.

Since generics were introduced way back in .Net 3.5, many .Net frameworks have used what I call the “IHandler of T” approach where you are almost inevitably asked to implement a common layer supertype interface like this:

public interface IHandler
{
    Task Handle(T message, IContext context);
{

From a framework author’s perspective, this is easy to implement, and most modern IoC frameworks have reasonably strong support for generic types (like Lamar generic types support for example). Off the top of my head, NServiceBus, MassTransit, and MediatR are examples of this approach (my recollection is that NServiceBus did this first and I distinctly remember Udi Dahan describing this years ago at an ALT.Net event).

The other general approach I’ve seen — and what Jasper itself uses — is to have a framework call your code through some kind of dynamic code that adapts the signature of your code to the shape or interface that the framework needs. ASP.Net MVC Core Controller methods are an example of this approach. Here’s a sample controller method to demonstrate this:

public class WithResponseController : ControllerBase
{
    private readonly ICommandBus _bus;

    public WithResponseController(ICommandBus bus)
    {
        _bus = bus;
    } 
    
    // MVC Core calls this method, and uses the signature
    // and attributes like the [FromBody] to "know" how
    // to call this code at runtime
    [HttpPost("/items/create2")]
    public Task Create([FromBody] CreateItemCommand command)
    {
        // Using Jasper as a Mediator, and receive the
        // expected response from Jasper
        return _bus.Invoke(command);
    }
}

To add some more complexity to designing frameworks, there’s also the issue of how you can combine the basic handlers with some sort of Russian Doll middleware approach to handle cross cutting concerns. There’s a couple ways to handle this:

  • If a framework uses the “IHandler of T” approach, folks often try to use the decorator pattern to wrap the inner “IHandler”. This approach can lead to folks crashing and burning by getting way too complex with generic constraints. It can also lead to some pretty severe levels of object allocations and garbage collection thrashing from the sheer number of objects being created and discarded. To add more fuel to the fire, this approach can easily lead to absurdly large stack trace exceptions that can be very intimidating to parse for the average developer.
  • ASP.Net Core’s middleware approach through functional composition. This can also lead to some dramatically bad stack traces and similar issues with object allocation.
  • Do some runtime code generation to piece together the middleware. I believe that NServiceBus does this internally with its version of Behaviors.

Lastly, most .Net web, service bus, or command execution frameworks use some sort of scoped IoC container (nested container in Lamar or StructureMap parlance) per request or command execution to deal with object scoping and cleanup or disposal in their execution pipelines.

Jasper’s Unique Approach

Jasper was originally conceived and planned as a replacement to the older FubuMVC project (but it’s gone off in different directions later of course). As a framework, some of FubuMVC’s design goals were to:

  1. Minimize the amount of framework artifacts (interfaces, base classes, attributes, et al) in your application code — and it generally succeeded at that
  2. Provide a very strong model for composable middleware (what we called Behaviors) in the runtime pipeline — which also succeeded in terms of capability, but at runtime it was extremely inefficient, object allocations were off the charts, the IoC integration was complex, and the exception stack traces when things went wrong were epically big.

With Jasper, the initial goals were to recreate the “cleanliness” of the application code and the flexibility of FubuMVC’s “Russian Doll” approach, but do so in a way that was much more efficient at runtime. And because this actually matters quite a bit, make sure that any exceptions thrown out of application code while running within Jasper have minimal exception stack traces for easier troubleshooting.

Roslyn introduced support for compiling C# code at runtime. Some time in 2015 I was in the office with some of the old FubuMVC core contributors and drew up on the whiteboard a new approach where we’d generate C# code at application bootstrapping time to weave in both the calls to the application code and designated middleware.

Later on, I expanded that vision to try to also encompass the object construction and cleanup functionality of an IoC container in the same generated code. The result of that initial envisioning has become the combination of Jasper and Lamar, where Jasper actually uses Lamar’s registration model to try to generate and inline the functionality that would normally be done at runtime through an IoC container. The theory here being that the fasted IoC container is no IoC container.

Alright, to make this concrete, let’s see how this plays out in real usage. In my last post, Introducing Jasper as an In Process Command Bus for .Net, I demonstrated a small message handler in a sample Jasper application with this code (I’ve stripped out the original comments to make it smaller):

public class ItemHandler
{
    [Transactional]
    public static ItemCreated Handle(
        CreateItemCommand command,
        ItemsDbContext db)
    {
        var item = new Item
        {
            Name = command.Name
        };

        db.Items.Add(item);

        return new ItemCreated
        {
            Id = item.Id
        };
    }
}

The code above is meant to:

  1. Create a new Item entity based on the incoming CreateItemCommand
  2. Persist that new Item with Entity Framework Core
  3. Publish a new ItemCreated event message to be handled somewhere else (how that happens isn’t terribly important for this blog post)

And lastly, the [Transactional] attribute tells Jasper to apply its transactional middleware and outbox support to the message handler, such that a single database commit will save both the new Item entity and do the “store” part of a “store and forward” operation to send out the cascading ItemCreated event.

Internally, Jasper is going to generate a new class that inherits from this base class (slightly simplified):

public abstract class MessageHandler
{
    public abstract Task Handle(
        IMessageContext context, 
        CancellationToken cancellation
    );
}

For that ItemHandler class shown up above in the sample application, Jasper is generating and compiling through Roslyn this (butt ugly, but remember that it’s generated) class:

    public class InMemoryMediator_Items_CreateItemCommand : Jasper.Runtime.Handlers.MessageHandler
    {
        private readonly Microsoft.EntityFrameworkCore.DbContextOptions _dbContextOptions;

        public InMemoryMediator_Items_CreateItemCommand(Microsoft.EntityFrameworkCore.DbContextOptions dbContextOptions)
        {
            _dbContextOptions = dbContextOptions;
        }



        public override async Task Handle(Jasper.IMessageContext context, System.Threading.CancellationToken cancellation)
        {
            var createItemCommand = (InMemoryMediator.Items.CreateItemCommand)context.Envelope.Message;
            using (var itemsDbContext = new InMemoryMediator.Items.ItemsDbContext(_dbContextOptions))
            {
                // Enroll the DbContext & IMessagingContext in the outgoing Jasper outbox transaction
                await Jasper.Persistence.EntityFrameworkCore.JasperEnvelopeEntityFrameworkCoreExtensions.EnlistInTransaction(context, itemsDbContext);
                var itemCreated = InMemoryMediator.Items.ItemHandler.Handle(createItemCommand, itemsDbContext);
                // Outgoing, cascaded message
                await context.EnqueueCascading(itemCreated);
                // Added by EF Core Transaction Middleware
                var result_of_SaveChangesAsync = await itemsDbContext.SaveChangesAsync(cancellation);
            }

        }

    }

If you want, you can see the raw code at any time by executing the dotnet run -- codegen command from the root of the sample project.

So here’s what I’m claiming is the advantage of Jasper’s approach:

  • Allows your application code to be “clean” of framework artifacts and much more decoupled from Jasper than you can achieve with many other application frameworks like MVC Core
  • Using the diagnostic commands to dump out the generated source code in the application, Jasper can tell you exactly how it’s handling a message at runtime
  • By just generating Poor Man’s Dependency Injection code to build up the EF Core dependency and also to deal with disposing it later, the generated code eliminates any need to use the IoC container at runtime. And even for the very fastest IoC container in the world — and Lamar isn’t a slouch on the performance side of things — pure dependency injection is faster. Do mote that Jasper + Lamar can’t do this for every possible message handler and will have to revert to a scoped container per message with service location in some circumstances, usually because of a scoped or transient Lambda service registration or internal types.
  • The generated code minimizes the number of object allocations compared to a typical .Net framework that depends on adapter types
  • Jasper will allow you to use either synchronous or asynchronous handlers as appropriate for your use case, so no wasted keystrokes typing out return Task.CompletedTask; littering up your code.
  • The stack traces are pretty clean and you’ll see effectively nothing related to Jasper in logged exceptions except for the outermost frame where the generated MessageHandler.Handle() method is called.
  • Some other .Net frameworks try to do what Jasper does conceptually by trying to generate Expression trees and compile those down to Func objects. I’ve certainly done that in other tools (Lamar, StructureMap, Marten), but that’s rarified air that most developers can’t deal with. It’s also batshit insane for async heavy code like you’ll inevitably hit with anything that involves IO these days. My theory, yet to be proven, is that by relying on C#, Jasper’s middleware approach will be much more approachable to whatever community Jasper attracts down the line.

 

For some follow up reading if you’re interested:

Leave a comment