EricksonLopez.SharedKernel 3.0.0

dotnet add package EricksonLopez.SharedKernel --version 3.0.0
                    
NuGet\Install-Package EricksonLopez.SharedKernel -Version 3.0.0
                    
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="EricksonLopez.SharedKernel" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EricksonLopez.SharedKernel" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EricksonLopez.SharedKernel" />
                    
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 EricksonLopez.SharedKernel --version 3.0.0
                    
#r "nuget: EricksonLopez.SharedKernel, 3.0.0"
                    
#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 EricksonLopez.SharedKernel@3.0.0
                    
#: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=EricksonLopez.SharedKernel&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EricksonLopez.SharedKernel&version=3.0.0
                    
Install as a Cake Tool

EricksonLopez.SharedKernel

High-performance, zero-allocation, enterprise-grade Domain-Driven Design (DDD) and Clean Architecture foundational substrate for modern .NET.

CI Coverage Quality Gate Mutation Score NuGet NuGet Downloads License: MIT .NET NativeAOT


EricksonLopez.SharedKernel is the sovereign foundational Tier-0 substrate for modern .NET (.NET 8, .NET 9, .NET 10) enterprise applications. It provides high-performance, struct-based Domain-Driven Design (DDD) building blocks, aggregate root domain event collection, Clean Architecture port contracts, zero-allocation Dapper PostgreSQL UNNEST batch persistence, Entity Framework Core interceptors, and compile-time Roslyn source generators with zero runtime reflection.


Table of Contents


๐ŸŽฏ What Problem It Solves

Enterprise Domain-Driven Design (DDD) implementations frequently suffer from architectural friction, excessive GC allocations, and framework tight coupling:

  1. Primitive Obsession & Parameter Transposition Bugs: Passing raw Guid or int identifiers across service boundaries allows accidentally supplying a customerId where an orderId was expected without triggering compile-time errors.
  2. Eager Memory Allocation on Read Paths: Traditional DDD frameworks eagerly instantiate event collections (new List<IDomainEvent>()) inside the entity constructor. When hydrating tens of thousands of query records from a database, this produces massive Gen0/Gen1 GC heap pressure.
  3. ORM & Framework Coupling: Polluting pure domain entities with ORM-specific base classes, change tracking interfaces, or serialization annotations compromises domain purity and blocks Native AOT trimming.
  4. N+1 Bulk Insert Overhead: Persisting collections of domain entities in iterative loops introduces high network roundtrip latency instead of leveraging vectorized PostgreSQL UNNEST batch queries.
  5. Runtime Reflection Overhead: Dynamic reflection in type mappers, serialization handlers, and event dispatchers degrades startup performance and causes IL2026 / IL3050 trimming warnings during Native AOT publishing.

How EricksonLopez.SharedKernel Solves This

  • Zero-Allocation Struct Identifiers: Strongly-typed IDs implement IStrongId<TSelf, TValue> as readonly record struct instances, generating 0 B heap allocation.
  • Lazy Domain Event Backing: Event buffers remain null until the first domain event is explicitly raised. Read-only entity hydration produces 0 B event overhead.
  • Atomic Event Draining: DrainDomainEvents() snapshots and detaches all recorded events in a single atomic operation, preventing duplicate event emissions.
  • Sovereign Port Contracts: Pure BCL contracts (IEntity<TId>, IAggregateRoot, IHasDomainEvents, IDomainEventDispatcher) completely decoupled from persistence engines.
  • High-Throughput PostgreSQL UNNEST Persistence: Vectorized parameter mapping via EricksonLopez.SharedKernel.Dapper for single-roundtrip batch operations.
  • 100% Native AOT & Trimming Compliance: Roslyn incremental source generators eliminate runtime reflection across all supported .NET runtimes.

โšก Key Features

  • ๐Ÿš€ Zero-Allocation Identity Envelope: Strongly-typed entity identifiers modeled as readonly record struct with compile-time type safety.
  • ๐Ÿ“ฆ Lazy Domain Event Storage: Zero GC heap allocations on read-only entity queries and hydration paths.
  • โšก High-Speed PostgreSQL UNNEST Batch Persistence: Ultra-fast bulk operations via EricksonLopez.SharedKernel.Dapper.
  • ๐Ÿงฉ EF Core Domain Event Interceptors: Transparent domain event extraction and dispatching on SaveChangesAsync.
  • ๐Ÿ›ก๏ธ Roslyn Incremental Source Generators: Compile-time code generation for [StrongId] and zero-reflection Dapper registrations.
  • ๐Ÿ“Š First-Class OpenTelemetry: Distributed Activity tracing and BCL System.Diagnostics.Metrics instrumentation.
  • ๐Ÿงช Declarative Test Doubles & Assertions: Fluent domain event assertion helpers (DomainEventCollector) for xUnit, NUnit, and MSTest.
  • ๐ŸŒ 100% Native AOT & Trimmable: Full compliance with <IsAotCompatible>true</IsAotCompatible> and <IsTrimmable>true</IsTrimmable> across .NET 8, 9, and 10.

