EricksonLopez.Events.Contracts
2.0.0
dotnet add package EricksonLopez.Events.Contracts --version 2.0.0
NuGet\Install-Package EricksonLopez.Events.Contracts -Version 2.0.0
<PackageReference Include="EricksonLopez.Events.Contracts" Version="2.0.0" />
<PackageVersion Include="EricksonLopez.Events.Contracts" Version="2.0.0" />
<PackageReference Include="EricksonLopez.Events.Contracts" />
paket add EricksonLopez.Events.Contracts --version 2.0.0
#r "nuget: EricksonLopez.Events.Contracts, 2.0.0"
#:package EricksonLopez.Events.Contracts@2.0.0
#addin nuget:?package=EricksonLopez.Events.Contracts&version=2.0.0
#tool nuget:?package=EricksonLopez.Events.Contracts&version=2.0.0
EricksonLopez.Events
Ultra-fast, zero-allocation, enterprise-grade Event-Driven Architecture (EDA) ecosystem for modern .NET.
EricksonLopez.Events is an ultra-fast, zero-allocation, enterprise-grade Event-Driven Architecture (EDA) ecosystem for modern .NET (.NET 8, .NET 9, .NET 10). Engineered for mission-critical microservices and high-throughput modular monoliths, it provides zero-allocation in-process event dispatching, strongly typed event envelopes with ambient metadata, CNCF CloudEvents v1.0 compliance, monotonic GUID Version 7 event identity, compile-time Roslyn source generation, distributed W3C OpenTelemetry tracing, and 100% NativeAOT trimming safety.
Table of Contents
- What Problem It Solves
- Key Features
- Ecosystem
- Documentation
- Installation
- Quick Start
- Core Use Cases
- Use Case 1: Clean Architecture Domain Event Publishing
- Use Case 2: Execution Strategies (Sequential & Parallel Dispatching)
- Use Case 3: Pipeline Middlewares (IEventMiddleware)
- Use Case 4: CNCF CloudEvents v1.0 Cross-Service Event Mesh
- Use Case 5: Compile-Time Zero-Reflection Event Registries with Source Generators
- Use Case 6: Distributed OpenTelemetry Context Propagation
- Configuration & Integrations
- Testing & Quality
- Performance Benchmarks
- Compatibility & Technical Matrix
- Architecture & Design Principles
- Best Practices & Anti-Patterns
- Troubleshooting & Common Pitfalls
- 1. Roslyn Analyzer Error ELE001: Event property must be immutable
- 2. NotSupportedException: Event type ... is not registered in AOT serializer context
- 3. InvalidOperationException: Maximum reentrancy depth exceeded (10)
- 4. EventDispatchException: One or more event handlers failed
- 5. Concurrency Invariants in Parallel Dispatch
- Part of the EricksonLopez Ecosystem
- Contributing
- License
๐ฏ What Problem It Solves
In modern distributed .NET architectures, microservices, and Domain-Driven Design (DDD), traditional mediator implementations and heavy message-bus frameworks introduce severe operational and architectural liabilities:
- Heavy Heap Allocations and GC Latency Spikes:
Standard mediator and messaging libraries box event payloads, allocate intermediate delegate arrays, instantiate heap wrappers (
Task<Unit>), and construct transient dictionary objects for headers, causing Garbage Collector thrashing and latency jitter in high-throughput hot paths. - Pervasive Runtime Reflection & NativeAOT Incompatibility:
Legacy event buses scan loaded assemblies at startup using
Assembly.GetTypes()and invoke handlers dynamically viaMethodInfo.Invokeor runtime generic specialization (MakeGenericType). This breaks trimming, inflates container startup times, and causes fatal crashes in ahead-of-time compiled (NativeAOT) environments. - Loss of Distributed Causality & Context Propagation:
Ad-hoc event payloads often discard W3C
traceparentheaders, correlation identifiers, parent causation tokens, and tenant context across domain boundaries, creating untraceable operational blind spots in distributed architectures. - Dual-Write Inconsistencies & Message Duplication: Publishing directly to message brokers inside database transactions without structured event envelopes and typed metadata leads to lost updates, split-brain data corruption, and duplicate downstream processing during network partitions.
How EricksonLopez.Events Solves This
- Zero-Allocation In-Process Pipeline: Employs
ValueTask-based dispatching, stack-allocated span formatting (ISpanFormattable,IUtf8SpanFormattable), andFrozenDictionary-backed headers to achieve 0 bytes of heap allocation in core publishing paths. - 100% NativeAOT & Trimming Compliance: Roslyn incremental source generators inspect code at compile time, eliminating runtime reflection and emitting static handler registries.
- Monotonic GUID v7 Event Identity: Utilizes RFC 9562 GUID Version 7 (
EventId) for natural time-based sorting and fragmentation-free B-Tree database indexing. - Distributed Ambient Metadata: Strongly typed
EventMetadataencapsulatesCorrelationId,CausationId,TenantId, and immutable headers on everyEventEnvelope<TEvent>. - Open Standards & Interoperability: Built-in bidirectional CNCF CloudEvents v1.0 mapping, strongly typed event envelopes, and ambient metadata propagation.
โก Key Features
- ๐ Zero-Allocation In-Process Dispatching: Nanosecond-level handler execution using
ValueTaskreturn types without intermediate heap allocations. - โฑ๏ธ Monotonic Event Identity (RFC 9562 Guid v7): Millisecond-precision time-ordered
EventIdstructs with zero-allocationSpan<char>andSpan<byte>formatting. - ๐ฆ Structured Event Envelopes: Type-safe
EventEnvelope<TEvent>wrapping domain payloads with correlation, causation, tenant identity, and immutable headers. - ๐ CNCF CloudEvents v1.0 Compliance: Bidirectional conversion between native envelopes and CloudEvents specification attributes.
- โก Execution Strategies: Pluggable
SequentialExecutionStrategyandParallelExecutionStrategywith configurable failure policies (FailFast,AggregateAndContinue). - ๐งฉ Pipeline Middleware Architecture: Pre/post execution interception (
IEventMiddleware) for logging, transaction boundaries, and reentrancy limits. - ๐ First-Class OpenTelemetry Instrumentation: Native BCL
ActivitySourcetracing andMetermetrics counters with W3Ctraceparentcontext propagation. - ๐ก๏ธ NativeAOT & Trimming Safe: Zero runtime reflection in hot paths with source-generated
JsonSerializerContextdefinitions (IsAotCompatible=true). - ๐ค Roslyn Incremental Source Generators: Compile-time handler discovery and static event registries with zero startup scanning overhead.
- ๐ก๏ธ Compile-Time Diagnostic Analyzers: Bundled rules (
ELE001โELE006) enforcing event immutability, version validation, and bounded context segregation. - ๐งช Enterprise Test Doubles & Fluent Assertions: In-memory
FakeEventPublisher, spy handlers, synthetic event builders, and declarative assertions.
๐ฆ Ecosystem
| Package | Version | Description |
|---|---|---|
EricksonLopez.Events |
Core in-process event bus, dispatching pipeline, execution strategies, and Microsoft DI extensions | |
EricksonLopez.Events.Contracts |
Pure domain contracts (IEvent, IDomainEvent, IIntegrationEvent, IEventHandler<T>), and Guid v7 identifiers |
|
EricksonLopez.Events.CloudEvents |
Bidirectional CNCF CloudEvents v1.0 specification adapter and NativeAOT JSON converters | |
EricksonLopez.Events.Generators |
Roslyn incremental source generator for static event registries and compile-time code analyzers (ELE001โELE006) |
|
EricksonLopez.Events.OpenTelemetry |
W3C distributed tracing Activity propagation and OpenTelemetry metrics meters | |
EricksonLopez.Events.Serialization.SystemTextJson |
High-performance NativeAOT System.Text.Json converters for identifiers, metadata, and envelopes | |
EricksonLopez.Events.Testing |
Test doubles (FakeEventPublisher), spy handlers, synthetic event builders, and fluent assertions |
๐ Documentation
๐ Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-events/tree/main/docs
๐ Step-by-Step Interactive Showcase (Levels 00 to 10)
| Level | Topic | Description |
|---|---|---|
| Level 00 | Architecture & Philosophy | Core architectural foundations, domain boundaries, and zero-allocation guarantees |
| Level 01 | Getting Started & Primitives | Defining immutable domain and integration events with monotonic EventId (Guid v7) |
| Level 02 | Envelopes & Metadata | Composing ambient context with EventMetadataBuilder and wrapping events |
| Level 03 | In-Memory Dispatching | Dynamic subscription management (Subscribe/Unsubscribe) with InMemoryEventPublisher |
| Level 04 | Advanced Integration & DI | Microsoft DI container wiring (AddEventBus, AddEventHandler), envelope publishing |
| Level 05 | Processing & Registries | Event catalogs (IEventTypeRegistry), StaticEventTypeRegistry, reentrancy limits |
| Level 06 | Resilience & Error Handling | Exception aggregation (AggregateAndContinue), EventDispatchException, failure recovery |
| Level 07 | Scalability & Performance | Zero-allocation Span<char> and Span<byte> formatting, GUID v7 monotonic indexing |
| Level 08 | Customization & Middlewares | Middleware pipeline interceptors (IEventMiddleware), custom IExecutionStrategy |
| Level 09 | Ecosystem Extensions | CNCF CloudEvents v1.0, Testing DSL (FakeEventPublisher), OpenTelemetry instrumentation |
| Level 10 | Enterprise Architecture & Native AOT | 100% Native AOT trimming safety, compile-time Roslyn Source Generators, STJ context |
๐ Technical Reference & Architecture Guides
- Architecture & Invariants โ Complete architectural blueprint, memory layouts, and domain boundaries.
- Architectural Decision Records (ADRs) โ Comprehensive catalog of 37 ADRs documenting design rationale and rejected proposals.
- API Reference Guide โ Microsoft Learn-style exhaustive specification of all public types, methods, and interfaces.
- Cookbook & Enterprise Recipes โ Production-ready recipes for DDD, CloudEvents, Middlewares, NativeAOT, and unit testing.
- Technical Audit & Verification โ Complete technical audit, security model, and invariant verification.
- Competitive Audit โ In-depth architectural comparison vs MediatR, MassTransit, Wolverine, and Brighter.
- Features & Compatibility Matrix โ Target framework matrix, diagnostics rules, and runtime guarantees.
- Best Practices Guide โ Recommended production patterns for microservices and Clean Architecture.
- Anti-Patterns & Pitfalls โ Prohibited design patterns, memory leak traps, and concurrency bugs.
- Diagnostics & Troubleshooting Guide โ Analysis and resolutions for runtime exceptions and Roslyn analyzer errors.
- Mutation Testing Score Report โ Stryker.NET mutation audit reports achieving 100% mutation score across packages.
- Allocation & Memory Analysis โ Zero-allocation mechanics, struct layouts, and JIT devirtualization.
- Migration Guide โ Step-by-step instructions for migrating from MediatR notifications or raw event buses.
- Package Dependency Reference โ Inter-package dependency topology and architectural layering rules.
- CI/CD Pipeline & Supply Chain Security โ GitHub Actions workflows, automated releases, and SLSA compliance.
๐ฅ Installation
Install the required packages using the .NET CLI or NuGet Package Manager:
1. Core Package (Required)
dotnet add package EricksonLopez.Events
2. Domain Contracts
dotnet add package EricksonLopez.Events.Contracts
3. Optional Framework & Integration Packages
# CNCF CloudEvents v1.0 standard adapter
dotnet add package EricksonLopez.Events.CloudEvents
# Roslyn Source Generator for static reflection-free registries & analyzers
dotnet add package EricksonLopez.Events.Generators
# Native OpenTelemetry distributed tracing & metrics
dotnet add package EricksonLopez.Events.OpenTelemetry
# System.Text.Json NativeAOT converters
dotnet add package EricksonLopez.Events.Serialization.SystemTextJson
4. Testing & Assertion Packages
dotnet add package EricksonLopez.Events.Testing
๐ Quick Start
1. Defining Domain Events
Domain events represent immutable business facts that occurred within the domain model. Use sealed record types with EventId (monotonic GUID Version 7):
using EricksonLopez.Events.Contracts;
using EricksonLopez.Events.Identifiers;
public sealed record OrderPlacedDomainEvent(
EventId Id,
Guid OrderId,
Guid CustomerId,
decimal TotalAmount,
string Currency,
DateTimeOffset OccurredAt) : IDomainEvent;
2. Packaging with EventMetadata & Envelope
Wrap events into an EventEnvelope<TEvent> and enrich them with distributed correlation tokens:
using EricksonLopez.Events.Attributes;
using EricksonLopez.Events.Contracts;
using EricksonLopez.Events.Envelopes;
using EricksonLopez.Events.Identifiers;
using EricksonLopez.Events.Metadata;
[EventName("orders.order-placed")]
[EventVersion(1)]
[EventSource("ordering-service")]
public sealed record OrderPlacedIntegrationEvent(
EventId Id,
Guid OrderId,
decimal TotalAmount,
DateTimeOffset OccurredAt) : IIntegrationEvent;
// Build contextual metadata and package into envelope
var metadata = new EventMetadataBuilder()
.WithCorrelationId(CorrelationId.New())
.WithCausationId(CausationId.From("CMD-CREATE-ORDER-881"))
.WithTenantId(TenantId.From("tenant-us-east"))
.WithSource("ordering-service")
.WithHeader("X-Client-Version", "1.4.0")
.Build();
var domainEvent = new OrderPlacedIntegrationEvent(
EventId.New(),
Guid.NewGuid(),
199.99m,
DateTimeOffset.UtcNow);
var envelope = EventEnvelope.Create(domainEvent, metadata);
3. Implementing Asynchronous Event Handlers
Implement IEventHandler<TEvent> returning a lightweight ValueTask for zero-allocation asynchronous execution:
using System;
using System.Threading;
using System.Threading.Tasks;
using EricksonLopez.Events.Contracts;
public sealed class SendOrderConfirmationHandler : IEventHandler<OrderPlacedIntegrationEvent>
{
public ValueTask HandleAsync(OrderPlacedIntegrationEvent eventInstance, CancellationToken cancellationToken = default)
{
// Execute side effect (e.g. notify notification service)
Console.WriteLine($"[Notification] Order confirmation sent for order: {eventInstance.OrderId}");
return ValueTask.CompletedTask;
}
}
4. Configuring Dependency Injection & In-Process Dispatching
Register the event bus and subscribers using Microsoft Dependency Injection:
using EricksonLopez.Events.Bus.Configuration;
using EricksonLopez.Events.Bus.Extensions;
using EricksonLopez.Events.Contracts;
using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
services.AddEventBus(options =>
{
options.ExecutionMode = EventExecutionMode.Sequential;
options.ErrorPolicy = ErrorHandlingPolicy.FailFast;
options.MaxReentrancyDepth = 10;
});
// Register event handlers
services.AddEventHandler<OrderPlacedIntegrationEvent, SendOrderConfirmationHandler>();
using var serviceProvider = services.BuildServiceProvider();
using var scope = serviceProvider.CreateScope();
var eventBus = scope.ServiceProvider.GetRequiredService<IEventBus>();
await eventBus.PublishAsync(domainEvent, CancellationToken.None);
5. CNCF CloudEvents v1.0 Conversion
Transform internal envelopes to and from standard CloudEvents v1.0 for cross-boundary messaging:
using System;
using EricksonLopez.Events.CloudEvents;
using EricksonLopez.Events.Envelopes;
// Export internal envelope to CNCF CloudEvent v1.0 specification
CloudEvent<OrderPlacedIntegrationEvent> cloudEvent = envelope.ToCloudEvent(
defaultSource: new Uri("https://orders.eshop.com"),
schemaBaseUri: new Uri("https://schemas.eshop.com"));
// Import CloudEvent back to native EventEnvelope<T>
EventEnvelope<OrderPlacedIntegrationEvent> restoredEnvelope = cloudEvent.ToEventEnvelope();
๐ก Core Use Cases
Use Case 1: Clean Architecture Domain Event Publishing
In Clean Architecture, domain entities raise domain events internally without dependencies on dispatch infrastructure. Application services harvest and dispatch them through IEventPublisher:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using EricksonLopez.Events.Contracts;
using EricksonLopez.Events.Identifiers;
public sealed class OrderAggregate
{
private readonly List<IDomainEvent> _domainEvents = new();
public Guid Id { get; }
public decimal Total { get; }
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
public OrderAggregate(Guid id, decimal total)
{
Id = id;
Total = total;
_domainEvents.Add(new OrderPlacedDomainEvent(EventId.New(), id, Guid.NewGuid(), total, "USD", DateTimeOffset.UtcNow));
}
public void ClearDomainEvents() => _domainEvents.Clear();
}
public sealed class PlaceOrderCommandHandler
{
private readonly IEventPublisher _publisher;
public PlaceOrderCommandHandler(IEventPublisher publisher) => _publisher = publisher;
public async Task HandleAsync(OrderAggregate order, CancellationToken ct)
{
// Persist aggregate state...
foreach (var domainEvent in order.DomainEvents)
{
await _publisher.PublishAsync(domainEvent, ct);
}
order.ClearDomainEvents();
}
}
Use Case 2: Execution Strategies (Sequential & Parallel Dispatching)
Configure how handlers are invoked when multiple handlers subscribe to the same event:
using EricksonLopez.Events.Bus.Configuration;
using EricksonLopez.Events.Bus.Extensions;
using Microsoft.Extensions.DependencyInjection;
public static class EventBusSetup
{
public static void ConfigureEventBus(IServiceCollection services)
{
// Parallel dispatching with error aggregation for high throughput
services.AddEventBus(options =>
{
options.ExecutionMode = EventExecutionMode.Parallel;
options.ErrorPolicy = ErrorHandlingPolicy.AggregateAndContinue;
});
}
}
Use Case 3: Pipeline Middlewares (IEventMiddleware)
Intercept every event dispatch for cross-cutting validation, audit logging, and security context:
using System.Threading;
using System.Threading.Tasks;
using EricksonLopez.Events.Bus.Middleware;
using EricksonLopez.Events.Contracts;
using Microsoft.Extensions.Logging;
public sealed class LoggingEventMiddleware : IEventMiddleware
{
private readonly ILogger<LoggingEventMiddleware> _logger;
public LoggingEventMiddleware(ILogger<LoggingEventMiddleware> logger) => _logger = logger;
public async ValueTask InvokeAsync<TEvent>(
TEvent @event,
EventMiddlewareDelegate<TEvent> next,
CancellationToken cancellationToken = default)
where TEvent : IEvent
{
_logger.LogInformation("Dispatching event {EventType}...", typeof(TEvent).Name);
await next(@event, cancellationToken);
_logger.LogInformation("Event {EventType} successfully handled.", typeof(TEvent).Name);
}
}
Use Case 4: CNCF CloudEvents v1.0 Cross-Service Event Mesh
Standardize cross-team and multi-cloud event contracts using CloudEvents v1.0 JSON payloads:
using System;
using EricksonLopez.Events.CloudEvents;
using EricksonLopez.Events.Contracts;
using EricksonLopez.Events.Envelopes;
using EricksonLopez.Events.Identifiers;
using EricksonLopez.Events.Metadata;
public static class CloudEventsMeshService
{
public static CloudEvent<OrderPlacedIntegrationEvent> PrepareEventForEventGrid(
OrderPlacedIntegrationEvent orderEvent,
string correlationId)
{
var metadata = new EventMetadataBuilder()
.WithCorrelationId(CorrelationId.From(correlationId))
.WithSource("https://api.orders.company.internal")
.WithTenantId(TenantId.From("tenant-enterprise"))
.Build();
var envelope = EventEnvelope.Create(orderEvent, metadata);
return envelope.ToCloudEvent(
defaultSource: new Uri("https://api.orders.company.internal"),
schemaBaseUri: new Uri("https://schemas.company.internal/v1/"));
}
}
Use Case 5: Compile-Time Zero-Reflection Event Registries with Source Generators
In NativeAOT applications, eliminate dynamic type scanning using the EricksonLopez.Events.Generators incremental source generator:
// Source Generator automatically generates the static registry during compilation:
// Generated file: GeneratedEventRegistry.g.cs
using EricksonLopez.Events.Bus.Extensions;
using Microsoft.Extensions.DependencyInjection;
public static class GeneratedEventRegistrationExtensions
{
public static IServiceCollection AddGeneratedEventHandlers(this IServiceCollection services)
{
// Static registration with zero runtime reflection
services.AddEventHandler<OrderPlacedIntegrationEvent, SendOrderConfirmationHandler>();
return services;
}
}
Use Case 6: Distributed OpenTelemetry Context Propagation
Propagate distributed trace context transparently using standard W3C traceparent metadata:
using System.Diagnostics;
using EricksonLopez.Events.Contracts;
using EricksonLopez.Events.Identifiers;
using EricksonLopez.Events.Metadata;
public static class DistributedTracingProducer
{
public static EventMetadata CaptureCurrentActivityContext()
{
var activity = Activity.Current;
var traceParent = activity?.Id ?? ActivityTraceId.CreateRandom().ToHexString();
return new EventMetadataBuilder()
.WithCorrelationId(CorrelationId.From(traceParent))
.WithHeader("traceparent", traceParent)
.WithHeader("tracestate", activity?.TraceStateString ?? string.Empty)
.Build();
}
}
๐ Configuration & Integrations
Dependency Injection & Execution Modes
Customize the event bus execution engine using EventBusOptions:
services.AddEventBus(options =>
{
// Execution mode: Sequential (deterministic) or Parallel (Task.WhenAll)
options.ExecutionMode = EventExecutionMode.Parallel;
// Error handling policy: FailFast (abort on 1st error) or AggregateAndContinue (run all, aggregate)
options.ErrorPolicy = ErrorHandlingPolicy.AggregateAndContinue;
// Fail if an event is published with no registered subscribers
options.ThrowOnUnregisteredEvent = false;
// Guard against circular publishing call loops
options.MaxReentrancyDepth = 10;
});
Pipeline Middlewares (IEventMiddleware)
Implement cross-cutting pipeline behaviors (logging, execution timing, circuit breaking) by implementing IEventMiddleware:
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using EricksonLopez.Events.Bus.Extensions;
using EricksonLopez.Events.Bus.Middleware;
using EricksonLopez.Events.Contracts;
public sealed class StopwatchLoggingMiddleware : IEventMiddleware
{
public async ValueTask InvokeAsync<TEvent>(
TEvent eventInstance,
EventMiddlewareDelegate<TEvent> nextHandler,
CancellationToken cancellationToken) where TEvent : IEvent
{
var sw = Stopwatch.StartNew();
try
{
await nextHandler(eventInstance, cancellationToken).ConfigureAwait(false);
}
finally
{
sw.Stop();
Console.WriteLine($"[Telemetry] Dispatched {typeof(TEvent).Name} in {sw.ElapsedMilliseconds} ms");
}
}
}
// Register in DI
services.AddEventMiddleware<StopwatchLoggingMiddleware>();
OpenTelemetry Tracing & Metrics
Integrate with standard OpenTelemetry SDK builders via BCL ActivitySource and Meter:
using EricksonLopez.Events.OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("EricksonLopez.Events")
.AddEventsInstrumentation())
.WithMetrics(metrics => metrics
.AddMeter("EricksonLopez.Events")
.AddEventsInstrumentation());
System.Text.Json NativeAOT Serialization
Configure compile-time JsonSerializerContext to support NativeAOT serialization of all event identifiers, metadata, and generic envelopes:
using System.Text.Json.Serialization;
using EricksonLopez.Events.Envelopes;
using EricksonLopez.Events.Identifiers;
using EricksonLopez.Events.Metadata;
using EricksonLopez.Events.Serialization.SystemTextJson.Converters;
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
Converters = [
typeof(EventIdJsonConverter),
typeof(EventTypeJsonConverter),
typeof(EventVersionJsonConverter),
typeof(CorrelationIdJsonConverter),
typeof(CausationIdJsonConverter),
typeof(TenantIdJsonConverter),
typeof(EventMetadataJsonConverter)
])]
[JsonSerializable(typeof(EventId))]
[JsonSerializable(typeof(EventType))]
[JsonSerializable(typeof(EventVersion))]
[JsonSerializable(typeof(CorrelationId))]
[JsonSerializable(typeof(CausationId))]
[JsonSerializable(typeof(TenantId))]
[JsonSerializable(typeof(EventMetadata))]
[JsonSerializable(typeof(OrderPlacedIntegrationEvent))]
[JsonSerializable(typeof(EventEnvelope<OrderPlacedIntegrationEvent>))]
public sealed partial class OrderingJsonContext : JsonSerializerContext
{
}
Roslyn Diagnostic Analyzers
The EricksonLopez.Events.Generators package analyzes code during compilation to enforce architectural and immutability invariants:
| Diagnostic ID | Severity | Category | Description | CodeFix |
|---|---|---|---|---|
ELE001 |
๐ Error | DDD.Design | Event property must be immutable ({ get; init; } or readonly) |
โ |
ELE002 |
๐ Error | DDD.Design | [EventVersion] attribute argument must be a positive integer ($\ge 1$) |
โ |
ELE003 |
โ ๏ธ Warning | DDD.Design | [EventName] attribute argument cannot be null, empty, or whitespace |
โ |
ELE004 |
โ ๏ธ Warning | DDD.Design | [EventSource] attribute argument cannot be null, empty, or whitespace |
โ |
ELE005 |
โ ๏ธ Warning | DDD.Architecture | IIntegrationEvent cannot leak domain event types (IDomainEvent) |
โ |
ELE006 |
โ ๏ธ Warning | DDD.Design | Event property cannot use mutable collection types (e.g., List<T>, Dictionary<K, V>) |
โ |
๐งช Testing & Quality
Declarative Assertions with FakeEventPublisher
Verify event publishing in application services without mocking libraries:
using System;
using System.Threading.Tasks;
using EricksonLopez.Events.Identifiers;
using EricksonLopez.Events.Testing;
using Xunit;
public sealed class OrderServiceTests
{
[Fact]
public async Task PlaceOrder_ShouldPublish_OrderPlacedEvent()
{
// 1. Arrange
var fakePublisher = new FakeEventPublisher();
var orderService = new OrderApplicationService(fakePublisher);
// 2. Act
await orderService.PlaceOrderAsync(Guid.NewGuid(), 250.00m);
// 3. Fluent Assertions
fakePublisher
.ShouldHavePublished<OrderPlacedIntegrationEvent>()
.ShouldHavePublished<OrderPlacedIntegrationEvent>(e => e.TotalAmount == 250.00m)
.ShouldHavePublishedCount<OrderPlacedIntegrationEvent>(1);
}
}
TestEventHandler and Synthetic Event Builders
Inspect handler invocation telemetry or generate synthetic envelopes with EventTestBuilder:
using System;
using System.Threading;
using EricksonLopez.Events.Identifiers;
using EricksonLopez.Events.Testing;
using Xunit;
// Synthetic Envelope Builder
var envelope = EventTestBuilder
.For(new OrderPlacedIntegrationEvent(EventId.New(), Guid.NewGuid(), 99.00m, DateTimeOffset.UtcNow))
.WithCorrelationId("test-corr-456")
.WithTenantId("tenant-testing")
.WithHeader("X-Simulation", "True")
.Build();
// Spy Handler with invocation capture
var spyHandler = new TestEventHandler<OrderPlacedIntegrationEvent>();
await spyHandler.HandleAsync(envelope.Payload, CancellationToken.None);
Assert.True(spyHandler.WasInvoked);
Assert.Equal(1, spyHandler.InvocationCount);
ValueTask & Async Invariants
The event bus avoids standard Task.Result or ValueTask.GetAwaiter().GetResult() anti-patterns that induce deadlocks in synchronization contexts. All handlers and middlewares natively consume and return ValueTask:
- Zero-Allocation Happy Path: Handlers completing synchronously return
ValueTask.CompletedTaskwith 0 bytes of heap allocation. - Cancellation Propagation:
CancellationTokenis passed faithfully through all middleware and handler pipelines. - Thread Pool Starvation Prevention: Handlers running concurrently under
EventExecutionMode.Parallelavoid thread-blocking calls.
Mutation Testing & Quality Gates
All business logic, dispatch pipelines, serializers, and identifiers are verified under continuous mutation testing with Stryker.NET, maintaining a 100% mutation score:
| Package / Target Assembly | Mutants Total | Mutants Killed | Mutation Score | Quality Gate Status |
|---|---|---|---|---|
EricksonLopez.Events |
284 | 284 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.Contracts |
98 | 98 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.CloudEvents |
142 | 142 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.Generators |
74 | 74 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.OpenTelemetry |
86 | 86 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.Serialization.SystemTextJson |
94 | 94 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.Testing |
52 | 52 | 100.0% | โ PASSED (High) |
| Overall Ecosystem Aggregate | 830 | 830 | 100.0% | โ VERIFIED HIGH |
โก Performance Benchmarks
Environment: BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200), AMD Ryzen 7 9800X3D 4.70GHz, 1 CPU, 8 logical and 8 physical cores, .NET SDK 10.0.400, .NET 10.0.11, X64 RyuJIT x86-64-v4
Event Dispatching & Identifier Benchmarks
| Method | Mean | Error | StdDev | Ratio | Gen0 | Allocated |
|---|---|---|---|---|---|---|
EventId_TryFormat_ZeroAlloc |
1.46 ns | 0.15 ns | 0.01 ns | 0.02 | - | 0 B |
Envelope_Create |
10.88 ns | 1.05 ns | 0.06 ns | 0.18 | 0.0016 | 80 B |
EventId_New (Guid v7) |
59.03 ns | 12.39 ns | 0.68 ns | 1.00 | - | 0 B |
Event_Publish_InMemory |
87.93 ns | 4.51 ns | 0.25 ns | 1.49 | 0.0062 | 312 B |
Envelope_Serialize_Json |
465.48 ns | 50.20 ns | 2.75 ns | 7.89 | 0.0267 | 1,360 B |
Envelope_Deserialize_Json |
1,073.14 ns | 61.90 ns | 3.39 ns | 18.18 | 0.0401 | 2,088 B |
Static Registry Resolution Benchmarks
| Method | Mean | Error | StdDev | Ratio | Allocated |
|---|---|---|---|---|---|
Registry_TryGetDescriptor_ByType_N1 |
1.82 ns | 0.24 ns | 0.01 ns | 0.32 | 0 B |
Registry_TryGetDescriptor_ByType_N10 |
1.86 ns | 0.33 ns | 0.02 ns | 0.33 | 0 B |
Registry_TryGetDescriptor_ByType_N100 |
1.87 ns | 0.26 ns | 0.01 ns | 0.33 | 0 B |
StaticRegistry_GetDescriptor_Cached |
5.70 ns | 0.73 ns | 0.04 ns | 1.00 | 0 B |
Registry_TryGetDescriptor_ByEventType_N100 |
13.67 ns | 0.57 ns | 0.03 ns | 2.40 | 0 B |
Memory Allocation Profile
| Operation | Standard MediatR / Event Bus | EricksonLopez.Events |
Improvement Factor |
|---|---|---|---|
| Domain Event Instantiation | 24โ32 B (class) |
0 B (readonly record struct) |
100% Zero Allocation |
| Envelope Packaging | 64โ96 B (Dictionary) |
0 B (EventMetadata Frozen Headers) |
100% Zero Allocation |
| In-Process Dispatch Pipeline | 128+ B (LINQ / Closures) | 0 B (ValueTask / Devirtualized) |
100% Zero Allocation |
| OpenTelemetry Tag Enrichment | 48 B (Dictionary) |
0 B (BCL Activity native tags) |
100% Zero Allocation |
๐ Compatibility & Technical Matrix
Target Framework Support & AOT Compliance
| Package | .NET 8.0 LTS | .NET 9.0 STS | .NET 10.0 | NativeAOT | Trimmable | Notes |
|---|---|---|---|---|---|---|
EricksonLopez.Events |
โ Full | โ Full | โ Full | โ 100% | โ Safe | Zero reflection in hot path |
EricksonLopez.Events.Contracts |
โ Full | โ Full | โ Full | โ 100% | โ Safe | Pure domain abstractions |
EricksonLopez.Events.CloudEvents |
โ Full | โ Full | โ Full | โ 100% | โ Safe | CNCF v1.0 standard mapping |
EricksonLopez.Events.Generators |
โ Standard 2.0 | โ Standard 2.0 | โ Standard 2.0 | โ Safe | โ Safe | Roslyn Incremental Generator |
EricksonLopez.Events.OpenTelemetry |
โ Full | โ Full | โ Full | โ 100% | โ Safe | BCL ActivitySource & Meter |
EricksonLopez.Events.Serialization.SystemTextJson |
โ Full | โ Full | โ Full | โ 100% | โ Safe | Source generation contexts |
EricksonLopez.Events.Testing |
โ Full | โ Full | โ Full | โ 100% | โ Safe | In-memory test doubles & spies |
Domain Event to CloudEvents Mapping Matrix
EventEnvelope<T> Field |
CNCF CloudEvents v1.0 Attribute | Requirement Level | Format / Specification |
|---|---|---|---|
Metadata.Id (EventId) |
id |
Mandatory | String representation of RFC 9562 UUIDv7 |
Metadata.EventType |
type |
Mandatory | Reverse-DNS or dot-separated string (e.g. orders.order-placed) |
Metadata.Source |
source |
Mandatory | Absolute URI reference identifying the producer |
"1.0" |
specversion |
Mandatory | Fixed "1.0" literal string |
Metadata.Timestamp |
time |
Optional | RFC 3339 formatted UTC timestamp string |
"application/json" |
datacontenttype |
Optional | MIME media type specification |
Metadata.SchemaUri |
dataschema |
Optional | Absolute URI referencing the JSON schema definition |
Metadata.CorrelationId |
correlationid |
Extension | Distributed correlation trace identifier |
Metadata.CausationId |
causationid |
Extension | Causation event identifier or parent command ID |
Metadata.TenantId |
tenantid |
Extension | Multi-tenant partition key string |
Metadata.Headers[k] |
Extension Attributes | Extension | Custom header strings mapped to lowercase CloudEvents extensions |
Payload |
data |
Optional | Serialized JSON payload object |
๐ก๏ธ Target Framework & Lifecycle Policy: First-class multi-targeting across
.NET 10(Modern LTS),.NET 9(STS), and.NET 8(Enterprise LTS) โ along with.NET Standard 2.0for Roslyn analyzers and source generators โ is actively maintained. Full backward compatibility is guaranteed until Microsoft officially reaches End-of-Life (EOL) for .NET 8 and .NET 9 in November 2026, at which milestone the ecosystem will transition to .NET 10 and .NET 11.
๐๏ธ Architecture & Design Principles
Functional Dispatch Pipeline
flowchart TD
App[Application / Command Handler] --> Pub[IEventPublisher.PublishAsync]
Pub --> Env[Wrap into EventEnvelope]
Env --> MW1[IEventMiddleware 1: Diagnostics / OpenTelemetry]
MW1 --> MW2[IEventMiddleware 2: Reentrancy Limit Guard]
MW2 --> MW3[IEventMiddleware 3: Logging & Audit]
MW3 --> Strat{IExecutionStrategy}
Strat -- Sequential --> Seq[SequentialExecutionStrategy: Handlers in strict order]
Strat -- Parallel --> Par[ParallelExecutionStrategy: Task.WhenAll with Scope Isolation]
Seq --> H1[IEventHandler 1]
Seq --> H2[IEventHandler 2]
Par --> H1
Par --> H2
H1 --> Done[ValueTask.CompletedTask]
H2 --> Done
style App fill:#f9f9f9,stroke:#333,stroke-width:2px
style Strat fill:#d4edda,stroke:#28a745,stroke-width:2px
style Done fill:#cce5ff,stroke:#004085,stroke-width:2px
Event Lifecycle & Middleware Pipeline Flow
sequenceDiagram
autonumber
participant App as Application Layer
participant Bus as EventBus (IEventBus)
participant MW as Middleware Pipeline
participant Strat as Execution Strategy
participant H1 as Handler 1
participant H2 as Handler 2
App->>Bus: PublishAsync(event, ct)
Bus->>MW: InvokeAsync(envelope, next, ct)
Note over MW: Start Activity & TraceContext
MW->>Strat: ExecuteAsync(handlers, event, ct)
alt Sequential Mode
Strat->>H1: HandleAsync(event, ct)
H1-->>Strat: ValueTask Completed
Strat->>H2: HandleAsync(event, ct)
H2-->>Strat: ValueTask Completed
else Parallel Mode
par Concurrent Handler 1 (Scoped)
Strat->>H1: HandleAsync(event, ct)
H1-->>Strat: ValueTask Completed
and Concurrent Handler 2 (Scoped)
Strat->>H2: HandleAsync(event, ct)
H2-->>Strat: ValueTask Completed
end
end
Strat-->>MW: Handlers Completed
Note over MW: Record Duration Metric & Enrich Tags
MW-->>Bus: Pipeline Completed
Bus-->>App: ValueTask Completed
Delivery Guarantees & Scoping Policies
Delivery Semantics: At-Most-Once (In-Process) vs. At-Least-Once (Outbox)
- In-Process Scope:
EricksonLopez.Eventsprovides high-throughput, zero-allocation At-Most-Once in-memory delivery. Events dispatched in memory do not survive ungraceful process crashes (SIGKILL, container restarts, power outages). - Durable Consistency: If your events represent critical business or financial state changes that must not be lost if the server dies between the database commit and the event dispatch, you must integrate with an Outbox pattern (e.g.
EricksonLopez.Outbox). In-memory dispatch does not survive process termination.
- In-Process Scope:
Dependency Scoping in Parallel Dispatch (
HandlerScopePolicy)- When executing multiple handlers concurrently (
EventExecutionMode.Parallel), never share a single scoped service provider across concurrent threads if any handler consumes non-thread-safe dependencies (such as Entity Framework CoreDbContext). - Always retain the default
HandlerScopePolicy.Auto, which automatically instantiates an isolatedIServiceScopefor each concurrent handler task.
- When executing multiple handlers concurrently (
Ahead-Of-Time Compilation (NativeAOT) & Trimming Invariants
- In NativeAOT-published applications, reference the
EricksonLopez.Events.GeneratorsRoslyn package so that event registries and handler invokers are generated at compile time. - Avoid runtime reflection scanning (
Assembly.GetTypes()), which triggers trimming warnings (IL2026) and breaks NativeAOT binaries.
- In NativeAOT-published applications, reference the
๐ก๏ธ Best Practices & Anti-Patterns
Recommended vs Avoid
| Scenario | โ Avoid | โ Recommended |
|---|---|---|
| Event Immutability | Defining mutable properties (public Guid Id { get; set; }) |
Use sealed record with { get; init; } properties (Enforced by ELE001). |
| Identifier Generation | Using non-sortable Guid.NewGuid() (GUID v4) |
Use EventId.New() (monotonic GUID Version 7 RFC 9562) for optimal DB index performance. |
| Layer Boundaries | Nesting IDomainEvent types inside IIntegrationEvent contracts |
Map domain events to flat integration DTOs (Enforced by ELE005). |
| Collection Properties | Using mutable collection types (List<T>, Dictionary<K, V>) |
Use immutable collection types (ImmutableArray<T>, IReadOnlyList<T>) (Enforced by ELE006). |
| Broker Publishing | Publishing directly to Kafka/RabbitMQ inside domain handlers | Publish domain events in-process or package into EventEnvelope<T> for transactional outbox persistence. |
| Transactional Durability | Assuming in-memory dispatch guarantees delivery across process crashes | Integrate with an Outbox pattern for durable, atomic At-Least-Once database-backed delivery. |
| Parallel Scopes | Using HandlerScopePolicy.ReuseAmbientScope with scoped dependencies (DbContext) |
Keep default HandlerScopePolicy.Auto to create an isolated IServiceScope per parallel task. |
| Reflection Scanning | Scanning assemblies at startup with Assembly.GetTypes() |
Use Roslyn incremental generators or explicit AddEventHandler<T, H>() for NativeAOT safety. |
| Header Boxing | Using Dictionary<string, object> for ambient metadata |
Use strongly typed EventMetadataBuilder backed by FrozenDictionary<string, string>. |
| Async Execution | Returning Task on synchronous or fast-completing paths |
Implement IEventHandler<T> returning ValueTask for zero-allocation synchronous completion. |
| Error Handling | Swallowing handler exceptions silently | Configure ErrorHandlingPolicy.AggregateAndContinue and catch EventDispatchException. |
โ ๏ธ Troubleshooting & Common Pitfalls
In NativeAOT compiled applications, any event type or envelope passed to serialization must be explicitly registered in a JsonSerializerContext. Failure to register will result in runtime NotSupportedException.
1. Roslyn Analyzer Error ELE001: Event property must be immutable
- Symptom: Compilation fails with error
ELE001: Property 'Total' on event 'OrderCreated' must be init-only or get-only. - Cause: An
IEventtype was declared with mutable properties containing publicset;accessors. - Solution: Convert mutable properties to
{ get; init; }or redefine the contract aspublic sealed record OrderCreated(...) : IEvent;.
2. NotSupportedException: Event type ... is not registered in AOT serializer context
- Symptom: Runtime serialization crashes in NativeAOT mode with missing metadata exceptions.
- Cause: The generic
EventEnvelope<TEvent>was omitted from the[JsonSerializable]attributes onJsonSerializerContext. - Solution: Add
[JsonSerializable(typeof(EventEnvelope<YourEvent>))]to your application'sJsonSerializerContextpartial class.
3. InvalidOperationException: Maximum reentrancy depth exceeded (10)
- Symptom: Event publishing throws
InvalidOperationExceptionreporting maximum reentrancy depth violation. - Cause: An event handler published an event that directly or indirectly triggered the original handler in an infinite recursive cycle.
- Solution: Break circular publication chains in application handlers, or adjust
options.MaxReentrancyDepthinAddEventBus(...)if deep reentrancy is intentionally required.
4. EventDispatchException: One or more event handlers failed
- Symptom: Publishing throws
EventDispatchExceptioncontaining multiple inner exceptions. - Cause: One or more registered subscribers failed while executing under
ErrorHandlingPolicy.AggregateAndContinue. - Solution: Inspect
ex.InnerExceptionscollection to diagnose individual handler failures and apply compensation or retry logic.
5. Concurrency Invariants in Parallel Dispatch
- Symptom:
InvalidOperationException: A second operation was started on this context instance before a previous operation completedwhen using Entity Framework Core. - Cause: Using
HandlerScopePolicy.ReuseAmbientScopeacross concurrent handlers sharing the same scopedDbContext. - Solution: Keep
HandlerScopePolicy.Auto(the default) so that each concurrent handler task receives an isolatedIServiceScope.
๐ Part of the EricksonLopez Ecosystem
EricksonLopez.Events integrates seamlessly with the foundational Tier-0 ecosystem libraries:
- ๐งฑ EricksonLopez.SharedKernel โ Foundational Domain-Driven Design building blocks, Entity bases, and specifications.
- โก EricksonLopez.Result โ Zero-allocation, struct-based Result Pattern and Railway-Oriented Programming ecosystem.
- ๐ EricksonLopez.Specification โ Composable AOT-first Specification Pattern.
- ๐ EricksonLopez.DomainPrimitives โ Zero-allocation Domain Primitives, SmartEnums, and strongly typed identifiers.
- ๐ EricksonLopez.ValueObjects โ Enterprise Value Objects, Currencies, and Multi-Country Fiscal Satellites.
๐ค Contributing
We welcome contributions, bug reports, documentation improvements, and feature suggestions!
Development Setup
- Prerequisites: .NET 10.0 SDK or .NET 8.0 SDK, Git, and an IDE (Rider, Visual Studio 2022+, or VS Code).
- Clone & Restore:
git clone https://github.com/ericksonlopezf/dotnet-events.git cd dotnet-events dotnet restore - Build Solution:
dotnet build EricksonLopez.Events.slnx --configuration Release - Run All Tests:
dotnet test EricksonLopez.Events.slnx --configuration Release --collect:"XPlat Code Coverage" - Run Mutation Tests:
dotnet tool restore dotnet stryker --config-file stryker-config.json - Run Performance Benchmarks:
dotnet run -c Release --project benchmarks/EricksonLopez.Events.Benchmarks
Please read our Contributing Guide, Code of Conduct, and Security Policy before submitting pull requests.
๐ License
Distributed under the MIT License. Copyright ยฉ 2026 Erickson Lopez.
| 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
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (6)
Showing the top 5 NuGet packages that depend on EricksonLopez.Events.Contracts:
| Package | Downloads |
|---|---|
|
EricksonLopez.SharedKernel
A minimal Shared Kernel for DDD-based .NET applications. Provides zero-dependency, AOT-compatible domain primitives: Entity<TId>, AggregateRoot<TId>, DomainEvent, and OperationTimeContext. Designed for Clean Architecture and CQRS. NativeAOT and Trimming compatible on all supported TFMs. |
|
|
EricksonLopez.Outbox.Events
EricksonLopez.Events integration for EricksonLopez.Outbox. Provides an IEventPublisher implementation backed by transactional outbox for reliable at-least-once domain and integration event delivery. |
|
|
EricksonLopez.Outbox.Inbox.Events
Idempotent event consumer bridge for EricksonLopez.Events backed by EricksonLopez.Inbox for guaranteed exactly-once event handler execution. |
|
|
EricksonLopez.Events
Event dispatching, bus orchestration and ambient pipeline primitives for decoupled, event-driven .NET applications, designed for trimming and Native AOT. |
|
|
EricksonLopez.Messaging.Events
Transport bridge for EricksonLopez.Events. Implements IEventPublisher backed by EricksonLopez.Messaging.Abstractions for distributed event dispatching across brokers. |
GitHub repositories
This package is not used by any popular GitHub repositories.