Thoughts on “Modular Monoliths”

TL;DR -> I’m dubious about whether or not the currently popular “modular monolith” idea will actually pay off, but first let’s talk about why we got to this point

The pendulum in software development can frequently swing back and forth between alternatives or extremes as the community struggles to achieve better results or becomes disillusioned with one popular approach or another. It’s easy — especially for older developers like me — to give into cynicism about the hot new idea being exactly the same as something from 5-10 years earlier. That cynicism isn’t necessarily helpful at all because there are often very real and important differences in the new approach that can easily be overlooked if you are jumping to making quick comparisons between the new thing and something familiar.

I’m working with several JasperFx Software clients right now that are either wrestling with brownfield systems they wish were more modular or getting started on large efforts where they can already see the need for modularity. Quite naturally, that brings the concept of “modular monoliths” to the forefront of my mind as a possible approach to creating more maintainable software for my clients.

What Do We Want?

Before any real discussion about the pendulum swinging from monoliths to micro-services and back to modular monoliths, let’s talk a bit about what I think any given software development team really wants from their large codebase:

  • The codebase is very easy to “clone n’ go”, meaning that there’s very little friction in being able to configure your development environment to run the code and test suites
  • Fast builds, fast test runs, snappy IDE performance. Call it whatever you want, but I want developers to always be working with quick feedback cycles
  • The code is easy to reason about. This is absolutely vital in larger, long lived codebases as a way to avoid causing regression bugs that can easily inflict larger systems. It’s also important just to keep teams productive over time
  • The ability to upgrade or even replace technologies over time without requiring risky major projects just to focus on technology upgrades that your business folks are very loathe to ever approve
  • This one might be just me, but I want a minimum of repetitive code ceremony within the codebase. I’ve routinely seen both monolithic codebases and micro-service strategies fail in some part because of too much repetitive code ceremony cluttering up the code and slowing down development work. The “Critter Stack” tools (Marten & Wolverine) are absolutely built with this low ceremony mantra in mind.
  • Any given part of the codebase should be small enough that a single “two pizza” team should be able to completely own that part of the code

Some Non-Technical Stuff That Matters

I work under the assumption that most of us are really professionals and do care about trying to do good work. That being said, even for skilled developers who absolutely care about the quality of their work, there are some common organizational problems that lead to runaway technical debt:

  • Micromanagement – Micromanagement crushes anybody’s sense of ownership or incentive to innovate, and developers are no different. As a diehard fan of early 2000’s idealistic Extreme Programming, I of course blame modern Scrum. Bad technical leads or architects can certainly hurt as well though. Even as the most senior technical person on a team, you have to be listening to the concerns of every other team member and allowing new ideas to come from anybody
  • Overloaded teams – I think that keeping teams running at near their throughput capacity on new features over timeinevitably leads to overwhelming technical debt that can easily dehabilitate future work in that system. The harsh reality that many business folks don’t understand is that it’s important in the long run for development teams to have some slack time for experimentation or incremental technical debt reduction tasks
  • Lack of Trust – This is probably closely related or the root cause of micromanagement, but teams are far more effective when there is a strong trust relationship between the technical folks and the business. Technical teams need to be able to communicate the need to occasionally slow down on feature work to address technical concerns, and have their concerns taken seriously by the business. Of course, as technical folks, we need to work on being able to communicate our concerns in terms of business impact and always seek to maintain the trust level from the business.

Old Fashioned Monoliths

Let’s consider the past couple swings of the pendulum. First there was the simple concept of building large systems as one big codebase. The “monolith” we all fear, even though none of us likely set out to purposely create one. Doing some research today, I saw people describe old fashioned monoliths as problematic because they were in Brian Foote and Joseph Yoder’s immortal words:

A BIG BALL OF MUD is haphazardly structured, sprawling, sloppy, duct-tape and bailing wire, spaghetti code jungle.

Brian Foote and Joseph Yoder’s Big Ball of Mud paper

My recent, very negative experiences with monolithic applications has been quite different though. What I’ve seen is that these monoliths had a consistent structure and clear architectural philosophy, but that the very ideas the teams had originally adopted to help keep the system maintainable were probably part of the causes for why their monolithic application was riddled with technical debt. In my opinion, I think these monolithic codebases have been difficult to work with because of:

  • The usage of prescriptive architectures like Clean Architecture or Onion Architecture solution templates that alternatively overcomplicated or failed to manage complexity over time because the teams did not deviate from the prescriptions
  • Overly relying on Ports and Adapters type thinking that led teams to introduce many more abstractions as the system grew, and that often leads to code being hard to follow or reason about. Also leads to potentially bad performance out of the sheer number of objects being created. Absolutely leads to poor performance when teams are not able to easily reason about their code’s interactions with databases because of the proliferation of abstractions
  • The predominance of layered architecture thinking in big systems means that closely related code is widely spread out over a big codebase
  • The difficulty in upgrading technologies over a monolithic codebase sheerly out of the size of the effort — and I partially blame the proliferation of common base types and marker interfaces promoted in the prescriptive Clean/Onion/Hexagonal/Ports & Adapters style architectural guidance

I hit these themes at NDC Oslo 2023 in this talk if you’re interested:

Alright, I think we can all agree on the pain of monolithic applications without enough structure, and we can agree to disagree about my negative opinions about Clean et al Architectures, but let’s move on to micro-services.

Obvious Problems with Micro-Services

I’m still bullish on the long term usefulness of micro-services — or really just “not massive services that contain just one cohesive bounded context and can actually run mostly independent.” But, let’s just acknowledge the very real downsides of micro-services that folks are running into:

  • Distributed development is hard, full stop. And micro-services inevitably mean more distributed development, debugging, and monitoring
  • It’s hard to get the service boundaries right in a complex business system, and it’s disastrous when you don’t get the boundaries right. If your team continuously finds itself having to frequently change multiple services at the same time, your boundaries are clearly wrong and you’re doing shotgun surgery — but it’s worse because you may be having to make changes in completely separate codebases and code repositories.
  • Again, if the boundaries aren’t quite right, you can easily get into a situation where the services have to be chatty with each other and send a lot more messages. That’s a recipe for poor performance and brittle systems.
  • Testing can be a lot harder if you absolutely need to do more testing between services rather than being able to depend on testing one service at a time

I’ve also seen high ceremony approaches completely defeat micro-service strategies by simply adding too much overhead to splitting up a codebase to gain any net value from splitting up the previous monolith. Again, I’m team low ceremony in almost any circumstance.

So what about Modular Monoliths?

Monoliths have been problematic, then micro-services turned out to be differently problematic. So let’s swing the pendulum back partway but focus more on making our monoliths modular for easier, more maintainable long term development. Great theory, and it’s spawning a lot of software conference talks and sage chin wagging.

This gets us to “Modular Monolith” idea that’s popular now, but I’m unfortunately dubious about the mechanics and whether or not this is just some “modular” lipstick on the old “monolith” pig.

In my next post, I’m going to try to describe my concerns and thoughts about how a “modular monolith” architecture might actually work out. I’m also concerned about how well both Marten and Wolverine are going to play within a modular monolith, and I’d like to get into some nuts and bolts about how those tools work now and how they maybe need to change to better accommodate the “modular monolith” idea.

