Skip to main content
Version: 0.3.x (0.3.0)

Class ValueObjectConventionExtensions

Namespace: AdCodicem.ValueObjects.EntityFrameworkCore
Assembly: AdCodicem.ValueObjects.EntityFrameworkCore.dll

Maps value objects onto their underlying database types.

public static class ValueObjectConventionExtensions

Inheritance​

object ← ValueObjectConventionExtensions

Inherited Members​

object.Equals(object?), object.Equals(object?, object?), object.GetHashCode(), object.GetType(), object.MemberwiseClone(), object.ReferenceEquals(object?, object?), object.ToString()

Methods​

AddValueObjectCheckConstraints(ModelConfigurationBuilder, Action<ValueObjectCheckOptions>?)​

Writes the rules value objects declare as CHECK constraints on the columns this package maps them to, so that the database refuses a row another writer stores with a value the type rejects.

public static ModelConfigurationBuilder AddValueObjectCheckConstraints(this ModelConfigurationBuilder builder, Action<ValueObjectCheckOptions>? configure = null)

Parameters​

builder ModelConfigurationBuilder

Model configuration builder, from DbContext.ConfigureConventions.

configure Action<ValueObjectCheckOptions>?

Chooses the rules and the dialect of patterns; when it is not given, lengths, bounds and closed value sets are written, and no pattern.

Returns​

ModelConfigurationBuilder

The same builder, so calls can be chained.

Examples​

protected override void ConfigureConventions(ModelConfigurationBuilder builder)
=> builder
.ConfigureValueObjects(typeof(Iban).Assembly)
.AddValueObjectCheckConstraints(checks => checks.Patterns = ValueObjectPatternDialect.PostgreSql);

Remarks​

Off unless called. Each column one of this package's converters maps, through ConfigureValueObjects, ConfigureEntityIds or one property at a time, gets a constraint per rule its value object declares: MinLength, the bounds of and , and the known values of a closed value set. A migration picks them up, so a rule changed on the type reaches the next migration, as a column size does; a table already holding a row the rule refuses fails that migration.

A constraint is laxer than the type where the database cannot say the rule as .NET reads it, never stricter: a length is counted in bytes where the database cannot count UTF-16 code units, and a bound or a closed set is left out where the column does not compare values as .NET does. A rule only code checks, in , stays in the application. SQL Server, PostgreSQL and SQLite are known; another provider gets no constraint. A column mapped to JSON, the elements of a primitive collection, and a property mapped by a converter of the application's own get none either.

A pattern is written only when names the dialect of the database, which the model cannot tell: REGEXP_LIKE on SQL Server 2025, ~ on PostgreSQL, none on SQLite. Naming one fails the model, with an , on a pattern the dialect would read apart from .NET, unless leaves that value object's pattern out.

The application's own writes pass, since the value objects it writes went through their rules. A constraint refuses what another writer stores, and an invalid instance a lax read let in and the application writes back, with the provider's error inside a for a write through Entity Framework Core: it complements strict: true, which refuses on read a row stored before the constraint existed. Each constraint's name ends with its rule, which , on the options of the context, adds to that error as the code Validate reports for the rule, and reads without it.

The options are read once per context type, as strict is: Entity Framework Core builds the model the first time a context type is used and caches it for every later instance. A second call replaces the first, whose options then apply nowhere, so that a context deriving from another can call it again with options of its own. The constraints are part of the model a migration, EnsureCreated or a script is made from, never of a compiled model, which reads none: nothing here runs per query or per row.

Exceptions​

ArgumentNullException

builder is null.

ArgumentOutOfRangeException

configure names a dialect of patterns does not define.

ConfigureValueObjects(ModelConfigurationBuilder, params Assembly[])​

Maps every value object declared in the given assemblies to its underlying column type.

public static ModelConfigurationBuilder ConfigureValueObjects(this ModelConfigurationBuilder builder, params Assembly[] assemblies)

Parameters​

builder ModelConfigurationBuilder

Model configuration builder, from DbContext.ConfigureConventions.

assemblies Assembly[]

Assemblies declaring the value objects. When none is given, everything already registered is mapped, which is enough as soon as the entity assembly has been loaded.

Returns​

ModelConfigurationBuilder

The same builder, so calls can be chained.

Remarks​

A value object declaring a maximum length also sizes its column, so an IBAN lands in nvarchar(34) on SQL Server, or in varchar(34) once it declares Unicode = false, rather than an unbounded column — the rule is stated once, on the type, and the schema follows.

A value object declaring Unicode = false is mapped to non-Unicode text, its optional form, each construction of a generic one and each element of a collection included, so that SQL Server stores it in varchar and declares a parameter compared with it varchar too. PostgreSQL and SQLite store text as they always do. Properties<TSelf>().AreUnicode(true) after this call, or IsUnicode(true) on a property, keeps Unicode text.

A value object over or is left alone: Entity Framework Core maps neither type, on any provider, so the column type, and the conversion to it, are the application's to choose with a converter of its own.

A generic value object, Code<T> or Outer<T>.Code, is mapped too, whatever its constructions: each property of one gets the converter, the comparer and the length closed over its own.

