A high-performance, zero-allocation event sourcing library for .NET with streaming capabilities and production-grade reliability features.
- Zero-Allocation Design: Optimized for performance-critical applications with minimal garbage collection
- Event Sourcing: Full event sourcing support with append-only event store
- Stream Consumers: Production-grade consumers for reliable event consumption
- Projections: Multiple projection types for denormalized views
- Snapshots: Optimize aggregate loading with configurable snapshot strategies
- SQL Adapters: SQL Server and PostgreSQL support with Testcontainers testing
- Checkpoint Tracking: Automatic position tracking with recovery capabilities
- Comprehensive Testing: Extensive test suite and integration testing patterns
dotnet add package ZeroAlloc.EventSourcingvar adapter = new SqlEventStoreAdapter(connectionString);
var serializer = new JsonEventSerializer();
var typeRegistry = new EventTypeRegistry();
var eventStore = new EventStore(adapter, serializer, typeRegistry);
// Append events
var streamId = new StreamId("order-123");
await eventStore.AppendAsync(
streamId,
new[] { (object)new OrderPlacedEvent { Id = "123", Amount = 100m } },
StreamPosition.Start
);
// Read events
var envelope = await eventStore.ReadAsync(streamId);
foreach (var evt in envelope.Events)
{
Console.WriteLine($"Event: {evt.Event}");
}| Package | Description |
|---|---|
ZeroAlloc.EventSourcing |
Core library with event store and serialization |
ZeroAlloc.EventSourcing.Aggregates |
Aggregate patterns and source generation |
ZeroAlloc.EventSourcing.InMemory |
In-memory event store for testing |
ZeroAlloc.EventSourcing.PostgreSql |
PostgreSQL adapter with native streams |
ZeroAlloc.EventSourcing.SqlServer |
SQL Server adapter with native streams |
ZeroAlloc.EventSourcing.Kafka |
Kafka stream consumer for external event sources |
ZeroAlloc.EventSourcing.Telemetry |
BCL ActivitySource + Meter decorator around IAggregateRepository<,> — OpenTelemetry spans and metrics with no OTel SDK dependency |
All packages follow zero-allocation principles and are optimized for high-throughput scenarios.
Correctness-matched overhead vs a hand-rolled SQLite event store (same connection, both transactional, both check stream version inside the transaction). .NET 8.0.26, i9-12900HK, BenchmarkDotNet v0.15.8.
| Operation | Hand-rolled | ZA.EventSourcing | Overhead |
|---|---|---|---|
| Append 1 event (transactional, OCC check) | 80.7 µs / 3.80 KB | 106.3 µs / 4.79 KB | +33% time, +26% alloc |
| Read 100-event stream (ordered) | 66.0 µs / 11.95 KB | 140.9 µs / 25.23 KB | +114% time, +111% alloc |
The delta is the cost of the IEventStore + IEventStoreAdapter + IEventSerializer + IEventTypeRegistry layer — what you get for it is typed events, pluggable serialization, optimistic concurrency through StreamPosition, and composability with Aggregate<T>, projections, snapshots, upcasters, and dead-letter handling. At real-database latency the abstraction tax becomes negligible vs the SQL round-trip.
Full methodology: docs/performance.md.
ZeroAlloc.EventSourcing includes production-grade stream consumers for reliable event consumption with automatic position tracking, retry logic, and configurable error handling.
- Position Tracking: Resume consumption from exact point after restart
- Batch Processing: Configurable batch sizes (1-10,000 events) for optimal throughput
- Retry Logic: Exponential backoff with configurable max retries
- Error Handling: FailFast, Skip, or DeadLetter strategies
- Commit Strategies: AfterEvent, AfterBatch, or Manual control
- Production Ready: SQL checkpoint store with atomic upsert, tested with Testcontainers
var consumer = new StreamConsumer(eventStore, checkpointStore, "my-consumer");
await consumer.ConsumeAsync(async (envelope, ct) => {
// Process event
Console.WriteLine(envelope.Event);
});See Stream Consumers Documentation for complete guide.
Consume events directly from Kafka topics with the same reliability features:
var options = new KafkaConsumerOptions
{
BootstrapServers = "localhost:9092",
Topic = "my-events",
GroupId = "my-service"
};
var consumer = new KafkaStreamConsumer(options, checkpointStore, serializer, registry);
await consumer.ConsumeAsync(async (envelope, ct) => {
// Process event from Kafka
await handler.ProcessAsync(envelope, ct);
});See Kafka Consumer Documentation for complete guide.
Build denormalized views of your event data with multiple projection types:
var projection = new Projection(eventStore, "order-summaries");
await projection.ProjectAsync(async (envelope, state, ct) => {
if (envelope.Event is OrderPlacedEvent ope)
{
state["total"] = (decimal)(state["total"] ?? 0m) + ope.Amount;
}
return state;
});Optimize aggregate loading with snapshots:
var options = new SnapshotOptions
{
Strategy = SnapshotStrategy.EveryNEvents(100)
};
var snapshot = await eventStore.GetSnapshotAsync(streamId, options);ZeroAlloc.EventSourcing.Telemetry adds a hand-rolled decorator around IAggregateRepository<TAggregate, TId> that records Activity spans and metrics for every aggregate LoadAsync and SaveAsync — without taking a dependency on the OTel SDK.
dotnet add package ZeroAlloc.EventSourcing.Telemetryservices
.AddEventSourcing()
.UseInMemoryEventStore()
.AddAggregate<OrderAggregate, Guid>()
.WithTelemetry(); // call after each aggregate registration, before Build()The decorator emits, under the ZeroAlloc.EventSourcing activity-source/meter name:
- Spans —
aggregate.loadandaggregate.save, tagged withaggregate.type(=typeof(TAggregate).Name); status set toErroron exception - Counters —
aggregate.loads_totalandaggregate.saves_total, incremented only whenResult.IsSuccess - Histograms —
aggregate.load_duration_msandaggregate.save_duration_ms, recorded for both success and failure paths
Any OpenTelemetry SDK wired to the process picks up all three instruments automatically.
Breaking change in v2.0:
WithTelemetry()now decoratesIAggregateRepository<,>instead ofIEventStore. The oldevent_store.append/event_store.read/event_store.subscribespans no longer exist; the deletedInstrumentedEventStoreand its[Instrument]attribute onIEventStoreare gone. Existing dashboards must be re-pointed ataggregate.load/aggregate.save. See docs/telemetry.md for the full migration table. The legacyUseEventSourcingTelemetry()extension remains as[Obsolete]and now delegates toWithTelemetry().
Complete documentation available at /docs:
- Getting Started
- Core Concepts
- Usage Guides
- Testing Strategies
- Performance & Benchmarks
- Advanced Topics
dotnet build ZeroAlloc.EventSourcing.slnx --configuration Releasedotnet test ZeroAlloc.EventSourcing.slnx --configuration Releasedotnet run --project benchmarks/ZeroAlloc.EventSourcing.Benchmarks -c ReleaseSee LICENSE file for details.
See CONTRIBUTING.md for guidelines.