DcsvIo.D2.Contacts.EntityFrameworkCore
0.1.1
dotnet add package DcsvIo.D2.Contacts.EntityFrameworkCore --version 0.1.1
NuGet\Install-Package DcsvIo.D2.Contacts.EntityFrameworkCore -Version 0.1.1
<PackageReference Include="DcsvIo.D2.Contacts.EntityFrameworkCore" Version="0.1.1" />
<PackageVersion Include="DcsvIo.D2.Contacts.EntityFrameworkCore" Version="0.1.1" />
<PackageReference Include="DcsvIo.D2.Contacts.EntityFrameworkCore" />
paket add DcsvIo.D2.Contacts.EntityFrameworkCore --version 0.1.1
#r "nuget: DcsvIo.D2.Contacts.EntityFrameworkCore, 0.1.1"
#:package DcsvIo.D2.Contacts.EntityFrameworkCore@0.1.1
#addin nuget:?package=DcsvIo.D2.Contacts.EntityFrameworkCore&version=0.1.1
#tool nuget:?package=DcsvIo.D2.Contacts.EntityFrameworkCore&version=0.1.1
DcsvIo.D2.Contacts.EntityFrameworkCore
Audience: backend .NET service engineers mapping
DcsvIo.D2.Contactsvalue objects into EF Core entity models via infraIEntityTypeConfiguration<T>.
Per-VO complex-type and value-converter mapping helpers for the DcsvIo.D2.Contacts value objects.
The helpers are called from the host's IEntityTypeConfiguration<T> implementation — the domain
aggregate holds plain VO-typed properties and carries zero EF references.
Each helper, in one call:
- Wires member value converters where needed (
Uri) - Applies
HasMaxLengthfromFieldConstraints.*caps - Writes the per-field anonymize defaults via the fluent
.Anonymize*API fromDcsvIo.D2.DataGovernance.EntityFrameworkCore
Ships no DbContext, no migrations, and no DI engine. The host owns all of those.
Install
dotnet add package DcsvIo.D2.Contacts.EntityFrameworkCore
Domain purity
Host aggregates hold VO-typed properties as plain CLR properties:
// Host domain aggregate — ZERO EF references.
public sealed class Person
{
public required Personal Name { get; init; }
public EmailAddress? Email { get; init; }
}
All EF mapping (converters, lengths, indexes, anonymize annotations) lives in the host's infra
IEntityTypeConfiguration<T> class, which calls the toolkit helpers:
// Host infra layer — PersonConfiguration.cs (NOT in the toolkit; illustrative).
internal sealed class PersonConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> b)
{
// Multi-field VO → complex type
b.ComplexProperty(p => p.Name, cp => cp.MapPersonal());
// Single-value VO → value converter, one column, caller-supplied anonymize
b.MapEmailAddress(p => p.Email)
.Unique("deletedUser{UserId}@deleted.user.dcsv.io");
}
}
Two CLR-shape concessions (not EF dependencies):
- Each mapped VO must be EF-materializable. Single-value VOs are handled by the
FromTrustedconverter; multi-field VOs are materialized viarequired initproperties (EF 10 usesGetUninitializedObject+ sets therequired initprops — no parameterless ctor needed). - Every mapped member must be EF-settable: the VOs use
init(set at materialization). ✓
Multi-field VO helpers (complex types)
Called from inside a b.ComplexProperty(…, cp => …) callback. No selector arg — the host
already opened the ComplexPropertyBuilder<TComplex>; the helper decorates cp's members.
| Helper | VO | Anonymize defaults |
|---|---|---|
cp.MapPersonal() |
Personal (FirstName/Middle/Last/Preferred/HashId) |
FirstName → "Deleted" (constant); Middle/Last/Preferred → SetNull; HashId → cleared sentinel |
cp.MapNameAffixes() |
NameAffixes (Prefix/PrefixCustom/Suffix/SuffixCustom) |
All four → SetNull |
cp.MapDemographics() |
Demographics (DateOfBirth/BiologicalSex) |
Both → SetNull |
cp.MapProfessional() |
Professional (CompanyName/JobTitle/Department/CompanyWebsite) |
CompanyName → "Deleted" (constant); Job/Dept/Website → SetNull |
Value converters encapsulated inside the helpers (the host never hand-wires them):
Professional.CompanyWebsite→Uri ↔ AbsoluteUri string+HasMaxLength(COMPANY_WEBSITE_MAX)
NameAffixes / Demographics all-nullable constraint. These VOs have no required scalar
property. EF Core requires a complex type to be REQUIRED (non-nullable) unless it has at least
one required property, so the host entity must declare its NameAffixes/Demographics member
as non-nullable or EF throws at model build.
Same-VO-type-twice (e.g. legal name + maiden name Personal) works natively: the host calls
MapPersonal() twice via two distinct host-property selectors. EF Core 10 prefixes columns
by the owning-property path automatically (LegalName_FirstName vs MaidenName_FirstName).
The helpers never call HasColumnName, which preserves this default uniquification.
Single-value VO helpers (value converters)
Called on the entity builder directly. Returns a coupling object that requires the caller to supply the anonymize policy — no toolkit default is written automatically.
b.MapEmailAddress(p => p.Email)
.Anonymize("deleted@deleted.user.dcsv.io"); // non-unique constant
b.MapEmailAddress(p => p.Email)
.Anonymize("deletedUser{UserId}@deleted.user.dcsv.io"); // non-unique template
b.MapEmailAddress(p => p.Email)
.Unique("deletedUser{UserId}@deleted.user.dcsv.io"); // unique index + template
MapEmailAddress / MapPhoneNumber each:
- Apply
HasConversion(EmailAddress/PhoneNumber ↔ string via FromTrusted) - Apply
HasMaxLength(EMAIL_MAX / PHONE_E164_MAX) - Return a coupling object (
EmailMapping/PhoneMapping)
The coupling object exposes:
.Anonymize(templateOrConstant)— writes the anonymize annotation; no index.Unique(uniqueTemplate)— the ONLY path to a unique index; requires a template that contains at least one{Token}so erased rows produce distinct values and never collide. ThrowsArgumentExceptionat map time if the template has no token.
"Unique-without-a-uniqueness-template" is unrepresentable. There is no parameterless
.Unique() — the type system removes the footgun.
Root-scoped anonymize templates. A value-converted email/phone column lives on the root
entity, so a {UserId} template resolves against the root entity's scalar siblings — the
DcsvIo.D2.DataGovernance V4 guard is satisfied by construction.
Example anonymize values (README examples, not toolkit defaults — the caller supplies the value):
- Non-unique email:
"deleted@deleted.user.dcsv.io" - Unique email:
"deletedUser{UserId}@deleted.user.dcsv.io"(requires a rootUserIdscalar) - Phone:
"10000000000"
EF Core 10 complex-member-index limitation
For the EF Core 10 limitation on indexing ComplexProperty member columns and the
CreateD2Index workaround, see
DcsvIo.D2.EntityFrameworkCore.
Per-VO anonymize-default table
| VO | Field | Default | Note |
|---|---|---|---|
| Personal | FirstName | "Deleted" |
Non-nullable — constant required |
| Personal | MiddleName/LastName/PreferredName | SetNull | Nullable |
| Personal | HashId | cleared sentinel ("v1." + 64×'0') |
|
| NameAffixes | Prefix/PrefixCustom/Suffix/SuffixCustom | SetNull | All nullable |
| Demographics | DateOfBirth/BiologicalSex | SetNull | All nullable |
| Professional | CompanyName | "Deleted" |
Non-nullable — constant required |
| Professional | JobTitle/Department/CompanyWebsite | SetNull | Nullable |
| EmailAddress | Value | CALLER-SUPPLIED | .Anonymize(…) or .Unique(template) |
| PhoneNumber | Value | CALLER-SUPPLIED | .Anonymize(…) or .Unique(template) |
Tombstone values are non-i18n literals — deliberately stable across locales.
Host responsibilities
- Register the anonymization engine:
services.AddD2DataGovernance(…). - Apply anonymization conventions: call
ApplyAnonymizationConventions()onModelConfigurationBuilderinConfigureConventions. - Implement
IUserOwned+IAnonymizationTrackableon entities that carry anonymizable contacts. - Supply a root scalar
UserIdproperty when using a{UserId}template on email/phone — theDcsvIo.D2.DataGovernanceV4 guard resolves template tokens against root-entity scalar siblings.
Telemetry
No telemetry surface — mapping helpers are pure model-build-time calls with no runtime span or metric emission.
Edge cases / gotchas
NameAffixes/Demographicsall-nullable constraint — EF Core requires a complex type to be REQUIRED (non-nullable) unless it has at least one required property. Declare the host entity'sNameAffixes/Demographicsmember as non-nullable, or EF throws at model build.- Same-VO-type-twice (e.g. two
Personalproperties for legal name + maiden name) works natively. EF Core 10 prefixes columns by the owning-property path automatically (LegalName_FirstNamevsMaidenName_FirstName). The helpers never callHasColumnName, which preserves this default uniquification. - Unique email/phone requires a template token —
.Unique(template)is the only path to a unique index on a single-value VO column. Passing a template without a{Token}throwsArgumentExceptionat map time.
Configuration
No configuration — the helpers carry no tunable behavior. All caps come from the shared FieldConstraints codegen catalog.
Dependencies
DcsvIo.D2.Contacts(contacts/core/) — the 6 VO types being mappedDcsvIo.D2.DataGovernance.EntityFrameworkCore— annotation key +AnonymizationRulefactoryMicrosoft.EntityFrameworkCore.Relational—EntityTypeBuilder<T>,ComplexPropertyBuilder<T>,PropertyBuilder<T>,HasMaxLength,HasConversion
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- DcsvIo.D2.Contacts (>= 0.1.1)
- DcsvIo.D2.DataGovernance.EntityFrameworkCore (>= 0.1.1)
- dotenv.net (>= 4.0.2)
- JetBrains.Annotations (>= 2025.2.4)
- Microsoft.EntityFrameworkCore (>= 10.0.7)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.7)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Caching.Memory (>= 10.0.7)
- Microsoft.Extensions.Configuration (>= 10.0.7)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Logging (>= 10.0.7)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.7)
- Microsoft.Extensions.Options (>= 10.0.7)
- Microsoft.IdentityModel.Tokens (>= 8.16.0)
- NodaTime (>= 3.2.2)
- Npgsql (>= 10.0.2)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.1)
- Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime (>= 10.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.