EricksonLopez.SharedKernel 1.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package EricksonLopez.SharedKernel --version 1.1.0
                    
NuGet\Install-Package EricksonLopez.SharedKernel -Version 1.1.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="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EricksonLopez.SharedKernel" Version="1.1.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 1.1.0
                    
#r "nuget: EricksonLopez.SharedKernel, 1.1.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@1.1.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=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=EricksonLopez.SharedKernel&version=1.1.0
                    
Install as a Cake Tool

EricksonLopez.SharedKernel

NuGet NuGet Downloads CI Coverage License: MIT .NET NativeAOT

A shared kernel for DDD-based .NET applications. Provides battle-tested abstractions for Clean Architecture projects: Entity, AggregateRoot, ValueObject, Result pattern, Domain Events, Specification pattern, and Pagination.

Key Features:

  • โšก Zero external dependencies
  • ๐Ÿ”’ Immutable by default โ€” ValueObject, Error, PagedList are sealed/records
  • ๐Ÿš€ Zero-alloc happy path โ€” Result.Success() is cached
  • ๐Ÿ”— Fluent pipelines โ€” Result supports Map, Bind, Match, Tap, Ensure, Recover, Try, Combine
  • ๐Ÿš€ NativeAOT + Trimming compatible โ€” IsAotCompatible and IsTrimmable enabled
  • โš™๏ธ Async-first โ€” Full Task<Result<T>> and ValueTask<Result<T>> extension methods
  • ๐Ÿงฉ No magic โ€” every abstraction is readable and debuggable

Table of Contents


Installation

dotnet add package EricksonLopez.SharedKernel

Requires .NET 10 or .NET Standard 2.0 compatible frameworks (e.g., .NET Framework 4.6.1+, .NET Core 2.0+).


Quick Start

Result Pattern

// Define errors as a static class per domain concept
public static class UserErrors
{
    public static Error NotFound(Guid id) =>
        Error.NotFound("User.NotFound", $"User '{id}' was not found.");

    public static readonly Error NameEmpty =
        Error.Validation("User.NameEmpty", "Name cannot be empty.");

    public static readonly Error Inactive =
        Error.Forbidden("User.Inactive", "User is not active.");
}

// Return Result instead of throwing
public Result<User> GetUser(Guid id)
{
    var user = _repository.Find(id);
    return user is null ? UserErrors.NotFound(id) : user;
}

Fluent pipeline:

var result = GetUser(id)
    .Ensure(u => u.IsActive, UserErrors.Inactive)
    .Map(u => new UserDto(u.Name, u.Email))
    .Tap(dto => _cache.Set(id, dto))
    .TapError(e => _logger.LogWarning("Failed: {Error}", e));

Pattern matching with Match:

return result.Match(
    user => Ok(user),
    error => Problem(error.Description));

Try-pattern (idiomatic .NET):

if (result.TryGetValue(out var user))
    Console.WriteLine(user.Name);

var name = GetUser(id)
    .Map(u => u.Name)
    .GetValueOrDefault("Unknown");

Destructuring:

var (ok, user, error) = GetUser(id);
if (ok) Console.WriteLine(user.Name);

Exception bridge:

var result = Result.Try(
    () => JsonSerializer.Deserialize<Config>(json),
    ex => Error.Unexpected("Config.ParseFailed", ex.Message));

Async pipelines (with ConfigureAwait(false)):

var result = await _repository.GetById(id)   // Task<Result<User>>
    .Ensure(u => u.IsActive, UserErrors.Inactive)
    .Map(u => u.ToDto())
    .Tap(dto => _cache.SetAsync(id, dto))
    .Recover(e => _fallbackRepo.GetById(id));

Error Types

Error.Failure(code, description)       // Generic domain error
Error.Validation(code, description)    // Input validation
Error.NotFound(code, description)      // Resource not found
Error.Conflict(code, description)      // State conflict
Error.Unauthorized(code, description)  // Authentication required
Error.Forbidden(code, description)     // Insufficient permissions
Error.Unavailable(code, description)   // Service unavailable
Error.Unexpected(code, description)    // System error / exceptions

Compound errors (e.g., multiple validation failures):

var error = Error.Validation("User.Invalid", "Validation failed",
    Error.Validation("User.Name.Required", "Name is required"),
    Error.Validation("User.Email.Invalid", "Invalid email format"));

error.HasInnerErrors   // true
error.InnerErrors      // [Name.Required, Email.Invalid]