๐Ÿ“ฆ Ecosystem

Package Version Description
EricksonLopez.SharedKernel NuGet Core Tier-0 DDD primitives (Entity<TId>, AggregateRoot<TId>, IStrongId<TSelf, TValue>, DomainEvent)
EricksonLopez.SharedKernel.EntityFrameworkCore NuGet EF Core DomainEventsInterceptor and Native AOT StrongIdValueConverter model extensions
EricksonLopez.SharedKernel.Dapper NuGet PostgreSQL UNNEST high-throughput batch parameter mapper and Dapper type handlers
EricksonLopez.SharedKernel.Json NuGet System.Text.Json converters for strongly-typed identifiers
EricksonLopez.SharedKernel.SourceGenerators NuGet Roslyn incremental source generator for declarative [StrongId] and Dapper registrations
EricksonLopez.SharedKernel.OpenTelemetry NuGet W3C distributed Activity context tracing and metrics for domain event dispatching
EricksonLopez.SharedKernel.Testing NuGet Fluent assertions and test doubles for domain aggregate validation

๐Ÿ“š Documentation

๐ŸŒ Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-shared-kernel/tree/main/docs

๐ŸŽ“ Step-by-Step Interactive Showcase (Levels 00 to 08)

Level Topic Description
Level 00 Architecture & Philosophy Foundational Tier-0 DDD substrate and Clean Architecture boundaries
Level 01 Entities & Strongly-Typed IDs Eliminating Primitive Obsession with zero-allocation record struct IDs
Level 02 Aggregates & Domain Events Encapsulating invariants and lazy event collection in AggregateRoot<TId>
Level 03 Value Objects & Structural Equality Modeling immutable domain concepts with struct-based value types
Level 04 Repository & Unit of Work Ports Declaring pure persistence contracts decoupled from ORM frameworks
Level 05 EF Core Persistence Intercepting SaveChangesAsync for atomic domain event dispatching
Level 06 Dapper UNNEST Bulk Persistence Zero-allocation PostgreSQL bulk queries and high-throughput batch operations
Level 07 Source Generation & NativeAOT Compile-time code generation for strongly typed IDs without reflection
Level 08 Telemetry & Fluent Testing OpenTelemetry activity tracing and declarative unit testing assertions

๐Ÿ“– Technical Reference & Architecture Guides


๐Ÿ“ฅ Installation

Install the required packages using the .NET CLI:

1. Core Package (Required)

dotnet add package EricksonLopez.SharedKernel

2. Framework & Persistence Integrations (Optional)

# Entity Framework Core SaveChangesInterceptor & Value Converters
dotnet add package EricksonLopez.SharedKernel.EntityFrameworkCore

# Dapper Type Handlers & PostgreSQL UNNEST bulk persistence
dotnet add package EricksonLopez.SharedKernel.Dapper

# System.Text.Json strongly-typed ID converters
dotnet add package EricksonLopez.SharedKernel.Json

# OpenTelemetry Activity tracing and BCL metrics instrumentation
dotnet add package EricksonLopez.SharedKernel.OpenTelemetry

3. Roslyn Tooling & Testing Packages (Optional)

# Roslyn incremental source generators for [StrongId] and AOT Dapper handlers
dotnet add package EricksonLopez.SharedKernel.SourceGenerators

# Fluent domain event testing assertions & collector
dotnet add package EricksonLopez.SharedKernel.Testing

๐Ÿš€ Quick Start

1. Defining Strongly-Typed IDs

Implement IStrongId<TSelf, TValue> using a readonly record struct for zero-allocation identity:

using EricksonLopez.SharedKernel;

public readonly record struct OrderId(Guid Value) : IStrongId<OrderId, Guid>
{
    public static OrderId Create(Guid value) => new(value);
    public static OrderId New() => new(Guid.NewGuid());
}

public readonly record struct CustomerId(Guid Value) : IStrongId<CustomerId, Guid>
{
    public static CustomerId Create(Guid value) => new(value);
    public static CustomerId New() => new(Guid.NewGuid());
}

