Frequently asked questions
About the library
What problem does it solve?
Primitive obsession: domain concepts carried as bare string, int and Guid, with their rules restated — and
drifting apart — in every layer. Primitive obsession describes the
problem and how the library answers it.
Which versions of .NET are supported?
.NET 10: every package targets net10.0, and so must the project that uses them. If your application cannot
move to .NET 10,
Compared with other libraries names alternatives
that run on older frameworks.
Is it ready for production?
It follows semantic versioning and is still at 0.x, which means the public surface may change between minor
releases; every change is recorded in the
changelog. Stable releases are
cut by hand, and every merge to main publishes a preview package in between.
How does it compare with Vogen, StronglyTypedId or Thinktecture?
Compared with other libraries has a feature table and says where each of them is the better choice. Migrate from another library maps their surface onto this one.
Declaring value objects
Why a readonly struct, and not a class or a record struct?
A struct wrapping a string costs exactly what the string costs, to the byte; a class adds 24 bytes per
instance. A record struct is refused because its with expression and field-wise equality would bypass
validation and the declared comparison. Design decisions has the reasoning and
Benchmarks the numbers.
Which underlying types can I use?
string, Guid, bool, char, every built-in integer including Int128 and UInt128, decimal, double,
float, DateOnly, TimeOnly, DateTime, DateTimeOffset and TimeSpan. Anything else is VO0003.
Can a value object hold several values?
No. A concept made of several values — an amount and its currency, a postal address — belongs in an ordinary type whose members are value objects.
My normalization or validation rule is never called. Why?
The interface is missing. A hook is found through IValueObjectNormalizer<T>, IValueObjectValidator<T> or a
formatter interface, not through its name, and VO0011 warns about a method that looks like a rule but is not
declared as one.
How do I see the code the generator writes?
Set <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> in the project and build: the files land
under obj/…/generated/AdCodicem.ValueObjects.Generators/. Generated members
lists what to expect.
Using value objects
How do I represent a missing value?
With T?. default(T) and new T() are build errors (VO0010), because an uninitialized struct would skip
every rule. A type whose zero value is genuinely meaningful can opt out with AllowDefault = true.
Can I throw my own exception type?
Create always throws ValueObjectException, which carries the error code, the type and the attempted value.
To throw something else, call TryCreate and throw your own exception from the ValidationResult it returns.
Can a value object be a dictionary key, or an EF Core key?
Yes to both. The generated equality and hashing make dictionary lookups allocation-free, and a value object serializes as a JSON property name. In EF Core it can be a primary or a foreign key; for a string key compared without case, give the column a matching collation.
Why are values read from the database not validated?
Because the EF Core read path is the hottest in most applications, and it reads values the same application
validated when writing them. ConfigureValueObjects(strict: true, …) validates reads for tables other systems
also write to.
Does it work with minimal APIs?
Yes, with nothing to install: a value object implements IParsable<T>, which is what minimal API parameter
binding looks for.
Does it work with Swashbuckle?
No. The OpenAPI integration targets the built-in .NET stack, Microsoft.AspNetCore.OpenApi.
Performance
Does a value object cost more than the primitive it wraps?
Holding one costs nothing more: a struct wrapper has the size of the value it wraps. Validation costs what the
rules cost, on the way in only. The struct gives something back when it is boxed — passed as object or through
a non-generic interface — which is why the generated equality, hashing and comparison keep the hot paths
generic. Benchmarks measures each case.
Does it use reflection?
Not in the generated code, and not to find value objects: a generated module initializer registers each type at start-up. The contracts, the generated code, the JSON package, FluentValidation and identifiers are marked AOT-compatible and built with the trimming and AOT analyzers on. The EF Core, ASP.NET Core, OpenAPI, Dapper and Newtonsoft.Json integrations are not, because the frameworks they plug into are not.