Testing Asynchronous Projections in Marten

Hey, did you know that JasperFx Software offers formal support plans for Marten and Wolverine? Not only are we making the “Critter Stack” tools be viable long term options for your shop, we’re also interested in hearing your opinions about the tools and how they should change. We’re also certainly open to help you succeed with your software development projects on a consulting basis whether you’re using any part of the Critter Stack or some completely different .NET server side tooling.

As a kind of follow up to my post yesterday on Wolverine’s Baked In Integration Testing Support, I want to talk about some improvements to Marten that just went live in Marten 7.5 that are meant to make asynchronous projections much easier to test.

First off, let’s say that you have a simplistic document that can “self-aggregate” itself as a “Snapshot” in Marten like this:

public record InvoiceCreated(string Description, decimal Amount);

public record InvoiceApproved;
public record InvoiceCancelled;
public record InvoicePaid;
public record InvoiceRejected;

public class Invoice
{
    public Invoice()
    {
    }

    public static Invoice Create(IEvent<InvoiceCreated> created)
    {
        return new Invoice
        {
            Amount = created.Data.Amount,
            Description = created.Data.Description,

            // Capture the timestamp from the event
            // metadata captured by Marten
            Created = created.Timestamp,
            Status = InvoiceStatus.Created
        };
    }

    public int Version { get; set; }

    public decimal Amount { get; set; }
    public string Description { get; set; }
    public Guid Id { get; set; }
    public DateTimeOffset Created { get; set; }
    public InvoiceStatus Status { get; set; }

    public void Apply(InvoiceCancelled _) => Status = InvoiceStatus.Cancelled;
    public void Apply(InvoiceRejected _) => Status = InvoiceStatus.Rejected;
    public void Apply(InvoicePaid _) => Status = InvoiceStatus.Paid;
    public void Apply(InvoiceApproved _) => Status = InvoiceStatus.Approved;
}

For asynchronous projections of any kind, we have a little bit of complication for testing. In a classic “Arrange, Act, Assert” test workflow, we’d like to exercise our projection — and mind you, I strongly recommend that testing happen within its integration with Marten rather than some kind of solitary unit tests with fakes — with a workflow like this:

  1. Pump in some new events to Marten
  2. Somehow magically wait for Marten’s asynchronous daemon running in a background thread progress to the point where it’s handled all of our newly appended events for all known, running projections
  3. Load the expected documents that should have been persisted or updated from our new events by the projections running in the daemon, and run some assertions on the expected system state

For right now, I want to worry about the second bullet point and introduce a new (old, but it actually works correctly now) WaitForNonStaleProjectionDataAsync API introduced in Marten 7.5. You can see the new API used in this test from the new documentation on Testing Projections:

[Fact]
public async Task test_async_aggregation_with_wait_for()
{
    // In your tests, you would most likely use the IHost for your
    // application as it is normally built
    using var host = await Host.CreateDefaultBuilder()
        .ConfigureServices(services =>
        {
            services.AddMarten(opts =>
                {
                    opts.Connection(
                        "Host=localhost;Port=5432;Database=marten_testing;Username=postgres;password=postgres;Command Timeout=5");
                    opts.DatabaseSchemaName = "incidents";

                    // Notice that the "snapshot" is running inline
                    opts.Projections.Snapshot<Invoice>(SnapshotLifecycle.Async);
                })

                // Using Solo in tests will help it start up a little quicker
                .AddAsyncDaemon(DaemonMode.Solo);
        }).StartAsync();

    var store = host.Services.GetRequiredService<IDocumentStore>();

    var invoiceId = Guid.NewGuid();

    // Pump in events
    using (var session = store.LightweightSession())
    {
        session.Events.StartStream<Invoice>(invoiceId, new InvoiceCreated("Blue Shoes", 112.24m));
        await session.SaveChangesAsync();

        session.Events.Append(invoiceId,new InvoiceApproved());
        session.Events.Append(invoiceId,new InvoicePaid());
        await session.SaveChangesAsync();
    }

    // Now, this is going to pause here in this thread until the async daemon
    // running in our IHost is completely caught up to at least the point of the
    // last event captured at the point this method was called
    await store.WaitForNonStaleProjectionDataAsync(5.Seconds());

    // NOW, we should expect reliable results by just loading the already
    // persisted documents built by rebuilding the projection
    await using var query = store.QuerySession();

    // Load the document that was "projected" from the events above
    // and immediately persisted to the document store
    var invoice = await query.LoadAsync<Invoice>(invoiceId);

    // Run assertions
    invoice.Description.ShouldBe("Blue Shoes");
    invoice.Status.ShouldBe(InvoiceStatus.Paid);
}

Time. What about System Time?

See Andrew Lock’s blog post Avoiding flaky tests with TimeProvider and ITimer for more information on using TimeProvider in tests.

In the example projection, I’ve been capturing the timestamp in the Invoice document from the Marten event metadata:

public static Invoice Create(IEvent<InvoiceCreated> created)
{
    return new Invoice
    {
        Amount = created.Data.Amount,
        Description = created.Data.Description,

        // Capture the timestamp from the event
        // metadata captured by Marten
        Created = created.Timestamp,
        Status = InvoiceStatus.Created
    };
}

But of course, if that timestamp has some meaning later on and you have any kind of business rules that may need to key off that time, it’s very helpful to be able to control the timestamps that Marten is assigning to create predictable automated tests. As of Marten 7.5, Marten uses the newer .NET TimeProvider behind the scenes, and you can replace it in testing like so:

[Fact]
public async Task test_async_aggregation_with_wait_for_and_fake_time_provider()
{
    // Hang on to this for later!!!
    var eventsTimeProvider = new FakeTimeProvider();

    // In your tests, you would most likely use the IHost for your
    // application as it is normally built
    using var host = await Host.CreateDefaultBuilder()
        .ConfigureServices(services =>
        {
            services.AddMarten(opts =>
                {
                    opts.Connection(
                        "Host=localhost;Port=5432;Database=marten_testing;Username=postgres;password=postgres;Command Timeout=5");
                    opts.DatabaseSchemaName = "incidents";

                    // Notice that the "snapshot" is running inline
                    opts.Projections.Snapshot<Invoice>(SnapshotLifecycle.Async);

                    opts.Events.TimeProvider = eventsTimeProvider;
                })

                // Using Solo in tests will help it start up a little quicker
                .AddAsyncDaemon(DaemonMode.Solo);
        }).StartAsync();

    var store = host.Services.GetRequiredService<IDocumentStore>();

    var invoiceId = Guid.NewGuid();

    // Pump in events
    using (var session = store.LightweightSession())
    {
        session.Events.StartStream<Invoice>(invoiceId, new InvoiceCreated("Blue Shoes", 112.24m));
        await session.SaveChangesAsync();

        session.Events.Append(invoiceId,new InvoiceApproved());
        session.Events.Append(invoiceId,new InvoicePaid());
        await session.SaveChangesAsync();
    }

    // Now, this is going to pause here in this thread until the async daemon
    // running in our IHost is completely caught up to at least the point of the
    // last event captured at the point this method was called
    await store.WaitForNonStaleProjectionDataAsync(5.Seconds());

    // NOW, we should expect reliable results by just loading the already
    // persisted documents built by rebuilding the projection
    await using var query = store.QuerySession();

    // Load the document that was "projected" from the events above
    // and immediately persisted to the document store
    var invoice = await query.LoadAsync<Invoice>(invoiceId);

    // Run assertions, and we'll use the faked timestamp
    // from our time provider
    invoice.Created.ShouldBe(eventsTimeProvider.Start);
}