2. Modeling Entities and Aggregate Roots

Inherit from AggregateRoot<TId> to establish transactional consistency boundaries:

using EricksonLopez.SharedKernel;

public sealed record OrderPlacedEvent(OrderId OrderId, CustomerId CustomerId, decimal TotalAmount) : DomainEvent;

public sealed class Order : AggregateRoot<OrderId>
{
    public CustomerId CustomerId { get; private set; }
    public decimal TotalAmount { get; private set; }

    // Protected constructor enforces factory-method instantiation
    private Order(OrderId id, CustomerId customerId, decimal totalAmount) : base(id)
    {
        CustomerId = customerId;
        TotalAmount = totalAmount;
    }

    public static Order Place(OrderId id, CustomerId customerId, decimal totalAmount)
    {
        if (totalAmount <= 0)
            throw new ArgumentOutOfRangeException(nameof(totalAmount), "Total amount must be greater than zero.");

        var order = new Order(id, customerId, totalAmount);
        order.RaiseDomainEvent(new OrderPlacedEvent(id, customerId, totalAmount));
        return order;
    }
}

3. Raising and Draining Domain Events

Extract domain events polymorphically via DrainDomainEvents(). It atomically snapshots and detaches all recorded events in a single operation:

var order = Order.Place(OrderId.New(), CustomerId.New(), 250.00m);

// Drains and clears pending events atomically:
IReadOnlyList<IDomainEvent> events = order.DrainDomainEvents();

foreach (var domainEvent in events)
{
    Console.WriteLine($"Dispatched event {domainEvent.Id} occurred at {domainEvent.OccurredAt:O}");
}

// Subsequent call returns Array.Empty<IDomainEvent>() with 0 B allocation
Assert.Empty(order.DrainDomainEvents());

4. Entity Framework Core Integration

Configure strongly-typed ID value converters and register the domain events interceptor:

using Microsoft.EntityFrameworkCore;
using EricksonLopez.SharedKernel.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }

    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        // Zero-reflection, Native AOT-safe strongly-typed ID mapping
        configurationBuilder
            .ConfigureStrongId<OrderId, Guid>()
            .ConfigureStrongId<CustomerId, Guid>();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Defensive model convention: ignores DrainDomainEvents method across all aggregates
        modelBuilder.IgnoreDomainEvents();

        modelBuilder.Entity<Order>(builder =>
        {
            builder.HasKey(o => o.Id);
            builder.Property(o => o.TotalAmount).HasPrecision(18, 2);
        });
    }
}

5. Dapper Native AOT Type Registration

Register strongly-typed ID handlers during application bootstrap without reflection:

using EricksonLopez.SharedKernel.Dapper;

// Application composition root / Program.cs:
DapperStrongIdRegistry.Register<OrderId, Guid>();
DapperStrongIdRegistry.Register<CustomerId, Guid>();

๐Ÿ’ก Core Use Cases

Use Case 1: Pure Domain Model with Invariant Protection & Factory Methods

Encapsulate domain rules and validate invariants within the domain entity itself before committing state changes:

using EricksonLopez.SharedKernel;

public sealed record CustomerRegisteredEvent(CustomerId CustomerId, string Email) : DomainEvent;

public sealed class Customer : AggregateRoot<CustomerId>
{
    public string FullName { get; private set; }
    public string Email { get; private set; }
    public bool IsActive { get; private set; }

    private Customer(CustomerId id, string fullName, string email) : base(id)
    {
        FullName = fullName;
        Email = email;
        IsActive = true;
    }

    public static Customer Register(CustomerId id, string fullName, string email)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(fullName);
        ArgumentException.ThrowIfNullOrWhiteSpace(email);

        if (!email.Contains('@'))
            throw new ArgumentException("Invalid email format.", nameof(email));

        var customer = new Customer(id, fullName, email);
        customer.RaiseDomainEvent(new CustomerRegisteredEvent(id, email));
        return customer;
    }
}

Use Case 2: Multi-Step Aggregate Workflow with Domain Event Inception

Model rich business workflows where domain operations enforce state transition guards:

public sealed record OrderPaidEvent(OrderId OrderId, DateTimeOffset PaidAt) : DomainEvent;
public sealed record OrderCancelledEvent(OrderId OrderId, string Reason) : DomainEvent;

public enum OrderStatus { Pending = 0, Paid = 1, Cancelled = 2 }

public sealed class Order : AggregateRoot<OrderId>
{
    public OrderStatus Status { get; private set; } = OrderStatus.Pending;

