Skip to main content

.NET 10 and later · MIT · NuGet

AdCodicem.
ValueObjects

An answer to primitive obsession in .NET: single-value DDD value objects, with no reflection and no allocation on the paths that matter.

An IBAN carried as a bare string is validated wherever someone remembered to. Declare the type and its rules once instead; the framework carries them into JSON, the database, model binding and the OpenAPI document, so they cannot drift apart.

dotnet add package AdCodicem.ValueObjects
[ValueObject<string>(
MinLength = 15,
MaxLength = 34,
Pattern = "^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$",
SchemaFormat = "iban")]
public readonly partial struct Iban : IValueObjectNormalizer<string>, IValueObjectValidator<string>
{
public static string NormalizeValue(string value) => /* strip separators, upper-case */;

public static ValidationResult ValidateValue(in string value)
=> HasValidCheckDigits(value)
? ValidationResult.Success
: ValidationResult.InvalidFormat("The IBAN check digits are incorrect.");
}
var iban = Iban.Create("fr76 3000 6000 0112 3456 7890 189");
iban.Value // "FR7630006000011234567890189"
JsonSerializer.Serialize(new { iban }) // {"iban":"FR7630006000011234567890189"}
01

Normalize, then validate, then assign

A non-default instance is by construction normalized and valid — everywhere except the EF Core read path, which trusts values this same application already validated.

02

Rules declared once

MaxLength = 34 validates the value, sizes the EF Core column, and becomes the OpenAPI maxLength keyword. Anything added to [ValueObject<T>] feeds all three.

03

No reflection, no extra allocation

Holding 100 000 struct wrappers allocates exactly what holding 100 000 bare values allocates, to the byte. The generated equality and hashing keep dictionary lookups and sorts allocation-free too.