CQRSlite and the small framework that stops at the building blocks
A lightweight framework to help creating CQRS and Eventsourcing applications in C#
At a glance
- What is it?
- CQRSlite is a C# library for applications built on CQRS and event sourcing that gives you aggregates, a unit of work, a repository, a message router, snapshots and caching, and nothing else. You write the event store yourself, every part sits behind an interface, and the repository even verifies that its own documentation snippets still match the code.
- Who is it for?
- Adopt CQRSlite if you want CQRS and event sourcing in a C# service and would rather own the event store and the persistence model than inherit them from a framework, since the package brings one dependency and a set of interfaces you can replace. Do not adopt it if you want a turnkey platform with a message bus, projections and read model management already wired, because the read side is where most of the work in a CQRS application actually lives.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 33 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- gautema/CQRSlite1,120 ★ · +1
| Repository | 2026-10-19 | 2026-11-07 |
|---|---|---|
| gautema/CQRSlite | 1.1k | 1.1k |
1 Stars in the last 19 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Building blocks, and no opinions
CQRSlite is described as a small framework for building applications with CQRS and event sourcing in C#. The README is precise about its scope: it gives you the building blocks, aggregates, a unit of work, repositories, a message router, snapshots and caching, and stays out of the way of everything else.
The reason that last clause carries the design is in the next sentence. Every part is behind an interface, so any of it can be replaced. That is a narrower promise than a full framework makes, and a stronger one for the kind of team that ends up needing to swap the message bus in year two.
The feature list is short and each item maps to a known problem. Command sending, event publishing and queries, with handlers registered automatically, which removes the boilerplate of wiring a handler to a message type. A session that tracks aggregates as a unit of work. A repository that loads aggregates from their events and saves new ones. Optimistic concurrency checking. Snapshots for aggregates with long histories, and caching of aggregates between commands.
The audience is a C# developer with a domain that has behaviour worth modelling as events, and who is willing to write the persistence layer. That last part is the deal. The library does not include an event store, so the part that is hardest to get right, how events are stored, appended and read back, is yours to build.
The documentation is organised into concepts, a tutorial walking through the sample application from commands to read models, guides covering aggregates, routing, event stores, snapshots and caching in depth, and an API reference.
It started as a 2010 sample project
CQRSlite started as a CQRS sample project that Greg Young and Gaute Magnussen made in 2010, and that sample still lives on as gregoryyoung/m-r. The README links it directly.
That history explains the shape of the library better than any feature list. A teaching sample is small by design, it demonstrates the pattern with the minimum of ceremony, and it deliberately does not solve the surrounding engineering problems, because a sample that tried to solve everything would stop being a sample. The library that grew out of it kept that discipline, with interfaces for every moving part and a single dependency.
The copyright line in the licence section reflects the same continuity, naming Gaute Magnussen for 2020, while the code carries the accumulated work of the contributors since the m-r days. The repository still has a .mailmap file, which is the sort of detail that shows a project with a long history of outside contributions.
The lineage also explains the relationship to the wider CQRS literature that the README collects. It points at a five part series on real-world CQRS and event sourcing with ASP.NET and Redis, and at a cross examination of how CQRS works, while being careful to say that those articles were written for older versions and that details have changed even though the ideas hold. That is a nice touch for a library in this space, since the number of blog posts about CQRS that describe a specific implementation is large and the number that describe yours is small.
Aggregates record events instead of setting fields
The core discipline of the library is visible in the sample. Business rules live in aggregates, and those aggregates record events instead of changing state directly. The sample InventoryItem class keeps its count private and updates it in Apply methods that react to events:
public class InventoryItem : AggregateRoot
{
private void Apply(ItemsCheckedInToInventory e)
{
_count += e.Count;
}
public void Remove(int count)
{
if (count <= 0) throw new InvalidOperationException("cant remove negative count from inventory");
ApplyChange(new ItemsRemovedFromInventory(Id, count));
}
}The pattern has two halves. The public method contains the rule and raises an event through ApplyChange, and the Apply method contains the state change that results from that event. The count is only ever changed in Apply, never in Remove.
The immediate benefit is that the in-memory state and the event history cannot drift apart, because the state is derived from the events and the events are the record. The second benefit is subtler and more valuable, which is that the Apply methods are the only place that knows how a state change works, so a replay of the event stream reconstructs exactly the same state, and the code that has to survive an event schema change lives in one small method.
The guards in Remove are the other half of the example. A non positive count and a count larger than the current stock both raise InvalidOperationException before any event is recorded, which is the correct place for a rule that must not produce an event.
The session is the unit of work, and the version is the lock
Command handlers follow a three step shape: load an aggregate through the session, call a method on it, and commit. The sample handler for removing items from inventory reads like this:
public async Task Handle(RemoveItemsFromInventory message, CancellationToken token)
{
var item = await _session.Get<InventoryItem>(message.Id, message.ExpectedVersion, token);
item.Remove(message.Count);
await _session.Commit(token);
}Three things are happening in three lines. The session is the unit of work and the aggregate cache in one, so the same aggregate loaded twice in one command is the same object, and the state change made by the business method is not written to the store until commit.
The ExpectedVersion argument is the optimistic concurrency check, and it comes from the message rather than from the aggregate, which is the important detail. The caller states which version of the aggregate it believes it is acting on, and the session compares that against what the store holds. If another command has already appended events, the versions no longer match and the operation fails instead of silently overwriting the other change. The same value is also passed along when the command is sent, in the sample as new RemoveItemsFromInventory(id, number, version).
The rest of the application does not touch the session or the aggregates at all. It sends commands and queries through the router, and the sample controller does exactly that with a single line, awaiting the command sender with the message and a cancellation token. Handlers are registered automatically, which is what keeps the wiring out of the application code.
You write the event store, and the package brings one dependency
The installation instruction is a single command:
dotnet add package CqrsLiteThe package targets .NET 10 and .NET Standard 2.0, so it also runs on .NET Framework 4.6.2 and later, and its only dependency is Microsoft.Extensions.Caching.Memory. One dependency, for a library that includes a caching feature, is a good sign about how the optional pieces are wired.
The event store is deliberately not included. The README says you provide the event store for your database and points to the event store guide, and the guides section covers event stores in depth alongside aggregates, routing, snapshots and caching. The repository interface is the seam: the framework defines how an aggregate is loaded from its events and how new ones are saved, and the guide describes how to back that with your storage.
That choice has consequences. In exchange for the work, you own the schema of your event log, which is the part of an event sourced system that is hardest to change later, and you can store events in whatever shape suits your database, a table with a sequence and a JSON payload, a document store, an append only log. In return for the convenience, you avoid depending on a framework's event store implementation and its own versioning of it.
The .NET Standard 2.0 target is what makes the library usable in older .NET Framework applications, which is broader than a .NET 10 only package would be, at the cost of a lower API ceiling.
Snapshots and caching for the two slow paths
Two of the features exist to solve performance problems that appear once an event stream gets long, and both are listed among the building blocks rather than hidden behind configuration.
Snapshots are for aggregates with long histories. Replaying thousands of events to rebuild one aggregate on every command is the classic cost of event sourcing, and a snapshot stores the state at a point in the stream so that the replay starts from there. The feature list treats it as a first class part of the framework, and the guides cover snapshots in depth alongside the other pieces.
Caching of aggregates between commands is the other one, and the dependency list explains the mechanism. Microsoft.Extensions.Caching.Memory is the only dependency, which means the aggregate cache is backed by the standard in-process memory cache rather than by something bespoke, and it means the cache is per process, not shared.
That last point is worth understanding before you design around it. A per-process aggregate cache is a latency optimisation inside one instance, not a distributed consistency mechanism, and it sits above the optimistic concurrency check rather than replacing it. Two application instances can each hold a cached aggregate, and the version check is still what prevents them from overwriting each other. The two features are complementary, and the concurrency check is the one that has to be right.
A repository that checks its own documentation
The licence story needs a note. The README states Apache License 2.0 with the full text, and the copyright line names Gaute Magnussen for 2020. The repository's own metadata, however, records the licence as something GitHub cannot classify, with a custom licence, and the file is named LICENSE.md rather than the conventional LICENSE. The two are not necessarily in conflict, since a LICENSE file with a modified header can confuse automated classifiers, but a team shipping this internally should read LICENSE.md rather than assume the Apache terms from the README paragraph.
The build story is straightforward. You need the .NET 10 SDK, and the two commands are:
dotnet build
dotnet testThe interesting part is what CI does with the documentation. The docs are checked against the code with a script, python3 .github/scripts/check_docs.py, which verifies that code snippets in the docs still match the files they come from and that links between pages work, and CI runs it on every push.
That is worth pausing on, because it is unusual and because it solves a real problem. Documentation with code samples rots, and the usual failure is a sample that no longer compiles and a reader who loses an afternoon. Here the samples in the README are marked with a snippet comment naming the file they come from, which is how the checker knows what to compare against. The result is documentation that fails the build when the code moves, which is a stronger guarantee than any convention about updating the README in the same pull request.
The repository layout follows a normal .NET shape, with CQRSlite.sln, a Framework/ directory for the library, a Sample/ directory with three projects, CQRSCode, CQRSTest and CQRSWeb, a docs/ directory, global.json and central package management through Directory.Packages.props. There are no GitHub releases, so the version to pin is the one on NuGet rather than a tag.
Editorial conclusion
Adopt CQRSlite if you want CQRS and event sourcing in a C# service and would rather own the event store and the persistence model than inherit them from a framework, since the package brings one dependency and a set of interfaces you can replace. Do not adopt it if you want a turnkey platform with a message bus, projections and read model management already wired, because the read side is where most of the work in a CQRS application actually lives. Verify three things first: that your storage can be adapted to the event store contract described in docs/guides/event-store.md, that the .NET version you target is covered, since the library targets .NET 10 and .NET Standard 2.0, and which licence text governs, because the README states Apache 2.0 while the repository's own metadata classifies the licence as custom. The last push was on 2026-10-05 and the repository has no GitHub releases.
Frequently asked questions
What does CQRSlite give you?
Building blocks for CQRS and event sourcing in C#, namely aggregates, a session that acts as a unit of work, a repository, a message router, snapshots and aggregate caching. Every part sits behind an interface, so any of them can be replaced, and handlers are registered automatically.
How do I install CQRSlite?
Run dotnet add package CqrsLite. The package targets .NET 10 and .NET Standard 2.0, so it also runs on .NET Framework 4.6.2 and later, and its only dependency is Microsoft.Extensions.Caching.Memory.
Do I have to write the event store for CQRSlite?
Yes. The framework supplies the repository interface and the aggregate lifecycle, but you provide the event store for your database, and the repository points to a guide at docs/guides/event-store.md for that part.
How does optimistic concurrency work in CQRSlite?
The expected version travels in the command message and is passed to the session when the aggregate is loaded, so the session can compare it with the stored version and refuse the operation if another command has appended events in the meantime. The sample handler shows the pattern with _session.Get and a message.ExpectedVersion value.
What are snapshots and caching used for in CQRSlite?
Snapshots are for aggregates with long histories, so that a state does not have to be rebuilt by replaying every event. Caching of aggregates between commands uses the in-process memory cache, which is why Microsoft.Extensions.Caching.Memory is the only dependency, and it is a per-process optimisation rather than a distributed consistency mechanism.
What licence is CQRSlite released under?
The README states the Apache License 2.0 with copyright 2020 Gaute Magnussen, but the repository's own metadata classifies the licence as custom, which GitHub cannot map to a standard type, and the file is LICENSE.md. The last push was on 2026-10-05 and the repository has no GitHub releases.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/gautema-cqrslite)Star history chart for your README
[](https://hysenlabs.com/projects/gautema-cqrslite)Was this useful?