Combining multiple results:

var result = Result.Combine(
    ValidateName(name),
    ValidateEmail(email),
    ValidateAge(age));
// Returns success if all pass, or compound error with all failures

// Typed combine into tuples:
var (user, account) = Result.Combine(GetUser(id), GetAccount(id)).Value;

AggregateRoot & Entity

// AggregateRoot โ€” the only entry point for Domain Events
public sealed class Order : AggregateRoot<Guid>
{
    public string Description { get; private set; }

    private Order(Guid id, string description)
    {
        Id = id;
        Description = description;
    }

    public static Order Create(Guid id, string description)
    {
        var order = new Order(id, description);
        order.RaiseDomainEvent(new OrderCreated(id));
        return order;
    }
}

// Entity โ€” identity-only, no domain events
public sealed class LineItem : Entity<Guid>
{
    public string ProductName { get; private set; } = string.Empty;
}

// Domain event
public sealed record OrderCreated(Guid OrderId) : IDomainEvent;

// In your Unit of Work โ€” after SaveChanges:
foreach (var aggregate in aggregates)
{
    var events = aggregate.DomainEvents.ToList();
    aggregate.ClearDomainEvents();
    foreach (var domainEvent in events)
        await _publisher.Publish(domainEvent);
}

ValueObject

public sealed class Money : ValueObject
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }

    // Optional: override for zero-boxing equality on hot paths
    // public override bool Equals(ValueObject? other)
    //     => other is Money m && Amount == m.Amount && Currency == m.Currency;
    // public override int GetHashCode()
    //     => HashCode.Combine(Amount, Currency);
}

Specification Pattern

public sealed class ActiveUserSpec : Specification<User>
{
    public override Expression<Func<User, bool>> ToExpression()
        => user => user.IsActive;

    // Optional: NativeAOT-safe override
    protected override bool Evaluate(User candidate)
        => candidate.IsActive;
}

// Compose with operators
var spec = new ActiveUserSpec() & new PremiumUserSpec();

// In-memory evaluation
var eligible = users.Where(spec.IsSatisfiedBy);

// LINQ-to-SQL (EF Core / Dapper)
var expression = spec.ToExpression();

Pagination

var parameters = PaginationParameters.Of(page: 2, pageSize: 10);

var items = await _connection.QueryAsync<ProductDto>(sql,
    new { limit = parameters.PageSize, offset = parameters.Skip });
var total = await _connection.ExecuteScalarAsync<int>(countSql);

var page = PagedList<ProductDto>.Create(items, total, parameters);

page.TotalCount    // Total items across all pages
page.TotalPages    // Ceiling(TotalCount / PageSize)
page.HasNextPage   // Navigation flag
page.Map(dto => new ProductResponse(dto.Id, dto.Name))  // Project preserving metadata

API Reference

Domain

Type Members Description
Entity<TId> Id, ==/!= Identity-based equality
AggregateRoot<TId> RaiseDomainEvent(), DomainEvents, ClearDomainEvents() Consistency boundary + event publishing
ValueObject GetEqualityComponents(), virtual Equals Structural equality
IDomainEvent marker interface Domain event contract

Result

Member Result Result<T> Description
IsSuccess / IsFailure โœ… โœ… State inspection
Error โœ… โœ… The failure error (Error.None on success)
Value โ€” โœ… Success value (throws on failure)
Map<TNext>(Func) โ€” โœ… Transform value
Bind<TNext>(Func) โ€” โœ… Chain Result-returning operations
Match<TOut>(onSuccess, onFailure) โœ… โœ… Exhaustive handling
Tap(Action) โœ… โœ… Side effect on success
TapError(Action) โœ… โœ… Side effect on failure
Ensure(predicate, error) โœ… โœ… Post-condition validation
Recover(Func) โ€” โœ… Fallback on failure
Finally(Action) โœ… โœ… Always execute
MapError(Func) โœ… โœ… Transform the error
TryGetValue(out T) โ€” โœ… Try-pattern
TryGetError(out Error) โœ… โœ… Try-pattern
GetValueOrDefault(T) โ€” โœ… Safe access
GetValueOrDefault(Func) โ€” โœ… Safe access with fallback logic
ToResult() โ€” โœ… Drop value (Result<T> โ†’ Result)
Deconstruct โ€” โœ… var (ok, value, error) = result;
Try(Action, errorHandler) โœ… โœ… Exception โ†’ Error bridge
Combine(params Result[]) โœ… โœ… Aggregate results

