Skip to main content
Version: Preview (0.3.0-preview.425)

Compared with other libraries

AdCodicem.ValueObjects is not the first answer to primitive obsession in .NET. Three established libraries generate value objects too, and each of them is the better choice for some projects. This page is meant to help you tell which one yours is.

The comparison was made in September 2026 against Vogen 8.0.7, its latest stable release (9.0 was in beta: 9.0.0-beta.1 came out in July 2026), StronglyTypedId 1.0.0-beta08 — the version most of its users install, although nuget.org still lists 0.2.1 as the latest stable — and Thinktecture.Runtime.Extensions 10.5.0, from their source, their documentation and, where the documentation was unclear, a compiled test project. Libraries move: if something here is out of date, open an issue and it will be corrected.

At a glance​

AdCodicem.ValueObjectsVogenStronglyTypedIdThinktecture
Consumer frameworks.NET 10 and laternetstandard2.0 and laterolder frameworks too, including .NET Framework, with a .NET 7+ SDK.NET 8 and later
Type kindsreadonly structclass, struct, recordstructclass, struct
Underlying types22 built-in typesany type but a collectionGuid, int, long, string; others through templatesany type
Length, pattern and range declared once, on the typeyes: length on the attribute, range through typed bound hooks, a pattern through a source-generated regex hooknonono
Validation and normalization hooksinterfaces, checked by the compilermethods found by namenonea partial method with ref parameters
Rejection carries a stable error codeyesno, a message—no, a message (custom error types possible)
default and new T() rejected at build timeyesyesnoyes, for structs
String comparisonordinal, or declared per typeordinal, configurableordinalcase-insensitive by default, configurable
System.Text.Jsonyesyesyesyes
Source-generated JsonSerializerContextdeclared on the context, at compile timea factory passed in the options at run timeconverters passed in the optionsnot documented
EF Core mappingevery value object of an assembly in one call, collections of them includedper property, or a marker class listing the typesper typeevery value object in one call
Column size from the type's rulesyes, and non-Unicode text on request (Unicode = false)nonono, a max-length strategy can be configured
CHECK constraints from the type's ruleson request: the minimum length, bounds, closed sets, and patterns on SQL Server 2025 and PostgreSQL; a violated one reported with its rule code (UseValueObjectCheckErrors())nonono
EF Core reads validateon request (strict: true)by default—no, reads use the constructor
EF Core .Value in LINQtranslated to the column, on request (UseValueObjectQueries())no: compare the value objectsno: compare the value objectsno
ASP.NET Core model bindingyesthrough the TypeConverterthrough the TypeConverteryes
Problem details carry the violated rule's codeMVC controllers (bodies and JSON Patch documents), minimal APIs and Azure Functions HTTP triggersnonono
OpenAPIbuilt-in stack, Swashbuckle 10 and NSwag 14, with lengths, pattern, bounds, enum and its namestype and format; Swashbuckle or built-in stacknoneSwashbuckle, type of the key
JSON Schema registry (Confluent's JSON serdes for Kafka)the rules in the registered schema, numbers as numbers, a refused message with its codeno (8.0.7 and 9.0.0-beta.1 hold nothing for NJsonSchema)not checkednot checked
FluentValidationyesthird-party packagenono
DataAnnotationsraw text held to the type's rules ([ParsesAs]), a default instance refused ([NotDefault]), the rule's code on the resultnonono
Dapperyesyesthrough a templateno
Other serializers and storesNewtonsoft.Json; XML (XmlSerializer, DataContractSerializer, opted in per assembly, read with validation, rules in the XSD); MongoDB.Driver (BSON, .Value in LINQ, read with validation, rules in a $jsonSchema validator, entity identifiers minted on insert); MessagePack and SignalR's MessagePack hub protocol (bare values, read with validation); protobuf-net and protobuf-net.Grpc code-first services (the underlying type's wire, read with validation, a refused request answered INVALID_ARGUMENT with its code)Newtonsoft.Json, LinqToDB, ServiceStack.Text, Orleans, MessagePack, BSON, XML (read without validation)Newtonsoft.JsonNewtonsoft.Json, MessagePack
Structured loggingSerilog: {@X}, and {X} on request, as the underlying valuenonoSerilog destructuring policy, {@X} only
Language-model tools and structured outputMicrosoft.Extensions.AI: the rules in the schemas, a refused tool argument answered with its codenonono
Model Context Protocol toolsthe C# SDK: the rules in inputSchema and outputSchema, a refused tool argument answered with its codenonono
GraphQL (HotChocolate)a scalar per value object, read through its rules, a refused value answered with its code in extensions.codenonono
Blazor formsan input per value object, read through its rules, its rule's message under the field and its code on the EditContextno (8.0.7 and 9.0.0-beta.1 hold no InputBase)no (1.0.0-beta08 holds none)no (10.5.1 holds none)
Azure Functions isolated workerroute values, query values and binding data bound through the rules; a refused one in an HTTP trigger answered 400 with its codeno (8.0.7 ships no Functions code)not checkednot checked
Contract test kit for your own typesyes, with the values its schema rules out, on requestnonono
Valid test data from the declared rulesAutoFixture, Bogus and FsCheck packages over a dependency-free samplerno (Vogen#153, AutoFixture specimen builders, closed as not planned)nono
Prefixed public identifiers (acc_…)yesnonono
Beyond single valuesnononocomplex value objects, smart enums, discriminated unions
LicenceMITApache-2.0MITBSD-3-Clause

Where this library differs​

A rule is declared once and reaches every boundary. MaxLength = 34 validates, sizes the EF Core column and becomes the OpenAPI maxLength; the bound of IValueObjectMinimum<T>, and the [GeneratedRegex] behind IValueObjectPatternValidator, do the same for the schema; known values become the enum, under the names a generated client gives its members; and, wherever the server reads them as .NET does, they reach a MongoDB server as a $jsonSchema validator (what is carried), and, on request, a relational database as CHECK constraints (what is written). In the other three, a length or a pattern is code inside a validation method, so the column and the schema have to be told separately — which is exactly the drift that primitive obsession produces.

A rejection is data a client can act on. Validation returns a ValidationResult struct holding a stable code and a message, and allocates nothing when the value is valid. The code travels to the problem details of MVC controllers, for a JSON body, read by System.Text.Json or Newtonsoft.Json, and a JSON Patch document's values, as for a query value, and of minimal APIs, native AOT included, for a route, query or header value, and for a body where ThrowOnBadRequest is on, to FluentValidation failures and to every exception an integration throws, so an API client branches on value_object.too_long rather than on English. The same code keys the message an application shows in its users' language, which MVC (for a route, query, header or form value, and a body System.Text.Json reads), minimal APIs, Azure Functions, HotChocolate, Blazor, FluentValidation and the DataAnnotations attributes look up in its translations with the length, the bounds or the known values the type declares (a message in the application's language).

Rules are found by interface, not by name. IValueObjectValidator<T> and IValueObjectNormalizer<T> let the compiler check each signature, and VO0011 reports the one mistake left: a rule written without its interface, which would otherwise never run.

Source-generated JSON is declared, not remembered. Naming ValueObjectJsonConverterFactory in [JsonSourceGenerationOptions] works at compile time, on the context itself. With Vogen and StronglyTypedId the converters have to be added to the options at run time instead, and forgetting them can fail quietly: in a test against Vogen 8.0.7, the value object was written as {} and read back uninitialized.

The integrations go further than serialization. Problem details, FluentValidation rules that defer to the type, an OpenAPI schema carrying the constraints, a contract kit that tests your own types, and Stripe-style public identifiers are part of the library rather than left to you. So is structured logging: with AdCodicem.ValueObjects.Serilog, Serilog logs a value object as the value it carries, with @ and, on request, without it, where Thinktecture's destructuring policy, Destructure.UsingThinktectureRuntimeExtensions(), covers {@Value} alone and leaves {Value} to ToString(), a number then reaching the sink as a string. And a model calling a tool through Microsoft.Extensions.AI reads the rules in the tool's schema, and the rule its argument broke, with its code, rather than "Error: Function failed." (language models), as does one calling the tool of a Model Context Protocol server, rather than "An error occurred invoking 'place_order'.".

Where the others are stronger​

They run in more places. This library requires .NET 10 or later. Vogen targets netstandard2.0, StronglyTypedId runs on older frameworks including .NET Framework, and Thinktecture supports .NET 8. For an application that cannot move to .NET 10, the choice is made.

They accept more shapes. Vogen wraps any type but a collection, as a class, a struct or a record; Thinktecture accepts any key, generic ones included. This library is deliberately limited to 22 underlying types and to readonly struct: Design decisions explains why, but if you need a class or a Uri, it is not for you.

They cover more stores and serializers. Vogen generates support for LinqToDB, ServiceStack.Text, Orleans, MessagePack and MongoDB's BSON; Thinktecture for MessagePack. Both this library and Vogen store a value object in BSON, apart: in a test against Vogen 8.0.7, its value object was still written {} until the application called the generated BsonSerializationRegisterFor<…>.TryRegister(), its value object over Guid threw until a Guid representation was configured, and LINQ could not translate .Value, where this library's says at start-up what is missing, translates .Value on the field itself, reads through the type's rules, and carries those rules to the server as a $jsonSchema collection validator, which none of the others builds. Both this library and Vogen implement XML serialization on request, apart: Vogen's generated ReadXml assigns the value straight from the reader, with neither validation nor normalization, and its option makes the struct's fields writable, where this library's reads through the type's rules, keeps the fields readonly and publishes the rules as XSD facets. All three write a value object to MessagePack, this library through a resolver placed in front of the options' own rather than a [MessagePackFormatter] on the type, which the same options also put in front of SignalR's MessagePack hub protocol, and which reads through the type's rules unless asked to trust the bytes (MessagePack and SignalR); Vogen's and Thinktecture's MessagePack support was not measured here. None of the others writes a value object to protobuf: Vogen's conversions hold no protobuf member, and Thinktecture's ProtoBuf package stopped at a beta, 8.7.0-beta02, with no project left in its sources, where this library's writes each as protobuf-net writes its underlying type, reads it through its rules, and answers a refused request of a code-first gRPC service with its code.

Thinktecture goes beyond single values. Complex value objects with several members, smart enums and discriminated unions are out of scope here.

Vogen validates what it reads from the database by default. This library skips validation on the EF Core read path unless asked, a performance choice that assumes the application owns its tables. For a database several systems write to, Vogen's default is the safer one; here, strict: true gives the same behaviour.

They are proven. Vogen and StronglyTypedId have millions of downloads and years of production use behind them. This library was first published in September 2026 and is still at 0.x.

Choosing​

  • On .NET 10 or later, with an API and a database, where a rule should be written once and reach the schema and the column: this library is built for that.
  • On an older framework, or with a store this library does not cover: Vogen.
  • Only strongly-typed identifiers, and nothing to validate: StronglyTypedId does that with the least ceremony.
  • Smart enums, discriminated unions or multi-member value objects alongside single values: Thinktecture.

Moving from one of them? Migrate from another library maps each one's surface onto this one.