Skip to content

About

The Well of Urð at Yggdrasil's roots, where the Norns of legend draw water to sustain the tree and carve fate into its trunk as runes. Norse.EntityFramework.* — entity base types, DbContext foundations, conventions, value converters, and the migrations chassis. The record of all that has become, governed by the laws of Asgard.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Urðarbrunnr

The Well of Urð, beneath Yggdrasil's roots, where the Norns draw the water that keeps the tree alive — the record of all that has become.

Urðarbrunnr — the Well of Urð beneath Yggdrasil's roots, where the Norns weave fate and keep the record of all that has become

Image credit: @norsemythologyclips — go follow them.

The well's record for the Norse Architecture — Norse.Persistence: the platform's persistence realm, covering any database or data-store mechanism, not just EF Core. Norse.Persistence.EntityFramework is today's live vendor family — entity base types, DbContext foundations, conventions, value converters, and the migrations chassis, the EF Core foundation that Midgard's concrete repository implementations ride on, governed by the contracts Asgard declares. Other ORMs, native drivers, or document/search stores land here as their own vendor-specific sibling under Norse.Persistence.*, the same drop-in pattern Heimdall used for FluentUI. In the dependency chain it sits below Asgard and Svartálfheim, and above Midgard.

The dependency graph

Arrows point at the thing depended on. Dashed arrows are packaging, not compilation — each gen/ generator ships inside the src/ package it serves, which is why a consumer writes one NorseRef with Generator="true" and receives both the library and its generator.

flowchart BT
	subgraph Urdarbrunnr["Urðarbrunnr — Norse.Persistence"]
		Design["EntityFramework.Design"]
		PostgreSQL["EntityFramework.PostgreSQL"]
		SqlServer["EntityFramework.SqlServer"]
		Migrations["EntityFramework.Migrations"]
		EF["EntityFramework"]
		Generator["EntityFramework.Generator"]
		MigGenerator["EntityFramework.Migrations.Generator"]
	end
	subgraph Asgard
		Emit["Abstractions.Emit"]
		AMigrations["Abstractions.Migrations"]
	end
	subgraph Svartalfheim["Svartálfheim"]
		Primitives["Norse.Primitives"]
	end
	Design --> EF
	Migrations --> EF
	PostgreSQL --> EF
	SqlServer --> EF
	Migrations --> AMigrations
	EF --> Primitives
	Generator --> Emit
	MigGenerator --> Emit
	EF -.-> Generator
	Migrations -.-> MigGenerator
	%% layout hints only, not dependencies — lift Svartálfheim onto the top tier beside Asgard
	Generator ~~~ Primitives
	MigGenerator ~~~ Primitives
Loading

The base EntityFramework is the only node touching Svartálfheim — SequentialGuid's byte-order converters and the IPiiScalar family reach every provider through it, transitive-first by house law. Each provider binding is one thin package with exactly one edge; Oracle or SQLite lands as one more node with that same single edge, zero edits to the foundation — the symmetry is the acceptance test, and the chart renders it.

Status

Live: five assemblies and two gen/ source generators built around a provider seam. Norse.Persistence.EntityFramework is the neutral foundation — INorseDbContext, the abstract NorseDbContext base, snake_case naming machinery, the temporal opt-in surface (ITemporalEntity and its finalizing convention, below), entity self-configuration (INorseEntity<TSelf>, a static abstract Configure the compiler refuses to let a concrete entity omit, enforced colocated by RequireEntityConfigurationConvention and applied by the generated ApplyNorseConfigurations() — the emitting Norse.Persistence.EntityFramework.Generator ships inside this package), column-level PII protection (ProtectedPiiValueConverter<TPii> behind ProtectPiiScalars(): canonical wire string ∘ protect on write, unprotect ∘ parse on read — one composed converter, so no converter-ordering problem can exist), and the seam itself: INorseEfProvider/INorseEfMigrationProvider, NorseNameRewriters, AddNorseContext<TContext>() with its IDbContextFactory-shaped sibling AddNorseContextFactory<TContext>(), and ApplyNorseProviderOptions(). Norse.Persistence.EntityFramework.Migrations carries EfMigrationContributor<TContext>, MigrationConnectionStringAttribute, AddNorseMigrationContext<TContext>(), and — consolidated from what used to be a per-provider generator pair — the provider-agnostic migrations generator (Norse.Persistence.EntityFramework.Migrations.Generator, shipped inside this package): it discovers every migration contributor, every seed contributor, and the single provider binding visible in a migrations service's compilation, and emits AddNorseMigrations(), reporting NORSE030–034 for anything ambiguous (no binding, more than one binding, no ModelSnapshot, more than one ModelSnapshot, a binding missing its required Instance member). Norse.Persistence.EntityFramework.Design ships DdlEmittingMigrationsScaffolder, AddNorseDesignTimeServices(), and the one neutral NorseDesignTimeDbContextFactory<TContext> — abstract, never connects to a database, no environment-variable escape hatch. Norse.Persistence.EntityFramework.PostgreSQL and .SqlServer are one sealed provider binding each (NorsePostgresEfProvider/NorseSqlServerEfProvider, each public static Instance) — the spec's acceptance test: Oracle or SQLite arrives as one more thin binding package, zero edits to the neutral foundation. Full design: Glitnir's docs/Urdarbrunnr/specs/2026-07-27-ef-provider-seam-repackaging-design.md.

