Event Sourcing with .NET and Marten
The year 2026 began with unusually heavy snowfall in the Netherlands. It was more than we are used to, and in some cases enough to cause buildings to collapse.
I work for an organization that provides Building Information Management services, so it made me think: what happens after a building collapses? There will probably be investigations into the specifications that were used at the time. Do we keep the full history? Do we know whether changes were made, why they were made, when they were made, and by whom?
I had already been looking for a reason to dive into event sourcing, and this felt like a useful example. Event sourcing is not only about storing the current state of something, but about storing the facts that led to that state. That makes it a good fit for systems where history matters.
In this post, I’ll use changing roof specifications as a small example to explain event sourcing, and show how it can be implemented in .NET with Marten.
When CRUD starts to fall short
In a typical CRUD application, we store the current state of things. A building plan has a roof specification, and when that specification changes, we update the existing data.
That works well when the current state is all we need.
But in this example, the current state is not enough. If an investigation starts after a collapse, we need to know what the system knew at a specific point in time. Which roof specification was active? What changed afterwards? Who made those changes, and why?
Of course, we can add audit tables, version numbers, or change logs to a CRUD system. Sometimes that is exactly the right choice. But it also means that history becomes something we add around the model, instead of something the model is built around.
There is another problem as well: a single update is often not just an update.
Changing a roof specification might need to trigger other work:
- store the new specification;
- record who made the change;
- mark the plan as needing approval again;
- recalculate compliance checks;
- notify reviewers;
- publish an integration event to another system.
And of course, a building model is much more than a roof specification. It contains geometry, materials, rooms, systems, regulations, approvals, comments, issues, and quality checks. Especially when working on model quality, things like clash detection and validation can produce a lot of changes and follow-up work.
That makes the idea of one perfect, always up-to-date current state harder to maintain. Some parts of the system may be updated immediately, while other views are updated later. A clash detection result, a reporting model, or a compliance overview might not need to be updated in the same transaction as the original change.
Event sourcing fits well with that kind of eventually consistent model. The events record what happened, and different projections can process those events into the views they need.
In a CRUD system, this logic often grows around a generic update operation. After a while, it becomes harder to see what actually happened in the domain. Was the roof specification corrected, replaced, approved, rejected, or recalculated?
Event sourcing takes a different approach. Instead of treating history as extra information, it makes history the source of truth.
What event sourcing changes
With event sourcing, we do not store only the latest version of the building plan. We store the facts that happened to it.
For example:
- a building plan was created;
- a roof specification was added;
- the allowed snow load was changed;
- the plan was sent for approval;
- the plan was approved.
These events are stored in order. To get the current state, we replay those events and apply them one by one.
That means the application can still answer the normal question: what does this building plan look like right now?
But it can also answer the historical question: how did it get there?
The tools used in this post
Marten
Marten is a .NET library that uses PostgreSQL to provide a document database experience and first-class support for event sourcing.
In the context of this post, Marten will give us:
- An event store (append-only streams of events per building plan).
- A way to rebuild aggregates by replaying events.
- Projections to create read models (so you can still answer “what does it look like right now?” efficiently).
The nice part: you get event sourcing without introducing a separate event-store technology, PostgreSQL stays in the center.
Wolverine
Wolverine is a .NET framework for messaging and background processing. Think: commands in, events out, handlers running reliably.
Why it matters in an event-sourced system:
- You often want events to trigger other work: update projections, send notifications, kick off workflows, integrate with other systems.
- Wolverine helps you structure that work as handlers and run it consistently.
- It pairs naturally with Marten so “store events” and “react to events” feels like one cohesive flow.
In this post, we’ll use Wolverine to handle actions around our building plan changes, so the example doesn’t stop at “we stored some events,” but shows how an event-driven .NET app actually hangs together.
The example application
For this post, I created a simple demo application where users can authenticate, create a building plan, and update the roof specification. The application uses Aspire, Keycloak, PostgreSQL, Marten, and Wolverine as the most important parts. Everything is done by using REST endpoints; there is no UI involved.
The architecture looks like this:

The important flow looks like this:
- the API receives a request;
- the request is translated into a command;
- Wolverine handles the command;
- the command handler appends one or more events to a Marten stream;
- Marten projections turn those events into read models;
- the API can return either the current state or the history.
That last distinction is the point of the example. The current roof specification is useful, but the sequence of changes is what helps answer the investigation questions.
The building plan as a stream
In this demo, a building plan is modeled as a stream of events. The stream contains everything that happened to that plan.
For example, a plan might have a history like this:
BuildingPlanCreatedRoofSpecificationAddedRoofSpecificationChanged
Those event names are more specific than a generic update. They describe what happened in the domain, which makes the history easier to understand later.
If someone asks what the current roof specification is, we can build that from the stream. If someone asks how the roof specification changed over time, we can inspect the same stream and see the individual events.
What this looks like in Marten
In code, events can be simple records. The important part is that they describe facts that happened in the domain.
public record BuildingPlanCreated(Guid BuildingPlanId, string Name);
public record RoofSpecificationAdded(
Guid BuildingPlanId,
decimal AllowedSnowLoad,
string Material);
public record RoofSpecificationChanged(
Guid BuildingPlanId,
decimal AllowedSnowLoad,
string Reason);
When something changes, we append a new event to the stream for that building plan.
session.Events.Append(
buildingPlanId,
new RoofSpecificationChanged(
buildingPlanId,
request.AllowedSnowLoad,
request.Reason));
await session.SaveChangesAsync();
The stream is the history. Projections can then turn those events into the current view of the building plan, or into other views such as a timeline, compliance overview, or reporting model.
From request to event
The API still looks familiar from the outside. A client sends a request to create a building plan or update a roof specification.
Internally, the interesting part is that the request does not just overwrite a row. It becomes a command, and the command decides which event should be stored.
The flow is roughly:
request to change a roof specification
-> ChangeRoofSpecification command
-> command handler validates the change
-> RoofSpecificationChanged event is appended to the stream
-> projections update the current read model
The event is the durable fact. The projection is a convenient view built from those facts.
That means we can optimize reads without giving up history. The API does not need to replay every event for every request if a projection already contains the current state. But if we need the history, the event stream is still there.
Reading state and history
This is where the model starts to pay off.
For normal application usage, we can query a read model and show the current building plan. That is the same kind of experience users expect from a CRUD application.
For investigation or audit scenarios, we can query the events. That gives us the timeline: what changed, when it changed, and who made the change if we store that information as metadata.
So the system can answer both questions:
- what does the building plan look like now?
- how did it get there?
A note on projections
One thing I like about Marten is that projections can be added or changed later. The events are the source of truth, so a projection is something we can rebuild from the existing event streams.
That is useful when requirements change. Maybe the first version of the application only needs the current roof specification, but later we want a compliance view, a reporting view, or a timeline for investigations. With event sourcing, those views can be built from the same history.
Of course, this does not mean projections are free. You still need to think about how they are updated, how much data they contain, and when they should be rebuilt. But the important thing is that the original events are still available.
Running the example
The full demo application is available on GitHub:
To run it locally:
dotnet run --project src/BIMEvents.AppHost/BIMEvents.AppHost.csproj
Aspire starts the supporting services, including PostgreSQL and Keycloak. When the application starts, the console shows a link to the Aspire dashboard.

After all services are running, the dashboard should look like this:

The repository contains .rest files that walk through the full scenario:
- getting an access token;
- creating a building plan;
- adding a roof specification;
- changing the roof specification;
- reading the current state;
- reading the activity history.
I am keeping those exact calls in the repository instead of copying them into this post, because the goal here is to explain the flow rather than document every endpoint.
Closing
This was intentionally a short dive into event sourcing. I mainly wanted to explore Marten, build something with it, and write down what I learned.
A real building model contains far more than a roof specification, and a real event-sourced system has many more design decisions to make. But that is also why I like this example: it shows the basic idea without making the demo too large.
If all you need is the current value, CRUD is often the simplest choice. But if you need to understand how that value came to be, or if one change needs to feed multiple views and processes, event sourcing becomes interesting.
Marten makes that approachable because it gives you an event store, projections, and document storage on top of PostgreSQL. Wolverine adds a nice way to handle the commands and follow-up work around those events.
There are still plenty of questions once you move beyond a demo. What about multi-tenant systems? What about millions of streams? What about storing history forever? What about rebuilding projections in production?
The good news is that “keep all events forever and replay everything all the time” is not the only operating mode. Real-world event-sourced systems use patterns like snapshots, stream compaction, archiving, and projections to keep things practical.
I only scratched the surface, but I do think event sourcing is a powerful way of working and worth investigating further.
I hope this post gives you a useful starting point.
Further Reading & Resources
- Wolverine: Easier message handling
- Marten: .NET Transactional Document DB and Event Store on PostgreSQL
- The Shade Tree Developer: A lot of blog posts about Marten, Wolverine and other related topics.
- Event Sourcing and CQRS with Marten: Article for the Code Magazine by Jeremy Miller.