In the sample above, I used the FakeTimeProvider from the Microsoft.Extensions.TimeProvider.Testing Nuget package.

Summary

We take testability and automated testing very seriously throughout the entire “Critter Stack.” The testing of asynchronous projections has long been a soft spot that we hope is improved by the new capabilities in this post. As always, feel free to pop into the Critter Stack Discord for any questions.

Wolverine’s Baked In Integration Testing Support

Hey, did you know that JasperFx Software offers formal support plans for Marten and Wolverine? Not only are we making the “Critter Stack” tools be viable long term options for your shop, we’re also interested in hearing your opinions about the tools and how they should change. We’re also certainly open to help you succeed with your software development projects on a consulting basis whether you’re using any part of the Critter Stack or some completely different .NET server side tooling.

Hey, when you’re building grown up software systems in a responsible way, who likes effective automated testing? Me, too! Moreover, I like automated tests that are reliable — and anyone who has ever been remotely near a large automated test suite testing through the web application layer with any kind of asynchronous behavior knows exactly how painful “flake-y” tests are that suffer from timing issues.

Wolverine of course is an application framework for performing background processing and asynchronous messaging — meaning that there’s no end of the exact kind of asynchronous behavior that is notoriously hard to deal with in automated tests. At a minimum, what you need is a way to exercise the message handling within Wolverine (the “act” in the “act, arrange, assert” test pattern), but wait until all cascading activity is really complete before allowing the automated test to continue making assertions on expected outcomes. Fortunately, Wolverine has that very functionality baked into its core library. Here’s a fake saga that we recently used to fix a bug in Wolverine:

public class LongProcessSaga : Saga
{
    public Guid Id { get; init; }
    
    [Middleware(typeof(BeginProcessMiddleware))]
    public static (LongProcessSaga, OutgoingMessages) Start(BeginProcess message, RecordData? sourceData = null)
    {
        var outgoingMessages = new OutgoingMessages();

        var saga = new LongProcessSaga
        {
            Id = message.DataId,
        };

        if (sourceData is not null)
        {
            outgoingMessages.Add(new ContinueProcess(saga.Id, message.DataId, sourceData.Data));
        }

        return (
            saga,
            outgoingMessages
        );
    }

    public void Handle(ContinueProcess process)
    {
        Continued = true;
    }

    public bool Continued { get; set; }
}

When the BeginProcess message is handled by Wolverine, it might also spawn a ContinueProcess message. So let’s write a test that exercises the first message, but waits until the second message that we expect to be spawned while handling the first message before allowing the test to proceed:

    [Fact]
    public async Task can_compile_without_issue()
    {
        // Arrange -- and sorry, it's a bit of "Arrange" to get an IHost
        var builder = WebApplication.CreateBuilder(Array.Empty<string>());

        builder.Services
            .AddMarten(options =>
            {
                options.Connection(Servers.PostgresConnectionString);
            })
            .UseLightweightSessions()
            .IntegrateWithWolverine();

        builder.Host.UseWolverine(options =>
        {
            options.Discovery.IncludeAssembly(GetType().Assembly);
            
            options.Policies.AutoApplyTransactions();
            options.Policies.UseDurableLocalQueues();
            options.Policies.UseDurableOutboxOnAllSendingEndpoints();
        });

        builder.Services.AddScoped<IDataService, DataService>();

        // This is using Alba, which uses WebApplicationFactory under the covers
        await using var host = await AlbaHost.For(builder, app =>
        {
            app.MapWolverineEndpoints();
        });

        // Finally, the "Act"!
        var originalMessage = new BeginProcess(Guid.NewGuid());
        
        // This is a built in extension method to Wolverine to "wait" until
        // all activity triggered by this operation is completed
        var tracked = await host.InvokeMessageAndWaitAsync(originalMessage);
        
        // And now it's okay to do assertions....
        // This would have failed if there was 0 or many ContinueProcess messages
        var continueMessage = tracked.Executed.SingleMessage<ContinueProcess>();
        
        continueMessage.DataId.ShouldBe(originalMessage.DataId);

    }

The IHost.InvokeMessageAndWaitAsync() is part of Wolverine’s “tracked session” feature that’s descended from an earlier system some former colleagues and I developed and used at my then employer about a decade ago. The original mechanism was quite successful for our integration testing efforts of the time, and was built into Wolverine quite early. This “tracked session” feature is very heavily used within the Wolverine test suites to test Wolverine itself.

But wait, there’s more! Here’s a bigger sample from the documentation just showing you some more things that are possible:

public async Task using_tracked_sessions_advanced(IHost otherWolverineSystem)
{
    // The point here is just that you somehow have
    // an IHost for your application
    using var host = await Host.CreateDefaultBuilder()
        .UseWolverine().StartAsync();

    var debitAccount = new DebitAccount(111, 300);
    var session = await host
            
        // Start defining a tracked session 
        .TrackActivity()
        
        // Override the timeout period for longer tests
        .Timeout(1.Minutes())
        
        // Be careful with this one! This makes Wolverine wait on some indication
        // that messages sent externally are completed
        .IncludeExternalTransports()
        
        // Make the tracked session span across an IHost for another process
        // May not be super useful to the average user, but it's been crucial
        // to test Wolverine itself
        .AlsoTrack(otherWolverineSystem)

        // This is actually helpful if you are testing for error handling 
        // functionality in your system
        .DoNotAssertOnExceptionsDetected()
        
        // Again, this is testing against processes, with another IHost
        .WaitForMessageToBeReceivedAt<LowBalanceDetected>(otherWolverineSystem)
        
        // There are many other options as well
        .InvokeMessageAndWaitAsync(debitAccount);

    var overdrawn = session.Sent.SingleMessage<AccountOverdrawn>();
    overdrawn.AccountId.ShouldBe(debitAccount.AccountId);
}

As hopefully implied by the earlier example, the “tracked session” functionality also gives you:

  • Recursive tracking of all message activity to wait for everything to finish
  • Enforces timeouts in case of hanging tests that probably won’t finish successfully
  • The ability to probe the exact messaging activity that happened as a result of your original message
  • Visibility into any exceptions recorded by Wolverine during message processing that might otherwise be hidden from you. This functionality will re-throw these exceptions to fail a test unless explicitly told to ignore processing exceptions — which you may very well want to do to test error handling logic
  • If a test fails because of a timeout, or doesn’t reach the expected conditions, the test failure exception will show you a (hopefully) neatly formatted textual table explaining what it did observe in terms of what messages were sent, received, started, and finished executing. Again, this is to give you more visibility into test failures, because those inevitably do happen!

Last Thoughts