This repackaging retired the earlier *.Design.PostgreSQL/*.Design.SqlServer package pair and their two separate generators (2026-07-22 through 2026-07-26 lineage below) — the generator that first proved this realm's Roslyn source-generation capability now lives consolidated in .Migrations, provider-agnostic from the start rather than duplicated per provider. Three conventions remain provider-aware exactly as before: [FixedLength(n)] only becomes SQL Server's IsFixedLength() (Postgres's own docs say fixed-length storage there is a pure downside, never a benefit), name rewriting is now expressed as each provider binding's NameRewriter delegate — Postgres's engine-native lower snake_case, SQL Server's raw PascalCase (NameRewriter is null, its temporal-rename hook inert by construction) — rather than an opt-in/opt-out parameter, and NorseModelConventions.Apply() still registers the provider-correct SequentialGuid byte-order converter, SqlServerSequentialGuidValueConverter on SQL Server and Rfc9562SequentialGuidValueConverter everywhere else. Snake_case naming remains in-house via NorseSnakeCaseNamingConvention and SnakeCaseNameRewriter, no external EFCore.NamingConventions dependency. Full history: the 2026-07-03 provider-aware conventions spec, the 2026-07-22 in-house naming-convention spec, the 2026-07-22 DDL-emission/chassis-rename spec, and the 2026-07-26 SequentialGuid converter spec.

Live: temporal (system-versioned) tables, a chassis capability rather than hand-rolled SQL. Adoption is two acts: add : ITemporalEntity to an entity — new or long-existing — and run dotnet ef migrations add. The scaffolded migration carries the whole history apparatus, and every later column change carries its mirror; no adopting realm writes SQL or remembers a call. The marker is an empty interface on purpose: the system period never touches the CLR type, a wire contract, or a WASM payload. A finalizing convention validates each marked entity at startup — primary key required, not owned or JSON-mapped, and the derived history/timeline names reserved so a collision fails at model build instead of surfacing late in a migration.

The two providers realize that marker in opposite places, and that is the point of the seam. SQL Server hands it straight to the engine as EF-native system-versioning, with period columns SystemPeriodStart/SystemPeriodEnd and a {Table}History history table; a table-split entity is a loud model-finalize error there unless it declares TemporalOnPrincipalTableOnly(), because EF cannot scope temporality per fragment until dotnet/efcore#26457 ships — with the opt-in, the principal table alone gets engine-native system-versioning with HIDDEN period columns EF never sees, the fragments get nothing, and the one cost is temporal LINQ for that entity on SQL Server. PostgreSQL has no native equivalent, so the apparatus is generated into migration SQL: a system_period tstzrange column, a history table under a WITHOUT OVERLAPS temporal primary key, a hardened SECURITY DEFINER versioning function with UPDATE/DELETE triggers, and a timeline view — with the PostgreSQL 19 floor and the btree_gist prerequisite asserted inside the migration itself rather than documented and hoped for. The clock is clock_timestamp() with a monotonicity clamp, never now(), so every recorded version has strictly positive length and the timeline stays gapless and overlap-free by arithmetic; an UPDATE that changes nothing records nothing. Enabling temporality on a table that already holds rows, disabling it, dropping the entity, and every column add/drop/rename/type change all evolve the apparatus automatically; the handful of changes that cannot be evolved without leaving history constraints quietly wrong — primary-key changes, schema moves, a marker transition bundled with column changes — fail by name with the sanctioned path in the message. Proven with DDL snapshot suites over EF's real model differ plus live application and runtime-semantics suites against a real postgres:19rc1 server. Full design: Glitnir's docs/Platform/specs/2026-08-04-temporal-tables-persistence-chassis-design.md.

Generated columns on PostgreSQL temporal tables (2026-10-25, chassis spec §3.2 and §3.4 as amended by identity PoC G6). A stored generated column (HasComputedColumnSql(…, stored: true)) is left out of the versioning function's no-op comparison, because PostgreSQL computes generated values after BEFORE triggers: NEW.<generated> is NULL there, and comparing it would turn every update into a change. The history copy still carries it, taken from OLD, where the value is materialized. History holds it as a plain column, and nothing ever writes the main table's generated column, which the engine refuses (428C9). Every path that regenerates the function applies the same rule: create, enable, and column evolution. Npgsql turns a column generated by dropping and re-adding the main column. The chassis never rebuilds the history column to match, so its recorded values survive. The chassis also owns norse_utf8_sha256(text), an IMMUTABLE SHA-256 over a value's UTF-8 bytes. A generation expression can't call sha256(convert_to(…)) inline, because convert_to is STABLE. The function is emitted as an idempotent CREATE OR REPLACE FUNCTION, once per migration, ahead of the first table or column whose generation expression names it, and never otherwise. So regenerating a migration can't lose it.

The SQL Server opt-in rides the same three-seam shape PostgreSQL uses. An annotation provider projects it onto the principal table alone, a migrations annotation provider carries it onto the drop, and a migrations SQL generator wraps SQL Server's stock DDL with engine-native versioning. Creating the table versions it inline, and dropping it turns versioning off first. Principal column adds, drops and widenings run with versioning left on, because the engine carries them to history itself. Fragment columns are plain DDL. Enabling on a populated table opens every existing row at one enable timestamp through named period defaults, and disabling drops those defaults along with the periods and the history. The few changes that cannot keep history honest are refused by name, each naming its manual sequence: a primary-key change, a rename, a schema move, a computed or identity column on the principal, a nullable principal column made required (the constraint would reach history, and history is the audit record, so the chassis never backfills it), or splitting and unsplitting in one migration. Full design: Glitnir's docs/Himinbjorg/specs/2026-10-24-identity-explicit-writes-temporal-split-design.md §6.

That lifecycle is proven against a live SQL Server 2025 in this realm's first SQL Server container fixture (Persistence.EntityFramework.SqlServer.IntegrationTests). Each lifecycle row is applied through a generated migration with a recorded version standing in history before and after it. The fixture skips only on Windows on ARM, where the image cannot run under emulation; it runs on Apple silicon and on every x64 CI runner. Every timing assertion in both providers' live suites reads the database's own clock, never the test host's.

Fragment check constraints (2026-10-25, identity split spec §3.1). EF Core cannot put a CHECK on an entity-splitting fragment: SplitTableBuilder<T> has no HasCheckConstraint, and EF resolves an entity-level check constraint to the principal table alone, so its CREATE TABLE names columns that live in the fragment and the DDL fails to apply. SplitToTable("…", t => t.HasFragmentCheckConstraint("ck_…", "<sql>")) declares one on the fragment itself. The SQL is provider-specific, as HasComputedColumnSql is, and the name is used verbatim. The constraint is stamped on the fragment's own mapping, which EF's snapshot generator already writes inside the fragment's SplitToTable, so it survives a regenerated InitialCreate and the next migration diffs it cleanly. Each provider's annotation provider projects it onto the fragment's table alone, and both generators turn it into the provider's own check-constraint DDL: ADD CONSTRAINT after the table is created or when the constraint is added, DROP CONSTRAINT when it is removed, and a drop then an add when its SQL changes. A fragment is never versioned, so the DDL is plain on both providers, including the fragment of a TemporalOnPrincipalTableOnly() entity. Proven through EF's real differ and generator on both providers, through a compiled and processed model snapshot, and against both live containers: the catalog shows the constraint on the fragment table, a violating direct write is refused, and a conforming one lands. The snake_case convention now carries a renamed fragment's annotations across with it, which this feature found missing.

The provider-seam adoption sweep across Himinbjörg, Mímisbrunnr, and Yggdrasil has landed, and Bifröst's dev-mode composition of the migrations path stands up norse_identity and norse_reference end to end from wiped volumes. Still ahead for the temporal story, in their own realms: Himinbjörg marking the PII-bearing identity tables, and Bifröst's proof that history reaches the streaming replica.

Design for what's next happens first: brainstorm → spec → plan, recorded in Glitnir's docs/Urdarbrunnr/, before any further project is scaffolded here.

The cosmos

Urðarbrunnr is one realm of the Norse Architecture. The whole platform composes at Bifröst — clone once, cross the bridge, and every session starts there so decisions get brainstormed across the entire landscape, not in isolation. Every design is tried in Glitnir, the design court, before code is forged here; this realm's specs and plans will live in the court's docs/Urðarbrunnr once they converge.

Soundtrack: Urðarbrunnr

Soundtrack: Urðarbrunnr

About

The Well of Urð at Yggdrasil's roots, where the Norns of legend draw water to sustain the tree and carve fate into its trunk as runes. Norse.EntityFramework.* — entity base types, DbContext foundations, conventions, value converters, and the migrations chassis. The record of all that has become, governed by the laws of Asgard.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages