Be concise and token-efficient. Give direct answers, minimal examples, and no extra background. No sycophantic openers or closing fluff. No emojis or em-dashes.
These rules apply to every task in this project unless explicitly overridden. Bias: caution over speed on non-trivial work. Use judgment on trivial tasks.
State assumptions explicitly. If uncertain, ask rather than guess. Present multiple interpretations when ambiguity exists. Push back when a simpler approach exists. Stop when confused. Name what's unclear.
Minimum code that solves the problem. Nothing speculative. No features beyond what was asked. No abstractions for single-use code. Test: would a senior engineer say this is overcomplicated? If yes, simplify.
Touch only what you must. Clean up only your own mess. Don't "improve" adjacent code, comments, or formatting. Don't refactor what isn't broken. Match existing style.
Define success criteria. Loop until verified. Don't follow steps. Define success and iterate. Strong success criteria let you loop independently.
Use me for: classification, drafting, summarization, extraction. Do NOT use me for: routing, retries, deterministic transforms. If code can answer, code answers.
Per-task: 4,000 tokens. Per-session: 30,000 tokens. If approaching budget, summarize and start fresh. Surface the breach. Do not silently overrun.
If two patterns contradict, pick one (more recent / more tested). Explain why. Flag the other for cleanup. Don't blend conflicting patterns.
Before adding code, read exports, immediate callers, shared utilities. "Looks orthogonal" is dangerous. If unsure why code is structured a way, ask.
Tests must encode WHY behavior matters, not just WHAT it does. A test that can't fail when business logic changes is wrong.
Summarize what was done, what's verified, what's left. Don't continue from a state you can't describe back. If you lose track, stop and restate.
Conformance > taste inside the codebase. If you genuinely think a convention is harmful, surface it. Don't fork silently.
"Completed" is wrong if anything was skipped silently. "Tests pass" is wrong if any were skipped. Default to surfacing uncertainty, not hiding it.
NEventStore.Persistence.MongoDB is a MongoDB persistence adapter that implements IPersistStreams for the NEventStore event sourcing library. It enables storing event commits, snapshots, and stream metadata in MongoDB instead of relational databases.
- Treat the contract defined by
IPersistStreamsas a behavioral specification, not implementation detail - Preserve commit ordering, optimistic concurrency, duplicate commit detection, and snapshot semantics
- Maintain thread-safe
MongoPersistenceEnginebehavior; persistence instances are shared across threads - Support both synchronous and asynchronous code paths in parallel (see Conventions below)
MongoPersistenceEngine(sync) /MongoPersistenceEngine.Async(async) — Main persistence logic managing three collections:Commits,Streams,SnapshotsMongoPersistenceFactory— Creates configuredMongoPersistenceEngineinstances with dependency injectionMongoPersistenceWireup— Fluent configuration API in theNEventStorenamespace (not nested) for transparent integrationMongoPersistenceOptions— Encapsulates MongoClient, WriteConcern, database name, and collection namingMongoCommit/MongoFields— BSON class map and field constant definitions
Three collections store the event stream state:
Commits— Event commits with headers, events, and checkpoint numbers (usesAcknowledgedwrite concern)Streams— Stream metadata (ETag, RecycleBin marker, usesUnacknowledgedwrite concern for performance)Snapshots— Snapshots per stream version (usesUnacknowledgedwrite concern)
# Start MongoDB (required for tests; see docker/ folder for Compose scripts)
.\docker\DockerComposeUp.ps1
# Interactive build + optional tests
.\build.ps1
# Direct commands
dotnet restore ./src/NEventStore.Persistence.MongoDB.Core.sln --verbosity m
dotnet build ./src/NEventStore.Persistence.MongoDB.Core.sln -c Release --no-restore
dotnet test ./src/NEventStore.Persistence.MongoDB.Core.sln -c Release --no-build- GitHub Actions build on Windows machines and run tests on MongoDB on linux.
- GitVersion auto-patches assembly info from Git tags (do NOT manually edit version metadata)
- GitFlow workflow:
release/*andhotfix/*branches lock version increments - NuGet packaging:
nuget pack ./src/.nuget/NEventStore.Persistence.MongoDB.nuspecoutputs symbols package
- Set env var:
NEventStore.MongoDB=mongodb://localhost:27017/NEventStore - NUnit is active test runner (configured via
DefineConstants=NUNITin.csproj) - Acceptance tests inherit from
AcceptanceTestMongoPersistenceFactorywhich appends a GUID to database names to prevent parallel test conflicts - Use filter
-Trait:"Explicit"in Visual Studio test explorer to exclude long-running explicit tests
- Every feature must exist in both
MongoPersistenceEngine.cs(sync) andMongoPersistenceEngine.Async.cs(async) - Sync methods use blocking LINQ-to-MongoDB; async methods use
IAsyncCursorandTaskpatterns - Copy the sync version, convert
IMongoCollection<T>operations to async equivalents, replace blocking loops with async iteration - Cancellation tokens are threaded through async paths; sync paths ignore them
- Behavioral parity is required (same logic, same error handling)
MongoDB driver v2.30.0 uses CSharpLegacy (legacy byte ordering); v3.0.0+ default to Standard (compatible across drivers).
For backward compatibility, always use CSharpLegacy:
// Register globally once at startup
BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.CSharpLegacy));
// Or configure per field
BsonClassMap.RegisterClassMap<MongoCommit>(cm =>
{
cm.AutoMap();
cm.GetMemberMap(c => c.CommitId).SetSerializer(new GuidSerializer(GuidRepresentation.CSharpLegacy));
});For Dictionary/Object serialization:
BsonSerializer.RegisterSerializer(new ObjectSerializer(
BsonSerializer.LookupDiscriminatorConvention(typeof(object)),
GuidRepresentation.CSharpLegacy,
ObjectSerializer.AllAllowedTypes));- Commits:
Acknowledged— Safety critical (cannot lose events) - Streams & Snapshots:
Unacknowledged— Performance optimization (metadata can be reconstructed) - Do not change without understanding the trade-off
Always use constants from MongoCommitFields or MongoFields:
// GOOD
update = Builders<MongoCommit>.Update.Set(x => x.CheckpointNumber, checkpoint);
// ACCEPTABLE (MongoCommitFields.CheckpointNumber = "CheckpointNumber")
update = Builders<MongoCommit>.Update.Set(MongoCommitFields.CheckpointNumber, checkpoint);
// AVOID
update = Builders<MongoCommit>.Update.Set("CheckpointNumber", checkpoint); // Magic stringMongoPersistenceWireup is placed in the NEventStore namespace (not nested) for seamless extension method discovery:
namespace NEventStore;
public static class MongoPersistenceWireup
{
public static MongoPersistenceOptions UseMongoPersistence(this Wireup wireup, ...)
{
// ...
}
}Users then configure via fluent API:
Wireup.Init()
.UseMongoPersistence(mongoClient, options)
.LogToOutputWindow()
.Build();- Deleted streams are marked with
MongoSystemBuckets.RecycleBin = ":rb" - Not automatically cleaned up since v12.0 (caller responsibility)
- Test isolation uses unique database names per run (see
AcceptanceTestMongoPersistenceFactory)
- Async methods accept
CancellationTokenparameters and honor them - Do not introduce breaking cancellation behavior changes unless explicitly required
- Sync code does not need cancellation support
The dependencies/NEventStore/ folder is a Git submodule containing core interfaces:
IPersistStreams,IEventStream,ICommit,ISnapshotPersistenceWireup,IPipelineHook,IPipelineHookAsync- Update with:
git submodule update --init --recursive
- MongoDB.Driver (latest stable; currently 3.x with
StandardGUID handling) - NUnit 4.6.0, MSTest for testing
- .NET Framework 4.7.2, .NET Standard 2.1, .NET 6.0+
MongoDB.Driver 3.0 defaults GUIDs to Standard representation (incompatible with older data). Existing deployments must explicitly register CSharpLegacy serializer at startup or migrate data.
AcceptanceTestMongoPersistenceFactory appends Guid to database names to prevent conflicts when tests run in parallel. Do not hardcode database names in tests.
Previously, deleted streams were auto-removed. Now they're marked with RecycleBin flag. Call PurgeRecycleBinAsync() or PurgeRecycleBin() if needed.
- See README.md for local setup and build instructions
- See Changelog.md for versioned behavior changes before altering compatibility-sensitive code
- See dependencies/NEventStore/Readme.md for core NEventStore architecture
- See docs/ in the NEventStore submodule for testing strategy, performance notes, and benchmarks
Before making changes, check for scoped instructions or skills in the parent NEventStore project that may override conventions above:
- Scoped instructions: Check
.github/instructions/for files matching the area you are changing (applyTo glob patterns) - Reusable skills: Check
.agents/skills/for step-by-step workflows for recurring tasks