Supporting a complicated OSS tool like Marten or Wolverine is a little bit like being trapped in somewhere in Jurassic Park while the raptors (users, and especially creative users) are prowling around the perimeter of your tool just looking for weak spots in your tools — a genuine bug, a use case you didn’t anticipate, an awkward API, some missing documentation, or even just some wording in your documentation that isn’t clear enough. The point is, it’s exhausting and sometimes demoralizing when raptors are getting past your defenses a little too often just because you rolled out a near complete rewrite of your LINQ provider subsystem:)

Yesterday I was fielding questions from a fellow whose team was looking to move to Wolverine from one of the older .NET messaging frameworks, and he was very complimentary of the integration testing support that’s the subject of this post. My only point here is to remember to celebrate your successes to balance out the constant worry about what’s not yet great about your tool or project or codebase.

And by success, I mean a very important feature that will absolutely help teams build reliable software more productively with Wolverine that does not exist in other .NET messaging frameworks. And certainly doesn’t exist in the yet-to-be-built Microsoft eventing framework where they haven’t even considered the idea of testability.

Background Work with Wolverine

Hey, did you know that JasperFx Software offers formal support plans for Marten and Wolverine? Not only are we making the “Critter Stack” tools be viable long term options for your shop, we’re also interested in hearing your opinions about the tools and how they should change. We’re also certainly open to help you succeed with your software development projects on a consulting basis whether you’re using any part of the Critter Stack or some completely different .NET server side tooling.

Drama came to the .NET community yet again in the past week or two when more folks noticed that Microsoft is planning to write some kind of “eventing framework” that may or may not end up competing with tools like Wolverine. By and large, that discussion seemed to be purposely designed to make my blood boil, but one comment caught my attention:

I have a fairly simple requirement where I want to fire an event and process that event in the background. A while back, I looked at many of the frameworks mentioned here, and they all seemed so convoluted. For the life of me, I could not figure out how to set up something as simple as this.

GitHub commenter

So challenge accepted! Starting with this post, I’m going to write another series of small posts to explore some of Wolverine’s rich capabilities for background processing much like the earlier Building a Critter Stack Application series.

Let’s start dirt simple and just build a little ASP.Net Core service that can use Wolverine to do some work in the background spawned by a call into an HTTP endpoint.

Let’s start by creating a new projection like so:

dotnet new webapi

Then add Wolverine — and just the core Wolverine for right now — with:

dotnet add package WolverineFx

For our simplistic service, let’s say that we have an HTTP endpoint that accepts a posted body like this type:

public record SignUpRequest(string Name, string Email);

and wants to send an email to that new user welcoming to our site. And we’d like to have that email sent in a background worker (in this case it’ll be published to a local, in memory queue and eventually run in a different thread from the original HTTP request).

Skipping ahead a little bit to the Wolverine usage, here’s a crude handler for our SignUpRequest using Wolverine:

public static class SendWelcomeEmailHandler
{
    public static void Handle(SignUpRequest @event, ILogger logger)
    {
        // Just logging, a real handler would obviously do something real
        // to send an email
        logger.LogInformation("Send a Send a welcome email to {Name} at {Email}", @event.Name, @event.Email);
    }
}

Now, let’s hook up Wolverine into our application with just one line of code in our Program file like so:

var builder = WebApplication.CreateBuilder(args);

// This is good enough for what we're trying to do
// at the moment
builder.Host.UseWolverine();

And finally, let’s build a little Minimal API endpoint in our system that will publish SignUpRequest as an “event” to be handled in the background by Wolverine like so:

app.MapPost("/signup", (SignUpRequest request, IMessageBus bus) 
    // IMessageBus is the primary entry point to Wolverine
    => bus.PublishAsync(request));

app.Run();

Alright, let’s fire up our app with F5, and run this endpoint from the Swashbuckle page:

And watching the console logging for our application, I see this log entry just proving that we did some background processing:

info: SignUpRequest[0]
      Send a Send a welcome email to Jeremy at jeremy@jasperfx.net

Woohoo, we’ve successfully utilized some background processing in our new ASP.Net Core system using Wolverine!

Alright, let’s talk about some details and facts I elided in the quick start:

  • Our message handler is found by Wolverine by looking for public classes suffixed with “Handler”, then public methods named “Handle”. The first argument is assumed to be the message type for that handler
  • When I called PublishAsync() up above, Wolverine is seeking to route that SignUpRequest as a message. In this simplistic case, Wolverine is defaulting to publishing the message to a local, in memory queue specifically for the SignUpRequest type
  • When that message is ultimately “received” by Wolverine, Wolverine is responsible for calling into our SignUpRequestHandler.Handle() method
  • There’s no farther wiring to do, Wolverine puts everything else together for us based on simple defaults

There’s of course, much, much more going on in terms of error handling, logging, possible parallelizing behind the scenes of even this simple example. There’s also certainly much more powerful abilities within Wolverine for much more advanced background processing that we’ll continue to explore later in this series.

Conventional Message Routing in Wolverine

I got a little feedback over the weekend that some folks newly encountering Wolverine for the first time think that it’s harder to use than some other tools because “it doesn’t do the message routing for you.” This fortunately isn’t true, but there’s obviously some work to do on improving documentation and samples to dispel that impression.

For the sake of this post, let’s assume that you want to use Wolverine for asynchronous messaging between processes using an external messaging broker transport (or using asynchronous messaging in a single application but queueing up work in an external message broker). And while Wolverine does indeed have support for interoperability with non-Wolverine applications, let’s assume that it’s going to be Wolverine on both sides of all the message pipes.

First though, just know that for all for external transports with Wolverine, the conventional routing is opt in, meaning that you have to explicitly turn it on when you configure Wolverine within the UseWolverine() bootstrapping. Likewise, know that you can also control exactly how Wolverine configures the listening or message sending behavior in the conventionally determined endpoints.

Now then, to just go fast and make Wolverine do all the message routing for you with predictable conventions “just” see these recipes:

For Wolverine’s Rabbit MQ integration, you can opt into conventional routing like this:

        using var host = await Host.CreateDefaultBuilder()
            .UseWolverine(opts =>
            {
                opts.UseRabbitMq()
                    // Opt into conventional Rabbit MQ routing
                    .UseConventionalRouting();
            }).StartAsync();

In this convention, message routing is to:

  1. Publish all messages to a Rabbit MQ exchange named after Wolverine’s message type name that’s effectively the alias for that message type. Most of the time that’s just the full name of the concrete .NET type.
  2. Create a Rabbit MQ queue named with Wolverine’s message type name for the message type of every known message handler within your application. Wolverine also configures a binding from a Rabbit MQ exchange with that same name to the Rabbit MQ queue for that message type.

This routing behavior was absolutely influenced by similar functionality in the older MassTransit framework. Imitation is the sincerest form of flattery, plus I just agree with what they did anyway. Wolverine adds additional value through its richer resource setup model (AutoProvision / AddResourceSetupOnStartup()) and stronger model of automatic handler discovery.

