MongoZen 0.16.3
See the version list below for details.
dotnet add package MongoZen --version 0.16.3
NuGet\Install-Package MongoZen -Version 0.16.3
<PackageReference Include="MongoZen" Version="0.16.3" />
<PackageVersion Include="MongoZen" Version="0.16.3" />
<PackageReference Include="MongoZen" />
paket add MongoZen --version 0.16.3
#r "nuget: MongoZen, 0.16.3"
#:package MongoZen@0.16.3
#addin nuget:?package=MongoZen&version=0.16.3
#tool nuget:?package=MongoZen&version=0.16.3
MongoZen
MongoDB is nice and all, but the driver experience in C# usually sucks. You either end up with reflection-heavy "automagical" repositories, or you're writing manual BsonDocument boilerplate for aggregation pipelines like it's 2011.
Now, the idea behind MongoZen is to take a Mongo driver then add "Unit of Work" and "Identity Map" patterns from EF Core or RavenDB. But I wanted them to actually be fast and as MongoDB-native as possible.
So, why should you care?
- No Reflection on the Hot Path: Instead, there are Roslyn Source Generators to wire up your
DbSetand sessions at compile-time. If it's slow, it's not because of us. - Identity Map: If you load the same document twice in one session, you get the same instance.
- Automatic Change Tracking: Modify POCOs directly. When you call
SaveChangesAsync(), we figure out what changed and flush it in a single bulk write operation per collection. Or a transaction if supported. - RavenDB-inspired API:
Store,Delete,LoadAsync. It's a clean API that doesn't get in your way. - In-Memory Provider: Write tests that run fast without spinning up a Docker "testcontainer" container every time.
Quick Start
1. Define your Context
You need a partial class so the generator can do its thing.
public partial class MyDbContext : MongoZen.DbContext
{
// These properties are automatically initialized
public IDbSet<Person> People { get; set; } = null!;
public MyDbContext(DbContextOptions options) : base(options) { }
}
2. Use it
Everything happens inside a session.
var options = DbContextOptions.CreateForMongo("mongodb://localhost:27017", "MyDatabase");
var db = new MyDbContext(options);
await using var session = db.StartSession();
// Fetch Alice
var alice = await session.LoadAsync<Person>("alice-id");
// Just change the property. No .Update() needed.
alice.Age = 31;
// Load someone else while we're at it
var bob = new Person { Name = "Bob", Age = 25 };
session.Store(bob);
// One network round-trip to commit everything
await session.SaveChangesAsync();
Testing
Just swap the options. It's that simple.
var options = new DbContextOptions(); // Default is In-Memory
var testDb = new MyDbContext(options);
Optimistic Concurrency
MongoZen supports optimistic concurrency out of the box. This prevents "last-write-wins" scenarios where multiple users might overwrite each other's changes concurrently.
How it works
- Concurrency Token: By default, MongoZen looks for a property named
Version(configurable viaConventions.ConcurrencyPropertyName). - Automatic Tracking: When an entity is loaded, its version is tracked in the session.
- Atomic Updates: When
SaveChangesAsync()is called, MongoZen includes the expected version in the update filter:{ _id: "doc-id", Version: 1 } - Automatic Increment: If the update succeeds, the version is automatically incremented in the database and in your local POCO.
- Conflict Detection: If another process modified the document (changing its version), the update filter won't match. MongoZen detects this mismatch, identifies the conflicting documents, and throws a
ConcurrencyException.
Transactional Guarantees
- Replica Sets / Sharded Clusters: MongoZen uses native MongoDB transactions by default. If a single document in a bulk operation fails a concurrency check, the entire session is rolled back, ensuring your database never ends up in a partially-applied state.
- Standalone Nodes: If transactions are not supported, MongoZen still enforces the version check, but a failure may result in a partial save. We strongly recommend running a single-node replica set even for local development.
try
{
await session.SaveChangesAsync();
}
catch (ConcurrencyException ex)
{
// ex.FailedIds contains the IDs of the documents that caused the conflict
foreach (var id in ex.FailedIds) { ... }
}
Performance & Benchmarks
We compare MongoZen against a hand-optimized raw driver baseline. The goal isn't just to be "as fast as" the driver; it's to prove that the architectural overhead of Change Tracking and Identity Maps is negligible—or even beneficial—compared to manual boilerplate.
Results (1,000 Entities)
Test Environment: .NET 10, MongoDB Replica Set in Docker (directConnection=true).
| Method | Category | Count | Mean | Ratio | Allocated |
|---|---|---|---|---|---|
| IdentityMap_MongoZen_FromMemory | IdentityMap | 1000 | 7.8 ms | 0.02 | 37 KB |
| IdentityMap_RawDriver_NoTracking | IdentityMap | 1000 | 439.1 ms | 1.00 | 3420 KB |
| ReadModify_MongoZen_Set_OptimisticConcurrency | ReadModify | 1000 | 198.5 ms | 1.32 | 7450 KB |
| ReadModify_MongoZen_Set_NoConcurrency | ReadModify | 1000 | 110.2 ms | 0.73 | 6120 KB |
| ReadModify_RawDriver_Replace_NoConcurrency | ReadModify | 1000 | 150.4 ms | 1.00 | 6680 KB |
| ReadModify_RawDriver_Replace_ManualConcurrency | ReadModify | 1000 | 410.2 ms | 2.73 | 9150 KB |
| ReadModify_RawDriver_Set_NoConcurrency | ReadModify | 1000 | 142.1 ms | 0.94 | 6510 KB |
| ReadModify_RawDriver_Set_ManualConcurrency | ReadModify | 1000 | 385.6 ms | 2.56 | 8920 KB |
| Insert_MongoZen_OptimisticConcurrency | Insert | 1000 | 172.3 ms | 3.01 | 2410 KB |
| Insert_MongoZen_NoConcurrency | Insert | 1000 | 71.5 ms | 1.25 | 2415 KB |
| Insert_RawDriver_Bulk | Insert | 1000 | 57.2 ms | 1.00 | 1720 KB |
What these numbers mean:
- IdentityMap (Repeated Reads): Serving data from memory is always faster than hitting the wire. MongoZen is ~50x to 100x faster for repeated loads because it bypasses the network and serialization entirely.
- ReadAndModify (Implicit vs. Manual Optimization):
- The "Convenience" Comparison: Compare
ReadModify_RawDriver_Replace_NoConcurrency(150ms) withReadModify_MongoZen_Set_NoConcurrency(110ms). Even though MongoZen is doing the work of tracking and diffing, it is ~25% faster than the "easy" Raw Driver replacement approach because it generates precise$setupdates. - The "Expert" Comparison: Compare
ReadModify_RawDriver_Set_ManualConcurrency(385ms) withReadModify_MongoZen_Set_OptimisticConcurrency(198ms). MongoZen is nearly 2x faster than a hand-written partial update with manual concurrency management. Our internal batching and unmanaged diffing engine out-performs manual boilerplate.
- The "Convenience" Comparison: Compare
- Insert (The "Tracking Tax"): There is an overhead on inserts when concurrency tracking is enabled, primarily due to the initial setup of the versioning metadata and shadow creation. However, for non-versioned entities, the overhead is manageable (71ms vs 57ms). This is a highly favorable trade-off for the performance and safety gains achieved during the rest of the entity lifecycle.
More Info
Check out our Wiki for:
License
MIT. Go build something cool.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MongoDB.Driver (>= 3.3.0)
- SharpArena (>= 0.7.21)
- System.IO.Hashing (>= 10.0.7)
-
net8.0
- MongoDB.Driver (>= 3.3.0)
- SharpArena (>= 0.7.21)
- System.IO.Hashing (>= 10.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
- Initial work on MongoZen.