EricksonLopez.SharedKernel
1.1.0
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
<PackageReference Include="EricksonLopez.SharedKernel" Version="1.1.0" />
<PackageVersion Include="EricksonLopez.SharedKernel" Version="1.1.0" />
<PackageReference Include="EricksonLopez.SharedKernel" />
paket add EricksonLopez.SharedKernel --version 1.1.0
#r "nuget: EricksonLopez.SharedKernel, 1.1.0"
#:package EricksonLopez.SharedKernel@1.1.0
#addin nuget:?package=EricksonLopez.SharedKernel&version=1.1.0
#tool nuget:?package=EricksonLopez.SharedKernel&version=1.1.0
EricksonLopez.SharedKernel
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 โ
IsAotCompatibleandIsTrimmableenabled - โ๏ธ Async-first โ Full
Task<Result<T>>andValueTask<Result<T>>extension methods - ๐งฉ No magic โ every abstraction is readable and debuggable
Table of Contents
- Installation
- Quick Start
- API Reference
- NativeAOT Compatibility
- Part of the EricksonLopez Ecosystem
- Architecture Decisions & Guides
- FAQ & Troubleshooting
- License
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):
- ADR-001: Result Pattern
- ADR-002: Zero Functional Dependencies
- ADR-003: Value Object Boxing Acceptance
- ADR-004: Validation Error Design
- ADR-005: Result Pattern Allocations Optimization
- ADR-006: Performance Analysis
- ADR-007: Native AOT Compatibility
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 | Versions 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. |
-
.NETStandard 2.0
- Microsoft.Bcl.HashCode (>= 1.1.1)
- System.Threading.Tasks.Extensions (>= 4.5.4)
-
net10.0
- No dependencies.
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.