That’s the quickest possible way to get started, but you have plenty of other levers, knobs, and dials to farther control the conventions as shown below:

        using var host = await Host.CreateDefaultBuilder()
            .UseWolverine(opts =>
            {
                opts.UseRabbitMq()
                    // Opt into conventional Rabbit MQ routing
                    .UseConventionalRouting(c =>
                    {
                        // Override naming conventions
                        c.QueueNameForListener(type => type.Name);
                        c.ExchangeNameForSending(type => type.Name);

                        // Override the configuration for each listener
                        c.ConfigureListeners((l, _) =>
                        {
                            l.ProcessInline().ListenerCount(5);
                        });

                        // Override the sending configuration for each subscriber
                        c.ConfigureSending((s, _) =>
                        {
                            s.UseDurableOutbox();
                        });
                    })
                    
                    // Let Wolverine both discover all these necessary exchanges, queues,
                    // and binding, then also build them as necessary on the broker if
                    // they are missing
                    .AutoProvision();
            }).StartAsync();

The “extra” optional configuration I used above hopefully show you how to take more exacting control over the conventional routing, but the pure defaults in the first sample will help you get up and going fast.

Let’s move on.

Azure Service Bus

Wolverine’s Azure Service Bus integration comes with a pair of conventional routing options. The simpler is to let Wolverine create and utilize an Azure Service Bus queue named after each message type name for both sending the receiving with this syntax:

        using var host = await Host.CreateDefaultBuilder()
            .UseWolverine(opts =>
            {
                opts.UseAzureServiceBus("some connection string")
                    .UseConventionalRouting();
            }).StartAsync();

Slightly more complicated is another option to use Azure Service Bus topics and subscriptions like so:

    // opts is a WolverineOptions object    
    opts.UseAzureServiceBusTesting()
                
        .UseTopicAndSubscriptionConventionalRouting(convention =>
                {
                    // Optionally control every aspect of the convention and
                    // its applicability to types
                    // as well as overriding any listener, sender, topic, or subscription
                    // options
                });

In this usage:

  1. Outgoing messages are routed to an Azure Service Bus topic named after Wolverine’s message type name for that type
  2. At application startup, Wolverine will listen for an Azure Service Bus subscription for each message type from the application’s known message handlers. Likewise, that subscription will automatically be bound to a topic named after the message type name

As was the case with the Rabbit MQ conventions shown first, you have complete control over the naming conventions, the listener configuration, and the subscriber configuration.

For Wolverine’s AWS SQS integration, you can conventionally route to SQS queues named after Wolverine’s message type name (by default) like this:

        var host = await Host.CreateDefaultBuilder()
            .UseWolverine(opts =>
            {
                opts.UseAmazonSqsTransport()
                    .UseConventionalRouting();

            }).StartAsync();

As was the case with the Rabbit MQ conventions shown first, you have complete control over the naming conventions, the listener configuration, and the subscriber configuration.

Wolverine’s Kafka integration is a little bit different animal. In this case you can set up rules to publish to different Kafka topics by message type like this:

        using var host = await Host.CreateDefaultBuilder()
            .UseWolverine(opts =>
            {
                opts.UseKafka("localhost:29092").AutoProvision();

                opts.PublishAllMessages().ToKafkaTopics();

                opts.Services.AddResourceSetupOnStartup();
            }).StartAsync();

In this case, Wolverine will send each message type to a topic name derived from the message type that’s either Wolverine’s message type name or an explicitly configured topic name configured by attribute like:

[Topic("color.purple")]
public class PurpleMessage
{
}

What if I want a completely different routing convention?!?

There’s an extension point for programmatic message routing rules in Wolverine that’s utilized by all the capabilities shown above called IMessageRouteSource, but if you think you really want to use that, maybe just head to the Wolverine Discord room and we’ll try to help you out!

Summary

Wolverine has strong support for conventional message routing using external brokers, but you need to make at least a one line of code entry in your configuration to explicitly add this behavior to your system. In all cases, Wolverine is able to help build the necessary queues, topics, subscriptions, binding, and exchanges derived by these conventions in your message broker for an efficient developer experience. Moreover, you have complete power to fine tune the usage of this conventional routing for your application.

“Partial” Document Updates in Marten 7

Just continuing a loose series about recent improvements in the big Marten 7.0 release:

As part of the big Marten 7 release a couple weeks ago, core team member Babu Annamalai made a huge contribution with a brand new model for making “partial” document updates that’s backed with native PostgreSQL operations. See Babu’s post Marten native partial updates – patching for much more information and details about everything that’s supported now.

Let’s put this into perspective with a quick sample. Here’s a simplistic Wolverine handler that updates a single member on a stored document:

 public static async Task Handle(ApproveInvoice command, IDocumentSession session)
    {
        // Load the invoice
        var invoice = await session.LoadAsync<Invoice>(command.InvoiceId);
        invoice.Approved = true;
        
        // Tell Marten to persist the new version
        session.Store(invoice);
        
        // Commit the one pending change
        await session.SaveChangesAsync();
    }

In the code above, I’m loading the whole document, changing one property, and committing the changes back to the database. Under the covers we’re making two round trips to the database, deserializing the starting state of the Invoice document, then serializing the end state of the Invoice document.

With the new patching support, let’s rewrite that handler to this:

 public static async Task Handle(ApproveInvoice command, IDocumentSession session)
    {
        // Tell Marten what to change
        session.Patch<Invoice>(command.InvoiceId).Set(x => x.Approved, true);

        // Commit the one pending change
        await session.SaveChangesAsync();
    }

In that second version, we’re doing a lot less work. There’s only one database call to overwrite the Approved value within the existing Invoice document. While it’s a nontrivial operation to reach inside the JSON in the database, we’re not having to do any serialization in memory.

Marten technically had this feature set already, but our older support depended on the PLv8 extension to PostgreSQL that’s more or less deprecated now. Babu’s work for Marten 7 brings this very important feature set back into play for the majority of users who don’t want to or can’t utilize the older PLv8 backed support.

LINQ Query Improvements in Marten 7

Working inside of the LINQ provider code that’s ultimately interpreting C# code and trying to turn that into the most efficient Pgpsql code possible somehow makes me think about Alice in Wonderland where I’m definitely in the role of “Alice.”

Just continuing a loose series about recent improvements in the big Marten 7.0 release:

A major set of emphasis for the Marten 7 release was to address the large accretion of LINQ related issues with a mostly new LINQ provider subsystem. In terms of improvements or just changes, we:

  • Completely removed the Remotion.Linq dependency (and the results of that are mixed so far)
  • Reworked the SQL generation for child collection querying
  • Greatly expanded the usage of .NET Dictionary types within LINQ queries
  • Expanded the reach of Select() transformations quite a bit — and that code hadn’t been touched or improved in any substantial way since Marten 1.0. You can see the list of patterns that Marten now supports in the acceptance test code.
  • Allowed for filtering of Include() queries, which has been a longstanding user “ask”

Child Collection Improvements

Alright, so back to the real problem. When Marten today encounters a LINQ query like this one:

var results = theSession.Query<Top>().Where(x =>
    x.Middles.Any(m => m.Color == Colors.Green && m.Bottoms.Any(b => b.Name == "Bill")));

Marten versions 4-6 had to generate a complicated SQL query using PostgreSQL Common Table Expressions to explode out the child collections into flat rows that can then be filtered to matching child rows, then finally uses a sub query filter on the original table to find the right rows. To translate, all that mumbo jumbo I said translates to “a big ass, slow query that doesn’t allow PostgreSQL to utilize its fancy GIN index support for faster JSONB querying.”