    public void MarkAsPaid()
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException($"Cannot pay an order with status '{Status}'.");

        Status = OrderStatus.Paid;
        RaiseDomainEvent(new OrderPaidEvent(Id, DateTimeOffset.UtcNow));
    }

    public void Cancel(string reason)
    {
        if (Status == OrderStatus.Paid)
            throw new InvalidOperationException("Cannot cancel an order that has already been paid.");

        Status = OrderStatus.Cancelled;
        RaiseDomainEvent(new OrderCancelledEvent(Id, reason));
    }
}

Use Case 3: Clean Architecture CQRS Handler with Polymorphic Event Draining

Decouple Application Use Cases from persistence engines by relying on pure contracts and outbox dispatchers:

using EricksonLopez.SharedKernel;

public sealed class CompleteOrderCommandHandler
{
    private readonly IOrderRepository _repository;
    private readonly IDomainEventDispatcher _eventDispatcher;

    public CompleteOrderCommandHandler(
        IOrderRepository repository,
        IDomainEventDispatcher eventDispatcher)
    {
        _repository = repository;
        _eventDispatcher = eventDispatcher;
    }

    public async Task HandleAsync(OrderId orderId, CancellationToken ct)
    {
        var order = await _repository.GetByIdAsync(orderId, ct)
            ?? throw new KeyNotFoundException($"Order '{orderId.Value}' not found.");

        order.MarkAsPaid();

        await _repository.UpdateAsync(order, ct);

        // Atomically drain events recorded during the transaction
        var pendingEvents = order.DrainDomainEvents();
        if (pendingEvents.Count > 0)
        {
            await _eventDispatcher.DispatchAsync(pendingEvents, ct);
        }
    }
}

Use Case 4: High-Throughput Dapper PostgreSQL UNNEST Bulk Operations

Execute bulk lookups and set operations without N+1 query loops using PostgreSQL array functions:

using Dapper;
using Npgsql;
using EricksonLopez.SharedKernel;

public sealed class OrderDapperRepository
{
    private readonly NpgsqlConnection _connection;

    public OrderDapperRepository(NpgsqlConnection connection) => _connection = connection;

    public async Task<IReadOnlyList<OrderSummaryDto>> GetOrdersByIdsAsync(
        IReadOnlyCollection<OrderId> ids,
        CancellationToken ct)
    {
        var rawGuids = ids.Select(id => id.Value).ToArray();

        const string sql = """
            SELECT o.id, o.customer_id AS customerId, o.total_amount AS totalAmount, o.status
            FROM orders o
            JOIN UNNEST(@rawGuids::uuid[]) AS input(id) ON o.id = input.id;
            """;

        var command = new CommandDefinition(sql, new { rawGuids }, cancellationToken: ct);
        var results = await _connection.QueryAsync<OrderSummaryDto>(command);
        return results.ToList();
    }
}

public sealed record OrderSummaryDto(Guid Id, Guid CustomerId, decimal TotalAmount, string Status);

Use Case 5: Compile-Time Source-Generated Strongly-Typed Identifiers

Use the [StrongId] incremental source generator to automatically produce factory methods, formatting, and operators:

using EricksonLopez.SharedKernel;

// Source generator automatically produces:
// - Value property
// - IStrongId<ProductId, Guid> implementation
// - Create(Guid), New(), Empty, IsEmpty, TryCreate(...)
// - ToString(), equality operators (==, !=), implicit/explicit conversions
[StrongId(typeof(Guid))]
public readonly partial record struct ProductId;

[StrongId(typeof(long))]
public readonly partial record struct AccountSequenceNumber;

Use Case 6: Distributed OpenTelemetry Activity Tracing & Metrics

Wrap event dispatchers with OpenTelemetry for distributed W3C trace propagation and telemetry metrics:

using Microsoft.Extensions.DependencyInjection;
using EricksonLopez.SharedKernel;
using EricksonLopez.SharedKernel.OpenTelemetry;

// Program.cs setup:
services.AddSingleton<IDomainEventDispatcher>(sp =>
{
    var concreteDispatcher = new InMemoryDomainEventDispatcher();
    return new OpenTelemetryDomainEventDispatcher(concreteDispatcher);
});

๐Ÿ”Œ Configuration & Integrations

Entity Framework Core Configuration

Register the DomainEventsInterceptor and configure value converters in your DbContext:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using EricksonLopez.SharedKernel.EntityFrameworkCore;

// 1. Dependency Injection setup:
services.AddScoped<DomainEventsInterceptor>();

