ZeroAlloc.EventSourcing.Telemetry 1.6.3

There is a newer version of this package available.
See the version list below for details.
dotnet add package ZeroAlloc.EventSourcing.Telemetry --version 1.6.3
                    
NuGet\Install-Package ZeroAlloc.EventSourcing.Telemetry -Version 1.6.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ZeroAlloc.EventSourcing.Telemetry" Version="1.6.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ZeroAlloc.EventSourcing.Telemetry" Version="1.6.3" />
                    
Directory.Packages.props
<PackageReference Include="ZeroAlloc.EventSourcing.Telemetry" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ZeroAlloc.EventSourcing.Telemetry --version 1.6.3
                    
#r "nuget: ZeroAlloc.EventSourcing.Telemetry, 1.6.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ZeroAlloc.EventSourcing.Telemetry@1.6.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ZeroAlloc.EventSourcing.Telemetry&version=1.6.3
                    
Install as a Cake Addin
#tool nuget:?package=ZeroAlloc.EventSourcing.Telemetry&version=1.6.3
                    
Install as a Cake Tool

ZeroAlloc.EventSourcing

NuGet Build License: MIT AOT GitHub Sponsors

A high-performance event sourcing library for .NET with streaming capabilities and production-grade reliability features. The append path allocates nothing of its own; see Allocations for what each path costs.

Key Features

  • Allocation-free appends: EventStore.AppendAsync and AggregateRepository.SaveAsync allocate nothing of their own, asserted in tests
  • 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
  • Storage Adapters: SQL Server, PostgreSQL and SQLite, tested against real databases; SQL Server and PostgreSQL through Testcontainers
  • 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

Every package targets net8.0, net9.0 and net10.0.

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 base class and repository
ZeroAlloc.EventSourcing.Generators Source generator that emits aggregate ApplyEvent dispatch and the event type registry
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.Sql Shared SQL checkpoint, snapshot, projection and dead-letter stores for SQL Server and PostgreSQL
ZeroAlloc.EventSourcing.Sqlite SQLite adapter for embedded and single-node use (README)
ZeroAlloc.EventSourcing.Kafka Kafka stream consumer for external event sources
ZeroAlloc.EventSourcing.Outbox At-least-once cross-aggregate dispatch through ZeroAlloc.Mediator (README)
ZeroAlloc.EventSourcing.Mediator Republishes live committed events as ZeroAlloc.Mediator notifications
ZeroAlloc.EventSourcing.Telemetry BCL ActivitySource + Meter decorator around IAggregateRepository<,> — OpenTelemetry spans and metrics with no OTel SDK dependency

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.

Allocations

What the library itself allocates, measured with allocation-free test doubles for the adapter, serializer and type registry:

Call Library allocation Test
EventStore.AppendAsync, 1 to 64 events 0 B EventStoreAllocationTests
AggregateRepository.SaveAsync 0 B AggregateRepositoryAllocationTests
EventStore.ReadAsync, one full read of 1 or 100 events 96 B per call, none per event EventStoreReadAllocationTests

The 96 B is the reader object ReadAsync returns, which is also its enumerator. Removing it means returning a struct reader instead of IAsyncEnumerable<EventEnvelope>, a breaking change held for the next major (#512). Storage adapters and serializers add their own allocations on top, as the SQLite numbers above show.

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.load and aggregate.save, tagged with aggregate.type (= typeof(TAggregate).Name); status set to Error on exception
  • Counters — aggregate.loads_total and aggregate.saves_total, incremented only when Result.IsSuccess
  • Histograms — aggregate.load_duration_ms and aggregate.save_duration_ms, recorded for both success and failure paths

Any OpenTelemetry SDK wired to the process picks up all three instruments automatically.

Spans and metrics sit at the aggregate-repository layer: every published version of the package decorates IAggregateRepository<,>, not IEventStore. UseEventSourcingTelemetry() is an [Obsolete] alias that delegates to WithTelemetry(). See docs/telemetry.md for the full guide.

Documentation

Complete documentation available at /docs:

Development

Build

dotnet build ZeroAlloc.EventSourcing.slnx --configuration Release

Tests

dotnet test ZeroAlloc.EventSourcing.slnx --configuration Release

Benchmarks

dotnet run --project src/ZeroAlloc.EventSourcing.Benchmarks -c Release

License

MIT — see LICENSE.

Contributing

See CONTRIBUTING.md for guidelines.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.6.4 58 10/16/2026
1.6.3 122 10/15/2026
1.6.2 167 10/11/2026
1.6.1 103 10/11/2026
1.6.0 86 10/10/2026
1.5.0 119 10/10/2026
1.4.0 167 9/30/2026
1.3.1 156 9/30/2026
1.3.0 105 9/28/2026
1.2.5 142 9/26/2026
1.2.4 126 9/20/2026
1.2.3 91 9/20/2026
1.2.2 90 9/20/2026
1.2.1 98 9/19/2026
1.0.0 151 4/29/2026