A property holding a collection of value objects, List<Iban>, Quantity[] or IReadOnlyList<Iban?>, in an entity type, an owned type or a complex type, is mapped as a primitive collection: each element gets the converter, the comparer and the length of its value object, so that an IBAN lands in a character varying(34)[] on PostgreSQL, and in a JSON array elsewhere, which SQL Server reads as nvarchar(34), or varchar(34) for a value object declaring Unicode = false. Only a property Entity Framework Core would map itself, one with a public getter and a setter, is made one, unless the application converts the whole collection itself; a primitive collection the application declares, over a read-only property, gets its elements configured too, unless the application set their converter.

Writing refuses a value the value object rejects, which only an instance equal to default(TSelf) can hold, rather than store it for a read to trust: a property of the value object's type throws a , and a property of its nullable type, TSelf?, stores a NULL. An element of a collection does the same, but for an element of TSelf? over a value type, which throws too: Entity Framework Core cannot write the null there.

A primary key of one property over a value object, which is no foreign key and which nothing generates, refuses an entity added without it, with an carrying , whatever the value object's rules, rather than store it with , a value a value object without a rule against it accepts. Have the key generated with , or written as it stands with ValueGeneratedNever(); the refusal changes neither the column nor the migration snapshot (see ).

The columns can also carry the rules as CHECK constraints, on request: see .

A compiled model, which dotnet ef dbcontext optimize generates, holds the same mapping. A property of TSelf? is compared by , which such a model can write.

What it configures is decided once, while the model is built. Nothing here happens per query or per row, but for the generator of a key, which Entity Framework Core creates once and asks for a value when an entity is added.

Each converter and comparer is closed over its value object through the type arguments the descriptor hands back to a visitor (), never with over a read off the descriptor. The converter of a TSelf? property, or of such an element over text, which C# cannot name without a constraint the visitor does not carry, is closed with it over those type arguments. Entity Framework Core builds no model under native AOT, where it reads a compiled model instead, so none of this runs there.

ConfigureValueObjects(ModelConfigurationBuilder, bool, params Assembly[])​

Maps every value object declared in the given assemblies, optionally re-validating on read.

public static ModelConfigurationBuilder ConfigureValueObjects(this ModelConfigurationBuilder builder, bool strict, params Assembly[] assemblies)

Parameters​

builder ModelConfigurationBuilder

Model configuration builder, from DbContext.ConfigureConventions.

strict bool

When true, values read from the database are normalized and validated again, the elements of a collection included. Use it for tables another system also writes to; it costs a validation per materialized value.

assemblies Assembly[]

Assemblies declaring the value objects.

Returns​

ModelConfigurationBuilder

The same builder, so calls can be chained.

Remarks​

strict is read once per context type, not per instance: Entity Framework Core builds the model the first time a context type is used and caches it for every later instance. A context that chooses the value from a constructor argument gets whichever model was built first. Give strict reads a context type of their own, or an that puts the choice in the cache key.

HasValueObjectConversion<TSelf, TValue>(PropertyBuilder<TSelf>, bool)​

Maps a single property to the underlying value of its value object.

public static PropertyBuilder<TSelf> HasValueObjectConversion<TSelf, TValue>(this PropertyBuilder<TSelf> builder, bool strict = false) where TSelf : struct, IValueObject<TSelf, TValue>

Parameters​

builder PropertyBuilder<TSelf>

Property builder.

strict bool

When true, values read from the database are validated again.

Returns​

PropertyBuilder<TSelf>

The same builder, so calls can be chained.

Type Parameters​

TSelf

Value object type.

TValue

Underlying value type.

Remarks​

Use this for a property that needs to depart from the convention; otherwise prefer the convention. It cannot map a value object over or , which Entity Framework Core maps to no column: such a property needs a converter of the application's own. Writing refuses a value the value object rejects with a , as the convention does. A primary key over a value object is refused unset only under ConfigureValueObjects, whose convention does it; HasValueObjectKeyGeneration() generates a key over a or an integer either way.

HasValueObjectKeyGeneration<TSelf>(PropertyBuilder<TSelf>)​

Generates a key over a when an entity is added, with GUIDs the database keeps in order: SQL Server's sequential ones on SQL Server, as Entity Framework Core generates for a key there, and version 7 ones on every other provider, as Npgsql generates for PostgreSQL.

public static PropertyBuilder<TSelf> HasValueObjectKeyGeneration<TSelf>(this PropertyBuilder<TSelf> builder) where TSelf : struct, IValueObject<TSelf, Guid>

Parameters​

builder PropertyBuilder<TSelf>

Property builder of the key.

Returns​

PropertyBuilder<TSelf>

The same builder, so calls can be chained.

Type Parameters​

TSelf

Value object type, over .

Remarks​

The key becomes generated on add, as a key is by convention, and gets the generator of , which a compiled model can write. ValueGeneratedOnAdd() alone would not do: Entity Framework Core would then wrap its random generator in a conversion on every provider, losing the order SQL Server's clustered index and PostgreSQL's version 7 keep. On SQLite and the other providers, where Entity Framework Core generates a random GUID for a key, the key is a version 7 one all the same.

A key the domain set is kept: Entity Framework Core asks a generator only for a key still at its default, default(TSelf), which a value object holding equals, so that one is generated over. The GUID goes through the value object's Create, so a rule it breaks fails the add with a . Attach or Update of an entity whose key is unset adds it, as for a key. The key is generated in the application: a default the column declares is not used.

It works with or without ConfigureValueObjects, beside . A key over a , an or a is generated by the database, through the method of the same name of , and , which a call picks by the key's type; a value object over any other type is a compile error.