Error

Factory ErrorType Semantic
Error.Failure(code, desc) Failure Generic domain error
Error.Validation(code, desc) Validation Input validation
Error.NotFound(code, desc) NotFound Resource not found
Error.Conflict(code, desc) Conflict State conflict
Error.Unauthorized(code, desc) Unauthorized Authentication required
Error.Forbidden(code, desc) Forbidden Insufficient permissions
Error.Unavailable(code, desc) Unavailable Service unavailable
Error.Unexpected(code, desc) Unexpected System error

All factories have an overload with params Error[] innerErrors for compound errors.

Specification

Member Description
ToExpression() Expression tree for LINQ-to-SQL
IsSatisfiedBy(T) In-memory evaluation via Evaluate()
Evaluate(T) protected virtual โ€” override for NativeAOT
And(spec) / & Logical AND
Or(spec) / \| Logical OR
Not() / ! Logical NOT

Pagination

Member Description
PagedList<T>.Create(items, total, params) Factory
PagedList<T>.Empty(params) Empty page
Items, TotalCount, TotalPages Page data
HasPreviousPage / HasNextPage Navigation
Map<TResult>(Func) Project preserving metadata

NativeAOT Compatibility

This library is fully NativeAOT and trimming compatible:

<IsTrimmable>true</IsTrimmable>
<IsAotCompatible>true</IsAotCompatible>

Specification in NativeAOT: The default Evaluate() method uses Expression.Compile() (requires JIT). For NativeAOT, override Evaluate() in your leaf specifications:

public sealed class ActiveSpec : Specification<Product>
{
    public override Expression<Func<Product, bool>> ToExpression()
        => p => p.IsActive;

    // NativeAOT-safe: no Expression.Compile()
    protected override bool Evaluate(Product candidate)
        => candidate.IsActive;
}

Composite specifications (And, Or, Not) are automatically NativeAOT-safe โ€” they delegate to children's IsSatisfiedBy() without compiling.


Performance Benchmarks

The SharedKernel is strictly optimized for low latency and minimal allocations. We use BenchmarkDotNet to ensure the happy path is completely allocation-free (0 bytes).

Operation Allocation Note
Result.Success() 0 B Cached static instance
Result.Failure() ~24 B Exceptional path
ValueObject.Equals (hot path) 0 B When Equals is manually overridden (see ADR)
Specification.IsSatisfiedBy 0 B Cached compiled expression lock

For details, see ADR-006: Performance Analysis.


Samples

Check the samples/ directory for working examples:

  • EricksonLopez.SharedKernel.Sample: Standard Web API project demonstrating the Result pattern and Specification pattern.
  • EricksonLopez.SharedKernel.AotConsole: A Native AOT console app demonstrating trimming and zero-reflection value objects.

Part of the EricksonLopez Ecosystem

SharedKernel is the foundational layer of a modular .NET ecosystem:

Package Description Depends on SharedKernel Status
SharedKernel DDD abstractions + Result pattern โ€” (this library) โœ… Published
DomainPrimitives Value Objects with Source Generators โœ… ๐Ÿ“‹ Planned
SqlBuilder SQL-first query builder for Dapper โœ… ๐Ÿ“‹ Planned
Outbox Transactional Messaging (Outbox + Inbox) โœ… ๐Ÿ“‹ Planned

Architecture Decisions & Guides

Design rationale and guides are documented in the docs folder:

Architectural Decision Records (ADRs):


License

MIT ยฉ Erickson Lรณpez


FAQ & Troubleshooting

Q: Why is Result a class and not a struct? A: Result is a class to provide inheritance (Result<TValue> : Result) and allow null-checks when used as a reference type. The allocations are minimal because Result.Success() uses a cached singleton and Error uses IReadOnlyList<Error>? which is null in the happy path.

Q: How do I handle multiple validation errors? A: You can pass multiple inner errors to Error.Validation using the params Error[] innerErrors overload. See Error Types for examples.

Q: I modified a ToString() method and my PR build failed with VerifyException. What should I do? A: We use Verify.Xunit for snapshot testing. If you intentionally changed the output format, you must review the generated .received.txt file and rename it to .verified.txt to accept the new snapshot. See our CONTRIBUTING.md for detailed instructions.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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.Testing

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

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.

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 111 8/12/2026
1.1.0 130 7/23/2026
1.0.1 112 7/21/2026
1.0.0 254 7/16/2026