<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged efcore</title>
<link>https://coderlegion.com/tag/efcore</link>
<description>Powered by Question2Answer</description>
<item>
<title>Put the Concurrency Gate Before the Side Effect</title>
<link>https://coderlegion.com/31923/put-the-concurrency-gate-before-the-side-effect</link>
<description>&lt;p&gt;Many concurrency bugs begin with code that looks entirely reasonable:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the current row.&lt;/li&gt;
&lt;li&gt;Check that the operation is still allowed.&lt;/li&gt;
&lt;li&gt;Create something durable elsewhere.&lt;/li&gt;
&lt;li&gt;Save the original row as complete.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Run once, the workflow is correct. Run twice at almost the same time, and both callers can pass the check before either caller records completion. If step three is irreversible, the system can create duplicate work even though every individual condition was true when evaluated.&lt;/p&gt;
&lt;p&gt;The core lesson is simple: when a state transition guards a side effect, make the database choose the winner before the side effect begins.&lt;/p&gt;
&lt;h3 id=&quot;why-read-then-check-is-not-ownership&quot;&gt;Why read-then-check is not ownership&lt;/h3&gt;
&lt;p&gt;An application-level check answers, “What did I observe?” It does not answer, “Do I exclusively own this transition?”&lt;/p&gt;
&lt;p&gt;Imagine a parent record representing an open request. Accepting one option creates a downstream record and closes the parent. Two callers can follow this timeline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Caller A reads the parent as open.&lt;/li&gt;
&lt;li&gt;Caller B reads the same parent as open.&lt;/li&gt;
&lt;li&gt;Caller A creates a downstream record.&lt;/li&gt;
&lt;li&gt;Caller B creates another downstream record.&lt;/li&gt;
&lt;li&gt;Both callers try to close the parent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Optimistic concurrency on the final save may reject one update, but by then the rejected caller may already have created the duplicate side effect. Detecting a conflict after the irreversible action is too late.&lt;/p&gt;
&lt;h3 id=&quot;move-the-decision-forward&quot;&gt;Move the decision forward&lt;/h3&gt;
&lt;p&gt;A stronger shape puts a conditional write immediately before downstream creation. In generalized SQL, the intention is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;UPDATE WorkItems
SET ClaimToken = @token, ClaimedAt = @now
WHERE Id = @id
  AND Status = 'Open'
  AND ClaimToken IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The affected-row count becomes the decision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One affected row means this caller owns the transition.&lt;/li&gt;