The Marten v7 support is smart enough to “know” when it can generate more efficient SQL for certain child collection filtering. In the case above, Marten v7 can use the PostgreSQL containment operator to utilize the GIN indexing support and just be simpler in general with SQL like this:

electd.id, d.data frompublic.mt_doc_top asd whereCAST(d.data ->> 'Middles'asjsonb) @> &1 LIMIT &2  &3: [{"Color":2,"Bottoms":[{"Name":"Bill"}]}]  

Another little micro-optimization we did for Marten V7 was to have as much SQL generation as possible use positional parameters (“&1”) as opposed to named parameters (“:p0”) that PostgreSQL itself expects to skip some SQL preprocessing in memory that we were forcing Npgsql to do for us earlier.

From early adopter feedback, some child collection queries in real systems have been a touch over an order of magnitude faster from V6 to V7 because of this change.

One more sample that I’m especially proud of. Let’s say you use this LINQ query:

var result = await theSession.Query<Root>()    
    .Where(r => r.ChildsLevel1.Count(c1 => c1.Name == "child-1.1") == 1)    
    .ToListAsync();

This one’s a little more complicated because you need to do a test of the number of matching child elements within a child collection. Again, Marten V6 will use a nasty and not terribly efficient common table expression approach to give you the right data. For Marten v7, we specifically asked the Marten user base if we could abandon support for any PostgreSQL versions lower than PostgreSQL 12 so we could use PostgreSQL’s JSONPath query support within our LINQ provider. That change got us to this SQL for the LINQ query from up above:

select d.id, d.data from public.mt_doc_root as d where jsonb_array_length(jsonb_path_query_array(d.data, '$.ChildsLevel1[*] ? (@.Name == $val1)', $1)) = $2
  &1: {"val1":"child-1.1"}
  &2: 1

That’s the child collection querying, which was absolutely a first class goal and time consuming task along the way.

Dictionary Queries

There was a large effort to improve Marten’s ability to query through Dictionary<TKey, TValue> members of a parent document type for V7 — which immediately led to plenty more user reported bugs we had to fix during the V7 pre-release cycle.

Here’a an example that’s now possible querying through Dictionary.ContainsKey():

        var results = await theSession
            .Query<Target>()
            
            // This is querying through a dictionary
            .Where(x => x.GuidDict.ContainsKey(guid))
            .ToListAsync();

And some SelectMany() action:

        var pairs = await theSession
            .Query<Target>()
            .SelectMany(x => x.StringDict.Keys)
            .ToListAsync();

And selecting the dictionary:

        var data = await theSession
            .Query<SelectDict>()
            .Select(x => x.Dict)
            .ToListAsync();

And querying through dictionary values:

        var values = await theSession.Query<Target>().SelectMany(x => x.StringDict.Values)
            .Where(x => x == "value2")
            .OrderBy(x => x)
            .ToListAsync();

Filtered Include

Marten’s Include() functionality is an important way to improve system performance by fetching related documents in one database round trip. Many folks through the years have asked for a way to limit the number of “included” documents returned in these queries by specifying a Where() filter against the included document type. That very functionality landed in Marten V7 with this syntax shown below from our testing code:

        var holders = await theSession.Query<TargetHolder>()
            // Limit the fetched Target documents from the Include()
            // by specifying a filter on Target documents
            .Include<Target>(x => x.TargetId, x => list.Add(x), t => t.Color == Colors.Blue)
            .ToListAsync();

Summary

While there are still some regression bugs coming in from the LINQ provider work (but they’re getting way more “edge case-y” and “WTH *would* you do that?”), I’m feeling good about the LINQ provider subsystem in Marten as a better foundation for us moving forward.

And for the appropriate coding soundtrack for me when I need to duck into the LINQ provider code in Marten to fix bugs:

Resiliency and Low Level Improvements in Marten 7

Just continuing a loose series about recent improvements in the big Marten 7.0 release:

I hate to tell you all this, but sometimes there’s going to be occasional hiccups with your system in production. Just sticking with using Marten and its connectivity to PostgreSQL under the covers, you could easily have:

  • Occasional network issues that cause transactions to fail
  • A database could be temporarily too busy and throw exceptions
  • Concurrency issues from trying to write to the same rows in the underlying database (Marten isn’t particularly prone to this, but I needed another example)

These typical transient errors happen, but that doesn’t mean that the operation that just failed couldn’t happily succeed if you just retried it in a little bit. To that end, Marten 7 replaced our previous homegrown resiliency approach (that didn’t really work anyway) with a reliance on the popular Polly library.

The default settings for Marten are shown below:

// Default Polly setup
var strategy = new ResiliencePipelineBuilder().AddRetry(new()
{
    ShouldHandle = new PredicateBuilder().Handle<NpgsqlException>().Handle<MartenCommandException>().Handle<EventLoaderException>(),
    MaxRetryAttempts = 3,
    Delay = TimeSpan.FromMilliseconds(50),
    BackoffType = DelayBackoffType.Exponential
}).Build();

The Marten docs here will tell you how to customize these policies at your discretion.

To put this into context, if you call IDocumentSession.SaveChangesAsync() to commit a unit of work, and there’s an exception that matches the criteria shown above, Polly will re-execute the queued operations from scratch with a brand new connection so the retry can succeed after the database or network has settled down.

Moreover, we followed the Polly recommendations on performance to utilize “static Lambdas” within the Marten internals to reduced the number of object allocations within Marten 7’s execution pipeline compared to the Marten 6 and earlier pipeline. It’s kind of ugly code, but you can see where and how we used Polly in QuerySession.Execution.cs.

Another big change for Marten 7 was how Marten is going to manage database connection lifecycles inside of Marten sessions. Prior to Marten 7, Marten sessions would open a database connection and keep it open until the session was ultimately disposed. This decision was originally made to optimize the integration between Marten and Dapper when Marten’s functionality was very limited.

Now though, Marten will only open a database connection within a session immediately before any operation that involves a database connection, and close that connection immediately after the operation is over (really just returning the underlying connection to the connection pool managed by Npgsql). With this change, it is now safe to run read-only queries through IQuerySession (or lightweight IDocumentSession) objects in multiple threads. That should make Marten be more effective within Hot Chocolate integrations:

  • Marten sessions can actually be thread safe so that you can let the default IoC registration behavior for Marten sessions happily work within Hot Chocolate requests where it wants to parallelize queries
  • Marten usage will be far, far less prone to connection leaks when developers create sessions without disposing them properly

And even if you’re not using Hot Chocolate at all, Marten 7 will be more parsimonious over all with how it uses database connections which should help with scalability of your system — and definitely will help with cloud hosting options that charge by the number of database connections used!

Final Thoughts

In the past month or so I’ve had more interaction with developers than usual who are highly suspicious of open source tooling and especially of alternative open source tooling in .NET that isn’t directly supported by Microsoft. It’s a fair point to some degree, because “Google/Bing-bility” is a highly underrated quality of a development tool. Some of that fear was thinking that the responsible developers behind a non mainstream tool are just going to get tired of it and abandon it on a whim when a shinier object comes around so it’s not even worth the time to care about those alternatives from the “cool kids.”

