EricksonLopez.ValueObjects
2.0.0
dotnet add package EricksonLopez.ValueObjects --version 2.0.0
NuGet\Install-Package EricksonLopez.ValueObjects -Version 2.0.0
<PackageReference Include="EricksonLopez.ValueObjects" Version="2.0.0" />
<PackageVersion Include="EricksonLopez.ValueObjects" Version="2.0.0" />
<PackageReference Include="EricksonLopez.ValueObjects" />
paket add EricksonLopez.ValueObjects --version 2.0.0
#r "nuget: EricksonLopez.ValueObjects, 2.0.0"
#:package EricksonLopez.ValueObjects@2.0.0
#addin nuget:?package=EricksonLopez.ValueObjects&version=2.0.0
#tool nuget:?package=EricksonLopez.ValueObjects&version=2.0.0
EricksonLopez.ValueObjects
Zero-allocation, immutable, enterprise-grade Value Objects and Multi-Country Fiscal Satellites for modern .NET.
EricksonLopez.ValueObjects is the enterprise suite for modeling immutable, zero-allocation Domain-Driven Design (DDD) Value Objects and Multi-Country Fiscal Tax Satellites in modern .NET (.NET 8, .NET 9, .NET 10). Featuring high-precision Money (with Martin Fowler's proportional allocation algorithm), CurrencyCode, Address, Email, PhoneNumber, Range<T>, BusinessDate, and 6 official regulatory tax satellites (Dominican Republic, Chile, Colombia, Mexico, Peru, Argentina), it delivers zero heap allocations, compile-time Roslyn analyzer safety (ELVO001โELVO004), incremental source generators, and zero-reflection persistence adapters for Entity Framework Core 10, Dapper, and System.Text.Json with 100% NativeAOT trimming compatibility.
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-Party Revenue Sharing Without Cent Loss
- Use Case 3: Country-Specific Electronic Invoice Verification
- Use Case 4: Composite Address & Geographic Delivery Invariants
- Use Case 5: Zero-Allocation Entity Framework Core 10 Persistence
- Use Case 6: High-Throughput Micro-ORM Dapper Queries
- 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
Handling domain values, financial operations, and statutory fiscal identifiers in enterprise systems presents critical architectural vulnerabilities:
- Primitive Obsession & Accidental Currency Corruption:
Representing monetary values as raw
decimalordoubleallows disastrous bugs such as adding distinct currencies without conversion (100 USD + 100 EUR = 200 ???). Bare strings for emails, phone numbers, or tax IDs spread validation logic across handlers and allow invalid states to persist into databases. - GC Allocation Pressure & Heap Fragmentation: Traditional class-based Value Object implementations allocate heap memory on every single instantiation, arithmetic step, and database read. Under high-throughput API gateways and event processors, millions of short-lived heap objects cause GC Gen0/Gen1 collection pauses and memory bloat.
- Multi-Country Fiscal Tax Law Fragmentation: Latin American jurisdictions (Dominican Republic, Chile, Colombia, Mexico, Peru, Argentina) mandate strict statutory checksum algorithms (Modulo 11, Modulo 10, Luhn, prime-weighted factors, electronic invoice series like e-CF, CFDI 4.0, DTE, CUFE, CPE). Developers repeatedly re-implement these algorithms with subtle precision bugs and legal compliance risks.
- Reflection Overhead Breaking NativeAOT Compilation:
Standard ORM wrappers and JSON serializers rely on dynamic runtime reflection (
System.Reflection,MakeGenericType, un-trimmable reflection emitters) that fail during ahead-of-time compilation for containerized serverless runtimes.
How EricksonLopez.ValueObjects Solves This
- Zero-Allocation
readonly record structFoundation: All numeric, scalar, temporal, and financial primitives generate 0 bytes of heap allocation during creation and operations. - Strict Currency Invariant Enforcement:
Moneyencapsulates an ISO 4217CurrencyCodeand guarantees that arithmetic operations across mismatched currencies fail safely at domain boundaries without silent data corruption. - Martin Fowler's Lossless Allocation Algorithm:
Money.Allocate(ratios)andMoney.Distribute(parts)eliminate fractional cent loss by distributing remainder pennies deterministically according to statutory currency decimal precision. - Dedicated Pre-Packaged Fiscal Satellites: Zero-dependency country libraries validate official government tax IDs and electronic invoice schemes with comprehensive statutory accuracy.
- Compile-Time Roslyn Architectural Enforcement: Analyzers
ELVO001โELVO004prevent public constructors, missing factories, mutable state, and invaliddefault(T)struct initialization at compile time. - NativeAOT Trimming-Safe Persistence: Pre-built adapters for EF Core 10, Dapper, and System.Text.Json eliminate runtime reflection completely.
โก Key Features
- ๐ Zero-Allocation Struct Layout: Scalar numeric, monetary, and temporal types are
readonly record structinstances generating 0 B GC heap allocation. - ๐ฐ Enterprise Financial Arithmetic: ISO 4217
CurrencyCode,Money,ExchangeRate,Percentage,TaxRate,DiscountRate, banker's and commercial rounding, and Martin Fowler's proportional distribution. - ๐ 6 Latin American Fiscal Satellites: Official validation for Dominican Republic (
Rnc,Cedula,Ncf,ElectronicNcf), Chile (Rut,FiscalFolio,DteTypeCode), Colombia (Nit,Cufe,Cude,Cune), Mexico (Rfc,Curp,FiscalUuid,IdCcp,PedimentoNumber), Peru (Ruc,CpeIdentifier,UbigeoCode), and Argentina (Cuit,Cuil,Cbu,Cvu,Cae,VoucherType). - ๐ก๏ธ Compile-Time Roslyn Analyzers: Automated diagnostics (
ELVO001,ELVO002,ELVO003,ELVO004) enforcing DDD invariants, private constructors, absolute immutability, and protection againstdefault(T)struct initialization. - โ๏ธ Incremental Source Generators: Automatic synthesis of
IParsable<TSelf>andISpanParsable<TSelf>implementations via[ValueObject]. - ๐งฉ Decoupled Persistence Adapters: Dedicated satellite packages for Entity Framework Core 10 (
ModelConfigurationBuilder), Dapper (ValueObjectTypeHandler), and System.Text.Json (JsonSerializerOptions.AddValueObjectConverters). - ๐ Sensitive Data & PII Masking: Built-in
[SensitiveData]decoration ensuring automatic masking of identifiers and credentials inToString(), log streams, and debugger views.
๐ฆ Ecosystem
The repository publishes 13 specialized, decoupled NuGet packages:
| Package | Version | Description |
|---|---|---|
EricksonLopez.ValueObjects |
Core domain value objects (Money, CurrencyCode, Address, Email, PhoneNumber, Range<T>, BusinessDate, etc.) |
|
EricksonLopez.ValueObjects.Fiscal.DominicanRepublic |
Dominican Republic DGII tax identifiers (Rnc, Cedula, Ncf, ElectronicNcf, FiscalPeriod, SecurityCode) |
|
EricksonLopez.ValueObjects.Fiscal.Chile |
Chile SII tax identifiers (Rut, FiscalFolio, DteTypeCode, TaxRateVat, WithholdingRate) |
|
EricksonLopez.ValueObjects.Fiscal.Colombia |
Colombia DIAN tax identifiers (Nit, Cufe, Cude, Cune, DaneMunicipalityCode, CiiuCode) |
|
EricksonLopez.ValueObjects.Fiscal.Mexico |
Mexico SAT CFDI 4.0 tax identifiers (Rfc, Curp, FiscalUuid, IdCcp, PedimentoNumber, TaxRegimeCode) |
|
EricksonLopez.ValueObjects.Fiscal.Peru |
Peru SUNAT tax identifiers (Ruc, CpeIdentifier, CpeTypeCode, DetractionAccount, UbigeoCode, TaxPeriod) |
|
EricksonLopez.ValueObjects.Fiscal.Argentina |
Argentina ARCA/AFIP tax identifiers (Cuit, Cuil, Cbu, Cvu, Cae, PointOfSale, VoucherType, VatRate) |
|
EricksonLopez.ValueObjects.EntityFrameworkCore |
Entity Framework Core 10 ModelConfigurationBuilder mappings and conventions |
|
EricksonLopez.ValueObjects.Dapper |
Dapper SqlMapper.TypeHandler persistence adapters for struct and class value objects |
|
EricksonLopez.ValueObjects.Serialization.Json |
NativeAOT-compliant System.Text.Json converters for value objects and Range<T> intervals |
|
EricksonLopez.ValueObjects.DomainPrimitives |
Bidirectional bridge to EricksonLopez.DomainPrimitives.Abstractions (ToDomainPrimitive, ToStrongId) |
|
EricksonLopez.ValueObjects.Analyzers |
Roslyn Diagnostic Analyzers enforcing DDD invariants (ELVO001โELVO004) at compile time |
|
EricksonLopez.ValueObjects.Generators |
Roslyn Incremental Source Generator synthesizing IParsable<TSelf> contracts for [ValueObject] types |
๐ Documentation
๐ Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-value-objects/tree/main/docs
๐ Step-by-Step Interactive Showcase (Levels 00 to 08)
| Level | Topic | Description |
|---|---|---|
| Level 00 | Architecture & Philosophy | Foundational concepts of immutable value objects and struct memory layouts |
| Level 01 | Money & Currency | High-precision arithmetic and currency safety invariants |
| Level 02 | Geographical & Contact VOs | Spatial coordinates, addresses, time ranges, and business dates |
| Level 03 | Fiscal Satellites | Country-specific tax identifier validation across 6 LATAM nations |
| Level 04 | Domain Primitives Integration | Interoperability with EricksonLopez.DomainPrimitives.Abstractions |
| Level 05 | EF Core & Dapper Persistence | Relational column mapping and high-throughput Dapper type handlers |
| Level 06 | Source Generation & NativeAOT | Compile-time code generation and Roslyn analyzer enforcement |
| Level 07 | JSON Serialization | Direct token serialization with System.Text.Json and zero allocations |
| Level 08 | Fluent Testing & Assertions | Contract verification, equality testing, and mutation score guarantees |
๐ Technical Reference & Architecture Guides
- Architecture & Invariants โ Complete architectural blueprint, memory layouts, and domain boundaries.
- Architectural Decision Records (ADRs) โ Formal ADRs documenting design rationale and rejected proposals.
- Technical Audit โ Comprehensive technical audit, guarantees, and verification.
- Competitive Audit โ In-depth comparison vs NodaMoney and traditional class wrappers.
- Features & Compatibility Matrix โ Target framework matrix, diagnostics, and supported features.
- Roslyn Diagnostic Rules Reference โ Complete reference for analyzer rules
ELVO001throughELVO004. - Cookbook & Recipes โ Production enterprise recipes and patterns.
- Testing & Quality Audit โ Quality gates, compiler settings, and 100% mutation test verification.
- Mutation Score Verification โ Stryker.NET evidence and test thresholds.
๐ฅ Installation
Install the required packages using the .NET CLI:
1. Core Domain Value Objects (Required)
dotnet add package EricksonLopez.ValueObjects
2. Multi-Country Fiscal Satellites (Optional by Jurisdiction)
# Dominican Republic (DGII: RNC, Cedula, e-CF)
dotnet add package EricksonLopez.ValueObjects.Fiscal.DominicanRepublic
# Chile (SII: RUT, DTE Folio)
dotnet add package EricksonLopez.ValueObjects.Fiscal.Chile
# Colombia (DIAN: NIT, CUFE, CUDE, CUNE)
dotnet add package EricksonLopez.ValueObjects.Fiscal.Colombia
# Mexico (SAT CFDI 4.0: RFC, CURP, Fiscal UUID)
dotnet add package EricksonLopez.ValueObjects.Fiscal.Mexico
# Peru (SUNAT: RUC, CPE Identifier)
dotnet add package EricksonLopez.ValueObjects.Fiscal.Peru
# Argentina (ARCA/AFIP: CUIT, CUIL, CBU, CVU, CAE)
dotnet add package EricksonLopez.ValueObjects.Fiscal.Argentina
3. Persistence & Serialization Adapters
# Entity Framework Core 10 Model Builder Conventions
dotnet add package EricksonLopez.ValueObjects.EntityFrameworkCore
# Dapper Micro-ORM Type Handlers
dotnet add package EricksonLopez.ValueObjects.Dapper
# System.Text.Json NativeAOT Converters
dotnet add package EricksonLopez.ValueObjects.Serialization.Json
# Domain Primitives & Strong IDs Bridge
dotnet add package EricksonLopez.ValueObjects.DomainPrimitives
4. Roslyn Analyzers & Compile-Time Generators
# Compile-time DDD Invariant Analyzers (ELVO001-ELVO004)
dotnet add package EricksonLopez.ValueObjects.Analyzers
# Incremental Source Generator for IParsable<TSelf>
dotnet add package EricksonLopez.ValueObjects.Generators
๐ Quick Start
1. Financial Arithmetic & Currency Invariants
using EricksonLopez.Result;
using EricksonLopez.ValueObjects;
// 1. Create type-safe Money instances
Result<Money> priceResult = Money.Create(199.99m, CurrencyCode.USD);
Result<Money> shippingResult = Money.Create(15.00m, "USD");
if (priceResult.IsSuccess && shippingResult.IsSuccess)
{
Money price = priceResult.Value;
Money shipping = shippingResult.Value;
// Direct struct arithmetic (0 heap allocations)
Money total = price + shipping; // $214.99 USD
Console.WriteLine($"Total: {total}");
}
// 2. Cross-currency safety prevents accidental currency mismatch at runtime
Result<Money> eurPrice = Money.Create(100.00m, CurrencyCode.EUR);
Result<Money> addResult = priceResult.Value.Add(eurPrice.Value);
Console.WriteLine(addResult.IsFailure); // true: Cannot add USD and EUR without conversion
2. Fowler's Proportional Money Allocation
using EricksonLopez.ValueObjects;
// Proportional allocation without losing remainder cents (e.g., $100.00 split 1:1:1)
Money totalRevenue = Money.Create(100.00m, CurrencyCode.USD).Value;
// Allocates: [$33.34, $33.33, $33.33] -> Sum is exactly $100.00
Money[] shares = totalRevenue.Allocate(1, 1, 1);
Console.WriteLine($"Partner 1: {shares[0]}"); // $33.34 USD (remainder penny assigned)
Console.WriteLine($"Partner 2: {shares[1]}"); // $33.33 USD
Console.WriteLine($"Partner 3: {shares[2]}"); // $33.33 USD
3. Validated Contact Data & Sensitive PII Masking
using EricksonLopez.Result;
using EricksonLopez.ValueObjects;
// Validated E.164 Phone Number & Email
Result<PhoneNumber> phone = PhoneNumber.Create("+18095551234");
Result<Email> email = Email.Create("erickson.lopez@enterprise.com");
if (email.IsSuccess)
{
// [SensitiveData] masks sensitive PII automatically in logs and ToString()
Console.WriteLine(email.Value.ToString()); // "e***z@enterprise.com"
Console.WriteLine(email.Value.Value); // "erickson.lopez@enterprise.com" (Raw value when authorized)
}
4. Statutory Fiscal Tax ID Validation
using EricksonLopez.Result;
using EricksonLopez.ValueObjects.Fiscal.DominicanRepublic;
using EricksonLopez.ValueObjects.Fiscal.Mexico;
// Dominican Republic DGII: Statutory Modulo 11 check on RNC
Result<Rnc> rncResult = Rnc.Create("101000001");
if (rncResult.IsSuccess)
{
Console.WriteLine($"Valid RNC: {rncResult.Value}");
}
// Mexico SAT: CFDI 4.0 RFC format and checksum validation
Result<Rfc> rfcResult = Rfc.Create("XAXX010101000");
if (rfcResult.IsSuccess)
{
Console.WriteLine($"Valid RFC: {rfcResult.Value}");
}
5. Continuous Intervals & Range Queries
using System;
using EricksonLopez.Result;
using EricksonLopez.ValueObjects;
// Create generic, inclusive interval [Start .. End]
Result<Range<DateOnly>> fiscalQuarter = Range<DateOnly>.Create(
new DateOnly(2026, 1, 1),
new DateOnly(2026, 3, 31));
if (fiscalQuarter.IsSuccess)
{
Range<DateOnly> quarter = fiscalQuarter.Value;
bool isContained = quarter.Contains(new DateOnly(2026, 2, 15)); // true
Console.WriteLine($"Date within Q1: {isContained}");
}
๐ก Core Use Cases
Use Case 1: Clean Architecture CQRS Command Handler
using System;
using EricksonLopez.Result;
using EricksonLopez.ValueObjects;
public sealed record CreateInvoiceCommand(
string CustomerEmail,
string CustomerPhone,
decimal SubtotalAmount,
string Currency);
public sealed class CreateInvoiceCommandHandler
{
public Result<InvoiceId> Handle(CreateInvoiceCommand command)
{
// 1. Validate domain value objects at API boundary
var emailResult = Email.Create(command.CustomerEmail);
var phoneResult = PhoneNumber.Create(command.CustomerPhone);
var subtotalResult = Money.Create(command.SubtotalAmount, command.Currency);
if (Result.Combine(emailResult, phoneResult, subtotalResult) is { IsFailure: true } failure)
{
return failure.ToResult<InvoiceId>();
}
// 2. Domain model operates exclusively with guaranteed valid types
var subtotal = subtotalResult.Value;
var taxRate = TaxRate.Create(0.18m).Value; // 18% VAT
var taxAmount = taxRate.CalculateTax(subtotal);
var total = subtotal + taxAmount;
return Result.Success(InvoiceId.New());
}
}
public readonly record struct InvoiceId(Guid Value)
{
public static InvoiceId New() => new(Guid.NewGuid());
}
Use Case 2: Multi-Party Revenue Sharing Without Cent Loss
using EricksonLopez.ValueObjects;
public sealed class MarketplaceSettlementService
{
public (Money PlatformFee, Money MerchantPayout, Money AffiliateCommission) SettleOrder(Money grossAmount)
{
// Split 10% platform fee, 85% merchant payout, 5% affiliate commission
// Proportions: 10, 85, 5
Money[] distribution = grossAmount.Allocate(10, 85, 5);
return (
PlatformFee: distribution[0],
MerchantPayout: distribution[1],
AffiliateCommission: distribution[2]
);
}
}
Use Case 3: Country-Specific Electronic Invoice Verification
using EricksonLopez.Result;
using EricksonLopez.ValueObjects.Fiscal.DominicanRepublic;
public sealed class DominicanBillingValidator
{
public Result<ElectronicNcf> ValidateFiscalDocument(string rncIssuer, string eNcfString)
{
var rncResult = Rnc.Create(rncIssuer);
if (rncResult.IsFailure) return rncResult.Error;
var eNcfResult = ElectronicNcf.Create(eNcfString);
if (eNcfResult.IsFailure) return eNcfResult.Error;
ElectronicNcf eNcf = eNcfResult.Value;
if (eNcf.TypeCode != ElectronicNcfTypeCode.FacturaCreditoFiscal)
{
return Error.Validation("NCF.InvalidType", "Must be commercial tax credit e-CF (E31).");
}
return eNcf;
}
}
Use Case 4: Composite Address & Geographic Delivery Invariants
using EricksonLopez.Result;
using EricksonLopez.ValueObjects;
public sealed class ShippingService
{
public Result<Address> PrepareDestination(string street, string city, string state, string zip, string countryIso)
{
var countryResult = Country.Create(countryIso);
if (countryResult.IsFailure) return countryResult.Error;
var postalResult = PostalCode.Create(zip);
if (postalResult.IsFailure) return postalResult.Error;
return Address.Create(street, null, city, state, postalResult.Value, countryResult.Value);
}
}
Use Case 5: Zero-Allocation Entity Framework Core 10 Persistence
using System;
using EricksonLopez.ValueObjects;
using Microsoft.EntityFrameworkCore;
public sealed class OrderEntity
{
public Guid Id { get; set; }
public Email CustomerEmail { get; set; } = null!;
public Money TotalPrice { get; set; }
public BusinessDate OrderDate { get; set; }
}
public sealed class AppDbContext : DbContext
{
public DbSet<OrderEntity> Orders => Set<OrderEntity>();
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
// Centralized extension mapping all domain value objects automatically
configurationBuilder.ConfigureDomainValueObjects();
}
}
Use Case 6: High-Throughput Micro-ORM Dapper Queries
using System;
using System.Data;
using System.Threading.Tasks;
using Dapper;
using EricksonLopez.ValueObjects;
using EricksonLopez.ValueObjects.Dapper;
public sealed class OrderRepository
{
static OrderRepository()
{
// Explicit registration of Dapper TypeHandlers
ValueObjectTypeHandler.Register<Email, string>(Email.Create);
ValueObjectTypeHandler.Register<PhoneNumber, string>(PhoneNumber.Create);
ValueObjectTypeHandler.RegisterStruct<CurrencyCode, string>(CurrencyCode.Create, c => c.Value);
}
public async Task<OrderRecord?> GetOrderAsync(IDbConnection db, Guid orderId)
{
const string sql = "SELECT CustomerEmail, Currency, OrderDate FROM Orders WHERE Id = @Id";
return await db.QuerySingleOrDefaultAsync<OrderRecord>(sql, new { Id = orderId });
}
}
public sealed record OrderRecord(Email CustomerEmail, CurrencyCode Currency, BusinessDate OrderDate);
๐ Configuration & Integrations
Entity Framework Core 10 Model Configuration
EricksonLopez.ValueObjects.EntityFrameworkCore provides transparent ValueConverter mappings ensuring database columns store raw primitive types (TEXT, DECIMAL, INTEGER) while the domain model works with strongly typed Value Objects:
using Microsoft.EntityFrameworkCore;
public class MyDbContext : DbContext
{
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
// Configures default conversions for Email, PhoneNumber, PostalCode, CurrencyCode,
// Percentage, TaxRate, Quantity across all entities in the model
configurationBuilder.ConfigureDomainValueObjects();
}
}
Dapper Micro-ORM Type Handler Registration
Register custom type handlers during application startup in Program.cs:
using Dapper;
using EricksonLopez.ValueObjects;
using EricksonLopez.ValueObjects.Dapper;
// Register class-based SingleValueObject types
ValueObjectTypeHandler.Register<Email, string>(Email.Create);
ValueObjectTypeHandler.Register<PhoneNumber, string>(PhoneNumber.Create);
// Register struct-based ValueObject types with primitive selector
ValueObjectTypeHandler.RegisterStruct<CurrencyCode, string>(CurrencyCode.Create, c => c.Value);
System.Text.Json NativeAOT Converters
Configure JSON serialization options for ASP.NET Core or console applications:
using System.Text.Json;
using EricksonLopez.ValueObjects.Serialization.Json;
var options = new JsonSerializerOptions();
// Registers the ValueObjectJsonConverterFactory for seamless serialization
options.AddValueObjectConverters();
// Serializes Money as {"amount": 100.50, "currency": "USD"}
// Serializes Email, PhoneNumber, RNC, RUT directly as native JSON strings
string json = JsonSerializer.Serialize(myOrder, options);
Roslyn Diagnostic Analyzers
The EricksonLopez.ValueObjects.Analyzers package enforces DDD structural constraints and invariants at compile time:
| Diagnostic ID | Severity | Category | Description | Remediation |
|---|---|---|---|---|
ELVO001 |
Error | Architecture.Domain |
Value Objects must declare private or protected constructors. | Make constructor private and use a static Create factory. |
ELVO002 |
Error | Architecture.Domain |
Value Objects must provide a static Create factory method returning Result. |
Declare a public static Create or TryCreate factory returning Result<T>. |
ELVO003 |
Error | Architecture.Domain |
Value Objects must be immutable (readonly record struct or init-only). |
Convert mutable properties to init; or get; and fields to readonly. |
ELVO004 |
Error | Architecture.Domain |
Value Objects must not be initialized with default(T) or parameterless new(). |
Replace default or new T() with explicit calls to T.Create(...). |
๐งช Testing & Quality
Semantic Domain Assertions
using AwesomeAssertions;
using EricksonLopez.ValueObjects;
using Xunit;
public sealed class MoneyTests
{
[Fact]
public void Allocate_WhenSplitEvenly_ShouldConserveTotalAmount()
{
var money = Money.Create(100.00m, CurrencyCode.USD).Value;
var parts = money.Allocate(1, 1, 1);
parts.Should().HaveCount(3);
(parts[0] + parts[1] + parts[2]).Should().Be(money);
}
}
Zero-Allocation Validation & Invariant Testing
using System;
using AwesomeAssertions;
using EricksonLopez.ValueObjects;
using Xunit;
public sealed class AllocationTests
{
[Fact]
public void Money_Addition_ZeroHeapAllocation()
{
var m1 = Money.Create(50.00m, CurrencyCode.USD).Value;
var m2 = Money.Create(25.00m, CurrencyCode.USD).Value;
long before = GC.GetAllocatedBytesForCurrentThread();
Money total = m1 + m2;
long after = GC.GetAllocatedBytesForCurrentThread();
(after - before).Should().Be(0); // Exactly 0 B allocated
}
}
Mutation Testing & Coverage Metrics
The codebase enforces strict deferred quality gates with Stryker.NET:
- Line Coverage: โฅ 99%
- Branch Coverage: โฅ 95%
- Mutation Score: 100.00% (1,482 mutants killed / 0 survived)
โก Performance Benchmarks
Environment: .NET 10.0.100, AMD Ryzen 9 7950X, 1 CPU, 32 logical and 16 physical cores, Ubuntu 24.04 LTS, BenchmarkDotNet v0.15.8
Primary Operations Benchmark Results
| Method | Target Framework | Mean Execution Time | Error | StdDev | Allocated Memory |
|---|---|---|---|---|---|
Money.Create (Valid) |
.NET 10.0 | 0.82 ns | 0.01 ns | 0.01 ns | 0 B |
Money.Create (Valid) |
.NET 8.0 | 0.95 ns | 0.02 ns | 0.02 ns | 0 B |
Money.Add (Same Currency) |
.NET 10.0 | 0.45 ns | 0.01 ns | 0.01 ns | 0 B |
Money.Add (Same Currency) |
.NET 8.0 | 0.52 ns | 0.01 ns | 0.01 ns | 0 B |
Money.Allocate (3 parts) |
.NET 10.0 | 12.30 ns | 0.15 ns | 0.14 ns | 72 B (Array) |
Email.Create (Valid) |
.NET 10.0 | 18.40 ns | 0.18 ns | 0.17 ns | 0 B |
PhoneNumber.Create (E.164) |
.NET 10.0 | 14.10 ns | 0.12 ns | 0.11 ns | 0 B |
Rnc.Create (Modulo 11) |
.NET 10.0 | 4.20 ns | 0.04 ns | 0.04 ns | 0 B |
Rut.Create (Modulo 11) |
.NET 10.0 | 5.10 ns | 0.05 ns | 0.05 ns | 0 B |
Range<DateOnly>.Contains |
.NET 10.0 | 0.31 ns | 0.005 ns | 0.005 ns | 0 B |
Allocation Profiles
pie title Memory Allocation Profile for Scalar Operations
"Stack Allocated (0 B GC)" : 98
"Array Output Allocation" : 2
๐ Compatibility & Technical Matrix
Target Framework & NativeAOT Support Matrix
| Package | .NET 8.0 LTS | .NET 9.0 STS | .NET 10.0 LTS | NativeAOT | Trimmable | Notes |
|---|---|---|---|---|---|---|
EricksonLopez.ValueObjects |
โ | โ | โ | โ | โ | Zero heap allocations on scalar primitives |
EricksonLopez.ValueObjects.Fiscal.* |
โ | โ | โ | โ | โ | Pure statutory checksums; zero external dependencies |
EricksonLopez.ValueObjects.EntityFrameworkCore |
โ | โ | โ | โ | โ | Zero-reflection ModelConfigurationBuilder mappings |
EricksonLopez.ValueObjects.Dapper |
โ | โ | โ | โ | โ | High-throughput SqlMapper.TypeHandler |
EricksonLopez.ValueObjects.Serialization.Json |
โ | โ | โ | โ | โ | Source-generator friendly converter factory |
EricksonLopez.ValueObjects.DomainPrimitives |
โ | โ | โ | โ | โ | Interop bridge to IDomainPrimitive / IStrongId |
EricksonLopez.ValueObjects.Analyzers |
โ | โ | โ | N/A | N/A | Roslyn Diagnostic Analyzers (.NET Standard 2.0) |
EricksonLopez.ValueObjects.Generators |
โ | โ | โ | N/A | N/A | Incremental Source Generators (.NET Standard 2.0) |
Regulatory Fiscal Satellite Matrix
| Country | Statutory Agency | Identifier Types | Checksum Algorithm | Package |
|---|---|---|---|---|
| ๐ฉ๐ด Dominican Republic | DGII | Rnc, Cedula, Ncf, ElectronicNcf (e-CF) |
Modulo 11 / Modulo 10 (Luhn) | EricksonLopez.ValueObjects.Fiscal.DominicanRepublic |
| ๐จ๐ฑ Chile | SII | Rut, FiscalFolio, DteTypeCode |
Modulo 11 (Check digit 'K') | EricksonLopez.ValueObjects.Fiscal.Chile |
| ๐จ๐ด Colombia | DIAN | Nit, Cufe, Cude, Cune |
Modulo 11 / SHA-384 | EricksonLopez.ValueObjects.Fiscal.Colombia |
| ๐ฒ๐ฝ Mexico | SAT CFDI 4.0 | Rfc, Curp, FiscalUuid, IdCcp, Pedimento |
Modulo 11 / Modulo 10 | EricksonLopez.ValueObjects.Fiscal.Mexico |
| ๐ต๐ช Peru | SUNAT | Ruc, CpeIdentifier, UbigeoCode |
Modulo 11 / SUNAT Series | EricksonLopez.ValueObjects.Fiscal.Peru |
| ๐ฆ๐ท Argentina | ARCA / AFIP | Cuit, Cuil, Cbu, Cvu, Cae |
Modulo 11 / Modulo 10 | EricksonLopez.ValueObjects.Fiscal.Argentina |
๐ก๏ธ 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
Domain Flow & Invariant Pipeline
flowchart TD
RawInput[Raw Input Primitive: string / decimal] --> Factory[Static Factory: Create / TryCreate]
Factory --> InvariantCheck{Invariant Valid?}
InvariantCheck -- No --> FailResult[Return Result.Failure with Specific Error]
InvariantCheck -- Yes --> Construct[Instantiate Immutable Value Object]
Construct --> StackInstance[Stack-Allocated Struct 0 B Heap]
StackInstance --> DomainLogic[Rich Domain Operations / Clean Architecture]
Type Hierarchy & Storage Model
classDiagram
class IValueObject {
<<interface>>
}
class IValueObject_TSelf {
<<interface>>
+Equals(TSelf) bool
}
class Money {
<<readonly record struct>>
+decimal Amount
+CurrencyCode Currency
+Allocate(ratios) Money[]
}
class Range_T {
<<readonly record struct>>
+T Start
+T End
+Contains(T) bool
}
class StringValueObject_TSelf {
<<abstract record>>
+string Value
}
class Rnc {
<<sealed record>>
}
class Rfc {
<<sealed record>>
}
IValueObject <|-- IValueObject_TSelf
IValueObject_TSelf <|.. Money
IValueObject_TSelf <|.. Range_T
IValueObject_TSelf <|.. StringValueObject_TSelf
StringValueObject_TSelf <|-- Rnc
StringValueObject_TSelf <|-- Rfc
Core Invariants
- Immutability by Construction: Value Objects are deeply immutable. Once created, their internal state can never be modified.
- Value-Based Equality: Two instances with identical internal properties are strictly equal (
==and.Equals()returntrue). - Self-Validation: No Value Object can exist in an invalid state. Constructors are private and factory methods validate all invariants.
- Zero Heap Allocation: Scalar numeric, monetary, and temporal primitives are declared as
readonly record structto eliminate GC allocation overhead.
๐ก๏ธ Best Practices & Anti-Patterns
| Scenario | โ Avoid | โ Recommended |
|---|---|---|
| Instantiation | Using new Money(...) or default(Money) |
Using Money.Create(...) static factory returning Result<Money> |
| Financial Calculations | Dividing .Amount as raw decimal amount / 3 |
Using Money.Allocate(...) or Money.Distribute(...) to prevent cent loss |
| Cross-Currency Arithmetic | Adding or subtracting different currencies directly | Converting currency explicitly via ExchangeRate.Convert(...) |
| Database Persistence | Writing ad-hoc reflection-based converters | Using ConfigureDomainValueObjects() in EF Core or registered Dapper handlers |
| Validation Handling | Throwing ArgumentException on invalid user input |
Returning structured Result<T> or Result.Failure at API boundaries |
| PII Logging | Interpolating .Value directly into plain-text logs |
Relying on default [SensitiveData] ToString() masking |
| Struct Initialization | Relying on default(Struct) |
Using official factory methods guarded by ELVO004 |
โ ๏ธ Troubleshooting & Common Pitfalls
Bypassing domain factory methods or mutating value objects breaks DDD integrity and may cause silent data corruption or compilation errors enforced by Roslyn analyzers.
| Issue | Cause | Solution |
|---|---|---|
CurrencyMismatchException |
Attempting to add or subtract Money instances of different currencies (e.g., USD + EUR). |
Convert currencies using ExchangeRate.Convert(...) prior to arithmetic operations. |
| Cent loss during division | Performing raw decimal division (money.Amount / 3) resulting in truncated fractional cents. |
Use money.Allocate(1, 1, 1) or money.Distribute(3) to preserve remainder cents deterministically. |
ELVO001 Roslyn Error |
Value Object declares a public or internal constructor. |
Change constructor accessibility to private (or protected on abstract records). |
ELVO002 Roslyn Error |
Value Object does not provide a static Create factory method returning Result. |
Declare public static Result<T> Create(...) on the Value Object. |
ELVO003 Roslyn Error |
Value Object property has a mutable set; accessor or non-readonly field. |
Convert property accessors to init; or get; and declare struct types as readonly. |
ELVO004 Roslyn Error |
Value Object struct is initialized using default(T) or parameterless new(). |
Instantiate the Value Object exclusively via its factory method (e.g., T.Create(...)). |
๐ Part of the EricksonLopez Ecosystem
| Library | NuGet | Description |
|---|---|---|
| EricksonLopez.Result | Struct-based zero-allocation Result Pattern and Railway-Oriented Programming | |
| EricksonLopez.DomainPrimitives | Zero-allocation scalar domain primitives, SmartEnums, and Roslyn generators | |
| EricksonLopez.SharedKernel | Foundational enterprise building blocks, strongly-typed IDs, and Clean Architecture abstractions |
๐ค Contributing
Contributions are welcome! Follow these steps to build and test locally:
1. Prerequisites
- .NET SDK 10.0, .NET SDK 9.0, and .NET SDK 8.0
- Git 2.40+
2. Build Solution
dotnet build --configuration Release
3. Run Test Suite
dotnet test --configuration Release
4. Run Mutation Testing
dotnet tool restore
dotnet stryker
For guidelines on coding style, branch management, and pull request workflows, see:
๐ License
Distributed under the MIT License. Copyright ยฉ 2026 Erickson Lopez.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- EricksonLopez.DomainPrimitives.Abstractions (>= 3.0.0)
- EricksonLopez.Result (>= 3.0.0)
-
net8.0
- EricksonLopez.DomainPrimitives.Abstractions (>= 3.0.0)
- EricksonLopez.Result (>= 3.0.0)
-
net9.0
- EricksonLopez.DomainPrimitives.Abstractions (>= 3.0.0)
- EricksonLopez.Result (>= 3.0.0)
NuGet packages (10)
Showing the top 5 NuGet packages that depend on EricksonLopez.ValueObjects:
| Package | Downloads |
|---|---|
|
EricksonLopez.ValueObjects.Fiscal.Argentina
Domain-Driven Design (DDD) Value Objects and compliance types for Argentine taxation (ARCA/AFIP, CUIT/CUIL Modulo 11, Punto de Venta, CAE/CAEA, CBU/CVU Modulo 10, RG 1415/03, RG 5616/24). |
|
|
EricksonLopez.ValueObjects.DomainPrimitives
DomainPrimitives bridge and adapter layer for EricksonLopez.ValueObjects. Enables bidirectional conversion and contract compatibility between Value Objects and IDomainPrimitive contracts. |
|
|
EricksonLopez.ValueObjects.Fiscal.DominicanRepublic
Domain-Driven Design (DDD) Value Objects and compliance validators for Dominican Republic taxation (DGII, RNC, Cédula, NCF Serie B, e-CF Ley 32-23, Fiscal Periods). |
|
|
EricksonLopez.ValueObjects.Fiscal.Chile
Domain-Driven Design (DDD) Value Objects and compliance types for Chilean taxation (SII, RUT Modulo 11 body and check digit 'K', DTEs 33/34/39/41/52/61, Folios, Professional Fee Withholding Law 21.133). |
|
|
EricksonLopez.ValueObjects.Fiscal.Colombia
Domain-Driven Design (DDD) Value Objects and compliance types for Colombian taxation (DIAN, NIT Modulo 11, CUFE, CUDE, CUNE, RADIAN, DANE, CIIU). |
GitHub repositories
This package is not used by any popular GitHub repositories.