Browse pages
EF Core Persistence
This extension states the Entity Framework Core form of the read, query and mapping rules.
Intent#
This extension states the Entity Framework Core form of the read, query and mapping rules. It applies to aggregates with accepted relational navigation, change-tracking, or provider requirements that the baseline provider cannot satisfy.
Activation#
Activation scope: local.
Applicable specification kinds: module, use-case.
The consumer enables efcore only after a decision records aggregate scope, relational requirement, data evolution effect, and the Marten baseline gap.
Baseline relationship#
For Entity Framework Core-owned aggregate paths, this extension replaces:
litespec/backend/dotnet/persistence#query-through-iquerysession: EF Core queries useIApplicationDbContext.litespec/backend/persistence#keep-mappings-and-aliases-explicit: EF Core mappings useIEntityTypeConfiguration<T>classes.litespec/backend/dotnet/packages#keep-application-dependencies-narrow: Application references the Entity Framework Core query surface its query interface needs.litespec/backend/architecture#separate-command-and-query-behavior: it replaces only the read half, and the invariant half still binds.
For an EF Core-owned command with durable delivery, this extension also replaces:
litespec/ext/outbox#stage-messages-with-business-worklitespec/ext/outbox#reject-independent-outbox-commits
Every other baseline provision binds EF Core-owned paths unchanged.
This extension switches on no optional record type.
Standards#
Record EF Core replacement scope#
ruleguidancelitespec/backend/dotnet/ext/efcore#record-ef-core-replacement-scopeAn EF Core adoption decision MUST name affected aggregates, data evolution, transaction requirements, query impact, package changes, and rollback plan.
Why
The decision describes the complete persistence replacement for its selected aggregates.
Checka reviewer answers
Does the decision name aggregates, evolution, transactions, queries, packages, and rollback?
Marked guidance: a reviewer answers this question.
Retain unaffected baseline persistence#
ruleguidancelitespec/backend/dotnet/ext/efcore#retain-unaffected-baseline-persistenceAn unaffected aggregate MUST retain baseline persistence rules.
Why
Local EF Core adoption does not change ownership of other aggregate paths.
Checka reviewer answers
Does every unaffected aggregate path retain its baseline persistence behavior?
Marked guidance: a reviewer answers this question.
Use one write provider#
rulecheckablelitespec/backend/dotnet/ext/efcore#use-one-write-providerAn EF Core-owned Command MUST stage aggregate writes through exactly one selected write provider.
Why
One provider gives the Command one atomic commit boundary.
Checkchecked by a tool
EfCoreTransactionTests stage aggregate work through one selected provider.
Marked checkable: the tool named here checks it.
Reject mixed provider writes#
rulecheckablelitespec/backend/dotnet/ext/efcore#reject-mixed-provider-writesA Command MUST NOT stage aggregate writes through both Marten-backed and EF Core-backed repositories.
Why
Independent provider commits cannot preserve one local aggregate invariant.
Checkchecked by a tool
EfCoreTransactionTests assert no mixed Marten and EF Core aggregate repository writes.
Marked checkable: the tool named here checks it.
Record provider selection#
ruleguidancelitespec/backend/dotnet/ext/efcore#record-provider-selectionAn EF Core adoption decision MUST name Command provider selection and its architecture-test evidence.
Why
The decision makes provider ownership reviewable for each selected Command path.
Checka reviewer answers
Does the decision name each Command path's provider and its test evidence?
Marked guidance: a reviewer answers this question.
Register one Command post-handler#
rulecheckablelitespec/backend/dotnet/ext/efcore#register-one-command-post-handlerAn EF Core-owned Command MUST register only its selected provider's LiteBus post-handler.
Why
A Marten post-handler cannot commit EF Core-owned work.
Checkchecked by a tool
EfCoreTransactionTests assert each command resolves only its selected provider post-handler.
Marked checkable: the tool named here checks it.
Resolve cross-provider invariants#
ruleguidancelitespec/backend/dotnet/ext/efcore#resolve-cross-provider-invariantsAn accepted atomic cross-provider invariant MUST move its aggregates to one provider or use a reviewed shared transaction mechanism.
Why
One local invariant requires one verified atomic persistence boundary.
Checka reviewer answers
Does each cross-provider invariant use one provider or a reviewed shared transaction?
Marked guidance: a reviewer answers this question.
Stage aggregate writes in repositories#
rulecheckablelitespec/backend/dotnet/ext/efcore#stage-aggregate-writes-in-repositoriesAn EF Core Infrastructure repository MUST use its scoped DbContext to stage aggregate changes.
Why
Repository staging keeps write persistence outside Application handlers.
Checkchecked by a tool
EfCoreWriteTests stage changes through the scoped DbContext.
Marked checkable: the tool named here checks it.
Exclude direct handler commits#
rulecheckablelitespec/backend/dotnet/ext/efcore#exclude-direct-handler-commitsAn EF Core Command handler MUST NOT inject DbContext or call SaveChangesAsync.
Why
The selected post-handler owns the one Command commit.
Checkchecked by a tool
EfCoreWriteTests assert handlers inject no DbContext and call no SaveChangesAsync.
Marked checkable: the tool named here checks it.
Define EF Core query roots#
rulecheckablelitespec/backend/dotnet/ext/efcore#define-ef-core-query-rootsApplication MUST own one public IApplicationDbContext exposing required generic sets or named query roots, referencing only the Entity Framework Core abstractions that interface needs.
Why
The interface defines the EF Core query boundary without exposing the Infrastructure DbContext. The permitted surface is the query types the interface exposes and the manifest-pinned Microsoft.EntityFrameworkCore package that declares them.
Checkchecked by a tool
EfCoreReadTests assert one owned IApplicationDbContext and no wider EF surface.
Marked checkable: the tool named here checks it.
Implement query roots in Infrastructure#
rulecheckablelitespec/backend/dotnet/ext/efcore#implement-query-roots-in-infrastructureInfrastructure MUST implement IApplicationDbContext with the application DbContext.
Why
The provider implementation remains behind Application's query interface.
Checkchecked by a tool
EfCoreReadTests assert the Infrastructure DbContext implements the owned query interface.
Marked checkable: the tool named here checks it.
Retain Marten query boundary#
rulecheckablelitespec/backend/dotnet/ext/efcore#retain-marten-query-boundaryA Marten-owned query path MUST retain the baseline IQuerySession boundary.
Why
EF Core adoption does not alter read ownership for Marten-backed aggregates.
Checkchecked by a tool
EfCoreReadTests assert Marten query handlers retain IQuerySession.
Marked checkable: the tool named here checks it.
Project EF Core queries directly#
rulecheckablelitespec/backend/dotnet/ext/efcore#project-ef-core-queries-directlyAn EF Core query handler MUST use AsNoTracking, filter before materialization, order and limit deterministically, and project records.
Why
Handlers return shaped results without loading mutable aggregate state. No command enforces an invariant from a read projection.
Checkchecked by a tool
EfCoreReadTests assert no tracking, authorized filters, deterministic limits, projections, and no invariant from a projection.
Marked checkable: the tool named here checks it.
Avoid per-aggregate read stores#
rulecheckablelitespec/backend/dotnet/ext/efcore#avoid-per-aggregate-read-storesAn EF Core query implementation MUST NOT add one read-store interface per aggregate.
Why
IApplicationDbContext supplies the selected EF Core query boundary.
Checkchecked by a tool
EfCoreReadTests assert no per-aggregate read-store interface pattern exists.
Marked checkable: the tool named here checks it.
Resolve the tenant at context creation#
rulecheckablelitespec/backend/dotnet/ext/efcore#resolve-the-tenant-at-context-creationAn EF Core DbContext MUST resolve the tenant when the context is created and apply the tenant filter to tenant-owned sets.
Why
Resolution at creation keeps the tenant out of every query, and the filter convention applies it to each tenant-owned set.
Checkchecked by a tool
EfCoreReadTests assert tenant resolution at creation and tenant filters on owned sets.
Marked checkable: the tool named here checks it.
Commit EF Core Command work once#
rulecheckablelitespec/backend/dotnet/ext/efcore#commit-ef-core-command-work-onceAn EF Core LiteBus post-handler MUST call SaveChangesAsync once for its owned Command.
Why
One post-handler commit preserves the Command's provider transaction boundary.
Checkchecked by a tool
EfCoreCommitTests observe one post-handler SaveChangesAsync call per owned Command.
Marked checkable: the tool named here checks it.
Process changed aggregate events#
rulecheckablelitespec/backend/dotnet/ext/efcore#process-changed-aggregate-eventsAn EF Core post-handler MUST collect Domain events from changed aggregates and follow their documented delivery classification.
Why
Event delivery follows the same aggregate changes that the handler commits.
Checkchecked by a tool
EfCoreCommitTests classify and stage Domain events correctly.
Marked checkable: the tool named here checks it.
Use durable outbox delivery#
rulecheckablelitespec/backend/dotnet/ext/efcore#use-durable-outbox-deliveryAn EF Core Command requiring durable delivery MUST use the outbox extension without committing a Marten session.
Why
EF Core-owned durable work remains in the selected provider transaction.
Checkchecked by a tool
EfCoreCommitTests assert EF Core durable Commands commit no Marten session.
Marked checkable: the tool named here checks it.
Stage EF Core outbox records together#
rulecheckablelitespec/backend/dotnet/ext/efcore#stage-ef-core-outbox-records-togetherAn EF Core-owned Command MUST map and stage outbox records with aggregate changes through the same DbContext.
Why
The DbContext transaction makes the outbox record and business work durable together.
Checkchecked by a tool
EfCoreOutboxTests stage message and business work through one DbContext.
Marked checkable: the tool named here checks it.
Reject cross-provider outbox storage#
rulecheckablelitespec/backend/dotnet/ext/efcore#reject-cross-provider-outbox-storageAn EF Core-owned Command MUST NOT store its required outbox record through a Marten session.
Why
A Marten outbox transaction cannot atomically commit an EF Core aggregate change.
Checkchecked by a tool
EfCoreOutboxTests assert EF Core Commands write no outbox record through Marten.
Marked checkable: the tool named here checks it.
Configure EF Core mappings in Infrastructure#
rulecheckablelitespec/backend/dotnet/ext/efcore#configure-ef-core-mappings-in-infrastructureInfrastructure MUST own IEntityTypeConfiguration<T> classes for EF Core persistence mappings.
Why
Relational mapping behavior belongs to the provider-owning layer.
Checkchecked by a tool
EfCoreMappingTests assert EF Core mappings use Infrastructure-owned configuration classes.
Marked checkable: the tool named here checks it.
Configure relational details explicitly#
ruleguidancelitespec/backend/dotnet/ext/efcore#configure-relational-details-explicitlyAn EF Core mapping MUST explicitly configure typed IDs, owned values, fields, indexes, constraints, precision, deletes, concurrency tokens, and database names.
Why
Explicit configuration makes storage contract changes visible in code review and migrations.
Checka reviewer answers
Does the mapping review record every declared relational configuration category?
Marked guidance: a reviewer answers this question.
Retain Domain state values#
rulecheckablelitespec/backend/dotnet/ext/efcore#retain-domain-state-valuesEF Core persistence MUST retain the Domain aggregate's single {Aggregate}State value.
Why
The aggregate remains the owner of lifecycle state and state-specific business facts.
A relational column holds the active state record and its discriminator, which is the same contract the baseline provider stores in its document. The column never holds an enumerated integer, because that discards the state-specific facts the record carries and reintroduces the shape litespec/backend/domain#model-every-closed-set-of-domain-values-as-a-closed-type-hierarchy removes. A state case with its own fields maps to an owned type or to its own table. The discriminator selects which one a read materializes.
Checkchecked by a tool
EfCoreStateTests retain the aggregate's one Domain state value.
Marked checkable: the tool named here checks it.
Map stable relational state shape#
rulecheckablelitespec/backend/dotnet/ext/efcore#map-stable-relational-state-shapeInfrastructure MUST map a stable discriminator and every state-specific value without adding Domain lifecycle flags or duplicate nullable state properties.
Why
Relational storage can use columns while Domain retains its state-record hierarchy.
Checkchecked by a tool
EfCoreStateTests assert no relational lifecycle flags or duplicate nullable state values.
Marked checkable: the tool named here checks it.
Use direct state mapping only when supported#
rulecheckablelitespec/backend/dotnet/ext/efcore#use-direct-state-mapping-only-when-supportedAn EF Core mapping MAY use direct owned or JSON state mapping only when pinned versions materialize, track, and round-trip each state record.
Why
A direct mapping needs complete provider behavior for every sealed state case.
Checkchecked by a tool
EfCoreStateTests materialize, track, and round-trip every state case.
Marked checkable: the tool named here checks it.
Map unsupported state shapes in Infrastructure#
rulecheckablelitespec/backend/dotnet/ext/efcore#map-unsupported-state-shapes-in-infrastructureAn unsupported direct state mapping MUST use an Infrastructure persistence type with discriminator and state-specific columns.
Why
The Infrastructure repository maps the persistence type to and from the Domain hierarchy.
Checkchecked by a tool
EfCoreStateTests map valid discriminator values to Domain states.
Marked checkable: the tool named here checks it.
Review new persisted states#
ruleguidancelitespec/backend/dotnet/ext/efcore#review-new-persisted-statesA new persisted state MUST include reviewed migration, mixed-version behavior, rollback behavior, and an integration fixture.
Why
New state values affect stored rows and application versions that read them.
Example
PostRow maps state_type, published_at, and archived_at to one PostState record.
Checka reviewer answers
Does the new state review cover migration, mixed versions, rollback, and a fixture?
Marked guidance: a reviewer answers this question.
Generate migrations for schema changes#
rulecheckablelitespec/backend/dotnet/ext/efcore#generate-migrations-for-schema-changesAn EF Core schema change MUST generate a migration.
Why
The migration is the explicit reviewed description of relational schema evolution.
Checkchecked by a tool
EfCoreMigrationsTests assert EF Core schema diffs include a generated migration.
Marked checkable: the tool named here checks it.
Review migration effects#
ruleguidancelitespec/backend/dotnet/ext/efcore#review-migration-effectsA migration review MUST cover tables, columns, indexes, constraints, data movement, destructive operations, and rollback compatibility.
Why
Each item can affect deployment safety or data recovery.
Checka reviewer answers
Does the migration review cover every declared storage and rollback effect?
Marked guidance: a reviewer answers this question.
Apply migrations as release work#
rulecheckablelitespec/backend/dotnet/ext/efcore#apply-migrations-as-release-workA release process MUST apply EF Core migrations before application startup.
Why
Release-owned execution controls ordering and evidence for schema work.
Checkchecked by a tool
EfCoreMigrationsTests place migration application before replica startup in the release record.
Marked checkable: the tool named here checks it.
Keep migration files together#
rulecheckablelitespec/backend/dotnet/ext/efcore#keep-migration-files-togetherAn EF Core schema change MUST commit migration, model snapshot, and reviewed SQL together.
Why
The three artifacts show intended migration source and resulting relational change.
Checkchecked by a tool
EfCoreMigrationsTests assert each migration commit holds migration, snapshot, and reviewed SQL.
Marked checkable: the tool named here checks it.
Test migration starting states#
rulecheckablelitespec/backend/dotnet/ext/efcore#test-migration-starting-statesAn EF Core migration MUST apply successfully from an empty database and previous release database.
Why
Both initial setup and upgrade paths need tested relational evolution.
The previous release database is the schema of the last released artifact, not the schema of the previous commit. The test builds it by applying that artifact's migrations to an empty database, then applies the current ones on top. A test that starts from the current migrations minus one proves that the last migration applies, which is the case least likely to be broken.
Checkchecked by a tool
EfCoreMigrationsTests run from empty and previous-release databases.
Marked checkable: the tool named here checks it.
Reject startup schema mutation#
rulecheckablelitespec/backend/dotnet/ext/efcore#reject-startup-schema-mutationProduction and staging startup MUST NOT call EnsureCreated, EnsureDeleted, or Migrate.
Why
Replica startup does not own hosted database schema change.
Checkchecked by a tool
EfCoreMigrationsTests assert startup calls none of the prohibited schema APIs.
Marked checkable: the tool named here checks it.
Configure optimistic tokens#
rulecheckablelitespec/backend/dotnet/ext/efcore#configure-optimistic-tokensAn EF Core aggregate using concurrency MUST configure a provider-backed token and carry expected version through its Command.
Why
The token connects a caller's expected aggregate version to EF Core write detection.
Checkchecked by a tool
EfCoreConcurrencyTests carry expected version and use the configured EF Core token.
Marked checkable: the tool named here checks it.
Map EF Core concurrency conflict#
rulecheckablelitespec/backend/dotnet/ext/efcore#map-ef-core-concurrency-conflictAn EF Core post-handler MUST translate DbUpdateConcurrencyException into the Application conflict contract.
Why
Application exposes a project-owned conflict rather than an EF Core exception type.
Checkchecked by a tool
EfCoreConcurrencyTests map the provider exception to the Application contract.
Marked checkable: the tool named here checks it.
Reject automatic command retry#
rulecheckablelitespec/backend/dotnet/ext/efcore#reject-automatic-command-retryAn EF Core post-handler MUST NOT retry the complete Command automatically after a concurrency exception.
Why
A retry can rerun Domain behavior against state that the caller did not approve.
Checkchecked by a tool
EfCoreConcurrencyTests prove no complete Command automatic retry.
Marked checkable: the tool named here checks it.
Conventions#
Use the EF Core Infrastructure layout#
conventionguidancelitespec/backend/dotnet/ext/efcore#use-the-ef-core-infrastructure-layoutUse Infrastructure/Persistence/EfCore/ for ApplicationDbContext, factory, post-handler, configurations, migrations, and repositories.
ReplacementA consumer records a departure from this default as an Exception record with reason, review date, and removal condition.
Example
Infrastructure/Persistence/EfCore/Configurations/ holds persistence mapping classes.
Checka reviewer answers
Does each EF Core Infrastructure path use the default layout or record an Exception?
Marked guidance: a reviewer answers this question.
Use snake-case relational names#
conventioncheckablelitespec/backend/dotnet/ext/efcore#use-snake-case-relational-namesUse the pinned naming-conventions package and verify generated names in each migration, with casing owned by litespec/backend/persistence#use-database-naming-conventions.
ReplacementA consumer records a departure from this default as an Exception record with reason, review date, and removal condition.
Why
The migration shows the exact names that reach the database.
Checkchecked by a tool
EfCoreTests assert generated migrations contain verified snake-case database names.
Marked checkable: the tool named here checks it.
Dependencies#
Microsoft.EntityFrameworkCoreNpgsql.EntityFrameworkCore.PostgreSQLEFCore.NamingConventions
Verification#
| ID | Mark | Evidence |
|---|---|---|
| litespec/backend/dotnet/ext/efcore#record-ef-core-replacement-scope | guidance | Does the decision name aggregates, evolution, transactions, queries, packages, and rollback? |
| litespec/backend/dotnet/ext/efcore#retain-unaffected-baseline-persistence | guidance | Does every unaffected aggregate path retain its baseline persistence behavior? |
| litespec/backend/dotnet/ext/efcore#use-one-write-provider | checkable | EfCoreTransactionTests stage aggregate work through one selected provider. |
| litespec/backend/dotnet/ext/efcore#reject-mixed-provider-writes | checkable | EfCoreTransactionTests assert no mixed Marten and EF Core aggregate repository writes. |
| litespec/backend/dotnet/ext/efcore#record-provider-selection | guidance | Does the decision name each Command path's provider and its test evidence? |
| litespec/backend/dotnet/ext/efcore#register-one-command-post-handler | checkable | EfCoreTransactionTests assert each command resolves only its selected provider post-handler. |
| litespec/backend/dotnet/ext/efcore#resolve-cross-provider-invariants | guidance | Does each cross-provider invariant use one provider or a reviewed shared transaction? |
| litespec/backend/dotnet/ext/efcore#stage-aggregate-writes-in-repositories | checkable | EfCoreWriteTests stage changes through the scoped DbContext. |
| litespec/backend/dotnet/ext/efcore#exclude-direct-handler-commits | checkable | EfCoreWriteTests assert handlers inject no DbContext and call no SaveChangesAsync. |
| litespec/backend/dotnet/ext/efcore#define-ef-core-query-roots | checkable | EfCoreReadTests assert one owned IApplicationDbContext and no wider EF surface. |
| litespec/backend/dotnet/ext/efcore#implement-query-roots-in-infrastructure | checkable | EfCoreReadTests assert the Infrastructure DbContext implements the owned query interface. |
| litespec/backend/dotnet/ext/efcore#retain-marten-query-boundary | checkable | EfCoreReadTests assert Marten query handlers retain IQuerySession. |
| litespec/backend/dotnet/ext/efcore#project-ef-core-queries-directly | checkable | EfCoreReadTests assert no tracking, authorized filters, deterministic limits, projections, and no invariant from a projection. |
| litespec/backend/dotnet/ext/efcore#avoid-per-aggregate-read-stores | checkable | EfCoreReadTests assert no per-aggregate read-store interface pattern exists. |
| litespec/backend/dotnet/ext/efcore#resolve-the-tenant-at-context-creation | checkable | EfCoreReadTests assert tenant resolution at creation and tenant filters on owned sets. |
| litespec/backend/dotnet/ext/efcore#commit-ef-core-command-work-once | checkable | EfCoreCommitTests observe one post-handler SaveChangesAsync call per owned Command. |
| litespec/backend/dotnet/ext/efcore#process-changed-aggregate-events | checkable | EfCoreCommitTests classify and stage Domain events correctly. |
| litespec/backend/dotnet/ext/efcore#use-durable-outbox-delivery | checkable | EfCoreCommitTests assert EF Core durable Commands commit no Marten session. |
| litespec/backend/dotnet/ext/efcore#stage-ef-core-outbox-records-together | checkable | EfCoreOutboxTests stage message and business work through one DbContext. |
| litespec/backend/dotnet/ext/efcore#reject-cross-provider-outbox-storage | checkable | EfCoreOutboxTests assert EF Core Commands write no outbox record through Marten. |
| litespec/backend/dotnet/ext/efcore#configure-ef-core-mappings-in-infrastructure | checkable | EfCoreMappingTests assert EF Core mappings use Infrastructure-owned configuration classes. |
| litespec/backend/dotnet/ext/efcore#configure-relational-details-explicitly | guidance | Does the mapping review record every declared relational configuration category? |
| litespec/backend/dotnet/ext/efcore#retain-domain-state-values | checkable | EfCoreStateTests retain the aggregate's one Domain state value. |
| litespec/backend/dotnet/ext/efcore#map-stable-relational-state-shape | checkable | EfCoreStateTests assert no relational lifecycle flags or duplicate nullable state values. |
| litespec/backend/dotnet/ext/efcore#use-direct-state-mapping-only-when-supported | checkable | EfCoreStateTests materialize, track, and round-trip every state case. |
| litespec/backend/dotnet/ext/efcore#map-unsupported-state-shapes-in-infrastructure | checkable | EfCoreStateTests map valid discriminator values to Domain states. |
| litespec/backend/dotnet/ext/efcore#review-new-persisted-states | guidance | Does the new state review cover migration, mixed versions, rollback, and a fixture? |
| litespec/backend/dotnet/ext/efcore#generate-migrations-for-schema-changes | checkable | EfCoreMigrationsTests assert EF Core schema diffs include a generated migration. |
| litespec/backend/dotnet/ext/efcore#review-migration-effects | guidance | Does the migration review cover every declared storage and rollback effect? |
| litespec/backend/dotnet/ext/efcore#apply-migrations-as-release-work | checkable | EfCoreMigrationsTests place migration application before replica startup in the release record. |
| litespec/backend/dotnet/ext/efcore#keep-migration-files-together | checkable | EfCoreMigrationsTests assert each migration commit holds migration, snapshot, and reviewed SQL. |
| litespec/backend/dotnet/ext/efcore#test-migration-starting-states | checkable | EfCoreMigrationsTests run from empty and previous-release databases. |
| litespec/backend/dotnet/ext/efcore#reject-startup-schema-mutation | checkable | EfCoreMigrationsTests assert startup calls none of the prohibited schema APIs. |
| litespec/backend/dotnet/ext/efcore#configure-optimistic-tokens | checkable | EfCoreConcurrencyTests carry expected version and use the configured EF Core token. |
| litespec/backend/dotnet/ext/efcore#map-ef-core-concurrency-conflict | checkable | EfCoreConcurrencyTests map the provider exception to the Application contract. |
| litespec/backend/dotnet/ext/efcore#reject-automatic-command-retry | checkable | EfCoreConcurrencyTests prove no complete Command automatic retry. |
| litespec/backend/dotnet/ext/efcore#use-the-ef-core-infrastructure-layout | guidance | Does each EF Core Infrastructure path use the default layout or record an Exception? |
| litespec/backend/dotnet/ext/efcore#use-snake-case-relational-names | checkable | EfCoreTests assert generated migrations contain verified snake-case database names. |