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
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
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
A pattern is written only when REGEXP_LIKE on SQL Server 2025, ~ on PostgreSQL, none on SQLite. Naming
one fails the model, with an
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 strict: true, which refuses on read a row stored before the constraint existed. Each constraint's name ends
with its rule, which
Validate reports for the rule, and
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
builder is null.
configure names a dialect of patterns
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
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
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
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 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
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 (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
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
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 ConfigureValueObjects, whose convention does
it; HasValueObjectKeyGeneration() generates a key over a
HasValueObjectKeyGeneration<TSelf>(PropertyBuilder<TSelf>)
Generates a key over a
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 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
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 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
It works with or without ConfigureValueObjects, beside