&lt;li&gt;Zero affected rows means another caller won, or the operation is no longer valid.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That losing result is not exceptional. It is a normal concurrency outcome and should stop before any downstream creation call.&lt;/p&gt;
&lt;p&gt;This is stronger than reading again because the predicate and the update are evaluated atomically by the database. There is no application-level gap between “still open” and “now mine.”&lt;/p&gt;
&lt;h3 id=&quot;the-claim-is-a-state-not-a-boolean-trick&quot;&gt;The claim is a state, not a boolean trick&lt;/h3&gt;
&lt;p&gt;Once a claim exists, the domain has an in-progress state whether or not the schema calls it that. Treating it explicitly improves the design.&lt;/p&gt;
&lt;p&gt;The workflow needs to answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can a user retry while a claim is fresh?&lt;/li&gt;
&lt;li&gt;Which failures can safely release the claim?&lt;/li&gt;
&lt;li&gt;When is a claim considered abandoned?&lt;/li&gt;
&lt;li&gt;Can another caller take over an abandoned claim?&lt;/li&gt;
&lt;li&gt;What happens if the process fails after downstream creation?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical rule is to release ownership only when the system knows that no durable side effect exists. If downstream creation fails before producing a record, clearing the claim can make a retry safe. Once a durable result may exist, blindly clearing the claim risks creating another one.&lt;/p&gt;
&lt;h3 id=&quot;a-timeout-is-recovery-policy-not-exactly-once&quot;&gt;A timeout is recovery policy, not exactly once&lt;/h3&gt;
&lt;p&gt;A bounded stale-claim timeout prevents an interrupted process from blocking the workflow forever. A new caller may take over only if the stored claim and timestamp still match what it read, so competing recovery attempts cannot both win.&lt;/p&gt;
&lt;p&gt;That solves abandoned ownership before the side effect. It does not solve every crash window.&lt;/p&gt;
&lt;p&gt;Suppose the downstream service successfully creates a record and the process dies before the parent records completion. After the timeout, replaying the operation could create a second record. The conditional claim narrowed the risk, but it did not create a distributed transaction.&lt;/p&gt;
&lt;p&gt;For that boundary, choose an additional mechanism according to the cost of duplication:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pass a stable idempotency key to the downstream operation.&lt;/li&gt;
&lt;li&gt;Record an outbox message in the same transaction as the claim.&lt;/li&gt;
&lt;li&gt;Make downstream creation upsert by the originating command identity.&lt;/li&gt;
&lt;li&gt;Reconcile incomplete claims against downstream records before replay.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The honest claim is “one winner during concurrent admission,” not “exactly once under every failure.”&lt;/p&gt;
&lt;h3 id=&quot;test-the-overlap-not-two-sequential-calls&quot;&gt;Test the overlap, not two sequential calls&lt;/h3&gt;
&lt;p&gt;A concurrency fix deserves a test that opens the original race window deliberately.&lt;/p&gt;
&lt;p&gt;Hold the first caller inside downstream creation. While it is paused, submit the same command again and also submit a competing choice. Then release the first caller.&lt;/p&gt;
&lt;p&gt;The useful assertions are concrete:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The downstream creation service was called once.&lt;/li&gt;
&lt;li&gt;One winner completed.&lt;/li&gt;
&lt;li&gt;Both competing calls received a clear losing result.&lt;/li&gt;
&lt;li&gt;The parent stored one selected result.&lt;/li&gt;
&lt;li&gt;Other candidates moved to their expected terminal state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Add two recovery tests as well. First, make downstream creation fail before producing anything and prove that a retry can acquire the released claim. Second, seed a fresh claim and an expired claim, proving that the fresh claim blocks while the expired one can be reclaimed.&lt;/p&gt;
&lt;p&gt;This test shape is more valuable than sending two tasks and hoping their timing overlaps. A controlled pause makes the race deterministic and keeps the assertion focused on the contract.&lt;/p&gt;
&lt;h3 id=&quot;a-compact-design-checklist&quot;&gt;A compact design checklist&lt;/h3&gt;
&lt;p&gt;Before shipping a command that crosses a data boundary:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Identify the first irreversible side effect.&lt;/li&gt;
&lt;li&gt;Put the ownership decision before it.&lt;/li&gt;
&lt;li&gt;Use one conditional database write, not a read-then-write promise.&lt;/li&gt;
&lt;li&gt;Treat zero affected rows as a normal loser.&lt;/li&gt;
&lt;li&gt;Release only when no durable effect exists.&lt;/li&gt;
&lt;li&gt;Define stale ownership and takeover semantics.&lt;/li&gt;
&lt;li&gt;Add idempotency or reconciliation for the post-side-effect crash window.&lt;/li&gt;
&lt;li&gt;Test with a deliberately paused winner and active losers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The broader principle is not limited to any framework. Checks describe observations. Conditional writes grant ownership. When a workflow can create something you cannot casually undo, that distinction is part of correctness.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31923/put-the-concurrency-gate-before-the-side-effect</guid>
<pubDate>Wed, 21 Oct 2026 05:40:24 +0000</pubDate>
</item>
<item>
<title>Soft Delete Is a Model Contract, Not a Repository Habit</title>
<link>https://coderlegion.com/31493/soft-delete-is-a-model-contract-not-a-repository-habit</link>
<description>&lt;p&gt;Soft deletion often begins innocently: add an IsDeleted flag, remember to exclude retained rows, and move on. That works until the application grows into several modules, several DbContext types, and configuration spread across assemblies.&lt;/p&gt;
&lt;p&gt;At that point, soft deletion is no longer a local query concern. It is a visibility boundary for the complete EF Core model.&lt;/p&gt;
&lt;p&gt;The difference is practical. A repository predicate protects the query paths that remember to use it. A model contract protects ordinary reads by default and makes exceptional reads visible in code review.&lt;/p&gt;
&lt;h3 id=&quot;the-dangerous-partial-success&quot;&gt;The dangerous partial success&lt;/h3&gt;
&lt;p&gt;Imagine a shared base context that scans the entity types it knows and installs a query filter. Later, a module applies another configuration assembly and adds more entities. The base code is correct for the model it saw, but its timing was wrong. Some retained rows are hidden and others are returned normally.&lt;/p&gt;
&lt;p&gt;This failure is uncomfortable because both sides look reasonable in isolation. The entity has the deletion marker. The shared context contains filter code. A few tests pass. Yet the effective model has uneven visibility rules.&lt;/p&gt;
&lt;p&gt;The lesson is to configure cross-cutting rules at a lifecycle point where the model is complete. An EF Core model-finalising convention can inspect every registered type after the contexts and configuration assemblies have contributed their mappings.&lt;/p&gt;
&lt;p&gt;For each root entity carrying the deletion marker, the convention can add the standard predicate. Owned types follow their owner, and derived types inherit the root filter. Existing filters must be preserved: combine with an anonymous filter, or add a separate named filter when the model uses named filters. Any always-visible audit type should be an explicit, narrow exception rather than an accidental omission.&lt;/p&gt;
&lt;h3 id=&quot;test-the-effective-predicate&quot;&gt;Test the effective predicate&lt;/h3&gt;
&lt;p&gt;Checking that a filter exists is too weak.&lt;/p&gt;
&lt;p&gt;An entity might have an unrelated visibility filter. It might even mention the deletion marker beneath a logical OR:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;not deleted OR visible to this user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That expression can still admit a retained row. The required property is stronger: not deleted must be a mandatory conjunct of the effective filter.&lt;/p&gt;
&lt;p&gt;A reusable model audit can enumerate all root entity types with the marker, exclude only documented exceptions, and fail if any type lacks that mandatory predicate. It should have tests against itself: prove that it rejects an unrelated filter and rejects the OR case. Otherwise the audit risks becoming a green check that does not guard the boundary it names.&lt;/p&gt;
&lt;p&gt;Provider-backed tests add the next layer of evidence. Insert one active row and one retained row. Show that normal queries return only the active row, while an explicit bypass can retrieve both. This verifies the generated query and provider behaviour rather than only evaluating a compiled expression in memory.&lt;/p&gt;
&lt;h3 id=&quot;a-correct-filter-changes-workflows&quot;&gt;A correct filter changes workflows&lt;/h3&gt;
&lt;p&gt;When global filtering becomes consistent, previously accidental behaviour may break. That is useful information.&lt;/p&gt;
&lt;p&gt;Restoration is the clearest example. Suppose a retained row still owns a unique business slot. A normal lookup cannot see it, so a naive create path tries to insert a replacement and collides with the database constraint.&lt;/p&gt;
&lt;p&gt;The solution is not to weaken the global filter. Give the restoration path an explicit way to ignore model filters, then make its intention precise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;creating into a slot held by a retained row restores that same row;&lt;/li&gt;
&lt;li&gt;editing a different active row into the reserved slot is refused;&lt;/li&gt;
&lt;li&gt;failure leaves both rows unchanged;&lt;/li&gt;
&lt;li&gt;historical or restoration reads are the only paths allowed to bypass the boundary.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Putting the bypass on a named specification or dedicated query object is easier to review than scattering IgnoreQueryFilters() through services. The name communicates why the exception exists, and focused tests can pin its scope.&lt;/p&gt;
&lt;h3 id=&quot;watch-required-navigations&quot;&gt;Watch required navigations&lt;/h3&gt;
&lt;p&gt;Global filters also interact with relationship shape. If a required navigation uses an inner join and the principal is filtered out, the dependent can disappear from a query even when the dependent itself is active.&lt;/p&gt;
&lt;p&gt;That may be correct, but it should be a decision.&lt;/p&gt;
&lt;p&gt;An inventory of required relationships that cross filtered and unfiltered types gives reviewers a bounded list of hazards. For each one, choose deliberately: align filters on both sides, make the navigation optional, change the query shape, or accept and test the disappearance.&lt;/p&gt;
&lt;p&gt;This is one reason metadata-driven verification scales better than a long list of hand-written repository tests. The model can tell you what exists now, including a new entity or relationship added by a future module.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-verification-stack&quot;&gt;A practical verification stack&lt;/h3&gt;
&lt;p&gt;A durable soft-delete boundary has four layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Apply the convention after the model is complete.&lt;/li&gt;
&lt;li&gt;Audit every soft-deletable root for the exact mandatory predicate.&lt;/li&gt;
&lt;li&gt;Keep bypasses explicit and limited to restoration or historical use.&lt;/li&gt;
&lt;li&gt;Run real-provider tests for visibility, restoration, uniqueness, and required-navigation behaviour.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is a cost: more model infrastructure, a deliberate exception list, and tests that can expose old assumptions. Some teams may decide that hard deletion or temporal tables better match particular data. The point is not that soft deletion is always right.&lt;/p&gt;
&lt;p&gt;The point is that, once chosen, it must behave like one coherent contract.&lt;/p&gt;
&lt;p&gt;The most valuable outcome is not the filter itself. It is that a future entity cannot quietly arrive outside the boundary—and that exceptional access reads like an exception.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31493/soft-delete-is-a-model-contract-not-a-repository-habit</guid>
<pubDate>Sat, 17 Oct 2026 05:45:59 +0000</pubDate>
</item>
<item>
<title>When the Tool Cannot See the Whole Model</title>
<link>https://coderlegion.com/30687/when-the-tool-cannot-see-the-whole-model</link>
<description>&lt;p&gt;Modular architecture creates an uncomfortable class of failure: the application can understand its complete model while design-time tooling sees only a legitimate subset.&lt;/p&gt;
&lt;p&gt;That difference is easy to misread as drift. It is also dangerous to dismiss. The useful engineering move is to identify exactly what the tool cannot observe, then relocate each safety check to a boundary with enough context to answer the question correctly.&lt;/p&gt;
&lt;h3 id=&quot;the-warning-was-real-but-its-conclusion-was-wrong&quot;&gt;The warning was real, but its conclusion was wrong&lt;/h3&gt;
&lt;p&gt;Consider a shared EF Core context that can be extended by optional runtime modules. Each module contributes mappings without forcing the lower-level persistence package to reference every higher-level feature package. That dependency direction is valuable: the shared module remains reusable, and optional features remain optional.&lt;/p&gt;
&lt;p&gt;At runtime, the host loads the contributing modules and the complete model is assembled. At design time, the tooling process starts from a narrower package. Loading one contributor directly would create a dependency cycle, so that contributor is absent.&lt;/p&gt;
&lt;p&gt;The migration snapshot still represents the complete model. The design-time model does not. A comparison therefore reports a difference and may propose removing an object that is not obsolete at all; it is merely invisible from that process.&lt;/p&gt;
&lt;p&gt;The warning accurately says, “these two views differ.” It cannot establish why they differ.&lt;/p&gt;
&lt;h3 id=&quot;separate-database-update-from-migration-authoring&quot;&gt;Separate database update from migration authoring&lt;/h3&gt;
&lt;p&gt;The first mistake would be treating every operation as though it asks the same question.&lt;/p&gt;
&lt;p&gt;A database update applies migrations that have already passed review and validation. If a known model subset causes a framework warning to block that operation, a narrowly scoped suppression can be reasonable. The important word is narrowly. The exception belongs in the design-time path that has the structural blind spot, with an explanation of why the complete model cannot be assembled there.&lt;/p&gt;
&lt;p&gt;Migration authoring is different. A scaffolder turns its limited view into proposed schema changes. If it cannot see an extension-owned table, a removal operation may look perfectly logical. Suppressing a warning does nothing to make that proposal safe.&lt;/p&gt;
&lt;p&gt;So the second boundary needs a stronger invariant: inspect the generated migration operations and fail if they remove, rename, or destructively alter protected extension data. That converts a warning in prose into an executable gate.&lt;/p&gt;
&lt;p&gt;This distinction matters. “Allow the known update path” and “trust every future generated migration” are not the same decision.&lt;/p&gt;
&lt;h3 id=&quot;keep-a-complete-model-drift-check&quot;&gt;Keep a complete-model drift check&lt;/h3&gt;
&lt;p&gt;Once a noisy comparison is suppressed, the obvious risk is hiding real drift. The answer is not to pretend the partial view became authoritative. Keep a separate test that runs in a host where all contributors are loaded, builds the complete runtime model, and compares that model with the snapshot.&lt;/p&gt;
&lt;p&gt;Now each check has one clear responsibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the design-time update path tolerates the known subset;&lt;/li&gt;
&lt;li&gt;the full-model test detects genuine model-to-snapshot drift;&lt;/li&gt;
&lt;li&gt;the migration-operation test blocks destructive scaffolding.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is more precise than relying on one framework signal for three different questions.&lt;/p&gt;
&lt;h3 id=&quot;prove-the-guard-is-not-vacuously-green&quot;&gt;Prove the guard is not vacuously green&lt;/h3&gt;
&lt;p&gt;Negative tests deserve a positive control.&lt;/p&gt;
&lt;p&gt;Imagine a test that scans every migration and asserts that none removes a protected table. It passes. Good news, perhaps. But it also passes if migration discovery silently returns an empty collection.&lt;/p&gt;
&lt;p&gt;Add a companion assertion that the scan finds the migration that originally created the protected object. If discovery breaks, the positive control fails. The pair proves both that the dangerous operation is absent and that the evidence set is present.&lt;/p&gt;
&lt;p&gt;This pattern generalizes beyond migrations. Any test that says “no forbidden item exists” should make you ask whether it would still pass when the scanner found nothing.&lt;/p&gt;
&lt;h3 id=&quot;shared-process-state-is-another-hidden-boundary&quot;&gt;Shared process state is another hidden boundary&lt;/h3&gt;
&lt;p&gt;The same change exposed a smaller reliability lesson. Tests for design-time factories temporarily set a process-wide configuration value. Separate test classes ran concurrently, and one could restore the value while another was still using it. Each class passed alone; together they failed intermittently.&lt;/p&gt;
&lt;p&gt;The repair was not another retry. The tests were placed in one non-parallel collection because they share mutable process state.&lt;/p&gt;
&lt;p&gt;That is the same ownership principle in miniature: the scope of the resource determines the scope of coordination. A process-wide variable cannot safely be treated as test-local state.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off-is-explicit-complexity&quot;&gt;The trade-off is explicit complexity&lt;/h3&gt;
&lt;p&gt;This approach is not free. There is now a documented exception, a complete-model drift test, a migration-operation guard, a positive control, and serialized tests around shared environment state. Future maintainers must understand why those pieces exist.&lt;/p&gt;
&lt;p&gt;But the alternative is false simplicity: a generic warning that blocks legitimate work, generated operations that can target valid data, and flaky tests that disguise the real contract.&lt;/p&gt;
&lt;p&gt;The practical rule I keep is this:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;When tooling has a structural blind spot, narrow the exception and move safety to executable checks that can observe the complete truth.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Do not silence evidence broadly. Do not force an architectural dependency merely to satisfy a tool. Give each check the context it needs, and make dangerous assumptions fail before they become database changes.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30687/when-the-tool-cannot-see-the-whole-model</guid>
<pubDate>Sun, 11 Oct 2026 06:55:17 +0000</pubDate>
</item>
<item>
<title>A Row Version Is a Cross-Layer Contract</title>
<link>https://coderlegion.com/29596/a-row-version-is-a-cross-layer-contract</link>
<description>&lt;p&gt;A concurrency column can exist in the database and still be wrong.&lt;/p&gt;
&lt;p&gt;The name may match. The migration may have run. Inserts may appear healthy. Yet optimistic concurrency can still be weaker than the application assumes because the runtime model, the physical schema, and the test environment disagree about what that column means.&lt;/p&gt;
&lt;p&gt;The lesson is broader than one ORM feature: database-generated values are cross-layer contracts. Treating them as a property of only the entity class or only the table leaves a gap exactly where correctness matters.&lt;/p&gt;
&lt;h3 id=&quot;what-the-contract-actually-contains&quot;&gt;What the contract actually contains&lt;/h3&gt;
&lt;p&gt;For a database-generated row version, at least five facts need to agree:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the physical type has true row-version semantics;&lt;/li&gt;
&lt;li&gt;the value is non-nullable;&lt;/li&gt;
&lt;li&gt;the database generates it on insert and update;&lt;/li&gt;
&lt;li&gt;the ORM marks it as a concurrency token;&lt;/li&gt;
&lt;li&gt;existing values remain intact during alignment or deployment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A binary column of the expected length is not automatically equivalent. A nullable token changes the invariant. A model that knows about concurrency but not database generation can try to write a value the database owns. Conversely, a correctly generated database column provides little application protection if the runtime model never includes it in optimistic-concurrency checks.&lt;/p&gt;
&lt;p&gt;The useful mental model is not “there is a column.” It is “every layer agrees who owns this value and how a stale write is detected.”&lt;/p&gt;
&lt;h3 id=&quot;prefer-verification-over-destructive-repair&quot;&gt;Prefer verification over destructive repair&lt;/h3&gt;
&lt;p&gt;Suppose the shipped database already has the correct non-null generated token, but the current EF Core model describes it incompletely. A tempting fix is to alter or recreate the column until the migration history looks tidy.&lt;/p&gt;
&lt;p&gt;That can be the riskiest part of the change. Replacing a generated token can touch every row, invalidate values held by active writers, or disguise an unexpected schema. The safer move is often smaller:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;align the runtime mapping with the physical contract;&lt;/li&gt;
&lt;li&gt;add a forward-only migration that asserts the exact expected shape;&lt;/li&gt;
&lt;li&gt;refuse to proceed when the real schema differs.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This kind of assertion migration changes no business data. It asks the database whether the table, column, type, length, nullability, and generation semantics match the known contract. Running it twice should be safe. Encountering a missing, nullable, or look-alike column should fail clearly.&lt;/p&gt;
&lt;p&gt;Failing a rollout is inconvenient. Quietly weakening stale-write protection is worse. A fail-closed migration turns unknown drift into an explicit operational decision instead of an accidental data-integrity change.&lt;/p&gt;
&lt;h3 id=&quot;test-at-three-different-levels&quot;&gt;Test at three different levels&lt;/h3&gt;
&lt;p&gt;No single test provider can prove the whole contract efficiently. A layered suite gives each test one honest job.&lt;/p&gt;
&lt;h4 id=&quot;1-model-metadata&quot;&gt;1. Model metadata&lt;/h4&gt;
&lt;p&gt;A cheap unit test can inspect EF Core metadata. It should verify that the property is required, marked as a concurrency token, and generated on add or update. This catches accidental mapping changes quickly, without starting a database.&lt;/p&gt;
&lt;p&gt;Metadata tests do not prove the database behaves that way. They prove the application describes its expectation correctly.&lt;/p&gt;
&lt;h4 id=&quot;2-lightweight-application-flows&quot;&gt;2. Lightweight application flows&lt;/h4&gt;
&lt;p&gt;In-memory providers are useful for service and orchestration tests, but they do not automatically behave like a relational engine with generated row versions. Once a token becomes required, fixtures may fail because no value is produced.&lt;/p&gt;
&lt;p&gt;A narrowly scoped, test-only save interceptor can generate a fresh fixed-length token for inserted or modified entities. That keeps fast tests representative of the required-value lifecycle. It should be described honestly as an emulator, not provider parity.&lt;/p&gt;
&lt;p&gt;The emulator proves that application code can carry changing tokens through its flows. It cannot prove the SQL type, migration assertion, or actual stale-write mechanics.&lt;/p&gt;
&lt;h4 id=&quot;3-a-focused-real-database-proof&quot;&gt;3. A focused real-database proof&lt;/h4&gt;
&lt;p&gt;The decisive test uses the real provider. Create the shipped shape, insert a synthetic row, and retain its generated token. Apply the alignment migration—ideally twice—and verify that both the row and original token survive.&lt;/p&gt;
&lt;p&gt;Then load the same row through two contexts. Save through the first and confirm its token changes. Attempt to save the stale copy through the second. The expected concurrency exception is the behavioural proof that the contract works end to end.&lt;/p&gt;
&lt;p&gt;Negative cases matter too. A missing table, missing column, nullable token, or ordinary binary column should be refused. These cases prevent a permissive assertion from becoming ceremonial.&lt;/p&gt;
&lt;h3 id=&quot;put-the-expensive-proof-on-the-required-path&quot;&gt;Put the expensive proof on the required path&lt;/h3&gt;
&lt;p&gt;A real-database test that developers can skip forever is documentation, not a gate. Register the small provider-faithful suite in the required CI job and include the relevant file scopes in change detection.&lt;/p&gt;
&lt;p&gt;This does not mean every application test should use a full database. That would make feedback slower and fixtures harder to maintain. The trade-off is deliberate separation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;metadata tests are cheap and broad;&lt;/li&gt;
&lt;li&gt;in-memory tests exercise application flows;&lt;/li&gt;
&lt;li&gt;real-database tests prove database-owned behaviour.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The suite costs more than one happy-path unit test. In return, failures become easier to interpret because each layer states what it can and cannot guarantee.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-review-checklist&quot;&gt;A practical review checklist&lt;/h3&gt;
&lt;p&gt;When reviewing a database-generated concurrency token, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the runtime model match the shipped type, nullability, and generation rules?&lt;/li&gt;
&lt;li&gt;Does the migration preserve rows and existing token values?&lt;/li&gt;
&lt;li&gt;Does unexpected schema fail closed?&lt;/li&gt;
&lt;li&gt;Do fast fixtures represent the required token lifecycle without claiming provider equivalence?&lt;/li&gt;
&lt;li&gt;Does a real-provider test prove token change and stale-write rejection?&lt;/li&gt;
&lt;li&gt;Is that proof part of required CI?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The broader engineering habit is simple: when a value is owned by the database, verify the contract where each promise is made. Model tests describe intent. Fast tests protect application flow. Only the real provider can prove the final integrity boundary.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29596/a-row-version-is-a-cross-layer-contract</guid>
<pubDate>Sat, 03 Oct 2026 06:24:18 +0000</pubDate>
</item>
<item>
<title>When EF Core's Design-Time Model Is Supposed to Be Incomplete</title>
<link>https://coderlegion.com/28685/when-ef-cores-design-time-model-is-supposed-to-be-incomplete</link>
<description>&lt;p&gt;Framework warnings are useful because they compress hard-earned experience into an early signal. But a signal is not an invariant. In an extensible EF Core architecture, design-time tooling can legitimately see less of the model than the running application. If that difference is intentional, treating every pending-model warning as real drift can block a safe database update—or encourage a destructive migration.&lt;/p&gt;
&lt;p&gt;The answer is not to ignore the warning casually. It is to understand exactly why it is false, then replace the lost signal with checks that understand the real architecture.&lt;/p&gt;
&lt;h3 id=&quot;how-a-valid-model-becomes-invisible&quot;&gt;How a Valid Model Becomes Invisible&lt;/h3&gt;
&lt;p&gt;Consider a modular .NET application with a DbContext owned by a foundational module. Higher-level modules can contribute entity configurations through runtime discovery. This keeps the dependency direction clean: the foundational data module does not need references back to every feature that extends it.&lt;/p&gt;
&lt;p&gt;At runtime, the host loads the feature assemblies and the DbContext receives the complete set of configurations. At design time, the EF Core tooling starts from the foundational project. Loading a higher-level contributor from there would create a circular dependency, so the design-time process builds a smaller model.&lt;/p&gt;
&lt;p&gt;The migration snapshot still represents the complete model. The design-time differ compares that snapshot with its partial view and concludes that the contributed entity has disappeared. A probe migration may therefore contain a drop operation for a table that remains valid and necessary at runtime.&lt;/p&gt;
&lt;p&gt;The tool is behaving consistently with the inputs it can see. The architectural context is what it lacks.&lt;/p&gt;
&lt;h3 id=&quot;prove-the-false-positive-before-suppressing-it&quot;&gt;Prove the False Positive Before Suppressing It&lt;/h3&gt;
&lt;p&gt;Before changing warning behaviour, reduce the difference to something explainable.&lt;/p&gt;
&lt;p&gt;Generate a disposable probe migration and inspect every operation. If the only reported difference is the known extension entity that design time cannot load, you have evidence for a visibility gap. If the probe also changes columns, indexes, relationships, or unrelated tables, stop: that may be genuine drift.&lt;/p&gt;
&lt;p&gt;Then verify the full model independently. Run a snapshot comparison in a test process where all contributing assemblies are loaded. That test should compare the migration snapshot against the same model the application actually builds.&lt;/p&gt;
&lt;p&gt;Only after both checks agree should the design-time factory suppress the specific pending-model warning. The suppression belongs at the narrow boundary where the false positive occurs, not in a broad configuration that hides the warning for unrelated contexts.&lt;/p&gt;
&lt;p&gt;This is an important distinction: the warning is disabled because a stronger source of truth exists, not because the warning is inconvenient.&lt;/p&gt;
&lt;h3 id=&quot;turn-the-destructive-failure-mode-into-a-build-failure&quot;&gt;Turn the Destructive Failure Mode Into a Build Failure&lt;/h3&gt;
&lt;p&gt;Even after database update is unblocked, migration scaffolding may still use the incomplete design-time model. A future developer can generate a migration and receive a plausible-looking drop operation.&lt;/p&gt;
&lt;p&gt;Comments and runbooks help, but they do not stop a merge. An executable guard can.&lt;/p&gt;
&lt;p&gt;Inspect every migration's operations and fail if a protected extension table is dropped, renamed, or destructively altered. Cover structured operations such as table drops and column drops, but also inspect raw SQL for equivalent destructive statements. The goal is not a general SQL parser. It is a focused tripwire for the known architectural blind spot.&lt;/p&gt;
&lt;p&gt;Add a positive control as well. The suite should prove that migration discovery found the migration which creates the protected table. Without that check, a broken scanner could inspect zero migrations and report a reassuring green result. A rejection-only test cannot distinguish “nothing is wrong” from “nothing ran.”&lt;/p&gt;
&lt;p&gt;That small positive assertion dramatically improves the credibility of the safety test.&lt;/p&gt;
&lt;h3 id=&quot;keep-the-test-harness-honest&quot;&gt;Keep the Test Harness Honest&lt;/h3&gt;
&lt;p&gt;Design-time factory tests often need temporary configuration, such as a process-wide environment variable containing a connection string. Two test classes that mutate the same variable can race: one restores the original value while the other is still constructing its context.&lt;/p&gt;
&lt;p&gt;That creates a flaky failure which disappears when either class runs alone.&lt;/p&gt;
&lt;p&gt;Treat process-wide configuration as shared mutable state. Put those tests in one serialized collection, restore the previous value in a &lt;code&gt;finally&lt;/code&gt; block, and make it explicit that the connection value is only used to construct options when no database call is required.&lt;/p&gt;
&lt;p&gt;This detail is adjacent to the model problem, but it matters. A safety test that flakes trains people to distrust or bypass it.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off-is-custom-safety-work&quot;&gt;The Trade-Off Is Custom Safety Work&lt;/h3&gt;
&lt;p&gt;This approach is not free. The team must maintain a full-model snapshot test, a focused migration-operation guard, a positive control, serialized environment-sensitive tests, and documentation explaining which CLI signal is intentionally unreliable for this context.&lt;/p&gt;
&lt;p&gt;The alternative is worse: reverse the dependency direction, abandon modular extension points, block legitimate database updates, or risk committing a destructive migration produced by a partial model.&lt;/p&gt;
&lt;p&gt;The general rule is simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Identify why the generic check is wrong.&lt;/li&gt;
&lt;li&gt;Prove the exact boundary of the false positive.&lt;/li&gt;
&lt;li&gt;Suppress only at that boundary.&lt;/li&gt;
&lt;li&gt;Replace the lost signal with a stronger invariant.&lt;/li&gt;
&lt;li&gt;Add a positive control so the invariant cannot pass vacuously.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Framework defaults are valuable, but architecture-specific evidence is stronger. When design time and runtime intentionally see different worlds, make that difference explicit—and make the dangerous interpretation impossible to merge unnoticed.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28685/when-ef-cores-design-time-model-is-supposed-to-be-incomplete</guid>
<pubDate>Sat, 26 Sep 2026 06:02:03 +0000</pubDate>
</item>
<item>
<title>When EF Core Sees Only Part of Your Model</title>
<link>https://coderlegion.com/27740/when-ef-core-sees-only-part-of-your-model</link>
<description>&lt;p&gt;Framework warnings are valuable because they turn hidden risk into visible friction. But a warning is still an interpretation of the evidence available to the tool. In a modular application, the command-line process may not load the same assemblies as the running application. When that happens, the design-time model can be a projection rather than the whole truth.&lt;/p&gt;
&lt;p&gt;That creates an uncomfortable situation: a migration warning may be both technically correct about the model it sees and dangerously wrong about the database operation it proposes.&lt;/p&gt;
&lt;p&gt;The answer is not to ignore warnings casually. It is to identify the boundary, narrow the exception, and replace the lost signal with a stronger executable invariant.&lt;/p&gt;
&lt;h3 id=&quot;the-model-can-change-with-the-process&quot;&gt;The model can change with the process&lt;/h3&gt;
&lt;p&gt;Consider a lower-level persistence module that owns a database context. At runtime, higher-level modules can contribute entity configurations through a discovery mechanism. This preserves the dependency direction: the lower-level module does not need references back to every feature that extends it.&lt;/p&gt;
&lt;p&gt;Now run command-line migration tooling from the lower-level project. The higher-level contributor may not be loaded, because adding that reference would create a project cycle. The tool builds a valid model, but it is only the portion visible from that process.&lt;/p&gt;
&lt;p&gt;The migration snapshot, meanwhile, includes the contributed table. It was created from the full application model and must remain there for every host that uses the shared schema.&lt;/p&gt;
&lt;p&gt;The differ compares those two views and concludes that the table exists in the snapshot but not in the current model. Its proposed fix is logical from that narrow perspective: drop the table.&lt;/p&gt;
&lt;p&gt;From the system’s perspective, that operation is destructive.&lt;/p&gt;
&lt;h3 id=&quot;do-not-fix-the-snapshot-to-match-a-blind-spot&quot;&gt;Do not “fix” the snapshot to match a blind spot&lt;/h3&gt;
&lt;p&gt;Regenerating the snapshot until the warning disappears is tempting. It is also exactly the wrong response when the design-time model is incomplete by construction.&lt;/p&gt;
&lt;p&gt;The first diagnostic question should be: what is the entire proposed difference? A probe migration can make the answer concrete. If the only change is removal of a table supplied by a module that the tooling process cannot load, the warning describes a visibility gap—not necessarily real drift.&lt;/p&gt;
&lt;p&gt;This distinction matters. “The warning is a false positive” is too broad. A particular warning, for a particular context, under a documented loading boundary may be a false positive. Other differences could still be real.&lt;/p&gt;
&lt;p&gt;The safe decision is therefore narrow: allow the legitimate database-update path to proceed despite that one known condition, while keeping a separate full-model comparison as the authoritative drift test.&lt;/p&gt;
&lt;h3 id=&quot;suppression-spends-a-safety-signal&quot;&gt;Suppression spends a safety signal&lt;/h3&gt;
&lt;p&gt;Ignoring a warning removes friction. It does not remove the underlying ambiguity.&lt;/p&gt;
&lt;p&gt;If a design-time factory suppresses a known pending-model warning, the team has spent a safety signal. That signal should be replaced with evidence closer to the failure mode.&lt;/p&gt;
&lt;p&gt;In this case, the feared outcome is not an abstract “model mismatch.” It is a migration that drops, renames, or damages a table the design-time process cannot see. That can be tested directly.&lt;/p&gt;
&lt;p&gt;An executable migration guard can inspect the operations produced by every committed migration and fail when it finds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a drop of the protected table;&lt;/li&gt;
&lt;li&gt;a rename that moves it out of the contract;&lt;/li&gt;
&lt;li&gt;a column removal targeting that table; or&lt;/li&gt;
&lt;li&gt;raw SQL containing the destructive operation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is more specific than the original warning, and that specificity is a strength. The test says exactly which invariant must survive.&lt;/p&gt;
&lt;h3 id=&quot;negative-guards-need-positive-controls&quot;&gt;Negative guards need positive controls&lt;/h3&gt;
&lt;p&gt;There is a subtle testing trap here. A test that scans migrations and finds no forbidden operation can pass because the migrations are safe—or because discovery found nothing.&lt;/p&gt;
&lt;p&gt;That is a vacuous pass.&lt;/p&gt;
&lt;p&gt;Add a positive control that proves the scan sees the migration that originally created the protected table. Now the pair of tests establishes both halves of the claim:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the evidence set is present; and&lt;/li&gt;
&lt;li&gt;none of that evidence violates the destructive-operation invariant.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This pattern generalizes well. Whenever a test says “nothing bad exists,” pair it with proof that the search space is populated. It is useful for dependency scans, authorization maps, route inventories, migration checks, and reflection-based conventions.&lt;/p&gt;
&lt;h3 id=&quot;keep-the-full-model-as-the-authority&quot;&gt;Keep the full model as the authority&lt;/h3&gt;
&lt;p&gt;The migration guard protects one known destructive edge. It does not prove that every legitimate model change has a migration.&lt;/p&gt;
&lt;p&gt;For that, retain a snapshot comparison that runs in a process where all runtime contributors are loaded. That test compares the snapshot with the full model rather than the design-time projection.&lt;/p&gt;
&lt;p&gt;These checks have different jobs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the full-model comparison detects real snapshot drift;&lt;/li&gt;
&lt;li&gt;the migration guard blocks the known destructive artifact;&lt;/li&gt;
&lt;li&gt;the positive control proves the guard actually inspected meaningful input; and&lt;/li&gt;
&lt;li&gt;the narrowly scoped suppression keeps the operational update path usable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Together, they are stronger than treating one framework warning as universally authoritative.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off-is-deliberate-maintenance&quot;&gt;The trade-off is deliberate maintenance&lt;/h3&gt;
&lt;p&gt;This approach is not free. The guard knows about a specific architectural boundary. If extension mechanics change, the tests and documentation must change with them. Contributors need to understand why the suppression exists and why generated migration output still requires review.&lt;/p&gt;
&lt;p&gt;The alternative is worse: either block legitimate operations forever, or normalize a habit of dismissing warnings and accepting generated drops.&lt;/p&gt;
&lt;p&gt;The practical review rule is simple: when tooling sees only a projection, document the blind spot, scope the exception, and encode the real safety property as a failing test. Framework diagnostics are excellent inputs. Your architecture still owns the final claim.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27740/when-ef-core-sees-only-part-of-your-model</guid>
<pubDate>Sat, 19 Sep 2026 12:18:35 +0000</pubDate>
</item>
<item>
<title>Your Reader Cannot Be Stricter Than Your Schema</title>
<link>https://coderlegion.com/27483/your-reader-cannot-be-stricter-than-your-schema</link>
<description>&lt;p&gt;&lt;code&gt;SingleOrDefault&lt;/code&gt; looks like a small query choice. In practice, it is an assertion about your data model: for this predicate, zero or one row may exist.&lt;/p&gt;
&lt;p&gt;That assertion is valuable when storage enforces the same rule. When the database permits duplicates, however, the reader is relying on an invariant that does not exist. One unexpected row can turn an ordinary lookup into an exception before validation, authorization, or other normal handling begins.&lt;/p&gt;
&lt;p&gt;I recently reviewed a committed recovery for exactly that class of mismatch. The useful lesson was not the specific lookup. It was how to contain the exception without pretending the data had become correct.&lt;/p&gt;
&lt;h3 id=&quot;a-query-can-encode-a-hidden-contract&quot;&gt;A query can encode a hidden contract&lt;/h3&gt;
&lt;p&gt;Consider a generalized account table:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rows permitted by storage: 0..many per normalized key
Rows expected by reader:    0..1 per normalized key
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The read path might be perfectly reasonable in isolation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var record = await records.SingleOrDefaultAsync(x =&amp;gt; x.LookupKey == key);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the method name does not create uniqueness. It only checks cardinality at read time. If two rows match, the query throws.&lt;/p&gt;
&lt;p&gt;This is why I now treat &lt;code&gt;Single&lt;/code&gt; and &lt;code&gt;SingleOrDefault&lt;/code&gt; as executable invariant assertions. During review, I ask: what proves the invariant? A unique index? A constrained aggregate? A transactionally protected write path? Or only an earlier existence check that can race?&lt;/p&gt;
&lt;h3 id=&quot;containment-is-not-repair&quot;&gt;Containment is not repair&lt;/h3&gt;
&lt;p&gt;When duplicate data already exists, immediately adding a unique index may be unsafe. Migration can fail on the existing rows. Previously accepted imports or registrations may begin failing without a useful remediation path.&lt;/p&gt;
&lt;p&gt;At the same time, leaving a critical read path to throw can make a narrow data defect disproportionately disruptive.&lt;/p&gt;
&lt;p&gt;A staged response separates two goals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Containment:&lt;/strong&gt; provide a deterministic, non-crashing resolution path while preserving security and making the defect visible.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Correction:&lt;/strong&gt; repair the existing data and enforce the intended rule for future writes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The distinction matters. A tolerant reader is a recovery mechanism, not evidence that duplicates are acceptable.&lt;/p&gt;
&lt;h3 id=&quot;properties-of-a-safe-recovery-path&quot;&gt;Properties of a safe recovery path&lt;/h3&gt;
&lt;p&gt;A useful containment pattern keeps the normal case boring:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;try
{
    return await ReadExpectedSingleAsync(key);
}
catch (InvalidOperationException)
{
    var matches = await ReadBoundedCandidatesAsync(key);
    if (matches.Count &amp;lt; 2)
        throw;

    ReportCollision(matches.Select(x =&amp;gt; x.OpaqueId));
    return ChooseDeterministically(matches);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sketch is intentionally incomplete, but it highlights several important properties.&lt;/p&gt;
&lt;h4 id=&quot;keep-the-fast-path-unchanged&quot;&gt;Keep the fast path unchanged&lt;/h4&gt;
&lt;p&gt;Most lookups are not ambiguous. Do not make every request pay for a corruption-recovery query. Invoke the fallback only after the expected lookup demonstrates the precise failure you are prepared to handle.&lt;/p&gt;
&lt;h4 id=&quot;prove-ambiguity-before-handling-it&quot;&gt;Prove ambiguity before handling it&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;InvalidOperationException&lt;/code&gt; can come from many places. Catching it and returning “not found” would turn an unrelated store failure into a silent wrong answer. Re-query narrowly; if there are not actually multiple matches, rethrow the original fault.&lt;/p&gt;
&lt;h4 id=&quot;choose-deterministically&quot;&gt;Choose deterministically&lt;/h4&gt;
&lt;p&gt;Never accept database enumeration order as a resolution policy. Define explicit ranking from legitimate domain facts, then add a stable final tie-break. The same data must produce the same candidate on every attempt.&lt;/p&gt;
&lt;p&gt;Determinism does not make the selected record correct. It prevents the recovery itself from becoming random.&lt;/p&gt;
&lt;h4 id=&quot;preserve-downstream-verification&quot;&gt;Preserve downstream verification&lt;/h4&gt;
&lt;p&gt;Resolving one candidate must not grant access, approve a transaction, or bypass an eligibility rule. Credential checks, authorization, state validation, and other security decisions still run after resolution. Containment should make the lookup deterministic, not weaken the boundaries that follow it. A legitimate candidate may still lose the ranking, so some affected operations can remain unavailable until the data is repaired.&lt;/p&gt;
&lt;h4 id=&quot;bound-and-observe-the-fallback&quot;&gt;Bound and observe the fallback&lt;/h4&gt;
&lt;p&gt;A pathological key should not cause an unbounded scan during a critical request. Cap diagnostic reads, raise a high-signal operational event, and log only what responders need. Opaque record identifiers may support repair; the sensitive lookup value often does not belong in logs.&lt;/p&gt;
&lt;h3 id=&quot;the-stronger-end-state-lives-in-storage&quot;&gt;The stronger end state lives in storage&lt;/h3&gt;
&lt;p&gt;After containment, the work is not finished. The durable sequence is usually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;inventory collisions
-&amp;gt; decide merge or deletion policy
-&amp;gt; repair existing rows
-&amp;gt; reject or reconcile conflicting writes
-&amp;gt; add the storage constraint
-&amp;gt; remove or narrow temporary recovery code
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For relational data, that often means a unique index aligned with the normalization and null semantics used by the application. The write path should return a useful domain result when the constraint rejects a conflict. A pre-insert lookup can improve the message, but the database remains the concurrency-safe authority.&lt;/p&gt;
&lt;p&gt;There is a real trade-off. Immediate enforcement gives the cleanest future guarantee but can break deployment against historical data. Temporary tolerance contains the exception but adds policy, cannot guarantee later verification will succeed, and can become permanent by neglect. Treat it as migration work with an owner, telemetry, and an exit condition.&lt;/p&gt;
&lt;h3 id=&quot;test-the-invariant-and-the-escape-hatch&quot;&gt;Test the invariant and the escape hatch&lt;/h3&gt;
&lt;p&gt;Focused tests should cover more than “duplicates do not throw.” I would pin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the ordinary single-row path remains untouched;&lt;/li&gt;
&lt;li&gt;selection is stable when candidates arrive in different orders;&lt;/li&gt;
&lt;li&gt;inactive or retired records cannot outrank valid ones;&lt;/li&gt;
&lt;li&gt;unrelated exceptions escape unchanged;&lt;/li&gt;
&lt;li&gt;downstream verification still decides the outcome;&lt;/li&gt;
&lt;li&gt;diagnostics contain repairable identifiers but no sensitive key;&lt;/li&gt;
&lt;li&gt;the candidate read is bounded;&lt;/li&gt;
&lt;li&gt;the future uniqueness constraint rejects a conflicting concurrent write.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The deeper lesson is architectural: reads, writes, and storage must agree on cardinality. If a reader says “exactly one,” make that promise explicit at the boundary able to enforce it. Until then, the code is not reading an invariant. It is discovering, at runtime, whether the hope was true.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27483/your-reader-cannot-be-stricter-than-your-schema</guid>
<pubDate>Thu, 17 Sep 2026 08:20:48 +0000</pubDate>
</item>
<item>
<title>Getting Started with GraphQL in ASP.NET</title>
<link>https://coderlegion.com/24083/getting-started-with-graphql-in-asp-net</link>
<description>&lt;p&gt;&lt;strong&gt;Getting Started with GraphQL in ASP.NET and EF Core&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GraphQL is a modern API technology that allows clients to request exactly the data they need. Unlike traditional REST APIs, GraphQL uses a single endpoint and gives consumers more control over the response structure.&lt;/p&gt;
&lt;p&gt;If you're building ASP.NET applications with Entity Framework Core, adding GraphQL can be a great way to expose your data efficiently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why GraphQL?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With REST APIs, clients often receive more data than they need or must call multiple endpoints to collect related information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GraphQL helps solve these problems by providing:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flexible data retrieval&lt;br&gt;
Fewer API requests&lt;br&gt;
Strongly typed schemas&lt;br&gt;
Better performance for complex data scenarios&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Create a New ASP.NET Project&lt;/p&gt;
&lt;p&gt;Start by creating a Web API project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet new webapi -n GraphQLDemo
cd GraphQLDemo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Install the required packages:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dotnet add package HotChocolate.AspNetCore
dotnet add package HotChocolate.Data.EntityFramework
dotnet add package Microsoft.EntityFrameworkCore.InMemory
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create the Entity&lt;/p&gt;
&lt;p&gt;Let's create a simple Product model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Configure EF Core&lt;/p&gt;
&lt;p&gt;Create a DbContext:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions&amp;lt;AppDbContext&amp;gt; options)
        : base(options)
    {
    }

    public DbSet&amp;lt;Product&amp;gt; Products =&amp;gt; Set&amp;lt;Product&amp;gt;();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a GraphQL Query&lt;/p&gt;
