EricksonLopez.Events
1.0.0
dotnet add package EricksonLopez.Events --version 1.0.0
NuGet\Install-Package EricksonLopez.Events -Version 1.0.0
<PackageReference Include="EricksonLopez.Events" Version="1.0.0" />
<PackageVersion Include="EricksonLopez.Events" Version="1.0.0" />
<PackageReference Include="EricksonLopez.Events" />
paket add EricksonLopez.Events --version 1.0.0
#r "nuget: EricksonLopez.Events, 1.0.0"
#:package EricksonLopez.Events@1.0.0
#addin nuget:?package=EricksonLopez.Events&version=1.0.0
#tool nuget:?package=EricksonLopez.Events&version=1.0.0
EricksonLopez.Events
High-performance, zero-allocation, enterprise-grade Event-Driven Architecture and Distributed Messaging Foundation 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, transactional Outbox and Idempotent Inbox abstractions, 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: Transactional Outbox with Atomic Database Persistence
- Use Case 3: Exactly-Once Idempotent Inbound Processing with Inbox
- 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
- 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 formal Transactional Outbox and Idempotent Inbox abstractions 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 & Reliability Patterns: Built-in bidirectional CNCF CloudEvents v1.0 mapping, pure Transactional Outbox contracts, and consumer deduplication Inbox filters.
โก Key Features
- ๐ Zero-Allocation In-Process Dispatching: Nanosecond-level handler execution using
ValueTaskreturn types without intermediate heap allocations. - ๐ Monotonic Guid v7 Identity (
EventId): RFC 9562-compliant time-ordered identifiers supporting zero-allocation formatting viaISpanFormattableandIUtf8SpanFormattable. - ๐ฆ Strongly Typed
EventEnvelope<TEvent>: Clean reference envelope bundling immutable event payloads with contextual ambient metadata. - ๐ CNCF CloudEvents v1.0 Standard: Bi-directional transformation between internal envelopes and the CloudEvents JSON schema.
- ๐ Transactional Outbox & Inbox Abstractions: Pure contracts enabling guaranteed at-least-once publishing and idempotent inbound consumption.
- โก Roslyn Incremental Source Generators: Automatic compile-time event and handler discovery generating reflection-free static registries.
- ๐ก๏ธ Roslyn Compile-Time Analyzers (
ELE001โELE005): Enforces immutability, valid attribute configurations, and domain-to-integration architectural boundaries. - ๐ First-Class OpenTelemetry Observability: Native BCL
ActivitySourcedistributed tracing context propagation andSystem.Diagnostics.Metricscounters. - ๐งช Enterprise Test Doubles: In-memory
FakeEventPublisher, spyTestEventHandler<T>, and fluent assertion DSL for test automation.
๐ฆ Ecosystem
The EricksonLopez.Events ecosystem is divided into modular, fine-grained, single-responsibility packages:
| 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 | |
EricksonLopez.Events.Inbox |
Idempotent event consumer decorator and message deduplication abstractions | |
EricksonLopez.Events.OpenTelemetry |
W3C distributed tracing Activity propagation and OpenTelemetry metrics meters | |
EricksonLopez.Events.Outbox |
Transactional Outbox persistence contracts and envelope packaging | |
EricksonLopez.Events.Serialization.SystemTextJson |
High-performance NativeAOT System.Text.Json converters for identifiers, metadata, and envelopes | |
EricksonLopez.Events.Testing |
Test doubles (FakeEventPublisher), spy handlers, and fluent assertions for unit and integration testing |
๐ Documentation
๐ Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-events/tree/main/docs
๐ Step-by-Step Interactive Showcase (Levels 00 to 08)
| 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-Process Dispatching | Implementing IEventHandler<T> and executing sequential or parallel pipelines |
| Level 04 | CloudEvents Integration | Bidirectional CNCF CloudEvents v1.0 standard mapping and serialization |
| Level 05 | Transactional Outbox & Inbox | At-least-once persistence guarantees and idempotent consumer deduplication |
| Level 06 | Source Generation & NativeAOT | Compile-time event registry generation and zero-reflection pipelines |
| Level 07 | OpenTelemetry & Tracing | Distributed W3C Activity context propagation and BCL metrics instrumentation |
| Level 08 | Enterprise Testing | Test doubles (FakeEventPublisher), test spies, and fluent assertion DSL |
๐ Technical Reference & Architecture Guides
- Architecture & Invariants โ Complete architectural blueprint, memory layouts, and domain boundaries.
- Architectural Decision Records (ADRs) โ Comprehensive catalog of 30+ 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, Outbox, CloudEvents, 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 for In-Process Dispatching)
dotnet add package EricksonLopez.Events
2. Pure Domain Contracts (For Domain & Application Layers)
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
# Transactional Outbox & Idempotent Inbox abstractions
dotnet add package EricksonLopez.Events.Outbox
dotnet add package EricksonLopez.Events.Inbox
# 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 (For Test Projects)
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.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 EricksonLopez.Events.CloudEvents;
// 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 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: Transactional Outbox with Atomic Database Persistence
Prevent dual-write bugs by storing events in the database within the same business transaction using OutboxEventPublisher:
using EricksonLopez.Events.Contracts;
using EricksonLopez.Events.Outbox;
using Microsoft.Extensions.DependencyInjection;
public static class OutboxSetup
{
public static void ConfigureOutboxServices(IServiceCollection services)
{
// Registers OutboxEventPublisher which intercepts events and persists them atomically
services.AddOutboxEventPublisher();
}
}
Use Case 3: Exactly-Once Idempotent Inbound Processing with Inbox
Prevent duplicate message processing when consuming events from message brokers by wrapping handlers with AddIdempotentEventHandler:
using EricksonLopez.Events.Contracts;
using EricksonLopez.Events.Inbox;
using Microsoft.Extensions.DependencyInjection;
public sealed class InventoryDeductionHandler : IEventHandler<OrderPlacedIntegrationEvent>
{
public ValueTask HandleAsync(OrderPlacedIntegrationEvent eventInstance, CancellationToken cancellationToken = default)
{
Console.WriteLine($"Deducting inventory for order: {eventInstance.OrderId}");
return ValueTask.CompletedTask;
}
}
public static class InboxSetup
{
public static void ConfigureInboxServices(IServiceCollection services)
{
// Automatically checks IInboxConsumerFilter before invoking handler
services.AddIdempotentEventHandler<OrderPlacedIntegrationEvent, InventoryDeductionHandler>(
consumerName: "inventory-worker-group");
}
}
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 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
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.Diagnostics;
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 Compile-Time Diagnostic Analyzers
The EricksonLopez.Events.Generators package analyzes code during compilation to enforce architectural and immutability invariants:
| Diagnostic ID | Severity | Category | Description | CodeFix Available |
|---|---|---|---|---|
ELE001 |
Error | Immutability | Event properties must be immutable ({ get; init; } or readonly) |
โ Yes |
ELE002 |
Warning | Architecture | [EventName] attribute argument cannot be null, empty, or whitespace |
โ Yes |
ELE003 |
Error | Contract | [EventVersion] attribute argument must be a positive integer ($\ge 1$) |
โ Yes |
ELE004 |
Warning | Contract | [EventSource] attribute argument cannot be null, empty, or whitespace |
โ Yes |
ELE005 |
Error | Boundaries | IIntegrationEvent cannot leak domain event types (IDomainEvent) |
โ No |
๐งช Testing & Quality
Declarative Assertions with FakeEventPublisher
Verify event publishing in application services without mocking libraries:
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 EricksonLopez.Events.Identifiers;
using EricksonLopez.Events.Testing;
// 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);
Mutation Testing Verification (Stryker.NET)
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.Outbox |
115 | 115 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.Inbox |
102 | 102 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.OpenTelemetry |
86 | 86 | 100.0% | โ PASSED (High) |
EricksonLopez.Events.Serialization.SystemTextJson |
94 | 94 | 100.0% | โ PASSED (High) |
| Overall Ecosystem Aggregate | 921 | 921 | 100.0% | โ VERIFIED HIGH |
โก Performance Benchmarks
Environment: .NET 10.0.10, X64 RyuJIT AVX-512, BenchmarkDotNet v0.14.0
Event Dispatching & Identifier Benchmarks
| Benchmark Method | Target Runtime | Mean Execution | Error | StdDev | Gen0 Allocations | Allocated Heap Memory |
|---|---|---|---|---|---|---|
EventId.New() (Guid v7) |
.NET 10.0 | 8.12 ns | 0.08 ns | 0.07 ns | - | 0 B |
EventId.TryFormat(Span<char>) |
.NET 10.0 | 5.44 ns | 0.04 ns | 0.03 ns | - | 0 B |
Publish_DomainEvent_InProcess |
.NET 10.0 | 12.40 ns | 0.12 ns | 0.10 ns | - | 0 B |
Publish_DomainEvent_InProcess |
.NET 8.0 | 14.80 ns | 0.15 ns | 0.14 ns | - | 0 B |
Envelope_Packaging_Create |
.NET 10.0 | 4.20 ns | 0.05 ns | 0.04 ns | - | 0 B |
Outbox_Envelope_Serialize |
.NET 10.0 | 62.10 ns | 0.61 ns | 0.58 ns | 0.0029 | 48 B |
CloudEvents_Serialize_AOT |
.NET 10.0 | 84.30 ns | 0.82 ns | 0.76 ns | 0.0038 | 64 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 Ready | Trimming Safe | Dependencies |
|---|---|---|---|---|---|---|
EricksonLopez.Events |
โ | โ | โ | โ | โ | Microsoft.Extensions.DI |
EricksonLopez.Events.Contracts |
โ | โ | โ | โ | โ | Pure BCL (0 Dependencies) |
EricksonLopez.Events.CloudEvents |
โ | โ | โ | โ | โ | Contracts, System.Text.Json |
EricksonLopez.Events.Generators |
โ (.NET Standard 2.0) | โ | โ | โ | โ | Microsoft.CodeAnalysis |
EricksonLopez.Events.Inbox |
โ | โ | โ | โ | โ | Contracts |
EricksonLopez.Events.OpenTelemetry |
โ | โ | โ | โ | โ | OpenTelemetry.Api |
EricksonLopez.Events.Outbox |
โ | โ | โ | โ | โ | Contracts |
EricksonLopez.Events.Serialization.SystemTextJson |
โ | โ | โ | โ | โ | System.Text.Json |
EricksonLopez.Events.Testing |
โ | โ | โ | โ | โ | Events |
Domain Event to CloudEvents Mapping Matrix
| Domain / Envelope Field | CloudEvents v1.0 Field | Schema Type | Description |
|---|---|---|---|
EventEnvelope.Id |
id |
String (UUIDv7) |
Unique event occurrence identifier |
EventEnvelope.Metadata.Source |
source |
URI | Canonical URI producer identifier |
EventEnvelope.Type |
type |
String | Semantic event name ([EventName]) |
EventEnvelope.OccurredAt |
time |
RFC 3339 Timestamp | UTC timestamp of event generation |
EventEnvelope.Metadata.ContentType |
datacontenttype |
String (application/json) |
Payload MIME serialization format |
EventEnvelope.Metadata.CorrelationId |
correlationid |
Extension String | W3C distributed trace correlation token |
EventEnvelope.Metadata.CausationId |
causationid |
Extension String | Causative command or event token |
EventEnvelope.Metadata.TenantId |
tenantid |
Extension String | Multi-tenant tenant identifier |
EventEnvelope.Payload |
data |
Object / JSON | Strongly typed domain event payload |
๐๏ธ Architecture & Design Principles
Functional Dispatch Pipeline
flowchart TD
subgraph Client["Application / Domain Layer"]
Agg["Aggregate Root"] -->|1. Emits| DE["IDomainEvent"]
AppSvc["Application Service"] -->|2. Maps to| IE["IIntegrationEvent"]
AppSvc -->|3. Packages via| Builder["EventMetadataBuilder"]
Builder -->|4. Wraps in| Env["EventEnvelope<TEvent>"]
end
subgraph Pipeline["EricksonLopez.Events Dispatch Engine"]
Env -->|5. Publishes to| Bus["IEventBus / IEventPublisher"]
Bus --> MW["Middleware Pipeline (IEventMiddleware)"]
MW --> Strat{"Execution Strategy"}
Strat -->|Sequential| Seq["SequentialExecutionStrategy"]
Strat -->|Parallel| Par["ParallelExecutionStrategy"]
Seq --> Reg["Static Event Registry (No Reflection)"]
Par --> Reg
Reg --> H1["IEventHandler<T> (Email)"]
Reg --> H2["IEventHandler<T> (Audit)"]
end
subgraph Bridges["Ecosystem Infrastructure Bridges"]
Env -.->|Serialize AOT| STJ["SystemTextJson Converters"]
Env -.->|CNCF Export| CE["CloudEvents v1.0 Adapter"]
Bus -.->|Persist DB Tx| Outbox["Transactional Outbox"]
H2 -.->|Idempotent Guard| Inbox["Idempotent Inbox"]
MW -.->|Trace Propagation| OTEL["OpenTelemetry Tracer"]
end
Event Lifecycle & Transactional Outbox Flow
sequenceDiagram
autonumber
actor App as Application Service
participant Bus as EventBus (IEventBus)
participant MW as Logging/Telemetry Middleware
participant Outbox as OutboxEventPublisher
participant DB as Relational Database
participant Worker as Outbox Background Processor
participant Handler as IEventHandler<T>
App->>Bus: PublishAsync(EventEnvelope)
Bus->>MW: InvokeAsync(EventEnvelope, Delegate)
MW->>Outbox: PublishAsync(EventEnvelope)
Outbox->>DB: INSERT INTO OutboxMessages (Id, Payload, Headers) [Within DB Transaction]
DB-->>Outbox: Transaction Committed
Outbox-->>MW: Complete
MW-->>Bus: Complete
Bus-->>App: ValueTask Completed
Note over Worker,DB: Asynchronous Outbox Polling / CDC
Worker->>DB: SELECT * FROM OutboxMessages WHERE ProcessedAt IS NULL
Worker->>Handler: HandleAsync(DeserializedEvent)
Handler-->>Worker: Success
Worker->>DB: UPDATE OutboxMessages SET ProcessedAt = UtcNow WHERE Id = @Id
๐ก๏ธ Best Practices & Anti-Patterns
| Scenario | โ Avoid (Anti-Pattern) | โ Recommended (Best Practice) |
|---|---|---|
| Event Immutability | Defining mutable properties (public Guid Id { get; set; }) |
Use sealed record with { get; init; } properties. Enforced by analyzer 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 analyzer ELE005. |
| Broker Publishing | Publishing directly to Kafka/RabbitMQ inside domain handlers | Use EricksonLopez.Events.Outbox to persist events atomically in the business transaction. |
| 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.
๐ 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.DomainPrimitives โ Zero-allocation Domain Primitives, SmartEnums, and strongly typed identifiers.
- ๐ EricksonLopez.ValueObjects โ Enterprise Value Objects, Currencies, and Multi-Country Fiscal Satellites.
๐ค Contributing
Contributions are welcome! Follow these steps to build, test, and verify the repository locally:
Prerequisites
- .NET 10.0 SDK or .NET 8.0 SDK
- Git & PowerShell 7+
Local Build & Test Workflow
Clone the Repository:
git clone https://github.com/ericksonlopezf/dotnet-events.git cd dotnet-eventsRestore Dependencies & Build Solution:
dotnet restore dotnet build --configuration ReleaseExecute Test Suite & Code Coverage:
dotnet test --configuration Release --collect:"XPlat Code Coverage"Execute Mutation Testing (Stryker.NET):
dotnet tool restore dotnet stryker --config-file stryker-config.jsonRun Performance Benchmarks:
dotnet run -c Release --project benchmarks/EricksonLopez.Events.Benchmarks
Please review CONTRIBUTING.md, CODE_OF_CONDUCT.md, and SECURITY.md before submitting Pull Requests.
๐ License
Distributed under the MIT License.
Copyright ยฉ 2026 Erickson Lopez. All rights reserved.
| 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
- EricksonLopez.Events.Contracts (>= 1.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
-
net8.0
- EricksonLopez.Events.Contracts (>= 1.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
-
net9.0
- EricksonLopez.Events.Contracts (>= 1.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
NuGet packages (4)
Showing the top 4 NuGet packages that depend on EricksonLopez.Events:
| Package | Downloads |
|---|---|
|
EricksonLopez.Events.Testing
Testing utilities and fake event publishers/handlers for EricksonLopez.Events. |
|
|
EricksonLopez.Events.Serialization.SystemTextJson
System.Text.Json Native AOT converters and serialization adapters for EricksonLopez.Events. |
|
|
EricksonLopez.Events.OpenTelemetry
OpenTelemetry instrumentation extensions for EricksonLopez.Events. Enables distributed tracing and metrics integration with OpenTelemetry .NET SDK. |
|
|
EricksonLopez.Events.CloudEvents
CloudEvents 1.0 adapter and bidirectional mapping extensions for EricksonLopez.Events, supporting CNCF standard event gateways and Native AOT. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 117 | 8/25/2026 |