What I would like to point out with Marten in particular is that it’s nearly a decade old as a project (the document db features in Marten actually predate CosmosDb in .NET world) and still very active and growing. The work in this post is all about making fine grained refinements to the core project and I would hope demonstrate a dedication on the part of the whole community toward continuously improving Marten for serious work. In other words, I think development shops can absolutely feel confident in placing a technical bet on Marten.

Recent Critter Stack Multi-Tenancy Improvements

Hey, did you know that JasperFx Software offers formal support plans for Marten and Wolverine? Not only are we making the “Critter Stack” tools be viable long term options for your shop, we’re also interested in hearing your opinions about the tools and how they should change. We’re also certainly open to help you succeed with your software development projects on a consulting basis whether you’re using any part of the Critter Stack or some completely different .NET server side tooling.

Marten 7.0 was released over the weekend, and Wolverine 2.0 followed yesterday mostly to catch up with the Marten dependency. One of the major improvements in this round of “Critter Stack” releases was to address a JasperFx client’s need for dynamically adding new tenant databases at runtime without having to do any kind of system deployment.

Doing that successfully meant adding a couple capabilities throughout the “Critter Stack.” First off, Marten needed a new multi-tenancy configuration strategy that allowed users to keep a list of valid tenant id and database connection strings for those tenants in a database table. Enter Marten 7’s new Master Table Tenancy Model. You can see the usage in this sample from the documentation:

using var host = await Host.CreateDefaultBuilder()
    .ConfigureServices(services =>
    {
        services.AddMarten(sp =>
            {
                var configuration = sp.GetRequiredService<IConfiguration>();
                var masterConnection = configuration.GetConnectionString("master");
                var options = new StoreOptions();

                // This is opting into a multi-tenancy model where a database table in the
                // master database holds information about all the possible tenants and their database connection
                // strings
                options.MultiTenantedDatabasesWithMasterDatabaseTable(x =>
                {
                    x.ConnectionString = masterConnection;

                    // You can optionally configure the schema name for where the mt_tenants
                    // table is stored
                    x.SchemaName = "tenants";

                    // If set, this will override the database schema rules for
                    // only the master tenant table from the parent StoreOptions
                    x.AutoCreate = AutoCreate.CreateOrUpdate;

                    // Optionally seed rows in the master table. This may be very helpful for
                    // testing or local development scenarios
                    // This operation is an "upsert" upon application startup
                    x.RegisterDatabase("tenant1", configuration.GetConnectionString("tenant1"));
                    x.RegisterDatabase("tenant2", configuration.GetConnectionString("tenant2"));
                    x.RegisterDatabase("tenant3", configuration.GetConnectionString("tenant3"));

                    // Tags the application name to all the used connection strings as a diagnostic
                    // Default is the name of the entry assembly for the application or "Marten" if
                    // .NET cannot determine the entry assembly for some reason
                    x.ApplicationName = "MyApplication";
                });

                // Other Marten configuration

                return options;
            })
            // All detected changes will be applied to all
            // the configured tenant databases on startup
            .ApplyAllDatabaseChangesOnStartup();;
    }).StartAsync();

With this tenancy model, Marten is able to discover newly added tenant databases at runtime as needed during normal transactions. And don’t fret about the extra activity, Marten is able to cache that information in memory to avoid making too many unnecessary database calls.

But what about if I need to decommission and remove a customer database? What if we need to move a tenant database at runtime? Can Marten create databases on the fly for us? How will I do that?!? And sorry, my reply is that will require at least an application restart for now, and any kind of fancier management for advanced multi-tenancy will likely go into the forthcoming “Critter Stack Pro” paid model later this year.

That’s part one. The next issue was that for users who also used Marten’s asynchronous projection feature, the “async daemon” subsystem in Marten needed to be able to discover new tenant databases in the background and ensure that all the asynchronous projections for these newly discovered databases are running in the background somewhere in the application. This led to a partial rewrite of the “async daemon” subsystem for Marten 7, but you can see the positive effect of that work in this test that “proves” that Marten is able to spin up projection building agents in the background at runtime:

    [Fact]
    public async Task add_tenant_database_and_verify_the_daemon_projections_are_running()
    {
        // In this code block, I'm adding new tenant databases to the system that I
        // would expect Marten to discover and start up an asynchronous projection
        // daemon for all three newly discovered databases
        var tenancy = (MasterTableTenancy)theStore.Options.Tenancy;
        await tenancy.AddDatabaseRecordAsync("tenant1", tenant1ConnectionString);
        await tenancy.AddDatabaseRecordAsync("tenant2", tenant2ConnectionString);
        await tenancy.AddDatabaseRecordAsync("tenant3", tenant3ConnectionString);

        // This is a new service in Marten specifically to help you interrogate or
        // manipulate the state of running asynchronous projections within the current process
        var coordinator = _host.Services.GetRequiredService<IProjectionCoordinator>();
        var daemon1 = await coordinator.DaemonForDatabase("tenant1");
        var daemon2 = await coordinator.DaemonForDatabase("tenant2");
        var daemon3 = await coordinator.DaemonForDatabase("tenant3");

        // Just proving that the configured projections for the 3 new databases
        // are indeed spun up and running after Marten's new daemon coordinator
        // "finds" the new databases
        await daemon1.WaitForShardToBeRunning("TripCustomName:All", 30.Seconds());
        await daemon2.WaitForShardToBeRunning("TripCustomName:All", 30.Seconds());
        await daemon3.WaitForShardToBeRunning("TripCustomName:All", 30.Seconds());
    }

Now, switching over to Wolverine 2.0 and its contribution to the party, the third part to make this dynamic tenant database discovery work throughout the entire “Critter Stack” was for Wolverine to be able to also discover the new tenant databases at runtime and spin up its “durability agents” for background message scheduling and its transactional inbox/outbox support. It’s admittedly not well at all documented yet, but Wolverine has an internal system for leader election and “agent assignment” between running nodes in an application cluster to distribute work. Wolverine uses this subsystem to distribute the transactional inbox/outbox work for each tenant database across the application cluster. Look for more information on this capability as JasperFx Software will be exploiting this for a different customer engagement this year.

Marten 7 makes “Write Model” Projections Super

Marten 7.0 was released this week with a bevy of improvements and some important new features. One important area of concentration was a series of changes sponsored by a JasperFx Software client to improve Marten‘s scalability and to achieve more seamless deployments that involve changes to event store projections.

To show off this new work, I’d like to review CQRS command handler from my earlier Building a Critter Stack Application series. In that series, we had a command handler that attempted to apply changes to an existing event stream representing an Incident within our system:

public static async Task Handle(
    CategoriseIncident command, 
    IDocumentSession session, 
    CancellationToken cancellationToken)
{
    // Find the existing state of the referenced Incident
    // but also set Marten up for optimistic version checking on
    // the incident upon the call to SaveChangesAsync()
    var stream = await session
        .Events
        .FetchForWriting<IncidentDetails>(command.Id, cancellationToken);
 
    // Don't worry, we're going to clean this up later
    if (stream.Aggregate == null)
    {
        throw new ArgumentOutOfRangeException(nameof(command), "Unknown incident id " + command.Id);
    }
     
    // We need to validate whether this command actually 
    // should do anything
    if (stream.Aggregate.Category != command.Category)
    {
        var categorised = new IncidentCategorised
        {
            Category = command.Category,
            UserId = SystemId
        };
 
        stream.AppendOne(categorised);
         
        // This call may throw a ConcurrencyException!
        await session.SaveChangesAsync(cancellationToken);
    }
}

