ZeroAlloc.EventSourcing.Telemetry
1.4.0
dotnet add package ZeroAlloc.EventSourcing.Telemetry --version 1.4.0
NuGet\Install-Package ZeroAlloc.EventSourcing.Telemetry -Version 1.4.0
<PackageReference Include="ZeroAlloc.EventSourcing.Telemetry" Version="1.4.0" />
<PackageVersion Include="ZeroAlloc.EventSourcing.Telemetry" Version="1.4.0" />
<PackageReference Include="ZeroAlloc.EventSourcing.Telemetry" />
paket add ZeroAlloc.EventSourcing.Telemetry --version 1.4.0
#r "nuget: ZeroAlloc.EventSourcing.Telemetry, 1.4.0"
#:package ZeroAlloc.EventSourcing.Telemetry@1.4.0
#addin nuget:?package=ZeroAlloc.EventSourcing.Telemetry&version=1.4.0
#tool nuget:?package=ZeroAlloc.EventSourcing.Telemetry&version=1.4.0
ZeroAlloc.EventSourcing
A high-performance, zero-allocation event sourcing library for .NET with streaming capabilities and production-grade reliability features.
Key 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
Quick Start
Installation
dotnet add package ZeroAlloc.EventSourcing
Basic Event Sourcing
// The adapter is the storage: InMemoryEventStoreAdapter for tests; SqlServerEventStoreAdapter,
// PostgreSqlEventStoreAdapter or SqliteEventStoreAdapter from their packages in production.
var adapter = new InMemoryEventStoreAdapter();
// serializer is your IEventSerializer; AddEventSourcing() registers the AOT-safe
// ZeroAllocEventSerializer. OrderEventTypeRegistry maps event names to types: the source
// generator emits it for an Order aggregate.
var eventStore = new EventStore(adapter, serializer, new OrderEventTypeRegistry());
// Append events
var streamId = new StreamId("order-123");
var appended = await eventStore.AppendAsync(
streamId,
new object[] { new OrderPlaced("alice"), new ItemAdded(100m) },
StreamPosition.Start);
// Read events
await foreach (var envelope in eventStore.ReadAsync(streamId))
{
Console.WriteLine($"Event {envelope.Position.Value}: {envelope.Event}");
}
Most applications work with aggregates instead of raw events: see Your First Aggregate.
Packages
| 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.
Performance
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.
Stream Consumers
ZeroAlloc.EventSourcing includes production-grade stream consumers for reliable event consumption with automatic position tracking, retry logic, and configurable error handling.
Key Features
- 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
Quick Start
var consumer = new StreamConsumer(eventStore, checkpointStore, "my-consumer");
await consumer.ConsumeAsync((envelope, ct) =>
{
// Process event
Console.WriteLine(envelope.Event);
return Task.CompletedTask;
});
See Stream Consumers Documentation for complete guide.
Kafka Integration
Consume events directly from Kafka topics with the same reliability features:
var options = new KafkaConsumerGroupOptions
{
BootstrapServers = "localhost:9092",
Topic = "my-events",
GroupId = "my-service",
ConsumerId = "my-service-1"
};
using var consumer = new KafkaConsumerGroupConsumer(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.
Projections
Build denormalized views of your event data by deriving from Projection<TReadModel>:
public sealed record OrderTotals(int Orders, decimal Revenue);
public sealed class OrderTotalsProjection : Projection<OrderTotals>
{
public OrderTotalsProjection() => Current = new OrderTotals(0, 0m);
protected override OrderTotals Apply(OrderTotals current, EventEnvelope @event) => @event.Event switch
{
OrderPlaced => current with { Orders = current.Orders + 1 },
ItemAdded e => current with { Revenue = current.Revenue + e.Price },
_ => current
};
}
// Feed it every stream's events, in append order
var projection = new OrderTotalsProjection();
await foreach (var envelope in eventStore.ReadAsync(StreamId.Global))
{
await projection.HandleAsync(envelope);
}
FilteredProjection<TReadModel>, BatchedProjection<TReadModel> and
ReplayableProjection<TReadModel> add filtering, batching and rebuilds; see the
Projections Usage Guide.
Snapshots
Optimize aggregate loading with snapshots:
// Loads start from the latest snapshot and replay only the newer events; saves write a new
// snapshot every 100 events
var repository = new SnapshotCachingRepositoryDecorator<Order, OrderId, OrderState>(
innerRepository: new AggregateRepository<Order, OrderId>(
eventStore,
() => new Order(),
id => new StreamId($"order-{id.Value}")),
snapshotStore: new InMemorySnapshotStore<OrderState>(),
strategy: SnapshotLoadingStrategy.ValidateAndReplay,
restoreState: (order, state, position) => order.RestoreState(state, position),
eventStore: eventStore,
streamIdFactory: id => new StreamId($"order-{id.Value}"),
aggregateFactory: () => new Order(),
snapshotPolicy: SnapshotPolicy.EveryNEvents(100),
extractState: order => order.State);
var loaded = await repository.LoadAsync(orderId);
The SQL packages provide snapshot stores for PostgreSQL and SQL Server; see the Snapshots Usage Guide.
OpenTelemetry Instrumentation
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.Telemetry
services
.AddEventSourcing()
.UseInMemoryEventStore()
.UseAggregateRepository<Order, OrderId>(() => new Order(), id => new StreamId($"order-{id.Value}"))
.WithTelemetry(); // call after the aggregate repository registrations, before BuildServiceProvider()
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().
Documentation
Complete documentation available at /docs:
- Getting Started
- Core Concepts
- Usage Guides
- Testing Strategies
- Performance & Benchmarks
- Advanced Topics
Development
Build
dotnet build ZeroAlloc.EventSourcing.slnx --configuration Release
Tests
dotnet test ZeroAlloc.EventSourcing.slnx --configuration Release
Benchmarks
dotnet run --project benchmarks/ZeroAlloc.EventSourcing.Benchmarks -c Release
License
See LICENSE file for details.
Contributing
See CONTRIBUTING.md for guidelines.
| 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 is compatible. 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
- Microsoft.Extensions.DependencyInjection (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- ZeroAlloc.AsyncEvents (>= 1.1.6)
- ZeroAlloc.Collections (>= 1.1.10)
- ZeroAlloc.EventSourcing (>= 1.4.0)
- ZeroAlloc.EventSourcing.Aggregates (>= 1.4.0)
- ZeroAlloc.Results (>= 1.2.4)
- ZeroAlloc.Serialisation (>= 2.4.7)
- ZeroAlloc.StateMachine (>= 1.6.1)
- ZeroAlloc.Telemetry (>= 1.7.0)
- ZeroAlloc.ValueObjects (>= 2.0.11)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- ZeroAlloc.AsyncEvents (>= 1.1.6)
- ZeroAlloc.Collections (>= 1.1.10)
- ZeroAlloc.EventSourcing (>= 1.4.0)
- ZeroAlloc.EventSourcing.Aggregates (>= 1.4.0)
- ZeroAlloc.Results (>= 1.2.4)
- ZeroAlloc.Serialisation (>= 2.4.7)
- ZeroAlloc.StateMachine (>= 1.6.1)
- ZeroAlloc.Telemetry (>= 1.7.0)
- ZeroAlloc.ValueObjects (>= 2.0.11)
-
net9.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- ZeroAlloc.AsyncEvents (>= 1.1.6)
- ZeroAlloc.Collections (>= 1.1.10)
- ZeroAlloc.EventSourcing (>= 1.4.0)
- ZeroAlloc.EventSourcing.Aggregates (>= 1.4.0)
- ZeroAlloc.Results (>= 1.2.4)
- ZeroAlloc.Serialisation (>= 2.4.7)
- ZeroAlloc.StateMachine (>= 1.6.1)
- ZeroAlloc.Telemetry (>= 1.7.0)
- ZeroAlloc.ValueObjects (>= 2.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.