EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators
3.0.0
dotnet add package EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators --version 3.0.0
NuGet\Install-Package EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators -Version 3.0.0
<PackageReference Include="EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators" Version="3.0.0" />
<PackageVersion Include="EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators" Version="3.0.0" />
<PackageReference Include="EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators" />
paket add EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators --version 3.0.0
#r "nuget: EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators, 3.0.0"
#:package EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators@3.0.0
#addin nuget:?package=EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators&version=3.0.0
#tool nuget:?package=EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators&version=3.0.0
EricksonLopez.DomainPrimitives
Allocation-minimized, compile-time validated Domain Primitives, SmartEnums, and Roslyn Code Analyzers for modern .NET enterprise systems.
EricksonLopez.DomainPrimitives is the enterprise standard for modeling provably valid, allocation-minimized scalar value types, strongly-typed identifiers, composite value objects, and SmartEnums in modern .NET (.NET 8, .NET 9, .NET 10). By combining compile-time Roslyn source generators, architectural code analyzers, and NativeAOT-first converters, it eliminates Primitive Obsession and defensive validation boilerplate while delivering bare-metal execution performance and allocation-minimized hot paths.
Table of Contents
- What Problem It Solves
- Key Features
- Ecosystem
- Documentation
- Installation
- Quick Start
- Core Use Cases
- Use Case 1: Clean Architecture / CQRS Command Handler
- Use Case 2: Multi-Step Domain Validation Pipeline
- Use Case 3: Zero-Allocation Minimal API Route & Body Model Binding
- Use Case 4: EF Core Relational Persistence Mapping
- Use Case 5: High-Throughput Microservice Queries with Dapper
- Use Case 6: Live Compile-Time Roslyn Architectural Enforcement
- 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
Primitive Obsession is among the most pervasive anti-patterns in enterprise software engineering:
- The Hidden Cost of Primitive Obsession:
Using raw
string,Guid,int, ordecimaltypes allows illegal and unnormalized values (such as empty strings, malformed email addresses, or negative monetary balances) to traverse domain boundaries undetected. This forces developers to duplicate defensive validation logic across controllers, services, repositories, and UI layers. - Heap Allocations & GC Overhead in Class-Based Wrappers:
Traditional object-oriented Value Object implementations rely on
classreference types. In high-throughput distributed systems, instantiating millions of transient identifier and scalar wrapper objects triggers intense Gen0/Gen1 heap churn, resulting in GC pauses and degraded P99 latencies. - Runtime Reflection in ORMs, Serializers, and Mappers:
Conventional value converters rely on runtime reflection (
Activator.CreateInstance,MethodInfo.Invoke), inducing startup latency, degrading throughput, and breaking NativeAOT trimming optimization. - Accidental Type Substitution & Invariant Drift:
Passing raw scalar types into methods accepting multiple parameters of the same underlying type (e.g.
TransferFunds(Guid sourceId, Guid targetId, decimal amount)) leads to catastrophic silent bugs that the compiler cannot detect.
How EricksonLopez.DomainPrimitives Solves This
- Guaranteed Validity by Construction: Instances cannot be created in an invalid state. Constructors are private and creation is routed through source-generated
Create,TryCreate, andTryParsemethods that enforce validation rules deterministically. - Allocation-Minimized Hot Paths: Source-generated primitives are
readonly partial record structtypes that reside entirely on the stack or inline within entity memory layouts. Non-string primitives and StrongIds achieve 0 bytes allocated. String primitives with NFC Unicode normalization incur exactly 1 string allocation per value (required for homoglyph-attack prevention per SEC-004 / ADR-027). - Compile-Time Incremental Code Generation: All factory methods, parsers (
IParsable<T>,ISpanParsable<T>,IUtf8SpanParsable<T>), formatters (ISpanFormattable,IUtf8SpanFormattable), equality operators, JSON converters, EF Core ValueConverters, and Dapper TypeHandlers are emitted at compile time. - Live IDE Architectural Enforcement: 18 dedicated Roslyn analyzers (DP0001βDP0018) intercept invalid modeling patterns, direct string comparisons, and public constructor bypasses in real time with automated code fixes.
- Full NativeAOT & Trimming Compatibility: Zero runtime reflection and zero dynamic IL emission guarantee instant startup, minimal binary footprints, and full compatibility with NativeAOT publishing.
β‘ Key Features
- π Allocation-Minimized Memory Footprint: Stack-allocated
readonly record structvalue types achieve 0 B on non-string paths (StrongId, SmartEnum, NumericPrimitive). String primitives incur 1 allocation for NFC normalization. See Performance Benchmarks. - π οΈ Roslyn Incremental Source Generators: Compile-time emission of
IParsable<T>,ISpanParsable<T>,IUtf8SpanParsable<T>,ISpanFormattable, and explicit conversion operators. - π Live Architectural Code Analyzers: 18 Roslyn diagnostic rules (DP0001βDP0018) with automated code fixes enforce immutability, validation integrity, and API surface budgets.
- π·οΈ 39 Pre-Configured Semantic Shortcuts: Instant domain modeling with built-in attributes for strings (
[Email],[Phone],[Url],[Slug],[CountryCode],[IBAN],[ISBN]) and numerics ([Money],[Price],[TaxRate],[Percentage],[Quantity],[Rating]). - π§© Zero-Contamination Persistence Adapters: Compile-time auto-discovery adapters for Entity Framework Core (
ConfigureDomainPrimitives) and Dapper (RegisterAll). - π NativeAOT & Trimming-First Architecture: 100% trim-safe execution with zero reflection for all core and integration packages (see compatibility table). The legacy
NewtonsoftJsonpackage is excluded β see footnote ΒΉ. - π― Railway-Oriented Result Pattern Interop: Seamless zero-overhead integration with
EricksonLopez.Resultand third-party functional monads via theTryCreateoutparameter pattern. - π§ͺ Comprehensive Testing & Data Tooling: Fluent assertions, scenario suites (
DomainPrimitiveScenarios), and realistic fake data generators (DomainPrimitiveFakeFactory).
π¦ Ecosystem
| Package | Version | Description |
|---|---|---|
EricksonLopez.DomainPrimitives |
Core domain primitives, SmartEnums, attributes, and Roslyn generators | |
EricksonLopez.DomainPrimitives.Abstractions |
Zero-dependency contracts (IDomainPrimitive<TSelf, TValue>, IStrongId<TSelf, TValue>, PrimitiveError) |
|
EricksonLopez.DomainPrimitives.AspNetCore |
ASP.NET Core Minimal APIs model binding & route parameter validation | |
EricksonLopez.DomainPrimitives.EFCore |
Entity Framework Core zero-contamination ValueConverter conventions | |
EricksonLopez.DomainPrimitives.Dapper |
Dapper compile-time type handlers and bulk auto-registration | |
EricksonLopez.DomainPrimitives.OpenApi |
Swagger / OpenAPI schema filter generators for primitive documentation | |
EricksonLopez.DomainPrimitives.Testing |
Fluent assertions, test builders, scenario data, and fake generators | |
EricksonLopez.DomainPrimitives.NewtonsoftJson |
Newtonsoft.Json contract resolvers and converters for legacy systems | |
EricksonLopez.DomainPrimitives.Analyzers |
Roslyn diagnostic analyzers (DP0001βDP0018) and automated code fixes | |
EricksonLopez.DomainPrimitives.Generators |
Roslyn incremental source generators emitting parsers, formatters, and factory methods | |
EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators |
Roslyn source generators emitting ASP.NET Core model binding and route parsers | |
EricksonLopez.DomainPrimitives.EFCore.SourceGenerators |
Roslyn source generators emitting EF Core ValueConverter configurations | |
EricksonLopez.DomainPrimitives.Dapper.SourceGenerators |
Roslyn source generators emitting Dapper TypeHandler registrations | |
EricksonLopez.DomainPrimitives.OpenApi.SourceGenerators |
Roslyn source generators emitting OpenAPI schema filters and descriptions |
The ecosystem consists of 14 coordinated NuGet packages released in lockstep, including 6 compile-time Roslyn Source Generator and Analyzer packages (Generators, Analyzers, AspNetCore.SourceGenerators, EFCore.SourceGenerators, Dapper.SourceGenerators, OpenApi.SourceGenerators). See the Full Packages Specification for the complete dependency graph and target framework matrix.
π Documentation
π Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-domain-primitives/tree/main/docs
π Step-by-Step Interactive Showcase (Levels 00 to 10)
π Showcase Catalog & Implementation Guide:
docs/showcase/readme.md
| Level | Topic | Showcase Projects | Description |
|---|---|---|---|
| Level 00 | Conceptual Architecture | README.md |
Core foundational philosophy, advantages, trade-offs, and competitive comparisons |
| Level 01 | Quick Start | 01-GettingStarted |
Minimum configuration, first functional primitive ([Email], [StrongId<Guid>]), TryCreate |
| Level 02 | Full Configuration | 08-SerializationAndMapping, 09-SourceGenerators, 11-SmartEnums |
Assembly defaults ([assembly: DomainPrimitivesDefaults]), JSON serialization, Smart Enums |
| Level 03 | Real Use Cases | 04-ValueObjects, 05-StronglyTypedIds, 13-DomainCollections |
38+ semantic shortcuts, composite Value Objects, collection extensions (ToDomainPrimitiveList) |
| Level 04 | Advanced Integration | 15-AspNetCoreIntegration, 16-EFCoreIntegration, 17-MediatRIntegration |
ASP.NET Core model binding, EF Core ConfigureDomainPrimitives(), MediatR CQRS pipelines |
| Level 05 | Processing | 21-BackgroundProcessing |
High-throughput Channel<T> producer/consumer with boundary reconstruction via TryCreate |
| Level 06 | Error Handling | 02-FirstResult, 03-Errors, 19-UnitTesting |
PrimitiveError struct, standardized error codes, Result pattern, fluent test builders |
| Level 07 | Scalability & Performance | 14-Performance |
Stack-allocated structs, zero GC allocations on success, span-based parsing (ISpanParsable<T>) |
| Level 08 | Customization | 22-CustomImplementations |
Custom validators (ICustomValidator<T>), normalizers (INormalizer<T>), PrimitiveBuilder |
| Level 09 | Extensions | 18-Observability, 23-DapperIntegration, 24-OpenApiIntegration |
OpenTelemetry metrics, static event sources, Dapper RegisterAll(), OpenAPI Swagger filters |
| Level 10 | Enterprise Architecture | 06-EntitiesAndAggregates, 07-DomainEvents, 12-Specifications, 20-EndToEndApplication |
Tactical DDD aggregates, domain events, specification pattern, full Clean Architecture |
π Technical Reference & Architecture Guides
- Public API Inventory β 100% authoritative inventory of public types, attributes, and extension methods.
- Architecture Functional Map β Complete 8-stage lifecycle mapping across presentation, domain, and persistence.
- API Reference β Comprehensive Microsoft Learn standard documentation for public methods.
- Architecture & Invariants β Complete architectural blueprint, memory layouts, and domain boundaries.
- Architecture & Flow Diagrams β 8 Mermaid diagrams covering architecture, sequence, state, and pipeline.
- Architectural Decision Records (ADRs) β 45 formal ADRs documenting design rationale and rejected proposals.
- Technical Audit β Comprehensive technical audit, guarantees, and system invariants.
- Competitive Audit β In-depth market comparison vs StronglyTypedId and Vogen.
- Features & Compatibility Matrix β Target framework matrix, diagnostics, and supported features.
- Roslyn Diagnostic Rules Reference β Complete reference for analyzer rules DP0001 through DP0018.
- Testing & Quality Audit β Quality gates, compiler settings, and 100% mutation test verification.
- Cookbook & Production Recipes β 18 ready-to-use production recipes for enterprise architectures.
- Best Practices & Guidelines β Official production guidelines for domain modeling and persistence.
- Troubleshooting & FAQ β Common compiler errors, Roslyn analyzer fixes, and FAQ (FAQ Guide).
- Migration Guide β Step-by-step upgrade guide, breaking changes, and deprecations.
- Allocation & Memory Analysis β Deep-dive memory analysis and zero-allocation proofs.
- Mutation Score Report β Package-by-package Stryker.NET mutation testing score report.
- Security Architecture β ReDoS prevention, Unicode NFC normalization, and PII protection specs.
- CI/CD & Build Pipeline β Automated GitHub Actions workflows, AOT probes, and release automation.
π₯ Installation
Install the necessary packages using the .NET CLI or NuGet Package Manager:
1. Core Package (Required)
dotnet add package EricksonLopez.DomainPrimitives
2. Optional Framework & Integration Packages
# ASP.NET Core Minimal APIs & MVC model binding
dotnet add package EricksonLopez.DomainPrimitives.AspNetCore
# Entity Framework Core ValueConverter auto-configuration
dotnet add package EricksonLopez.DomainPrimitives.EFCore
# Dapper TypeHandler registration
dotnet add package EricksonLopez.DomainPrimitives.Dapper
# Swagger / OpenAPI Schema generation
dotnet add package EricksonLopez.DomainPrimitives.OpenApi
# Newtonsoft.Json legacy serialization support
dotnet add package EricksonLopez.DomainPrimitives.NewtonsoftJson
3. Testing & Assertion Packages
# Fluent assertions, fake data factories, and scenario runners
dotnet add package EricksonLopez.DomainPrimitives.Testing
π Quick Start
1. Declarative Domain Primitive
Decorate a readonly partial record struct with semantic attributes. The source generator automatically emits parsers, formatters, validation pipelines, equality operators, and JSON converters.
using EricksonLopez.DomainPrimitives;
// String primitive with normalization and regex constraints
[StringPrimitive]
[Trim, UpperCase, Length(2, 2)]
public readonly partial record struct CountryIsoCode;
// Built-in shortcut for RFC 5321 compliant email addresses
[Email]
public readonly partial record struct EmailAddress;
// Usage:
CountryIsoCode code = CountryIsoCode.Create(" us "); // Value: "US"
EmailAddress email = EmailAddress.Create("user@example.com"); // Validated & normalized
2. Strongly-Typed Identifier
Eliminate identifier transposition bugs by declaring strongly-typed IDs backed by Guid, long, int, or string:
using EricksonLopez.DomainPrimitives;
[StrongId<Guid>]
public readonly partial record struct CustomerId;
[StrongId<long>]
public readonly partial record struct OrderId;
// Usage:
CustomerId customerId = CustomerId.Create(); // Generates new Guid
OrderId orderId = OrderId.Create(1001L); // Validated non-empty identifier
3. Type-Safe SmartEnum
Model exhaustive, polymorphic business states with $O(1)$ dictionary lookups and compile-time pattern matching:
using EricksonLopez.DomainPrimitives;
[SmartEnum<int>]
public readonly partial record struct OrderStatus
{
public static readonly OrderStatus Pending = new(1);
public static readonly OrderStatus Processing = new(2);
public static readonly OrderStatus Shipped = new(3);
public static readonly OrderStatus Delivered = new(4);
}
// Compile-time exhaustive pattern matching:
OrderStatus status = OrderStatus.Processing;
string description = status.Match(
whenPending: () => "Awaiting payment",
whenProcessing: () => "Fulfilling items in warehouse",
whenShipped: () => "In transit with carrier",
whenDelivered: () => "Successfully delivered");
4. Composite Value Object
Model multi-property domain concepts that enforce cross-property invariants via partial validation hooks:
using EricksonLopez.DomainPrimitives;
using EricksonLopez.DomainPrimitives.Validation;
[ValueObject]
public readonly partial record struct Address(string Street, string City, string ZipCode)
{
static partial void Validate(ref Address value, ref PrimitiveError error)
{
if (string.IsNullOrWhiteSpace(value.Street))
error = new PrimitiveError("Address.EmptyStreet", "Street cannot be empty.");
else if (string.IsNullOrWhiteSpace(value.City))
error = new PrimitiveError("Address.EmptyCity", "City cannot be empty.");
else if (string.IsNullOrWhiteSpace(value.ZipCode))
error = new PrimitiveError("Address.EmptyZipCode", "Zip code cannot be empty.");
}
}
5. Zero-Allocation Validation & Result Pipeline
Execute high-throughput validation without throwing exceptions or incurring heap allocations:
using EricksonLopez.DomainPrimitives.Validation;
// Stack-allocated TryCreate with out PrimitiveError (0 bytes allocated)
if (EmailAddress.TryCreate(userInput, out var email, out PrimitiveError error))
{
Console.WriteLine($"Valid email: {email.Value}");
}
else
{
Console.WriteLine($"Validation failed [{error.Code}]: {error.Message}");
}
// High-performance UTF-8 byte span parsing (Zero string allocations)
ReadOnlySpan<byte> utf8Buffer = "alice@example.com"u8;
if (EmailAddress.TryParse(utf8Buffer, null, out var parsedEmail))
{
Console.WriteLine($"Parsed from UTF-8 span: {parsedEmail}");
}
π‘ Core Use Cases
Use Case 1: Clean Architecture / CQRS Command Handler
Strongly-typed IDs and primitives guarantee invariant integrity before business logic executes in Application handlers:
using EricksonLopez.DomainPrimitives;
public readonly record struct RegisterCustomerCommand(
CustomerId Id,
EmailAddress Email,
AccountBalance InitialDeposit);
public sealed class RegisterCustomerHandler
{
private readonly ICustomerRepository _repository;
public RegisterCustomerHandler(ICustomerRepository repository) => _repository = repository;
public async Task<CustomerId> HandleAsync(RegisterCustomerCommand command, CancellationToken ct)
{
// Command parameters are guaranteed valid and non-null by the type system
var customer = new Customer(command.Id, command.Email, command.InitialDeposit);
await _repository.SaveAsync(customer, ct);
return customer.Id;
}
}
Use Case 2: Multi-Step Domain Validation Pipeline
Bridge TryCreate with EricksonLopez.Result for Railway-Oriented Programming without coupling domain types to external monad libraries:
using EricksonLopez.DomainPrimitives;
using EricksonLopez.Result;
public static class DomainResultBridge
{
public static Result<EmailAddress> ToResult(string raw) =>
EmailAddress.TryCreate(raw, out var email, out var error)
? Result<EmailAddress>.Success(email)
: Result<EmailAddress>.Failure(error.Code, error.Message);
}
// Chained functional flow:
Result<CustomerProfile> profileResult = DomainResultBridge.ToResult(rawEmail)
.Map(email => new CustomerProfile(email));
Use Case 3: Zero-Allocation Minimal API Route & Body Model Binding
Primitives automatically bind from route parameters, query strings, and JSON bodies via IParsable<T> and IUtf8SpanParsable<T>:
using EricksonLopez.DomainPrimitives;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
var app = WebApplication.Create();
// Automatically parsed from route via IParsable<CustomerId>
app.MapGet("/api/customers/{id}", (CustomerId id) => Results.Ok(new { Id = id.Value }));
// Automatically deserialized and validated from JSON body
app.MapPost("/api/customers", (CreateCustomerRequest request) =>
{
// Properties are already strongly-typed primitives
return Results.Created($"/api/customers/{request.Id}", request);
});
Use Case 4: EF Core Relational Persistence Mapping
Persist domain primitives into relational databases without contaminating domain models with persistence attributes:
using EricksonLopez.DomainPrimitives.EFCore.Generated;
using Microsoft.EntityFrameworkCore;
public sealed class ApplicationDbContext : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
// Automatically discovers and applies ValueConverters & column lengths for all primitives
configurationBuilder.ConfigureDomainPrimitives();
}
}
Use Case 5: High-Throughput Microservice Queries with Dapper
Execute high-performance database queries where Dapper maps database scalar columns directly into domain structs:
using Dapper;
using EricksonLopez.DomainPrimitives.Dapper.Generated;
// Startup registration (single call in Program.cs):
DapperDomainPrimitivesRegistration.RegisterAll();
// Queries materialize directly into domain types with zero reflection overhead:
var customer = await connection.QuerySingleAsync<Customer>(
"SELECT Id, Email, Balance FROM Customers WHERE Id = @Id",
new { Id = customerId });
Use Case 6: Live Compile-Time Roslyn Architectural Enforcement
Roslyn analyzers guard domain invariants at edit time inside the IDE, preventing common pitfalls before compilation:
// β Roslyn Error DP0001: Domain primitive must be declared as 'partial'
[StringPrimitive]
public readonly record struct ApiKey;
// β Roslyn Error DP0002: Domain primitive must be declared as 'readonly'
[StringPrimitive]
public partial record struct SessionToken;
// β Roslyn Warning DP0007: Avoid using default constructor for domain primitive
EmailAddress email = default; // Analyzer flags uninitialized state
π Configuration & Integrations
ASP.NET Core Binding
Register model binding support in your ASP.NET Core application for MVC and Minimal APIs:
var builder = WebApplication.CreateBuilder(args);
// Register source-generated model binders
builder.Services.AddControllers()
.AddDomainPrimitivesModelBinding();
OpenAPI / Swagger Schema Generation
Enable OpenAPI schema filters to document primitives accurately as primitive types (e.g. string format email) rather than complex objects:
builder.Services.AddSwaggerGen(options =>
{
options.ConfigureDomainPrimitives();
});
Entity Framework Core Value Converters
Register all source-generated ValueConverter instances in one line using EF Core convention discovery:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.ConfigureDomainPrimitives();
}
Dapper Type Handlers
Register all source-generated Dapper SqlMapper.TypeHandler<T> instances at application startup:
using EricksonLopez.DomainPrimitives.Dapper.Generated;
// Program.cs
DapperDomainPrimitivesRegistration.RegisterAll();
System.Text.Json & NativeAOT
All primitives implement source-generated JSON converters that serialize directly to scalar JSON tokens (e.g. "user@example.com" instead of {"Value":"user@example.com"}). For NativeAOT, include your types in your JsonSerializerContext:
[JsonSerializable(typeof(CustomerDto))]
[JsonSerializable(typeof(EmailAddress))]
[JsonSerializable(typeof(CustomerId))]
public partial class AppJsonContext : JsonSerializerContext;
Newtonsoft.Json Migration Integration
For legacy applications using Newtonsoft.Json:
using EricksonLopez.DomainPrimitives.NewtonsoftJson;
using Newtonsoft.Json;
var settings = new JsonSerializerSettings();
settings.AddDomainPrimitives(); // Registers ContractResolver and converters
Roslyn Diagnostic Analyzers
The EricksonLopez.DomainPrimitives.Analyzers package provides 18 compile-time rules to enforce domain modeling invariants:
| Diagnostic ID | Severity | Category | Description | CodeFix |
|---|---|---|---|---|
| DP0001 | Error | Correctness | Domain primitive type must be declared as partial |
β Available |
| DP0002 | Error | Correctness | Domain primitive type must be declared as readonly |
β Available |
| DP0003 | Error | Correctness | Domain primitive type must be declared as record struct |
β Available |
| DP0004 | Error | Correctness | Invalid regular expression pattern in [Regex] attribute |
β Manual |
| DP0005 | Error | Correctness | Conflicting normalization attributes (e.g. [LowerCase] and [UpperCase]) |
β Available |
| DP0006 | Error | Correctness | Invalid constraint bounds (Min value cannot be greater than Max) | β Manual |
| DP0007 | Warning | Design | Avoid uninitialized domain primitive via default constructor |
β Available |
| DP0008 | Error | Correctness | ValueObject properties must declare init accessors |
β Available |
| DP0009 | Warning | Design | Domain primitive lacks validation rules | β Manual |
| DP0010 | Warning | Performance | Raw string compared directly with domain primitive using == |
β Available |
| DP0011 | Warning | Performance | string assigned directly from domain primitive without accessing .Value |
β Available |
| DP0012 | Warning | Design | Public constructor bypasses source-generated domain primitive validation | β Available |
| DP0013 | Info | Design | Possible duplicate domain primitive logic detected | β Manual |
| DP0014 | Warning | ApiReview | API surface budget exceeded on domain primitive | β Manual |
| DP0015 | Warning | ApiReview | Public member on domain primitive is missing XML documentation | β Manual |
| DP0016 | Warning | ApiReview | Custom factory method must be named Create, TryCreate, Parse, or TryParse |
β Manual |
| DP0017 | Error | Correctness | Invalid custom exception type in [DomainPrimitivesDefaults] |
β Manual |
| DP0018 | Warning | Design | Value object property should not be a mutable collection or array | β Manual |
π§ͺ Testing & Quality
Fluent Assertions API
The EricksonLopez.DomainPrimitives.Testing package provides declarative assertions for xUnit, NUnit, and MSTest:
using AwesomeAssertions;
using EricksonLopez.DomainPrimitives.Testing;
using Xunit;
public class DomainPrimitiveTests
{
[Fact]
public void EmailAddress_ValidInput_ShouldSucceed()
{
var email = DomainPrimitiveAssertionsExtensions
.ShouldSucceedCreation<EmailAddress, string>("user@example.com");
email.Should().HavePrimitiveValue<EmailAddress, string>("user@example.com");
}
[Fact]
public void EmailAddress_InvalidInput_ShouldFailWithErrorCode()
{
DomainPrimitiveAssertionsExtensions
.ShouldFailCreationWith<EmailAddress, string>("invalid-email", "FORMAT");
}
}
Realistic Test Data Generation
Generate curated valid and invalid test datasets with DomainPrimitiveFakeFactory:
using EricksonLopez.DomainPrimitives.Testing;
// Valid and invalid sample datasets for parameterized tests
string[] validEmails = DomainPrimitiveFakeFactory.Strings.ValidEmails;
string[] invalidEmails = DomainPrimitiveFakeFactory.Strings.InvalidEmails;
decimal[] validMoney = DomainPrimitiveFakeFactory.Numerics.ValidMoneyAmounts;
int[] validAges = DomainPrimitiveFakeFactory.Numerics.ValidAges;
// Grouped test scenarios
var scenarios = DomainPrimitiveScenarios.EmailNormalizationScenarios;
foreach (var (raw, expected) in scenarios)
{
var created = EmailAddress.Create(raw);
Assert.Equal(expected, created.Value);
}
Mutation Testing & Quality Gates
Every build is verified against a strict quality gate pipeline enforcing 100% mutant eradication:
| Package | Mutation Score | Status |
|---|---|---|
EricksonLopez.DomainPrimitives |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.Abstractions |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.Generators |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.Analyzers |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.AspNetCore |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.AspNetCore.SourceGenerators |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.EFCore |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.EFCore.SourceGenerators |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.Dapper |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.Dapper.SourceGenerators |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.OpenApi |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.OpenApi.SourceGenerators |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.NewtonsoftJson |
100.0% | β PASSED |
EricksonLopez.DomainPrimitives.Testing |
100.0% | β PASSED |
| Ecosystem Aggregate (14 packages) | 100.0% | β VERIFIED |
β‘ Performance Benchmarks
Environment: AMD Ryzen 7 9800X3D 4.70GHz (8 cores, 16 threads), .NET 10.0.10, X64 RyuJIT x86-64-v4, BenchmarkDotNet v0.15.8
Primary Operations Benchmark
| Method | Mean | Ratio | Allocated | Zero-Alloc? |
|---|---|---|---|---|
Raw Guid (baseline β no wrapper) |
0.00 ns | 1.00 | 0 B | β |
[StrongId<Guid>] Creation (CustomerId.Create(guid)) |
0.00 ns | 1.00 | 0 B | β |
[StrongId<Guid>] TryParse (CustomerId.TryParse(...)) |
12.63 ns | 1.00 | 0 B | β |
[Email] Creation (EmailAddress.Create(...)) |
49.53 ns | - | 48 B* | β οΈ (NFC Norm) |
[SmartEnum] Lookup (OrderStatus.FromValue(2)) |
2.14 ns | - | 0 B | β |
[NumericPrimitive] Add (Money.Add(a, b)) |
0.19 ns | - | 0 B | β |
*Note: String normalization requires 1 allocation for string.Normalize(NormalizationForm.FormC) per Unicode security standards (SEC-004 / ADR-027).
BCL Span & UTF-8 Zero-Allocation Paths
| Benchmark | Interface Tested | Mean | Allocated | Zero-Alloc? |
|---|---|---|---|---|
DomainPrimitives_TryParse |
IParsable<T> |
12.63 ns | 0 B | β |
DomainPrimitives_SpanParse |
ISpanParsable<T> |
11.84 ns | 0 B | β |
DomainPrimitives_Utf8SpanParse |
IUtf8SpanParsable<T> |
13.10 ns | 0 B | β |
DomainPrimitives_SpanFormat |
ISpanFormattable |
4.82 ns | 0 B | β |
DomainPrimitives_Utf8SpanFormat |
IUtf8SpanFormattable |
5.10 ns | 0 B | β |
Integration Overhead (EF Core & Dapper)
| Benchmark | Integration Layer | Mean | Allocated |
|---|---|---|---|
Dapper_TypeHandler_SetValue |
Dapper Parameter Binding | 0.21 ns | 0 B |
Dapper_TypeHandler_Parse |
Dapper Reader Materialization | 0.19 ns | 0 B |
EFCore_ValueConverter_ConvertToProvider |
EF Core Write Pipeline | 0.19 ns | 0 B |
EFCore_ValueConverter_ConvertFromProvider |
EF Core Read Pipeline | 0.19 ns | 0 B |
π Compatibility & Technical Matrix
Target Frameworks & NativeAOT
| Package | .NET 8.0 LTS | .NET 9.0 STS | .NET 10.0 | NativeAOT | Trimming Safe |
|---|---|---|---|---|---|
EricksonLopez.DomainPrimitives |
β Supported | β Supported | β Supported | β Supported | β 100% Trim-Safe |
EricksonLopez.DomainPrimitives.Abstractions |
β Supported | β Supported | β Supported | β Supported | β 100% Trim-Safe |
EricksonLopez.DomainPrimitives.AspNetCore |
β Supported | β Supported | β Supported | β Supported | β 100% Trim-Safe |
EricksonLopez.DomainPrimitives.EFCore |
β Supported | β Supported | β Supported | β Supported | β 100% Trim-Safe |
EricksonLopez.DomainPrimitives.Dapper |
β Supported | β Supported | β Supported | β Supported | β 100% Trim-Safe |
EricksonLopez.DomainPrimitives.OpenApi |
β Supported | β Supported | β Supported | β Supported | β 100% Trim-Safe |
EricksonLopez.DomainPrimitives.Testing |
β Supported | β Supported | β Supported | β Supported | β 100% Trim-Safe |
EricksonLopez.DomainPrimitives.NewtonsoftJson |
β Supported | β Supported | β Supported | β Not CompatibleΒΉ | β Not Trim-SafeΒΉ |
ΒΉ
EricksonLopez.DomainPrimitives.NewtonsoftJsonis a legacy compatibility package targeting projects that cannot migrate toSystem.Text.Json. It uses Newtonsoft.Json's reflection-based pipeline ([RequiresDynamicCode]is applied on the converter) and is therefore not NativeAOT or trimming compatible (IsAotCompatible=false,IsTrimmable=falsein the project file). For NativeAOT scenarios, useSystem.Text.Jsonwith the source-generated converters bundled inEricksonLopez.DomainPrimitivesinstead. See ADR-026.
Primitive Category Taxonomy & Generated Interfaces
| Category | Decorator Attribute | Underlying Type | Key Generated Interfaces & Capabilities |
|---|---|---|---|
| Strong ID | [StrongId<T>] |
Guid, long, int, string |
IDomainPrimitive<TSelf, TValue>, IStrongId<TSelf, TValue>, IParsable<T>, ISpanParsable<T> |
| String Primitive | [StringPrimitive], [Email], [Phone], ... |
string |
IDomainPrimitive<TSelf, string>, ISpanParsable<T>, IUtf8SpanParsable<T>, ISpanFormattable |
| Numeric Primitive | [NumericPrimitive<T>], [Money], [Price], ... |
decimal, double, int, long |
IDomainPrimitive<TSelf, T>, IComparable<T>, arithmetic operators (+, -, *, /) |
| Date Primitive | [DatePrimitive] |
DateOnly, DateTime, DateTimeOffset |
IDomainPrimitive<TSelf, TDate>, IComparable<T>, past/future invariant guards |
| SmartEnum | [SmartEnum<T>] |
int, string |
IDomainPrimitive<TSelf, T>, exhaustive Match<T>, Map<T>, $O(1)$ dictionary lookups |
| Value Object | [ValueObject] |
Composite | IDomainPrimitive<TSelf>, IParsable<T>, ISpanParsable<T>, structural equality |
π‘οΈ Target Framework & Lifecycle Policy: First-class multi-targeting across
.NET 10(Modern LTS),.NET 9(STS), and.NET 8(Enterprise LTS) β along with.NET Standard 2.0for Roslyn analyzers and source generators β is actively maintained. Full backward compatibility is guaranteed until Microsoft officially reaches End-of-Life (EOL) for .NET 8 and .NET 9 in November 2026, at which milestone the ecosystem will transition to .NET 10 and .NET 11.
ποΈ Architecture & Design Principles
End-to-End Architectural Pipeline
flowchart TD
Client(["HTTP Client / Caller"])
subgraph Presentation ["Presentation & Serialization Layer"]
Json["System.Text.Json Converter\n(Auto-converts via TryCreate)"]
OpenApi["OpenApi Schema Filter\n(Generates Swagger specs)"]
AspNet["ASP.NET Core Model Binder\n(Route & Query Binding)"]
end
subgraph Domain ["Domain Layer (Zero Heap Allocation)"]
Prim["Domain Primitive\n(readonly record struct)"]
Pipeline["Validation Pipeline:\n1. Unicode Normalization (NFC)\n2. Built-in Range / Regex Rules\n3. Custom Partial Validator"]
end
subgraph Persistence ["Persistence Layer"]
EF["EF Core ValueConverter\n(ConfigureDomainPrimitives)"]
Dapper["Dapper TypeHandler\n(RegisterAll)"]
DB[("Database")]
end
Client -->|"JSON Request Body"| Json
Client -->|"Route / Query Parameter"| AspNet
Client -->|"API Documentation"| OpenApi
Json --> Prim
AspNet --> Prim
Prim --> Pipeline
Pipeline -->|"Valid struct"| EF
Pipeline -->|"Valid struct"| Dapper
EF --> DB
Dapper --> DB
Primitive Lifecycle & State Machine
stateDiagram-v2
[*] --> RawInput: Caller invokes Create() or TryCreate()
RawInput --> Normalizing: Has [Trim] / [LowerCase] / [UpperCase]
RawInput --> Validating: No normalization
Normalizing --> Validating: Normalized value
Validating --> InvalidState: Built-in validation fails (LENGTH, REGEX, RANGE)
Validating --> CustomValidating: Built-in validation passes
CustomValidating --> InvalidState: Custom partial Validate() fails
CustomValidating --> ValidState: All invariants satisfied
InvalidState --> ThrowsException: Create() path -> Throws DomainPrimitiveValidationException
InvalidState --> ReturnsFalse: TryCreate() path -> Returns false + PrimitiveError
ThrowsException --> [*]
ReturnsFalse --> [*]
ValidState --> Instantiated: readonly record struct allocated on Stack
Instantiated --> Serialized: System.Text.Json / Newtonsoft.Json
Instantiated --> Persisted: EF Core / Dapper
Instantiated --> [*]: Zero GC overhead (Stack released)
π‘οΈ Best Practices & Anti-Patterns
| Scenario | β Avoid | β Recommended |
|---|---|---|
| Control Flow | Throwing exceptions for business validation | Using TryCreate with stack-allocated PrimitiveError |
| Memory Allocation | Declaring domain primitives as class reference types |
Using readonly partial record struct for zero GC allocations |
| Struct Initialization | Using default(Primitive) or parameterless new() |
Instantiating via source-generated Create() or TryCreate() |
| String Comparison | Comparing raw string directly with a primitive (str == email) |
Parsing the raw string into the primitive or using email.Value |
| Value Object Mutation | Modifying property values directly | Creating a new instance with updated properties (immutable replacement) |
| Persistence Mapping | Contaminating domain models with EF Core annotations | Using zero-contamination ConfigureDomainPrimitives() in DbContext |
| Validation Architecture | Running asynchronous I/O or DB queries inside primitive validators | Keeping domain primitive validators 100% synchronous and deterministic |
β οΈ Troubleshooting & Common Pitfalls
Always use generated factory methods (Create, TryCreate, TryParse) rather than default structs to ensure validation invariants are enforced.
1. Uninitialized Struct via default Constructor
- Symptom: A domain primitive struct contains a null or uninitialized backing value, bypassing domain invariants.
- Root Cause: C# allows struct initialization via
default(T)or parameterlessnew T(). - Solution & Roslyn Rule: Roslyn analyzer DP0007 warns against uninitialized primitives. Always use
Primitive.Create(...)orPrimitive.TryCreate(...).
2. Bypassing Validation via Public Constructors
- Symptom: Developers instantiate primitives with raw data that violates regex, range, or length rules.
- Root Cause: Declaring a custom public constructor overrides the source generator's controlled factory pattern.
- Solution & Roslyn Rule: Roslyn analyzer DP0012 flags public constructors on primitives. Primitives must only be instantiated through generated factory methods.
3. Direct String Comparisons Bypassing Type Safety
- Symptom: Comparing a domain primitive directly against a raw
string(email == "admin@example.com") fails to normalize the input. - Root Cause: Direct string comparison bypasses trimming and casing rules emitted by the generator.
- Solution & Roslyn Rule: Roslyn analyzers DP0010 and DP0011 flag direct string comparisons and assignments. Parse the raw string into the primitive first.
4. Mutating Value Objects Instead of Replacement
- Symptom: Compile errors or invariant drift when attempting to mutate properties on a
[ValueObject]. - Root Cause: Value objects are immutable by design.
- Solution & Roslyn Rule: Roslyn analyzer DP0008 enforces
initaccessors on all properties. Create new instances when updating values.
5. Missing partial or readonly Modifiers
- Symptom: Compilation error stating the source generator cannot augment the type definition.
- Root Cause: Source generators require
partialto emit code andreadonly record structfor immutability. - Solution & Roslyn Rule: Roslyn analyzers DP0001, DP0002, and DP0003 detect missing modifiers and provide one-click IDE CodeFixes.
π Part of the EricksonLopez Ecosystem
- β‘ EricksonLopez.Result β High-Performance Struct-Based Result Pattern & Railway-Oriented Programming.
- π¬ EricksonLopez.Events β Enterprise Event-Driven Architecture & Distributed Messaging Substrate.
- π§± EricksonLopez.SharedKernel β Sovereign Tier-0 DDD Foundational Substrate & Specifications.
- π EricksonLopez.ValueObjects β Pre-Built Enterprise Value Objects & Multi-Country Fiscal Satellites.
- π EricksonLopez.Specification β Composable AOT-First Specification Pattern for .NET.
π€ Contributing
Contributions are welcome! Follow these steps to set up your local development environment:
Prerequisites
- .NET 8.0 SDK, .NET 9.0 SDK, or .NET 10.0 SDK
- Git & modern C# IDE (Visual Studio 2022 v17.12+, JetBrains Rider 2024+, or VS Code with C# Dev Kit)
Development Workflow
- Clone the repository:
git clone https://github.com/ericksonlopezf/dotnet-domain-primitives.git cd dotnet-domain-primitives - Build the solution:
dotnet build EricksonLopez.DomainPrimitives.slnx - Execute unit & integration tests:
dotnet test EricksonLopez.DomainPrimitives.slnx - Run mutation testing quality gates:
dotnet stryker
Please review our Contributing Guidelines and Code of Conduct before submitting pull requests.
π License
Distributed under the MIT License. Copyright Β© 2026 Erickson Lopez. All rights reserved.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.