Mostly, I want to call your attention to this Marten API:

    var stream = await session
        .Events
        .FetchForWriting<IncidentDetails>(command.Id, cancellationToken);

The FetchForWriting() API is an important building block for using Marten inside of a CQRS command handler where you need to quickly fetch the “write model” for an event stream that represents enough of the state of that stream to make decisions about next steps.

This API completely hides away how Marten is deriving that aggregated state as well as setting the command handler up to easily opt into Marten’s optimistic concurrency features. Before 7.0, this API can either be doing “Live” aggregation where Marten fetches the raw events and applies them in memory to create the aggregated state, or uses an “Inline” aggregation where Marten is always updating the projected aggregate at the time that new events are captured.

Both of those strategies give you the all important strong consistency between the raw event data and the aggregated state, but the “Live” mode can be expensive with bigger or longer event streams and the “Inline” mode adds some expense to every single transaction with Marten that involves capturing new events.

With an important assist from the new numeric revisioning strategy, Marten 7.0 now enables users to utilize the asynchronous projection feature in Marten as a potentially much more efficient way to achieve strong consistency between the raw event data and the aggregated state served up by FetchForWriting() API.

In the sample incident tracking, help desk application, we have this projection for calculating the IncidentDetails model:

public class IncidentDetailsProjection: SingleStreamProjection<IncidentDetails>
{
    public static IncidentDetails Create(IEvent<IncidentLogged> logged) =>
        new(logged.StreamId, logged.Data.CustomerId, IncidentStatus.Pending, Array.Empty<IncidentNote>());

    public IncidentDetails Apply(IncidentCategorised categorised, IncidentDetails current) =>
        current with { Category = categorised.Category };

    public IncidentDetails Apply(IncidentPrioritised prioritised, IncidentDetails current) =>
        current with { Priority = prioritised.Priority };

    public IncidentDetails Apply(AgentAssignedToIncident prioritised, IncidentDetails current) =>
        current with { AgentId = prioritised.AgentId };

    public IncidentDetails Apply(IncidentResolved resolved, IncidentDetails current) =>
        current with { Status = IncidentStatus.Resolved };

    public IncidentDetails Apply(ResolutionAcknowledgedByCustomer acknowledged, IncidentDetails current) =>
        current with { Status = IncidentStatus.ResolutionAcknowledgedByCustomer };

    public IncidentDetails Apply(IncidentClosed closed, IncidentDetails current) =>
        current with { Status = IncidentStatus.Closed };
}

Now, we have these three options for the projection registration that are all going to be supported by the FetchForWriting() API:

builder.Services.AddMarten(opts =>
{
    // Do note that the three options below are all mutually
    // exclusive
    
    // Pre 7.0, our options were to either run the projection inline like this:
    opts.Projections.Add<IncidentDetailsProjection>(ProjectionLifecycle.Inline);
    
    // or rely on in memory calculation like:
    opts.Projections.Add<IncidentDetailsProjection>(ProjectionLifecycle.Live);
    
    // NOW though, with 7.0, we have this option:
    opts.Projections.Add<IncidentDetailsProjection>(ProjectionLifecycle.Async);

    // other configuration
})

Running the “write model” aggregation of the IncidentDetails projection gives us some immediate benefits:

  • There’s less work going on at the time of writing new event data to an incident stream if we don’t also have to do the work of updating the IncidentDetails model. That may make our system more responsive to users or outside clients of our application’s services
  • At the time that FetchForWriting() is called, Marten starts with whatever the current persisted state of the IncidentDetails aggregate document that is saved in the database, and apply any newer events on top of the persisted state for a strongly consistent model with the raw events
  • There’s some significant batching happening in the asynchronous daemon process that can be more efficient overall than the “Inline” projection calculation can be. That could be considerable for systems that need to capture events against multiple streams in the same transaction (that came up very recently from a user in our Discord room)
  • As I’m about to show next, this “Async” aggregation gives Marten the ability to allow for zero downtime deployments even if our IncidentDetails projection changes. It even allows for the ability to do blue/green deployments where the newer and older versions of IncidentDetails can coexist at runtime on the same database before the older version is completely retired!

I would like to point out that the mechanism for FetchForWriting() with asynchronous projections is batching up the necessary database queries in one round trip. I would like to argue that this is indicative of our attention to detail with Marten and a sign that Marten represents some serious minded engineering that you can feel good about as a technical dependency for important systems.

In the course of our system, someone up high decides we need to track whether issues are being duplicated. Maybe we add a new event like so:

public record IncidentDuplicated(Guid OtherId);

And update our projection to use this new event like so:

    public IncidentDetails Apply(IncidentDuplicated duplicated, IncidentDetails current) =>
        current with { DuplicateIds = current.DuplicateIds.Concat(new[] { duplicated.OtherId }).ToArray() };

To make this sample make more sense, let’s say that there were also some breaking changes to the IncidentDetails projection that made it incompatible with the old projected model.

Now, we’d like to utilize this new version of the projection immediately — but, wait! The existing system already has the old projection model persisted in database tables! If you used the “Inline” lifecycle, you would have to shut down the system, and rebuild the projection from scratch to replace the persisted data.

Now though, with Marten 7.0, we can instead make one more change to our projection and add this line to mark it as version 2:

    public IncidentDetailsProjection()
    {
        ProjectionVersion = 2;
    }

And finally, we’ll deploy our new version of the application — but because we’re being fancy, we’ll deploy in a blue/green way to only some of the application nodes while the older version of the application runs on other nodes.

When the application starts up with the new version of IncidentDetails, Marten will (this can vary based on your configuration, but let’s assume defaults for now):

  • Treat the V2 version of IncidentDetailsProjection as being completely separate from the original version. This means that Marten will create parallel tables for the V2 version of IncidentDetails and start calculating the IncidentDetailsProjection V2 from the starting position of the event store in the background (this is worth a much longer conversation later). Marten pulls this off by just adding an “_#” suffix to the tables and functions for the document where “#” is the version number if the version is greater than 1.
  • When FetchForWriting() is called on an individual stream, it might be effectively doing a “Live” aggregation until the projection catches up in the background with the “high water mark” of the event store, but at least you have zero required downtime for your IncidentDetails “write model.”
  • Marten’s async daemon subsystem got some spiffy improvements for V7, so now when you use the HotCold mode, the daemon just makes sure that every single projection for every individual database is running on exactly one node at a time, so the newly revisioned projection will be started up and actively run on one of the nodes that is running the latest revision of the system.

In the older version of the application still running on some nodes, Marten can happily just skip the “unknown” IncidentDuplicated events when it encounters them in the running asynchronous projections there rather than failing with repetitive exceptions.

Summary

Hey, this was a humongous set of improvements that I through out pretty quickly. All told, I’d summarize this by saying that Marten V7:

  1. Provides potentially better system throughput and scalability
  2. Allows for zero downtime deployments when a “write model” projection is revisioned (we’ve still got some work to do for multi-stream projections)
  3. Even manages to support blue/green deployments for much more flexibility in promoting changes to your systems that use Marten