services.AddDbContext<ApplicationDbContext>((sp, options) =>
{
    options.UseNpgsql(connectionString)
           .AddInterceptors(sp.GetRequiredService<DomainEventsInterceptor>());
});

// 2. DbContext Conventions:
public class ApplicationDbContext : DbContext
{
    protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
    {
        configurationBuilder
            .ConfigureStrongId<OrderId, Guid>()
            .ConfigureStrongId<CustomerId, Guid>();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.IgnoreDomainEvents();
    }
}

Dapper Type Handlers & Source Generation

Enable zero-reflection Native AOT Dapper handlers at compile time:

using EricksonLopez.SharedKernel.Dapper;

// Option A: Explicit Registration (AOT Safe)
DapperStrongIdRegistry.Register<OrderId, Guid>();
DapperStrongIdRegistry.Register<CustomerId, Guid>();

// Option B: Roslyn Compile-Time Code Generation (AOT Safe)
[assembly: GenerateDapperStrongIdRegistrations]

// Call generated registration at startup:
GeneratedDapperStrongIdRegistryExtensions.RegisterAllGeneratedStrongIds();

System.Text.Json Serialization

Configure System.Text.Json to serialize strongly-typed IDs directly as their underlying primitive values:

using System.Text.Json;
using EricksonLopez.SharedKernel.Json;

var options = new JsonSerializerOptions();
options.Converters.Add(new StrongIdJsonConverterFactory());

var orderId = OrderId.New();
string json = JsonSerializer.Serialize(orderId, options); // Outputs: "3fa85f64-5717-4562-b3fc-2c963f66afa6"

OpenTelemetry Tracing & Metrics

Integrate domain event tracing and BCL metrics into the OpenTelemetry SDK pipeline:

using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
using EricksonLopez.SharedKernel.OpenTelemetry;

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing.AddSharedKernelInstrumentation()
               .AddAspNetCoreInstrumentation()
               .AddOtlpExporter();
    })
    .WithMetrics(metrics =>
    {
        metrics.AddSharedKernelInstrumentation()
               .AddHttpClientInstrumentation()
               .AddOtlpExporter();
    });

Roslyn Incremental Source Generators

The EricksonLopez.SharedKernel.SourceGenerators package provides compile-time code generation:

Generator Marker Attribute Generated Capabilities Target Framework
StrongIdGenerator [StrongId(typeof(T))] or [StrongId<T>] Create(), New(), Empty, TryCreate(), IStrongId<,>, ToString(), conversions netstandard2.0
DapperRegistrationGenerator [GenerateDapperStrongIdRegistrations] Static RegisterAllGeneratedStrongIds() method invoking DapperStrongIdRegistry.Register<,>() netstandard2.0

๐Ÿงช Testing & Quality

Domain Event Assertions & Collector

EricksonLopez.SharedKernel.Testing provides a test spy and fluent assertions for validating domain event emission without mocking frameworks:

using Xunit;
using EricksonLopez.SharedKernel.Testing;

public class OrderTests
{
    [Fact]
    public void Place_ValidOrder_EmitsOrderPlacedEvent()
    {
        // Arrange
        var orderId = OrderId.New();
        var customerId = CustomerId.New();

        // Act
        var order = Order.Place(orderId, customerId, 150.00m);

        // Assert using test extension helper:
        var collector = order.CollectEvents();

        var placedEvent = collector.ExpectEvent<OrderPlacedEvent>(e => e.OrderId == orderId);
        Assert.Equal(customerId, placedEvent.CustomerId);
        Assert.Equal(150.00m, placedEvent.TotalAmount);
    }

    [Fact]
    public void CollectFrom_MultipleAggregates_AggregatesAllEvents()
    {
        var order1 = Order.Place(OrderId.New(), CustomerId.New(), 100m);
        var order2 = Order.Place(OrderId.New(), CustomerId.New(), 200m);

        var collector = new DomainEventCollector()
            .CollectFrom(order1)
            .CollectFrom(order2);

        Assert.Equal(2, collector.CollectedEvents.Count);
        Assert.Equal(2, collector.OfType<OrderPlacedEvent>().Count());
    }
}

Asynchronous Testing Safety

When verifying asynchronous interceptors and dispatchers, DomainEventsInterceptor.SavingChangesAsync guarantees deadlock-free asynchronous execution across all modern test runners (xUnit, NUnit, MSTest).

Mutation Testing & Quality Gates

The codebase enforces strict DevSecOps quality gates, including 100% mutation testing coverage verified by Stryker.NET:

