Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/data/search-index.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs-src/onboarding/group-07-persistence-ef-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -1815,7 +1815,7 @@ survives a module being pulled out into its own service.

`[Rubric §8, Data Architecture]` assesses whether persistence is deliberate: one save boundary, one concurrency story. This interface has no `Save` and no `SaveChangesAsync`. The doc comment states the division (`IRepository.cs:359-363`): the repository stages changes and never flushes them, because persisting is the unit of work's job, so every repository touched in a scope is written as one unit under one audit stamp. A handler that mutates through a repository and then forgets [`IUnitOfWork.SaveChangesAsync`](#iunitofwork) has written nothing.
- **Concept introduced, optimistic-concurrency wiring and change-tracking-bypass writes.** Five members carry the weight.
- `SetOriginalRowVersion(TEntity entity, byte[] rowVersion)` (`IRepository.cs:406`): plants the client's last-observed `RowVersion` as the tracked entity's *original* concurrency token, so the next save emits its `WHERE RowVersion = @original` and raises `DbUpdateConcurrencyException` when the row moved since the client read it (`IRepository.cs:399-403`). On an endpoint marked `[SupportsIfMatch]` that is answered as `412 Precondition Failed` (`MMCA.Common/Source/Presentation/MMCA.Common.API/Concurrency/SupportsIfMatchAttribute.cs:130-138`); on an endpoint without it the exception falls through to `DbUpdateExceptionHandler`'s plain `409 Conflict` ([ADR-035](https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html)). The implementation sets `OriginalValue` on the tracked entry's `RowVersion` property and rejects a null token outright (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFRepository.cs:75-84`).
- `SetOriginalRowVersion(TEntity entity, byte[] rowVersion)` (`IRepository.cs:406`): plants the client's last-observed `RowVersion` as the tracked entity's *original* concurrency token, so the next save emits its `WHERE RowVersion = @original` and raises `DbUpdateConcurrencyException` when the row moved since the client read it (`IRepository.cs:399-403`). The token only ever arrives through `If-Match`, so `[SupportsIfMatch]` answers that exception as `412 Precondition Failed` (`MMCA.Common/Source/Presentation/MMCA.Common.API/Concurrency/SupportsIfMatchAttribute.cs:130-138`, [ADR-035](https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html)). The implementation sets `OriginalValue` on the tracked entry's `RowVersion` property and rejects a null token outright (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFRepository.cs:75-84`).
- `SetOriginalRowVersion(Domain.Interfaces.IRowVersioned childEntity, byte[] rowVersion)` (`IRepository.cs:417`): the same protection for a tracked **child** of the aggregate, for example a `ProductVariant` under a `Product`. The doc comment explains why a second overload exists at all (`IRepository.cs:408-414`): the repository's `TEntity` is the root, so the typed overload cannot reach children, and this one accepts any [`IRowVersioned`](group-02-domain-building-blocks.md#irowversioned) entity instead ([ADR-035](https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html)). It reaches the entry through an `(object)` cast (`EFRepository.cs:86-95`).
- `TouchConcurrencyToken(TEntity entity)` (`IRepository.cs:440`): forces the aggregate ROOT to take part in the next save when a conditional write left it untouched, so the precondition the caller sent is actually evaluated (SEC-Common-77). The remarks explain the gap it closes (`IRepository.cs:424-439`): `SetOriginalRowVersion` only stamps the tracked entry's ORIGINAL value, it does not make the entry dirty, so an applier that changes only child rows leaves the root `Unchanged`, EF emits no root `UPDATE`, no `WHERE RowVersion = @token` reaches the database, and the stale-token check silently does not fire, letting two administrators editing different children of the same aggregate from the same ETag both get `200` while the second silently discards the first's edit. Touching the root restores the `412` [ADR-035](https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html) promises. It is declared with a default no-op body (`IRepository.cs:440-443`) so an existing implementer stays source- and binary-compatible; a repository that does not override it keeps the pre-hardening behaviour rather than failing to compile. `EFRepository<TEntity, TIdentifierType>` overrides it.
- `ExecuteDeleteAsync(Expression<Func<TEntity, bool>> where, CancellationToken)` (`IRepository.cs:453-455`): a set-based delete run directly in the database, one statement, no change tracker. The doc comment warns in capitals that it does **not** trigger domain events, audit stamps, or soft delete, and is for maintenance scenarios only (`IRepository.cs:445-452`). The implementation is a one-liner over the `DbSet` (`EFRepository.cs:118-124`).
Expand Down
2 changes: 1 addition & 1 deletion docs/onboarding/group-07-persistence-ef-core.html
Original file line number Diff line number Diff line change
Expand Up @@ -1978,7 +1978,7 @@ <h3 id="iwriterepositorytentity-tidentifiertype">IWriteRepository&lt;TEntity, TI
</li>
<li><p><strong>Concept introduced, optimistic-concurrency wiring and change-tracking-bypass writes.</strong> Five members carry the weight.</p>
<ul>
<li><code>SetOriginalRowVersion(TEntity entity, byte[] rowVersion)</code><span class="cite"><span> (<code>IRepository.cs:406</code>)</span></span>: plants the client&#39;s last-observed <code>RowVersion</code> as the tracked entity&#39;s <em>original</em> concurrency token, so the next save emits its <code>WHERE RowVersion = @original</code> and raises <code>DbUpdateConcurrencyException</code> when the row moved since the client read it<span class="cite"><span> (<code>IRepository.cs:399-403</code>)</span></span>. On an endpoint marked <code>[SupportsIfMatch]</code> that is answered as <code>412 Precondition Failed</code><span class="cite"><span> (<code>MMCA.Common/Source/Presentation/MMCA.Common.API/Concurrency/SupportsIfMatchAttribute.cs:130-138</code>)</span></span>; on an endpoint without it the exception falls through to <code>DbUpdateExceptionHandler</code>&#39;s plain <code>409 Conflict</code> (<a href="https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html" target="_blank" rel="noopener">ADR-035</a>). The implementation sets <code>OriginalValue</code> on the tracked entry&#39;s <code>RowVersion</code> property and rejects a null token outright<span class="cite"><span> (<code>MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFRepository.cs:75-84</code>)</span></span>.</li>
<li><code>SetOriginalRowVersion(TEntity entity, byte[] rowVersion)</code><span class="cite"><span> (<code>IRepository.cs:406</code>)</span></span>: plants the client&#39;s last-observed <code>RowVersion</code> as the tracked entity&#39;s <em>original</em> concurrency token, so the next save emits its <code>WHERE RowVersion = @original</code> and raises <code>DbUpdateConcurrencyException</code> when the row moved since the client read it<span class="cite"><span> (<code>IRepository.cs:399-403</code>)</span></span>. The token only ever arrives through <code>If-Match</code>, so <code>[SupportsIfMatch]</code> answers that exception as <code>412 Precondition Failed</code> (<span class="cite"><span><code>MMCA.Common/Source/Presentation/MMCA.Common.API/Concurrency/SupportsIfMatchAttribute.cs:130-138</code></span></span>, <a href="https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html" target="_blank" rel="noopener">ADR-035</a>). The implementation sets <code>OriginalValue</code> on the tracked entry&#39;s <code>RowVersion</code> property and rejects a null token outright<span class="cite"><span> (<code>MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFRepository.cs:75-84</code>)</span></span>.</li>
<li><code>SetOriginalRowVersion(Domain.Interfaces.IRowVersioned childEntity, byte[] rowVersion)</code><span class="cite"><span> (<code>IRepository.cs:417</code>)</span></span>: the same protection for a tracked <strong>child</strong> of the aggregate, for example a <code>ProductVariant</code> under a <code>Product</code>. The doc comment explains why a second overload exists at all<span class="cite"><span> (<code>IRepository.cs:408-414</code>)</span></span>: the repository&#39;s <code>TEntity</code> is the root, so the typed overload cannot reach children, and this one accepts any <a href="group-02-domain-building-blocks.html#irowversioned"><code>IRowVersioned</code></a> entity instead (<a href="https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html" target="_blank" rel="noopener">ADR-035</a>). It reaches the entry through an <code>(object)</code> cast<span class="cite"><span> (<code>EFRepository.cs:86-95</code>)</span></span>.</li>
<li><code>TouchConcurrencyToken(TEntity entity)</code><span class="cite"><span> (<code>IRepository.cs:440</code>)</span></span>: forces the aggregate ROOT to take part in the next save when a conditional write left it untouched, so the precondition the caller sent is actually evaluated (SEC-Common-77). The remarks explain the gap it closes<span class="cite"><span> (<code>IRepository.cs:424-439</code>)</span></span>: <code>SetOriginalRowVersion</code> only stamps the tracked entry&#39;s ORIGINAL value, it does not make the entry dirty, so an applier that changes only child rows leaves the root <code>Unchanged</code>, EF emits no root <code>UPDATE</code>, no <code>WHERE RowVersion = @token</code> reaches the database, and the stale-token check silently does not fire, letting two administrators editing different children of the same aggregate from the same ETag both get <code>200</code> while the second silently discards the first&#39;s edit. Touching the root restores the <code>412</code> <a href="https://ivanball.github.io/docs/adr/035-optimistic-concurrency.html" target="_blank" rel="noopener">ADR-035</a> promises. It is declared with a default no-op body<span class="cite"><span> (<code>IRepository.cs:440-443</code>)</span></span> so an existing implementer stays source- and binary-compatible; a repository that does not override it keeps the pre-hardening behaviour rather than failing to compile. <code>EFRepository&lt;TEntity, TIdentifierType&gt;</code> overrides it.</li>
<li><code>ExecuteDeleteAsync(Expression&lt;Func&lt;TEntity, bool&gt;&gt; where, CancellationToken)</code><span class="cite"><span> (<code>IRepository.cs:453-455</code>)</span></span>: a set-based delete run directly in the database, one statement, no change tracker. The doc comment warns in capitals that it does <strong>not</strong> trigger domain events, audit stamps, or soft delete, and is for maintenance scenarios only<span class="cite"><span> (<code>IRepository.cs:445-452</code>)</span></span>. The implementation is a one-liner over the <code>DbSet</code><span class="cite"><span> (<code>EFRepository.cs:118-124</code>)</span></span>.</li>
Expand Down
Loading