
Also see our What is Event Sourcing? content on the Marten website
The Cratis team recently published Event sourcing in .NET with Chronicle: from zero to first projection as a nice, gentle introduction to Event Sourcing using their stack against a simple problem domain. Let's do the same thing using Marten with a guest appearance near the end from Wolverine to get to the full "Critter Stack."
Setting up
First up, you need a PostgreSQL database. Somewhere, anywhere. One of the great things about PostgreSQL is that it's nearly ubiquitous now with first class managed hosting in all major cloud providers. It also plays very nicely within Docker, making it possible for us to quickly spin up a new database with something like this:
docker run -d --name postgres -p 5433:5432 -e POSTGRES_PASSWORD=postgres postgres:17Next let's seed a little .NET project just to play with Marten:
mkdir Quickstart && cd Quickstart
dotnet new console --framework net10.0
dotnet add package MartenNext, let's create some events that we'll be persisting to model the workflow of a book in a library:
public record BookAdded(string Title, string Isbn);
public record BookBorrowed(string MemberName);
public record BookReturned;That's it. Marten (and the Critter Stack in general) tries really hard to keep marker interfaces, base classes, and attributes to a bare minimum, so events are "just code." You don't have to use C# or F# record types, but many users prefer that for the terseness of the code and because events should be treated as immutable facts anyway.
On to actually creating the Marten store for that database:
await using var store =
DocumentStore.For("Host=localhost;Port=5433;Database=postgres;Username=postgres;Password=postgres");In the real world though, you're almost certainly going to be mixing Marten into a .NET application with DI registrations using the IServiceCollection.AddMarten() extension method like so:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("Marten")!);
opts.DatabaseSchemaName = "library";
// Same two read models as the console quickstart, same Evolve() methods
opts.Projections.Snapshot<Book>(SnapshotLifecycle.Inline);
opts.Projections.Snapshot<BorrowedBook>(SnapshotLifecycle.Inline);
})
// Wolverine's transactional middleware + outbox around Marten sessions. Fast event
// forwarding publishes each appended event to Wolverine handlers as the session commits.
.IntegrateWithWolverine(x =>
{
// This ugly looking option unlocks a lot of scalability
// and flexible deployment options
x.UseWolverineManagedEventSubscriptionDistribution = true;
// Just enabling Marten events to be published through to
// Wolverine message handlers
x.UseFastEventForwarding = true;
});Don't worry about the Wolverine part yet. Let's move on to...
Persisting your first events
This is starting up with all the default behavior that is purposely optimized for a smoother getting started story for new users. To actually store some events, let's write this code:
// You'll likely not often write this code by hand,
// but this is Marten's unit of work
// With the DI integration, you'd probably inject this
// IDocumentSession into a controller or message handler
await using var session = store.LightweightSession();
// Start a new stream
var streamId = session
.Events
.StartStream(new BookAdded("A Tale of Two Cities", "ABCD"), new BookBorrowed("Jeremy")).Id;
// Persist all outstanding changes
await session.SaveChangesAsync();
// Append more events to the new stream
session.Events.Append(streamId, new BookReturned());
// Commit the 2nd transaction
await session.SaveChangesAsync();Notice that at no point have I talked about running database scripts. Marten does automatic database migrations behind the scenes as needed for an "it just works" development experience -- but Marten can also play nicely with more controlled database migration policies.
Marten also supports "Dynamic Consistency Boundary" style event sourcing
Next, we really do need to see what the state of a Book is, so let's introduce our first projection. For a simple example, I'm going to first show a mutable aggregate that uses our conventional method approach:
public class Book
{
public Guid Id { get; set; }
public string Title { get; set; }
public string Isbn { get; set; }
public bool OnLoan { get; set; }
public string? BorrowedBy { get; set; }
public static Book Create(BookAdded added)
=> new() { Title = added.Title, Isbn = added.Isbn };
public void Apply(BookBorrowed borrowed)
{
BorrowedBy = borrowed.MemberName;
OnLoan = true;
}
public void Apply(BookReturned returned)
{
BorrowedBy = null;
OnLoan = false;
}
}Or, if you prefer a little more explicit code and want your aggregate type to be immutable, you could do this perfectly valid alternative:
public record Book(Guid Id, string Title, string Isbn, bool OnLoan, string? BorrowedBy, DateTimeOffset? BorrowedTime)
{
public Book Evolve(IEvent e) => e.Data switch
{
BookAdded added => new Book(e.StreamId, added.Title, added.Isbn, OnLoan: false, BorrowedBy: null, BorrowedTime: null),
// I snuck in a change here just to show off the usage of Marten metadata
// like the timestamp in projections
BookBorrowed borrowed => this with { OnLoan = true, BorrowedBy = borrowed.MemberName, BorrowedTime = e.Timestamp},
BookReturned returned => this with { OnLoan = false, BorrowedBy = null, BorrowedTime = null},
_ => this
};
}Marten has been around a long time now, and it's been used by a lot of different people with different approaches, and that has admittedly led to alternative syntax options. Some people obviously won't like that, but other people will appreciate having alternatives, so ¯_(ツ)_/¯.
So far I'd say it's mostly a wash between Marten and Cratis at the very bare-bones basics, except that I think that Marten's programming model for defining projections is clearer than the attribute-heavy syntax that Cratis requires -- but I'm obviously biased here.
As we move into options for querying projection data and incorporation into automated tests though, Marten is going to pull away.
First though...
Let's talk about consistency
To get the most current version of that Book, we'll add this single line of code to our script above:
// Get the most current version of the Book aggregate
// for the supplied event stream
var book = await session.Events.FetchLatest<Book>(streamId);The code above is going to give you a Book object that reflects the current events for that event stream regardless of whatever the ProjectionLifecycle is for the Book type.
But wait, you're asking, what does ProjectionLifecycle mean?!?
And on that note, let's stop and talk about strong consistency vs eventual consistency. By strong consistency we mean that querying the projection data should always give us system state that exactly reflects the currently persisted events. In other words, the projected data is always perfectly synchronized with the raw event data. Using eventual consistency, the persisted projection data is being built by some kind of asynchronous process and the projected data may often lag the raw, master event data. Even though it's genuinely challenging to use for us poor developers and sometimes quite confusing for users, eventual consistency does have some real advantages for system efficiency and scalability.
Almost every event sourcing technology supports eventual consistency, but Marten is one of the very few event sourcing tools that happily supports options for both strong and eventual consistency depending on your use case.
Backing up to the ProjectionLifecycle concept that I mentioned before, Marten supports three distinct options for when a projection is built:
Liveis when Marten builds a projected object likeBookfrom the raw events by querying the raw events and applying them in memory to create the aggregated view. In this case, Marten is never storing the actual state of theBookobjects in the database.Inlinemeans that Marten will update persisted projections at the same time that it appends event data and in the same database transaction. In this case theBookdata is persisted as a Marten document in the database. This gives you strong consistency between events and projections.Asyncis Marten's support for eventual consistency where the projections are built by an asynchronous process called the async daemon and persisted as a Marten document.
Our initial usage up above was treating the Book type as a Live projection by default, and our usage of FetchLatest<Book>(id) worked by fetching the raw event data into memory and creating a Book object by applying each event. The Live lifecycle is perfectly fine for relatively low numbers of events at a time, but it can be sluggish in real systems that try to hydrate projections from thousands of events at a time.
Let's switch the Book projection to run Inline first:
await using var store =
DocumentStore.For(opts =>
{
opts.Connection("Host=localhost;Port=5433;Database=postgres;Username=postgres;Password=postgres");
// Let's update Book inline
opts.Projections.Snapshot<Book>(SnapshotLifecycle.Inline);
});To illustrate how Inline works, let's try to immediately query against the Book projections after appending events using Marten's strong support for LINQ querying:
// You'll likely not often write this code by hand,
// but this is Marten's unit of work
await using var session = store.LightweightSession();
// Start a new stream
var streamId = session
.Events
.StartStream(new BookAdded("A Tale of Two Cities", "ABCD"), new BookBorrowed("Jeremy")).Id;
// Persist all outstanding changes
await session.SaveChangesAsync();
// Append more events to the new stream
session.Events.Append(streamId, new BookReturned());
// Commit the 2nd transaction
await session.SaveChangesAsync();
// Let's go query for our new Book against the projection
var ourBook = await session.Query<Book>()
.Where(x => x.Title.Contains("Two Cities", StringComparison.OrdinalIgnoreCase))
.FirstOrDefaultAsync();
Console.WriteLine($"This book has been borrowed by {ourBook.BorrowedBy}");If you've ever used NoSQL databases or other event sourcing tools (like Cratis), you might be happily surprised that the LINQ query against the projection works. Of course, if you're coming from traditional relational databases that are ACID-compliant (strongly consistent), you wouldn't expect anything different and you're going to be unpleasantly surprised by some of your early usages of alternative approaches.
As a contrast, let's look at some of the sample code from Cratis:
// Some elided code that appends and persists events
// Give the freshly registered read models a moment to come online before the first query.
await Task.Delay(TimeSpan.FromSeconds(5));
var books = await eventStore.ReadModels.GetInstances<Book>();
foreach (var book in books)
{
Console.WriteLine($"Book read model: {book.Title} ({book.Isbn}) OnLoan={book.OnLoan} BorrowedBy={book.BorrowedBy}");
}First off, it's just sample code to show a concept, but using Task.Delay() or Thread.Sleep() in automated tests for asynchronous behavior is frequently harmful, and we strongly recommend against doing that in real work.
The bigger issue with the sample above is that Chronicle's read models are eventually consistent by default. With Marten, you can happily opt into strong consistency with the Inline lifecycle.
Even so, eventual consistency can absolutely provide some real value to your system, so let's switch Book to using the Async mode and look at how Marten makes this easier. First, the configuration -- and I'm switching to an IHost model here because that's what you'll likely do in a real system using asynchronous projections:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("Marten")!);
opts.DatabaseSchemaName = "library";
opts.Projections.Snapshot<Book>(SnapshotLifecycle.Async);
})
// Wolverine's transactional middleware + outbox around Marten sessions. Fast event
// forwarding publishes each appended event to Wolverine handlers as the session commits.
.IntegrateWithWolverine(x =>
{
// This ugly looking option unlocks a lot of scalability
// and flexible deployment options and turns on the Marten
// async daemon
x.UseWolverineManagedEventSubscriptionDistribution = true;
// Just enabling Marten events to be published through to
// Wolverine message handlers for a later example
x.UseFastEventForwarding = true;
});Now, let's write the same sample where we append events then try to query the projected data, but this time we do need to account for the eventual consistency -- at least in tests anyway!
// You'll likely not often write this code by hand,
// but this is Marten's unit of work
await using var session = store.LightweightSession();
// Start a new stream
var streamId = session
.Events
.StartStream(new BookAdded("A Tale of Two Cities", "ABCD"), new BookBorrowed("Jeremy")).Id;
// Persist all outstanding changes
await session.SaveChangesAsync();
// Append more events to the new stream
session.Events.Append(streamId, new BookReturned());
// Commit the 2nd transaction
await session.SaveChangesAsync();
// Watch for the change here!
var ourBook = await session.QueryForNonStaleData<Book>(5.Seconds())
.Where(x => x.Title.Contains("Two Cities", StringComparison.OrdinalIgnoreCase))
.FirstOrDefaultAsync();
Console.WriteLine($"This book has been borrowed by {ourBook.BorrowedBy}");You'll have to squint to see the difference, but this time I used the QueryForNonStaleData<Book>() API that first waits for asynchronous projections in the current database to catch up to the last known event sequence at the time the query is first made, then runs a LINQ query as normal.
Mostly for testing, here's another way to do this:
// Wait for all asynchronous projections and subscriptions to be caught up to the latest
await store.WaitForNonStaleProjectionDataAsync(5.Seconds());
// Watch for the change here!
var ourBook = await session.Query<Book>()
.Where(x => x.Title.Contains("Two Cities", StringComparison.OrdinalIgnoreCase))
.FirstOrDefaultAsync();Marten is the product of a community that highly values automated testing, so we quite naturally have a robust set of integration test helpers
Sneak peek at command handlers with Wolverine
Adding Wolverine to the mix gives you a robust full-stack CQRS framework. Used idiomatically, command handlers with Marten event sourcing can be expressed using our version of the "Decider" pattern like so:
public record ReturnBook(Guid Id);
public static class ReturnBookHandler
{
// Use the Decider pattern
[WolverinePost("/api/books/return")]
public static BookReturned? Post(
ReturnBook command,
[WriteAggregate] Book book)
{
if (!book.OnLoan)
{
return null;
}
return new BookReturned();
}
}The gist of it is that you can mostly write pure functions that take in a command (ReturnBook) and an object that represents the current state of the event stream (Book), then "decide" what events should be emitted based on the inputs. And while we've invested a great deal of effort in making all parts of the Critter Stack play well inside automated integration tests, you can also write little focused "solitary" unit tests against your actual business logic as is hopefully shown above.
As you might guess, Wolverine and Marten are doing quite a bit around that little pure function:
- Fetching the current version of the
Bookdata for the most current events regardless of what theProjectionLifecyclehappens to be - Doing some optimistic concurrency checks for you both coming and going
- Tracking metadata on any events persisted for correlation, causation, the current user
- Emitting Open Telemetry spans with all that same correlation metadata
- Managing the transactional boundaries, including any usage of Wolverine's transactional outbox if any cascading messages are being sent at the same time
- Wolverine middleware could be used for validation up above as well
DCB usage with Marten & Wolverine would still have you using the "Decider" pattern, but with slightly different syntax
Read much more in our tutorial Event Sourcing and CQRS with Marten.
Summary
We've invested a lot in having a developer-friendly getting started story for Marten and I hope that showed. We've also been around for quite some time and Marten has been used in a wide variety of systems and by this point, Marten is very battle-hardened in a way that a new tool just won't be able to match. We've also built in a lot of behind-the-scenes support for resiliency, observability, and scalability that we think makes Marten stand apart from any other Event Sourcing solution for .NET and frankly competitive with the very best of anything else in other development ecosystems. The combination with Wolverine adds a lot more capabilities to the Critter Stack for asynchronous messaging and much more advanced command handling.
On the development side, the Critter Stack can do much more to simplify your application code than any other Event Sourcing or Event Driven Architecture tooling in .NET and we have far and away the best story for test automation integration. At production support time, the Critter Stack has a soup-to-nuts observability feature set as well as the newer CritterWatch add-on as your eye in the sky.
We may get around to our version of a comparison to Cratis Chronicle + Arc, but for now I'd just say that the Critter Stack has vastly more functionality, a more proven track record, and still comes with an easy getting started story even with all that functionality.
The finished code is in CritterStackSamples/LibraryQuickstart -- but just be aware that I wrote some simplified code just for this post.