&lt;p&gt;GraphQL queries define the data clients can request.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class Query
{
    public IQueryable&amp;lt;Product&amp;gt; GetProducts(
        [Service] AppDbContext context)
    {
        return context.Products;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Returning IQueryable allows Hot Chocolate to optimize filtering, sorting, and projections.&lt;/p&gt;
&lt;p&gt;Register Services&lt;/p&gt;
&lt;p&gt;In Program.cs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext&amp;lt;AppDbContext&amp;gt;(options =&amp;gt;
    options.UseInMemoryDatabase(&quot;ProductsDb&quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;builder.Services
    .AddGraphQLServer()
    .AddQueryType&amp;lt;Query&amp;gt;();

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService&amp;lt;AppDbContext&amp;gt;();

    db.Products.AddRange(
        new Product { Id = 1, Name = &quot;Laptop&quot;, Price = 999.99m },
        new Product { Id = 2, Name = &quot;Mouse&quot;, Price = 29.99m }
    );

    db.SaveChanges();
}

app.MapGraphQL();

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run Your First Query&lt;/p&gt;
&lt;p&gt;Navigate to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://localhost:5001/graphql
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Execute:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query {
  products {
    id
    name
    price
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data&quot;: {
    &quot;products&quot;: [
      {
        &quot;id&quot;: 1,
        &quot;name&quot;: &quot;Laptop&quot;,
        &quot;price&quot;: 999.99
      },
      {
        &quot;id&quot;: 2,
        &quot;name&quot;: &quot;Mouse&quot;,
        &quot;price&quot;: 29.99
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Enable Filtering and Sorting&lt;/p&gt;
&lt;p&gt;One of the best features of Hot Chocolate is built-in filtering and sorting.&lt;/p&gt;
&lt;p&gt;Update the query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using HotChocolate.Data;

public class Query
{
    [UseFiltering]
    [UseSorting]
    public IQueryable&amp;lt;Product&amp;gt; GetProducts(
        [Service] AppDbContext context)
    {
        return context.Products;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Register the features:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;builder.Services
    .AddGraphQLServer()
    .AddQueryType&amp;lt;Query&amp;gt;()
    .AddFiltering()
    .AddSorting();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now clients can run queries like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;query {
  products(
    where: { price: { gt: 100 } }
    order: { price: DESC }
  ) {
    name
    price
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Conclusion&lt;/p&gt;
&lt;p&gt;GraphQL and EF Core work very well together in ASP.NET applications. With Hot Chocolate, you can expose database entities through GraphQL with minimal configuration while gaining powerful features like filtering, sorting, and projections.&lt;/p&gt;
&lt;p&gt;If you're already using EF Core, adding GraphQL is an easy way to make your APIs more flexible and developer-friendly. Start with simple queries, then move on to mutations, authentication, and real database providers like SQL Server or PostgreSQL.&lt;/p&gt;
&lt;p&gt;Happy coding! 🚀&lt;/p&gt;
&lt;p&gt;References&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GraphQL Official Documentation, Learn GraphQL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://graphql.org/learn/&quot;&gt;https://graphql.org/learn/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GraphQL Official Documentation, Queries&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://graphql.org/learn/queries/&quot;&gt;https://graphql.org/learn/queries/&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hot Chocolate Documentation, Getting Started with GraphQL in .NET&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://chillicream.com/docs/hotchocolate/get-started-with-graphql-in-net-core&quot;&gt;https://chillicream.com/docs/hotchocolate/get-started-with-graphql-in-net-core&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hot Chocolate Documentation, GraphQL Server for .NET&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://chillicream.com/docs/hotchocolate&quot;&gt;https://chillicream.com/docs/hotchocolate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24083/getting-started-with-graphql-in-asp-net</guid>
<pubDate>Tue, 04 Aug 2026 08:44:29 +0000</pubDate>
</item>
<item>
<title>EF Core Mapping for DDD — for Dummies</title>
<link>https://coderlegion.com/22529/ef-core-mapping-for-ddd-for-dummies</link>
<description>&lt;p&gt;You built a nice domain model with rules and private properties — but &lt;strong&gt;EF Core&lt;/strong&gt; wants simple classes with public &lt;code&gt;get; set;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Don't worry. This guide shows you how to &lt;strong&gt;save DDD models to the database&lt;/strong&gt; without messing up your code. Short sentences. Real examples. No jargon overload.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Before you start:&lt;/strong&gt; know basic C# and what EF Core is. &lt;strong&gt;DDD&lt;/strong&gt; just means putting business rules inside your C# classes instead of scattering them everywhere.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-what-s-the-problem-&quot;&gt;1 · What's the Problem?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;DDD (Domain-Driven Design)&lt;/strong&gt; is a fancy name for a simple idea: put your business rules inside your C# classes.&lt;/p&gt;
&lt;p&gt;For example, &lt;em&gt;&quot;you can't ship an empty order&quot;&lt;/em&gt; should live on the &lt;code&gt;Order&lt;/code&gt; class — not in some random service file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core&lt;/strong&gt; is the tool that saves your C# objects to SQL Server. It works great with simple classes that have public &lt;code&gt;{ get; set; }&lt;/code&gt; on everything.&lt;/p&gt;
&lt;p&gt;But DDD classes are different — they use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private set&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;factory methods like &lt;code&gt;Order.Create()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;hidden lists&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's where &lt;strong&gt;mapping&lt;/strong&gt; comes in: a separate config file that tells EF Core &lt;em&gt;&quot;here's how to read and write my class&quot;&lt;/em&gt; — without changing the class itself.&lt;/p&gt;
&lt;h4 id=&quot;bad-model-vs-good-model&quot;&gt;Bad model vs good model&lt;/h4&gt;
&lt;p&gt;The bad one is easy for EF Core but anyone can break the rules. The good one protects itself — EF Core just needs a little help to save it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// ❌ BAD — no rules, anyone can change anything
public class Order
{
    public Guid Id { get; set; }
    public string Status { get; set; } = &quot;&quot;;
    public List&amp;lt;OrderLine&amp;gt; Lines { get; set; } = [];
    // Someone can write: order.Status = &quot;Shipped&quot; on an empty order. Oops.
}

// ✅ GOOD — rules live here, properties are protected
public sealed class Order
{
    public OrderId Id { get; private set; }
    public OrderStatus Status { get; private set; }
    private readonly List&amp;lt;OrderLine&amp;gt; _lines = [];
    public IReadOnlyCollection&amp;lt;OrderLine&amp;gt; Lines =&amp;gt; _lines.AsReadOnly();

    private Order() { } // EF Core needs this empty constructor — you never call it

    public static Order Create(CustomerId customerId) { /* ... */ }
    public void AddLine(ProductId productId, int qty, Money price) { /* checks rules */ }
    public void Place() { /* checks rules before shipping */ }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;keep-database-stuff-out-of-your-business-code&quot;&gt;Keep database stuff out of your business code&lt;/h4&gt;
&lt;p&gt;Your &lt;strong&gt;Domain&lt;/strong&gt; project (business rules) should never mention EF Core or SQL Server.&lt;/p&gt;
&lt;p&gt;All the &lt;em&gt;&quot;how to save to the database&quot;&lt;/em&gt; code goes in a separate &lt;strong&gt;Infrastructure&lt;/strong&gt; project.&lt;/p&gt;
&lt;p&gt;Think of it like this: &lt;strong&gt;your business code shouldn't know a database exists.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Domain/Billing.Domain.csproj
//   → NO EntityFrameworkCore package. Just plain C#.

// Infrastructure/Billing.Infrastructure.csproj
//   → Has EF Core. References Domain. Contains mapping files.

// Example mapping file:
// Infrastructure/Persistence/Configurations/OrderConfiguration.cs
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;💡 &lt;strong&gt;Think of it like a translator.&lt;/strong&gt; Your C# class speaks &quot;business rules.&quot; The database speaks &quot;tables and columns.&quot; The mapping file is the translator — and it lives in Infrastructure, not in your business code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-where-does-the-code-go-&quot;&gt;2 · Where Does the Code Go?&lt;/h3&gt;
&lt;p&gt;Split your solution into folders (projects):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Project &lt;/th&gt;&lt;th&gt;What goes here&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Domain&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Business classes — NO EF Core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Application&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&quot;Place order&quot;, &quot;Cancel order&quot; — NO EF Core&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;EF Core and mapping files&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Only the &lt;strong&gt;&quot;main&quot; object&lt;/strong&gt; of each group gets a &lt;code&gt;DbSet&lt;/code&gt; in EF Core. We'll explain that group (called an &lt;strong&gt;aggregate&lt;/strong&gt;) in the next section.&lt;/p&gt;
&lt;h4 id=&quot;folder-layout&quot;&gt;Folder layout&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;src/
├── Billing.Domain/              # Business rules — NO EF Core here
│   ├── Orders/
│   │   ├── Order.cs             # The &quot;boss&quot; object (aggregate root)
│   │   ├── OrderLine.cs         # A line item inside an order
│   │   └── OrderId.cs           # A safe ID type (section 6)
│   └── Common/
│       └── Money.cs             # A small object with no ID (section 4)
│
├── Billing.Application/         # Handlers — NO EF Core
│
└── Billing.Infrastructure/      # EF Core lives HERE
    └── Persistence/
        ├── ApplicationDbContext.cs
        └── Configurations/
            └── OrderConfiguration.cs   # &quot;How to save Order to SQL&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;dbcontext-only-register-the-boss-objects&quot;&gt;DbContext — only register the &quot;boss&quot; objects&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;DbSet&lt;/code&gt; means &lt;em&gt;&quot;EF Core can load and save this type directly.&quot;&lt;/em&gt; Only the main object (like &lt;code&gt;Order&lt;/code&gt;) gets one. Line items (&lt;code&gt;OrderLine&lt;/code&gt;) are saved automatically when you save the order.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class ApplicationDbContext(DbContextOptions&amp;lt;ApplicationDbContext&amp;gt; options)
    : DbContext(options)
{
    // ✅ YES — Order is the &quot;boss&quot; of its group
    public DbSet&amp;lt;Order&amp;gt; Orders =&amp;gt; Set&amp;lt;Order&amp;gt;();
    public DbSet&amp;lt;Customer&amp;gt; Customers =&amp;gt; Set&amp;lt;Customer&amp;gt;();

    // ❌ NO — OrderLine is saved through Order, not on its own
    // public DbSet&amp;lt;OrderLine&amp;gt; OrderLines =&amp;gt; ...  DON'T DO THIS

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Finds all mapping files automatically
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-the-main-object-aggregate-root-&quot;&gt;3 · The Main Object (Aggregate Root)&lt;/h3&gt;
&lt;p&gt;An &lt;strong&gt;aggregate&lt;/strong&gt; is a group of objects that belong together — like an &lt;code&gt;Order&lt;/code&gt; and its &lt;code&gt;OrderLine&lt;/code&gt; items.&lt;/p&gt;
&lt;p&gt;You always load and save the whole group through one &lt;strong&gt;&quot;boss&quot; object&lt;/strong&gt; called the &lt;strong&gt;aggregate root&lt;/strong&gt;. In our example, &lt;code&gt;Order&lt;/code&gt; is the boss.&lt;/p&gt;
&lt;p&gt;EF Core needs a private empty constructor (&lt;code&gt;private Order() { }&lt;/code&gt;) to rebuild objects from the database. Your app code should &lt;strong&gt;never&lt;/strong&gt; call it — always use &lt;code&gt;Order.Create()&lt;/code&gt; instead.&lt;/p&gt;
&lt;h4 id=&quot;the-order-class-business-rules-inside&quot;&gt;The Order class — business rules inside&lt;/h4&gt;
&lt;p&gt;Notice: you can't change status directly. You call &lt;code&gt;Place()&lt;/code&gt; and the class checks the rules for you.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Domain/Orders/Order.cs
public sealed class Order : BaseEntity
{
    public OrderId Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public Money Total { get; private set; } = Money.Zero;

    private readonly List&amp;lt;OrderLine&amp;gt; _lines = [];
    public IReadOnlyCollection&amp;lt;OrderLine&amp;gt; Lines =&amp;gt; _lines.AsReadOnly();

    private Order() { } // EF Core only — your code never calls this

    public static Order Create(CustomerId customerId)
    {
        var order = new Order
        {
            Id = OrderId.New(),
            CustomerId = customerId,
            Status = OrderStatus.Draft
        };
        order.Raise(new OrderCreatedEvent(order.Id));
        return order;
    }

    public void AddLine(ProductId productId, int qty, Money unitPrice)
    {
        if (Status != OrderStatus.Draft)
            throw new DomainException(&quot;Cannot modify a placed order.&quot;);
        if (qty &amp;lt;= 0)
            throw new DomainException(&quot;Quantity must be positive.&quot;);

        _lines.Add(new OrderLine(productId, qty, unitPrice));
        Total = Money.Sum(_lines.Select(l =&amp;gt; l.LineTotal));
    }

    public void Place()
    {
        if (_lines.Count == 0) throw new DomainException(&quot;Empty order.&quot;);
        Status = OrderStatus.Placed;
        Raise(new OrderPlacedEvent(Id, CustomerId, Total));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-mapping-file-tells-ef-core-how-to-save-order&quot;&gt;The mapping file — tells EF Core how to save Order&lt;/h4&gt;
&lt;p&gt;This file lives in Infrastructure. It says: table name, how to save IDs, how to save Money, how to save line items, and what to ignore.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Infrastructure/Persistence/Configurations/OrderConfiguration.cs
public sealed class OrderConfiguration : IEntityTypeConfiguration&amp;lt;Order&amp;gt;
{
    public void Configure(EntityTypeBuilder&amp;lt;Order&amp;gt; builder)
    {
        builder.ToTable(&quot;Orders&quot;);

        builder.HasKey(o =&amp;gt; o.Id);
        builder.Property(o =&amp;gt; o.Id)
            .HasConversion(id =&amp;gt; id.Value, v =&amp;gt; new OrderId(v));

        builder.Property(o =&amp;gt; o.CustomerId)
            .HasConversion(id =&amp;gt; id.Value, v =&amp;gt; new CustomerId(v));

        builder.Property(o =&amp;gt; o.Status)
            .HasConversion&amp;lt;string&amp;gt;()
            .HasMaxLength(32);

        // Money is a small object — save it as extra columns (section 4)
        builder.OwnsOne(o =&amp;gt; o.Total, money =&amp;gt;
        {
            money.Property(m =&amp;gt; m.Amount).HasColumnName(&quot;TotalAmount&quot;).HasPrecision(18, 2);
            money.Property(m =&amp;gt; m.Currency).HasColumnName(&quot;TotalCurrency&quot;).HasMaxLength(3);
        });

        // Line items — saved in a separate table, linked to Order
        builder.HasMany&amp;lt;OrderLine&amp;gt;(&quot;_lines&quot;)
            .WithOne()
            .HasForeignKey(l =&amp;gt; l.OrderId)
            .OnDelete(DeleteBehavior.Cascade);

        // Events are in-memory only — don't save to database
        builder.Ignore(o =&amp;gt; o.DomainEvents);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Watch out:&lt;/strong&gt; don't make setters public just to fix an EF error. If EF Core complains about a &lt;code&gt;private set&lt;/code&gt;, fix it in the mapping file — don't weaken your class.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-small-objects-with-no-id-value-objects-&quot;&gt;4 · Small Objects With No ID (Value Objects)&lt;/h3&gt;
&lt;p&gt;Some objects don't need their own ID.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Money&lt;/code&gt; is just an amount and a currency.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Address&lt;/code&gt; is just street, city, zip.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two &lt;code&gt;Money(10, &quot;EUR&quot;)&lt;/code&gt; objects are the same thing — that's a &lt;strong&gt;value object&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;EF Core saves them as &lt;strong&gt;extra columns&lt;/strong&gt; on the parent table (not a separate table). You use &lt;code&gt;OwnsOne&lt;/code&gt; or, in .NET 8+, the simpler &lt;code&gt;ComplexType&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&quot;what-a-value-object-looks-like&quot;&gt;What a value object looks like&lt;/h4&gt;
&lt;p&gt;No Id property. Just data. You can add validation in the constructor — like &lt;em&gt;&quot;street can't be empty.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Money — just amount + currency, nothing else
public sealed record Money(decimal Amount, string Currency)
{
    public static Money Zero =&amp;gt; new(0m, &quot;EUR&quot;);

    public static Money Sum(IEnumerable&amp;lt;Money&amp;gt; items)
    {
        var list = items.ToList();
        if (list.Count == 0) return Zero;
        return new Money(list.Sum(m =&amp;gt; m.Amount), list[0].Currency);
    }
}

// Address — another common value object
public sealed record Address(string Street, string City, string PostalCode, string Country)
{
    public Address(string street, string city, string postalCode, string country)
    {
        if (string.IsNullOrWhiteSpace(street)) throw new DomainException(&quot;Street required.&quot;);
        Street = street.Trim();
        City = city.Trim();
        PostalCode = postalCode.Trim();
        Country = country.Trim();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;ownsone-save-it-as-extra-columns&quot;&gt;OwnsOne — save it as extra columns&lt;/h4&gt;
&lt;p&gt;The address columns go on the Customers table — no new table needed. You pick the column names.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Creates columns on Customers table:
//   ShipStreet, ShipCity, ShipPostalCode, ShipCountry

builder.OwnsOne(c =&amp;gt; c.ShippingAddress, addr =&amp;gt;
{
    addr.Property(a =&amp;gt; a.Street).HasColumnName(&quot;ShipStreet&quot;).HasMaxLength(200);
    addr.Property(a =&amp;gt; a.City).HasColumnName(&quot;ShipCity&quot;).HasMaxLength(100);
    addr.Property(a =&amp;gt; a.PostalCode).HasColumnName(&quot;ShipPostalCode&quot;).HasMaxLength(20);
    addr.Property(a =&amp;gt; a.Country).HasColumnName(&quot;ShipCountry&quot;).HasMaxLength(60);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;complextype-even-easier-net-8-&quot;&gt;ComplexType — even easier (.NET 8+)&lt;/h4&gt;
&lt;p&gt;If you're on .NET 8 or later, slap &lt;code&gt;[ComplexType]&lt;/code&gt; on the record and use &lt;code&gt;ComplexProperty&lt;/code&gt; in the mapping. Less typing, same result.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;[ComplexType]
public sealed record Money(decimal Amount, string Currency) { /* ... */ }

builder.ComplexProperty(o =&amp;gt; o.Total, money =&amp;gt;
{
    money.Property(m =&amp;gt; m.Amount).HasColumnName(&quot;TotalAmount&quot;).HasPrecision(18, 2);
    money.Property(m =&amp;gt; m.Currency).HasColumnName(&quot;TotalCurrency&quot;).HasMaxLength(3);
});

// Which one to pick?
//   OwnsOne     → works on older EF Core, more options
//   ComplexType   → .NET 8+, simpler, always extra columns
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;ownsmany-a-list-of-small-objects-needs-its-own-table&quot;&gt;OwnsMany — a list of small objects needs its own table&lt;/h4&gt;
&lt;p&gt;Usually one value object = extra columns. But if you have a &lt;em&gt;list&lt;/em&gt; of them (like discount codes), EF Core creates a small separate table.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class Order : BaseEntity
{
    private readonly List&amp;lt;DiscountCode&amp;gt; _discounts = [];
    public IReadOnlyCollection&amp;lt;DiscountCode&amp;gt; Discounts =&amp;gt; _discounts.AsReadOnly();
}

builder.OwnsMany(o =&amp;gt; o.Discounts, d =&amp;gt;
{
    d.ToTable(&quot;OrderDiscounts&quot;);
    d.WithOwner().HasForeignKey(&quot;OrderId&quot;);
    d.Property(x =&amp;gt; x.Code).HasMaxLength(20);
    d.Property(x =&amp;gt; x.Percentage).HasPrecision(5, 2);
    d.HasKey(&quot;OrderId&quot;, nameof(DiscountCode.Code));
});
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-lists-inside-an-object-backing-fields-&quot;&gt;5 · Lists Inside an Object (Backing Fields)&lt;/h3&gt;
&lt;p&gt;Don't expose a public &lt;code&gt;List&amp;lt;OrderLine&amp;gt;&lt;/code&gt; that anyone can add to or clear.&lt;/p&gt;
&lt;p&gt;Instead:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep a &lt;strong&gt;private list&lt;/strong&gt; (&lt;code&gt;_lines&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Expose a &lt;strong&gt;read-only view&lt;/strong&gt; (&lt;code&gt;Lines&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Changes go through methods like &lt;code&gt;AddLine()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;EF Core can still read and write your private list — you just tell the mapping file where it is.&lt;/p&gt;
&lt;h4 id=&quot;private-list-public-read-only-view&quot;&gt;Private list, public read-only view&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// In your Order class:
private readonly List&amp;lt;OrderLine&amp;gt; _lines = [];
public IReadOnlyCollection&amp;lt;OrderLine&amp;gt; Lines =&amp;gt; _lines.AsReadOnly();

public void AddLine(ProductId productId, int qty, Money unitPrice)
{
    _lines.Add(new OrderLine(productId, qty, unitPrice));
}

// In your mapping file — point EF Core at &quot;_lines&quot;
builder.HasMany&amp;lt;OrderLine&amp;gt;(&quot;_lines&quot;)
    .WithOne()
    .HasForeignKey(l =&amp;gt; l.OrderId)
    .OnDelete(DeleteBehavior.Cascade);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;orderline-belongs-inside-order-not-on-its-own&quot;&gt;OrderLine — belongs inside Order, not on its own&lt;/h4&gt;
&lt;p&gt;OrderLine is not the &quot;boss.&quot; You never load it directly from the database — you load the Order and its lines come with it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class OrderLine
{
    public OrderLineId Id { get; private set; }
    public OrderId OrderId { get; private set; }
    public ProductId ProductId { get; private set; }
    public int Quantity { get; private set; }
    public Money UnitPrice { get; private set; }
    public Money LineTotal =&amp;gt; new(Quantity * UnitPrice.Amount, UnitPrice.Currency);

    private OrderLine() { } // EF Core only

    internal OrderLine(ProductId productId, int qty, Money unitPrice)
    {
        Id = OrderLineId.New();
        ProductId = productId;
        Quantity = qty;
        UnitPrice = unitPrice;
    }
}

// Mapping file for OrderLine
public sealed class OrderLineConfiguration : IEntityTypeConfiguration&amp;lt;OrderLine&amp;gt;
{
    public void Configure(EntityTypeBuilder&amp;lt;OrderLine&amp;gt; builder)
    {
        builder.ToTable(&quot;OrderLines&quot;);
        builder.HasKey(l =&amp;gt; l.Id);
        builder.Property(l =&amp;gt; l.Id)
            .HasConversion(id =&amp;gt; id.Value, v =&amp;gt; new OrderLineId(v));
        builder.OwnsOne(l =&amp;gt; l.UnitPrice, m =&amp;gt; { /* column names */ });
        builder.Ignore(l =&amp;gt; l.LineTotal); // calculated — don't save to DB
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ &lt;strong&gt;Watch out:&lt;/strong&gt; don't query line items on their own. Don't add &lt;code&gt;DbSet&amp;lt;OrderLine&amp;gt;&lt;/code&gt;. Always load the &lt;code&gt;Order&lt;/code&gt; first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-safe-ids-no-more-guid-mix-ups-&quot;&gt;6 · Safe IDs (No More Guid Mix-ups)&lt;/h3&gt;
&lt;p&gt;If everything is a &lt;code&gt;Guid&lt;/code&gt;, you can accidentally pass a customer ID where an order ID is expected — and C# won't warn you.&lt;/p&gt;
&lt;p&gt;The fix: wrap each ID in its own small type (&lt;code&gt;OrderId&lt;/code&gt;, &lt;code&gt;CustomerId&lt;/code&gt;). Swap them by mistake and the &lt;strong&gt;compiler catches it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;EF Core still saves them as plain &lt;code&gt;Guid&lt;/code&gt; in SQL — you just add a one-line conversion in the mapping file.&lt;/p&gt;
&lt;h4 id=&quot;wrap-your-guids&quot;&gt;Wrap your Guids&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public readonly record struct OrderId(Guid Value)
{
    public static OrderId New() =&amp;gt; new(Guid.NewGuid());
}

public readonly record struct CustomerId(Guid Value);
public readonly record struct ProductId(Guid Value);

// This won't compile — you swapped the IDs:
// void Ship(OrderId orderId, CustomerId customerId) { }
// Ship(customerId, orderId); // ❌ compiler error — saved you!
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;tell-ef-core-how-to-save-them&quot;&gt;Tell EF Core how to save them&lt;/h4&gt;
&lt;p&gt;Write this helper once, then use one line per ID in every mapping file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public static class StronglyTypedIdExtensions
{
    public static PropertyBuilder&amp;lt;TId&amp;gt; HasStronglyTypedIdConversion&amp;lt;TId&amp;gt;(
        this PropertyBuilder&amp;lt;TId&amp;gt; propertyBuilder)
        where TId : struct
    {
        var converter = new ValueConverter&amp;lt;TId, Guid&amp;gt;(
            id =&amp;gt; (Guid)typeof(TId).GetProperty(&quot;Value&quot;)!.GetValue(id)!,
            value =&amp;gt; (TId)Activator.CreateInstance(typeof(TId), value)!);

        return propertyBuilder.HasConversion(converter);
    }
}

// In OrderConfiguration — one line each:
builder.Property(o =&amp;gt; o.Id).HasStronglyTypedIdConversion&amp;lt;OrderId&amp;gt;();
builder.Property(o =&amp;gt; o.CustomerId).HasStronglyTypedIdConversion&amp;lt;CustomerId&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-the-mapping-file-putting-it-all-together-&quot;&gt;7 · The Mapping File (Putting It All Together)&lt;/h3&gt;
&lt;p&gt;One mapping class per entity, in Infrastructure.&lt;/p&gt;
&lt;p&gt;It answers: what table? what columns? how to save IDs and value objects? what to ignore?&lt;/p&gt;
&lt;p&gt;Here's a complete example for &lt;code&gt;Order&lt;/code&gt; — use it as a template.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class OrderConfiguration : IEntityTypeConfiguration&amp;lt;Order&amp;gt;
{
    public void Configure(EntityTypeBuilder&amp;lt;Order&amp;gt; builder)
    {
        builder.ToTable(&quot;Orders&quot;, schema: &quot;billing&quot;);

        // ID
        builder.HasKey(o =&amp;gt; o.Id);
        builder.Property(o =&amp;gt; o.Id).HasStronglyTypedIdConversion&amp;lt;OrderId&amp;gt;();

        // Link to Customer — just store the ID, no Customer object here
        builder.Property(o =&amp;gt; o.CustomerId).HasStronglyTypedIdConversion&amp;lt;CustomerId&amp;gt;();

        // Status as readable text in SQL (&quot;Draft&quot;, &quot;Placed&quot;, ...)
        builder.Property(o =&amp;gt; o.Status)
            .HasConversion&amp;lt;string&amp;gt;()
            .HasMaxLength(32)
            .IsRequired();

        // Money value object → extra columns
        builder.OwnsOne(o =&amp;gt; o.Total, money =&amp;gt;
        {
            money.Property(m =&amp;gt; m.Amount).HasColumnName(&quot;TotalAmount&quot;).HasPrecision(18, 2);
            money.Property(m =&amp;gt; m.Currency).HasColumnName(&quot;TotalCurrency&quot;).HasMaxLength(3);
        });

        // Line items via private _lines field
        builder.HasMany&amp;lt;OrderLine&amp;gt;(&quot;_lines&quot;)
            .WithOne()
            .HasForeignKey(l =&amp;gt; l.OrderId)
            .OnDelete(DeleteBehavior.Cascade);

        // Don't save events to the database
        builder.Ignore(o =&amp;gt; o.DomainEvents);

        // Speed up common searches
        builder.HasIndex(o =&amp;gt; o.CustomerId);
        builder.HasIndex(o =&amp;gt; o.Status);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-goes-where-&quot;&gt;What goes where?&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;✅ Put in mapping file (Infrastructure):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Table name&lt;/li&gt;
&lt;li&gt;How to save IDs, enums, value objects&lt;/li&gt;
&lt;li&gt;How to save lists (&lt;code&gt;_lines&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Ignore events and calculated properties&lt;/li&gt;
&lt;li&gt;Column sizes (&lt;code&gt;MaxLength&lt;/code&gt;, &lt;code&gt;Precision&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;✅ OK to leave alone (EF guesses correctly):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt; properties&lt;/li&gt;
&lt;li&gt;Column names same as property names&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;❌ Never put on Domain classes:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;[Table]&lt;/code&gt;, &lt;code&gt;[Column]&lt;/code&gt;, &lt;code&gt;[ForeignKey]&lt;/code&gt; attributes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;public setters&lt;/code&gt; &quot;because EF needs them&quot;&lt;/li&gt;
&lt;li&gt;Any &lt;code&gt;using Microsoft.EntityFrameworkCore;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-saving-loading-amp-mistakes-to-avoid&quot;&gt;8 · Saving, Loading &amp;amp; Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;A &lt;strong&gt;repository&lt;/strong&gt; is a thin wrapper that loads and saves your &quot;boss&quot; objects (&lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;Customer&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;Your handler calls &lt;code&gt;GetById&lt;/code&gt;, changes the object through its methods, then calls &lt;code&gt;SaveChanges&lt;/code&gt;. That's it.&lt;/p&gt;
&lt;h4 id=&quot;how-to-load-change-and-save&quot;&gt;How to load, change, and save&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// Interface — only works with Order, not OrderLine
public interface IOrderRepository
{
    Task&amp;lt;Order?&amp;gt; GetByIdAsync(OrderId id, CancellationToken ct);
    void Add(Order order);
}

// Implementation
public sealed class OrderRepository(ApplicationDbContext db) : IOrderRepository
{
    public Task&amp;lt;Order?&amp;gt; GetByIdAsync(OrderId id, CancellationToken ct) =&amp;gt;
        db.Orders
            .Include(o =&amp;gt; o.Lines)   // don't forget this — or lines won't load!
            .FirstOrDefaultAsync(o =&amp;gt; o.Id == id, ct);

    public void Add(Order order) =&amp;gt; db.Orders.Add(order);
}

// Typical flow in a handler:
var order = await repo.GetByIdAsync(cmd.OrderId, ct)
    ?? throw new NotFoundException();
order.Place();                    // business rules run here
await db.SaveChangesAsync(ct);    // EF saves changes to SQL
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;domain-events-memory-only-not-in-the-database&quot;&gt;Domain events — memory only, not in the database&lt;/h4&gt;
&lt;p&gt;When something important happens (order placed), the class raises an event. These live in memory until &lt;code&gt;SaveChanges&lt;/code&gt; sends them out. &lt;strong&gt;Never save them as a table column.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;// In mapping file — always ignore events:
builder.Ignore(o =&amp;gt; o.DomainEvents);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-lookup-i-have-x-how-do-i-map-it-&quot;&gt;Quick lookup — &quot;I have X, how do I map it?&quot;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;You have… &lt;/th&gt;&lt;th&gt;Map it with…&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Main object (Order) &lt;/td&gt;&lt;td&gt;&lt;code&gt;DbSet&lt;/code&gt; + mapping file&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Item inside Order (Line) &lt;/td&gt;&lt;td&gt;&lt;code&gt;HasMany(&quot;_lines&quot;)&lt;/code&gt;, no DbSet&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Money, Address (no ID) &lt;/td&gt;&lt;td&gt;&lt;code&gt;OwnsOne&lt;/code&gt; or &lt;code&gt;ComplexType&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;List of small objects &lt;/td&gt;&lt;td&gt;&lt;code&gt;OwnsMany&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OrderId, CustomerId &lt;/td&gt;&lt;td&gt;&lt;code&gt;HasStronglyTypedIdConversion&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;private set&lt;/code&gt; on property &lt;/td&gt;&lt;td&gt;works fine — don't change it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;private Order() { }&lt;/code&gt; &lt;/td&gt;&lt;td&gt;EF Core needs it, your code doesn't&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DomainEvents &lt;/td&gt;&lt;td&gt;&lt;code&gt;builder.Ignore(...)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Calculated property &lt;/td&gt;&lt;td&gt;&lt;code&gt;builder.Ignore(...)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Status enum &lt;/td&gt;&lt;td&gt;save as string&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Link to another aggregate &lt;/td&gt;&lt;td&gt;store ID only, no &lt;code&gt;Customer&lt;/code&gt; property&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-mistakes-beginners-make&quot;&gt;5 mistakes beginners make&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Making everything public set&lt;/strong&gt; — fix it in the mapping file, not the class.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DbSet for line items&lt;/strong&gt; — only the boss object gets a DbSet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Putting &lt;code&gt;[Table]&lt;/code&gt; on domain classes&lt;/strong&gt; — mapping files go in Infrastructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Customer property on Order&lt;/strong&gt; — store &lt;code&gt;CustomerId&lt;/code&gt; only, not the whole Customer object.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forgetting &lt;code&gt;.Include(o =&amp;gt; o.Lines)&lt;/code&gt;&lt;/strong&gt; — line items won't load and you'll wonder why the list is empty.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-s-next-&quot;&gt;What's next?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Clean Architecture &amp;amp; CQRS&lt;/strong&gt; — how the whole app fits together&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EF Core In Depth&lt;/strong&gt; — migrations, speed tips, production setup&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing .NET&lt;/strong&gt; — test your save/load code with a real database&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Full tutorial with more examples: [www.web-partner.gr)&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Written by &lt;a rel=&quot;nofollow&quot; href=&quot;http://www.web-partner.gr&quot;&gt;Spyros Ponaris&lt;/a&gt; — .NET Tech Lead, Athens.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22529/ef-core-mapping-for-ddd-for-dummies</guid>
<pubDate>Mon, 13 Jul 2026 19:04:55 +0000</pubDate>
</item>
<item>
<title>SQL Server and Azure SQL Updates with EF Core 10</title>
<link>https://coderlegion.com/9100/sql-server-and-azure-sql-updates-with-ef-core-10</link>
<description>&lt;p&gt;EF Core 10 was released in November 2025 together with .NET 10 and is an LTS release supported until 2028. This version mainly focuses on better alignment with new SQL Server capabilities rather than introducing radically new concepts.&lt;/p&gt;
&lt;p&gt;Below are a few changes that are worth noting from a practical point of view.&lt;/p&gt;
&lt;h3 id=&quot;vector-data-type-support&quot;&gt;Vector data type support&lt;/h3&gt;
&lt;p&gt;EF Core 10 adds direct support for the &lt;strong&gt;vector&lt;/strong&gt; data type introduced in SQL Server 2025 and Azure SQL. This makes it possible to store embeddings directly in the database without additional workarounds.&lt;/p&gt;
&lt;p&gt;A simple entity example looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class Blog
{
    public int Id { get; set; }

    [Column(TypeName = &quot;vector(1536)&quot;)]
    public SqlVector&amp;lt;float&amp;gt; Embedding { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the embedding is generated on the application side, it can be saved using the standard EF workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;context.Blogs.Add(new Blog
{
    Embedding = new SqlVector&amp;lt;float&amp;gt;(embedding)
});

await context.SaveChangesAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Similarity queries can also be written directly in LINQ using &lt;code&gt;EF.Functions.VectorDistance&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var blogs = await context.Blogs
    .OrderBy(b =&amp;gt; EF.Functions.VectorDistance(&quot;cosine&quot;, b.Embedding, queryVector))
    .Take(3)
    .ToListAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful in scenarios such as semantic search or basic recommendation logic, without needing custom SQL or external query layers.&lt;/p&gt;
&lt;h3 id=&quot;native-json-data-type-mapping&quot;&gt;Native JSON data type mapping&lt;/h3&gt;
&lt;p&gt;With SQL Server 2025, JSON can now be stored using a dedicated &lt;strong&gt;json&lt;/strong&gt; data type. EF Core 10 supports this natively and uses it automatically when the compatibility level allows it.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public class Blog
{
    public int Id { get; set; }
    public string[] Tags { get; set; }
    public BlogDetails Details { get; set; }
}

public class BlogDetails
{
    public int Viewers { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Model configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;modelBuilder.Entity&amp;lt;Blog&amp;gt;()
    .ComplexProperty(b =&amp;gt; b.Details, b =&amp;gt; b.ToJson());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this setup, both &lt;code&gt;Tags&lt;/code&gt; and &lt;code&gt;Details&lt;/code&gt; are created as &lt;code&gt;json&lt;/code&gt; columns in the database.&lt;/p&gt;
&lt;p&gt;Querying remains straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;var blogs = await context.Blogs
    .Where(b =&amp;gt; b.Details.Viewers &amp;gt; 3)
    .ToListAsync();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;EF Core translates this into SQL using the appropriate JSON functions. For domains that already rely on JSON-like structures, this results in cleaner models and fewer mapping concerns.&lt;/p&gt;
&lt;h3 id=&quot;naming-default-constraints&quot;&gt;Naming default constraints&lt;/h3&gt;
&lt;p&gt;EF Core 10 allows explicitly naming default constraints instead of relying on database-generated names. This can help reduce unnecessary changes in migrations and make schema updates easier to track.&lt;/p&gt;
&lt;p&gt;Example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;modelBuilder.Entity&amp;lt;Post&amp;gt;()
    .Property(p =&amp;gt; p.CreatedDate)
    .HasDefaultValueSql(&quot;GETDATE()&quot;, &quot;DF_Post_CreatedDate&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It’s also possible to enable automatic naming across the model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;modelBuilder.UseNamedDefaultConstraints();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This change is mostly about maintainability, especially in projects with long migration histories.&lt;/p&gt;
&lt;h3 id=&quot;closing-thoughts&quot;&gt;Closing thoughts&lt;/h3&gt;
&lt;p&gt;EF Core 10 feels like a refinement release rather than a disruptive one. The added support for vector and JSON data types mainly improves how EF integrates with newer SQL Server features, while smaller changes like constraint naming help with long-term maintenance.&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/9100/sql-server-and-azure-sql-updates-with-ef-core-10</guid>
<pubDate>Tue, 30 Dec 2025 21:06:54 +0000</pubDate>
</item>
<item>
<title>Why Most .NET Developers Misuse Records — and What It’s Costing Them</title>
<link>https://coderlegion.com/3821/why-most-net-developers-misuse-records-and-what-its-costing-them</link>
<description>&lt;p&gt; Most .NET devs are misusing records—and it's costing them more than they think. Memory overhead. Identity tracking bugs. EF Core confusion. &lt;/p&gt;
&lt;p&gt;I broke it all down in my latest Medium post:  &lt;strong&gt;Why Most .NET Developers Misuse Records and What It’s Costing Them&lt;/strong&gt;  &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@yaseer.arafat/why-most-net-developers-misuse-records-and-what-its-costing-them-afb17f9d05ae&quot;&gt;Read here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; Inside:&lt;/p&gt;
&lt;p&gt;Why value equality isn't always the win you expect&lt;/p&gt;
&lt;p&gt;How EF Core gets tripped up by &lt;code&gt;records&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Benchmark graphs showing real clone costs in hot loops&lt;/p&gt;
&lt;p&gt;When to ditch the with expression and reach for a constructor&lt;/p&gt;
&lt;p&gt;This one's for every architect, lead dev, and performance-obsessed coder who wants to use records right.&lt;/p&gt;
&lt;p&gt;️ Read it. Share it. Argue with it. Let’s rethink how we use C# records.&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/3821/why-most-net-developers-misuse-records-and-what-its-costing-them</guid>
<pubDate>Tue, 08 Jul 2025 07:24:07 +0000</pubDate>
</item>
</channel>
</rss>