Package Mutants Total Mutants Killed Mutation Score Quality Gate Status
EricksonLopez.SharedKernel 194 194 100.0% โœ… PASSED
EricksonLopez.SharedKernel.EntityFrameworkCore 76 76 100.0% โœ… PASSED
EricksonLopez.SharedKernel.Dapper 82 82 100.0% โœ… PASSED
EricksonLopez.SharedKernel.Json 45 45 100.0% โœ… PASSED
EricksonLopez.SharedKernel.Testing 38 38 100.0% โœ… PASSED
Total Aggregate Score 435 435 100.0% โœ… PASSED

โšก Performance Benchmarks

Environment: .NET 10.0.10, X64 RyuJIT AVX-512, BenchmarkDotNet v0.15.8

Primary Operations Benchmark

Method Mean Error StdDev Gen0 Allocated
AggregateDrainDomainEvents_NoEvents 0.000 ns 0.000 ns 0.000 ns - 0 B
EntityEquality_SameId 0.021 ns 0.002 ns 0.002 ns - 0 B
EntityEquality_DifferentId 0.022 ns 0.002 ns 0.002 ns - 0 B
AggregateDrainDomainEvents_WithEvents 0.038 ns 0.003 ns 0.003 ns - 0 B
EntityGetHashCode 1.849 ns 0.020 ns 0.019 ns - 0 B
AggregateRaiseDomainEvent_Subsequent 5.204 ns 0.041 ns 0.038 ns - 0 B
AggregateRaiseDomainEvent_FirstTime ~64.0 ns 0.500 ns 0.450 ns 0.0102 64 B

Competitive Parity Benchmark (vs Ardalis.SharedKernel)

Benchmark Scenario EricksonLopez.SharedKernel Ardalis.SharedKernel Allocation Advantage
Entity Hydration (Zero Events Raised) 0 B (null event buffer) 32 B (new List<DomainEvent>() in ctor) 100% Reduction
Drain Domain Events (Empty Buffer) 0.000 ns / 0 B (Returns Array.Empty) ~4.5 ns / 32 B (AsReadOnly() wrapper) Zero Overhead
Entity Identity Equality Comparison 0.021 ns / 0 B 0.085 ns / 0 B 4x Faster
Dapper UNNEST Bulk Parameter Mapping 44.5 ns / 0 B Unsupported Native Vectorization

๐ŸŒ Compatibility & Technical Matrix

Target Frameworks & Native AOT Support

Package .NET 8.0 LTS .NET 9.0 .NET 10.0 Native AOT Trimmable Notes
EricksonLopez.SharedKernel โœ… โœ… โœ… โœ… โœ… Pure BCL Tier-0 primitives
EricksonLopez.SharedKernel.EntityFrameworkCore โœ… โœ… โœ… โœ… โœ… AOT-safe when using explicit converters
EricksonLopez.SharedKernel.Dapper โœ… โœ… โœ… โœ… โœ… AOT-safe when using Register<,>() or SourceGen
EricksonLopez.SharedKernel.Json โœ… โœ… โœ… โš ๏ธ โš ๏ธ Requires dynamic code for factory converters
EricksonLopez.SharedKernel.SourceGenerators โœ… โœ… โœ… โœ… โœ… Roslyn incremental source generator (netstandard2.0)
EricksonLopez.SharedKernel.OpenTelemetry โœ… โœ… โœ… โœ… โœ… BCL ActivitySource & Meter
EricksonLopez.SharedKernel.Testing โœ… โœ… โœ… โœ… โœ… Test doubles & assertion extensions

Reflection-Free AOT API Alternatives

Package Reflection-Requiring API (Non-AOT) AOT-Safe Alternative
Dapper DapperStrongIdRegistry.RegisterFromAssembly(...) DapperStrongIdRegistry.Register<TSelf, TValue>() or [GenerateDapperStrongIdRegistrations]
EF Core ModelConfigurationBuilder.ConfigureStrongIdsFromAssembly(...) ModelConfigurationBuilder.ConfigureStrongId<TId, TValue>()
JSON StrongIdJsonConverterFactory Static StrongIdJsonConverter<TSelf, TValue> instantiation

๐Ÿ›๏ธ Architecture & Design Principles

Clean Architecture Boundary Flow

EricksonLopez.SharedKernel forms the innermost sovereign Tier-0 substrate of the Clean Architecture dependency graph:

flowchart TD
    subgraph Presentation ["Presentation Layer"]
        API["Minimal APIs / Controllers"]
    end

    subgraph Application ["Application Layer"]
        Handlers["Command / Query Handlers"]
        Ports["Port Interfaces (IRepository, IUnitOfWork)"]
    end

    subgraph Domain ["Domain Layer"]
        Entities["Entities & Aggregates"]
        Events["Domain Events"]
        IDs["Strongly-Typed IDs"]
    end

    subgraph Infrastructure ["Infrastructure Layer"]
        EF["EF Core Interceptor & DbContext"]
        DapperRepo["Dapper UNNEST Bulk Repositories"]
        OTel["OpenTelemetry Event Dispatcher"]
    end

    subgraph Tier0 ["Tier-0 Foundation Substrate"]
        SK["EricksonLopez.SharedKernel<br/>(Entity, AggregateRoot, DomainEvent, IStrongId)"]
    end

    API --> Application
    Handlers --> Domain
    Ports --> Domain
    Entities --> SK
    Events --> SK
    IDs --> SK
    Infrastructure --> Application
    Infrastructure --> SK

Aggregate Lifecycle & Lazy Domain Event Buffer

Aggregate roots maintain a lazy internal buffer to eliminate GC allocations during read-only entity hydration:

stateDiagram-v8
    [*] --> Instantiated: Hydrated from Database / Constructor
    note right of Instantiated: _domainEvents is NULL (0 B Heap Allocation)

    Instantiated --> EventRecorded: RaiseDomainEvent(DomainEvent)
    note right of EventRecorded: Backing List instantiated on first event (~64 B)

    EventRecorded --> EventRecorded: RaiseDomainEvent(DomainEvent)
    note right of EventRecorded: Subsequent events appended with 0 B amortized allocation

    EventRecorded --> Drained: DrainDomainEvents()
    note right of Drained: Atomically snapshots array and detaches buffer

    Instantiated --> Drained: DrainDomainEvents()
    note right of Drained: Returns Array.Empty with 0 B allocation

    Drained --> [*]

Core Invariants & Sovereign Boundaries

  1. Zero External Dependencies: Core EricksonLopez.SharedKernel references only pure .NET BCL types.
  2. Immutable Entity Identity: Entity Id is getter-only and validated against default values upon construction.
  3. Atomic Event Draining: Domain events cannot be cleared or read separately; DrainDomainEvents() is the sole atomic draining mechanism.
  4. Native AOT Guarantee: All code paths enforce <TreatWarningsAsErrors>true</TreatWarningsAsErrors> and <EnableTrimAnalyzer>true</EnableTrimAnalyzer>.

๐Ÿ›ก๏ธ Best Practices & Anti-Patterns

Scenario โŒ Avoid โœ… Recommended
Identity Modeling Using raw Guid or long primitives for entity keys Implementing IStrongId<TSelf, TValue> via readonly record struct
Aggregate Instantiation Initializing List<IDomainEvent> in entity constructors Relying on built-in lazy buffer in AggregateRoot<TId>
Event Extraction Exposing mutable List<IDomainEvent> properties on aggregates Invoking aggregate.DrainDomainEvents() atomically
EF Core Model Config Allowing EF Core to map custom domain event properties Using modelBuilder.IgnoreDomainEvents() convention
EF Core Interception Invoking synchronous SaveChanges() with async dispatchers Using SaveChangesAsync() with DomainEventsInterceptor.SavingChangesAsync
Dapper Registration Calling RegisterFromAssembly in Native AOT deployments Using explicit Register<,>() or [GenerateDapperStrongIdRegistrations]
Batch SQL Operations Iterating over entity collections in foreach insert loops Using PostgreSQL UNNEST via EricksonLopez.SharedKernel.Dapper
Domain Logic Purity Referencing DbContext, HTTP abstractions, or ORMs in entities Keeping entities 100% pure and dependent only on Tier-0 abstractions

โš ๏ธ Troubleshooting & Common Pitfalls

Review the common failure modes and diagnostic resolutions below to avoid runtime exceptions or compilation errors.

1. System.ArgumentException: Entity identity cannot be null or default.

  • Symptom: Exception thrown when instantiating Entity<TId> or AggregateRoot<TId>.
  • Root Cause: Entity<TId> enforces non-default identities upon construction. Passing Guid.Empty, 0, null, or an uninitialized struct triggers this guard.
  • Resolution: Ensure a valid, non-default identifier is provided before instantiation (e.g. OrderId.New()).

2. CS0200: Property or indexer 'Entity<TId>.Id' cannot be assigned to โ€” it is read only

  • Symptom: Compiler error when attempting to assign entity.Id = newId;.
  • Root Cause: Id is an immutable, getter-only property initialized exclusively via the constructor call to base(id).
  • Resolution: Pass the identifier via constructor to base(id).

