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.
- 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
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.
- 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.
- 🚀 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.
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 |
🌐 Official Documentation Hub: https://github.com/ericksonlopezf/dotnet-value-objects/tree/main/docs
| 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 |
- 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.
Install the required packages using the .NET CLI:
dotnet add package EricksonLopez.ValueObjects# 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# 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# Compile-time DDD Invariant Analyzers (ELVO001-ELVO004)
dotnet add package EricksonLopez.ValueObjects.Analyzers
# Incremental Source Generator for IParsable<TSelf>
dotnet add package EricksonLopez.ValueObjects.Generatorsusing 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 conversionusing 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 USDusing 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)
}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}");
}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}");
}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());
}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]
);
}
}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;
}
}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);
}
}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();
}
}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);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();
}
}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);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);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(...). |
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);
}
}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
}
}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)
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
| 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 |
pie title Memory Allocation Profile for Scalar Operations
"Stack Allocated (0 B GC)" : 98
"Array Output Allocation" : 2
| 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) |
| 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.
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]
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
- 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.
| 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 |
Caution
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(...)). |
| 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 |
Contributions are welcome! Follow these steps to build and test locally:
- .NET SDK 10.0, .NET SDK 9.0, and .NET SDK 8.0
- Git 2.40+
dotnet build --configuration Releasedotnet test --configuration Releasedotnet tool restore
dotnet strykerFor guidelines on coding style, branch management, and pull request workflows, see:
Distributed under the MIT License. Copyright © 2026 Erickson Lopez.