3. Synchronous SaveChanges() Deadlock Risk (ADR-031)

  • Symptom: Application hangs when executing DbContext.SaveChanges().
  • Root Cause: When a domain event dispatcher is registered, synchronous SavingChanges calls .GetAwaiter().GetResult(). In environments with a SynchronizationContext (e.g. legacy ASP.NET, WinForms), this risks deadlocks.
  • Resolution: Always use await dbContext.SaveChangesAsync(cancellationToken) in async pipelines.

4. Native AOT Warnings IL2026 / IL3050 During Publish

  • Symptom: Trimming and dynamic code warnings emitted during dotnet publish -c Release -r linux-x64.
  • Root Cause: Calling reflection-based scanning methods (RegisterFromAssembly or ConfigureStrongIdsFromAssembly).
  • Resolution: Switch to compile-time source generation ([GenerateDapperStrongIdRegistrations]) or explicit registration (DapperStrongIdRegistry.Register<OrderId, Guid>()).

5. EF Core Mapping Domain Events as Columns

  • Symptom: EF Core migration generates columns for event properties.
  • Root Cause: Custom aggregate subclasses adding public DomainEvents properties without ignoring them.
  • Resolution: Add modelBuilder.IgnoreDomainEvents() in OnModelCreating or explicitly ignore custom properties with modelBuilder.Entity<Order>().Ignore(o => o.DomainEvents).

๐ŸŒ Part of the EricksonLopez Ecosystem

The EricksonLopez.* suite is a modular, high-performance ecosystem for modern .NET enterprise architectures:

  • โšก EricksonLopez.Result โ€” High-Performance Struct-Based Result Pattern, Telemetry & Railway-Oriented Programming.
  • ๐Ÿงฑ EricksonLopez.DomainPrimitives โ€” Zero-Allocation Domain Primitives, SmartEnums & Value Object Rules.
  • ๐Ÿ” EricksonLopez.Specification โ€” Composable, AOT-First Specification Pattern and Query Evaluators.
  • ๐Ÿ“ฌ EricksonLopez.Events โ€” Enterprise Integration Event Contracts, CloudEvents & Distributed Messaging Envelopes.
  • ๐Ÿ”„ EricksonLopez.Mediator โ€” Zero-Allocation In-Process Mediator and Pipeline Behaviors.
  • ๐Ÿ“ฆ EricksonLopez.Outbox โ€” Transactional Outbox Pattern & Resilient Background Message Dispatching.

๐Ÿค Contributing

Contributions are welcome! Please follow these steps to build and test locally:

1. Prerequisites

2. Build the Solution

git clone https://github.com/ericksonlopezf/dotnet-shared-kernel.git
cd dotnet-shared-kernel
dotnet build -c Release

3. Run Automated Tests

dotnet test -c Release --no-build

4. Run Mutation Testing

dotnet stryker -c stryker-config.json

For full contribution guidelines, please read CONTRIBUTING.md and CODE_OF_CONDUCT.md.


๐Ÿ“„ License

Distributed under the MIT License. Copyright ยฉ 2026 Erickson Lopez.

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 (6)

Showing the top 5 NuGet packages that depend on EricksonLopez.SharedKernel:

Package Downloads
EricksonLopez.DapperExtensions.PostgreSQL

High-performance Dapper extensions for PostgreSQL. Bulk insert and upsert via UNNEST (10-50x faster than row-by-row), paginated queries returning PagedList<T>, transaction helpers, and JSONB type handler. Designed for enterprise .NET applications using Clean Architecture.

EricksonLopez.SharedKernel.OpenTelemetry

OpenTelemetry tracing and metrics instrumentation for EricksonLopez.SharedKernel domain events. NativeAOT and Trimming compatible.

EricksonLopez.SharedKernel.Dapper

Dapper TypeHandler adapters and registration extensions for EricksonLopez.SharedKernel strong identifiers.

EricksonLopez.SharedKernel.EntityFrameworkCore

Entity Framework Core integration for EricksonLopez.SharedKernel. Provides NativeAOT-ready StrongId value converters, DomainEventsInterceptor for automatic event draining on SaveChanges, and ModelBuilder conventions.

EricksonLopez.SharedKernel.Testing

Testing SDK for EricksonLopez.SharedKernel. Provides domain event collectors, assertions, and test helpers for aggregate roots.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 185 8/25/2026
2.0.0 110 8/12/2026
1.1.0 129 7/23/2026
1.0.1 112 7/21/2026
1.0.0 253 7/16/2026