Architecture Decision Record
ADR-094: Client-Side Entity Data-Access Contract
Status
Accepted (2026-08-23). Revised 2026-08-31: GetByIdAsync is recorded with its real signature (id,
includeChildren, CancellationToken; a missing entity is a NotFound failure, there is no
treatNotFoundAsDefault switch), ChildEntityServiceBase is recorded with both PostAsync overloads
and with a missing join row answering NotFound rather than false, and the line anchors into
EntityServiceBase, IEntityService, ChildEntityServiceBase, DataGridListPageBase and the
idempotency-retry tests are re-pinned. Revised 2026-09-19: the optional client read cache and the
If-Match conditional-write header are recorded as part of the contract, and the adoption inventory
is recounted. Revised 2026-10-01 (Blazor Server per-circuit UI state is recorded as a fitness-enforced rule, and Store's
CartStateService now derives from AuthenticatedServiceBase; see Revision below). Revised 2026-10-06: the retry
policy is recorded as never replaying a POST or PATCH that carries no Idempotency-Key, write
invalidation as skipping only a pre-write rejection, and the direct-root count as twenty-two (see
Revision below).
Context
ADR-034 decided the server half of entity data access: a generic controller base with a dynamic
query contract, where filters arrive as filters[Property].operator / filters[Property].value query
pairs bound by QueryFilterModelBinder
(MMCA.Common/Source/Presentation/MMCA.Common.API/ModelBinders/QueryFilterModelBinder.cs:12,20,92).
It says nothing about who calls that surface or how.
In practice the calling half is just as decided, and just as load-bearing, but it was never recorded.
Every Blazor and MAUI head in both applications reaches the API through one hand-written class
hierarchy in MMCA.Common.UI, and three of its choices are the kind that a new module author copies
without knowing they were choices: the framework ships hand-written typed bases instead of a
generated client, the user-facing retry lives in the client base rather than in ADR-009's
standard resilience handler, and the Idempotency-Key that ADR-017 requires is minted on the client
and held constant across retry attempts. ADR-017 specifies only the server filter and says the
client owns the identity of an operation; it never says which client code mints the value or what
keeps it stable. ADR-051 covers only how a bearer token is obtained, stored and refreshed across
render modes, not how an entity request is shaped.
This record fixes the client-side contract, and (because the same layer owns it) the generic list-page contract that consumes it.
Decision
Client-side entity data access goes through one hand-written base hierarchy in MMCA.Common.UI.
- One HTTP root:
AuthenticatedServiceBase(MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Api/AuthenticatedServiceBase.cs:16). It owns the named"APIClient"(:29), the bearer attachment (CreateAuthenticatedClientAsync,:59-78, tolerating the SSR pre-render case where JS interop is unavailable,:72-75), the explicit-token variant used to replay a 401 with a freshly refreshed token (CreateClientWithToken,:88-95, the client end of ADR-051), the retry policy, and the idempotency-key mint. The client itself is registered once, inAddUIShared(.../MMCA.Common.UI/DependencyInjection.cs:114-147): base address fromApiSettings,Accept: application/json,AuthDelegatingHandlerplusCultureDelegatingHandler, and a transport timeout pinned to the shared 90-second budget (:142,MMCA.Common/Source/Core/MMCA.Common.Shared/Resilience/HttpResilienceDefaults.cs:19) so the BCL's uncoordinated 100-second default cannot cut a call off mid-policy. - Typed CRUD is
EntityServiceBase<TEntityDTO, TIdentifierType>(.../MMCA.Common.UI/Services/Api/EntityServiceBase.cs:43), implementingIEntityService<TEntityDTO, TIdentifierType>(.../MMCA.Common.UI/Common/Interfaces/IEntityService.cs:20). It is a hand-written typed base over the ADR-034 REST surface, not a generated client: no Refit, Kiota or NSwag client generator appears in any of the four repos'Directory.Packages.props. - The client owns query-string construction for the dynamic query contract.
GetPagedAsyncbuildspageNumber,pageSize,sortColumn,sortDirection,includeChildren(EntityServiceBase.cs:95-102), addsincludeFKs=Trueonly when a subclass overridesPagedIncludeFKs(:65, applied at:104-107), and emits exactly the bracketed filter pairs the server binder parses, escaping every component (:109-120, the pairs at:115and:117).GetAllAsync(:68-83),GetAllForLookupAsync(:132-140) andGetByIdAsync(:143-159, takingid,includeChildrenand the caller'sCancellationToken) cover the remaining reads. A read for a missing entity is aNotFoundfailure, not a default value (:155-158): the caller tells it apart from a transport failure throughResultUiExtensions.IsNotFound(.../MMCA.Common.UI/Common/ResultUiExtensions.cs:329) rather than by asking for a null. - Reads go through an optional client read cache. The constructor takes an optional
IUiReadCache(EntityServiceBase.cs:47, exposed to subclasses asReadCache,:58), the client half of ADR-040. All four reads callGetCachedAsync(:253-284, called at:80,:123,:137and:158) rather than dispatching directly. With no cache registered, or withbypassCacheset, it falls straight through to the normal dispatch (:260-263); with one, a fresh entry answers the read with no HTTP call at all (:265-268), and only a successful non-null response is stored (:278-281), so a transient outage or a 404 is not pinned in front of the user for the whole TTL. The cache key is the request path plus its full query string, stored verbatim so it matches the server-side output-cache key shape. After every write,InvalidateAfterWrite(:300-306, callingReadCache?.InvalidatePrefix(Endpoint), invoked fromAddAsyncat:177,UpdateAsyncat:199andDeleteAsyncat:225) drops this endpoint's entries unless every error is a refusal issued before the server touched state (IsRejectedBeforeWrite,:309-314: validation, unprocessable, unauthorized, forbidden, rate limited). Those changed nothing, so they invalidate nothing; any other failure (a 412, 404 or 409 that may answer the retry of a write whose first attempt landed, a 5xx, a transport failure) leaves the server state changed or unknown and invalidates like a success. - Retry is owned by the client base, not by a resilience handler.
RetryPolicy(AuthenticatedServiceBase.cs:27, built byBuildRetryPolicy,:170-173) is a static Polly policy: three retries after the initial attempt, onHttpRequestExceptionor a retryable response, with 2s / 4s / 8s exponential backoff plus up to 1000 ms of jitter so a fleet of clients does not re-converge on one instant, and every retried response is disposed inonRetry(:173) so only the final one reaches the caller.IsRetryableResponse(:108-122) retries 5xx except 501 and 505 (permanent verdicts) and adds 408 and 429 (the server explicitly inviting a later attempt), but only for a request that is safe to send again:IsReplaySafe(:134-148, checked at:115) answers false for a POST or PATCH carrying noIdempotency-Key, so a keyless write is never replayed. Every dispatch runs throughSendRequestAsync(EntityServiceBase.cs:351-369for the value-returning overload,:381-397for the body-less one, both taking an optional idempotency key andIf-Matchvalue), which passes the caller'sCancellationTokeninto the policy (:365,:393) so cancellation aborts the wait between attempts instead of sleeping out the backoff budget. - The
Idempotency-Keyis minted client-side and survives retries.NewIdempotencyKey()returns a compact GUID (AuthenticatedServiceBase.cs:51); the header name is the shared constantIdempotencyHeaders.IdempotencyKey(MMCA.Common/Source/Core/MMCA.Common.Shared/Http/IdempotencyHeaders.cs:19). OnlyAddAsyncsupplies one (EntityServiceBase.cs:171-175, the mint at:174): creates are the one CRUD verb that is not naturally idempotent, so reads, fullPUTupdates and deletes send no key (UpdateAsync,:188-201;DeleteAsync,:215-227). The key is set as a default header on the singleHttpClientthat serves every attempt (CreateRequestClientAsync,:403-428, the header added at:413), which is what makes the value constant across the retry burst and therefore dedupable by the ADR-017 filter. Both properties are pinned by tests: the same key on every retry (MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/Api/EntityServiceBaseIdempotencyRetryTests.cs:96), no key on reads, updates or deletes (:118,130,143), and the 501-not-retried / 429-retried edges (:156,171). - The
If-Matchprecondition is set the same way, from the DTO's own concurrency token.UpdateAsyncis the only verb that sends one: it passesConcurrencyTagOf(entity)(EntityServiceBase.cs:196) into the dispatch, and that helper (:209-212) renders anIConcurrencyAwareDTO'sRowVersionas a weak entity tag and answers null when the DTO type carries no token. The header rides on the same per-operationHttpClientas the idempotency key (CreateRequestClientAsync,:416-425, added at:424under the shared nameConcurrencyETag.IfMatchHeaderName), so every retry attempt states the same precondition rather than a later attempt succeeding against a version the caller never saw; when the first attempt landed and only its response was lost, the retry answers 412 (or 404 for a soft-deleted row), which is why such an outcome still invalidates the read cache (:418-423). This is the client end of ADR-035: theIf-Matchheader is the only route the token travels, and a DTO carrying none sends no header and is refused instead of overwriting another editor's change (:183-186). - The dispatch returns a
Result; it does not throw (2026-08-27, v1.164.0).SendRequestAsyncwraps the whole send-and-read inHttpResultExecutor.ExecuteAsyncand hands the response toProblemDetailsResultReader, in both the value-returning overload (EntityServiceBase.cs:351, composition at:359-368) and the body-less one (:381, at:389-396). The reader turns a non-success response back into the errors the server described, with the originalErrorTypepreserved when the payload carries the MMCA error array, and the executor turns a call that never got a response (connection, DNS, socket, timeout) into a failure of its own. A page therefore branches on aResultinstead of catching, and sees the business reason rather than "500" without any exception being minted to carry it (ADR-013). This replaces the earlier arrangement, in which the client pulled the domain wording out of the ProblemDetails body and rethrew it as aDomainInvariantViolationExceptionbefore falling back toEnsureSuccessStatusCode: that helper is deleted, not deprecated.ChildEntityServiceBasewas converted in the same pass (.../MMCA.Common.UI/Services/Api/ChildEntityServiceBase.cs:37,:53,:71). - Join entities use
ChildEntityServiceBase(.../MMCA.Common.UI/Services/Api/ChildEntityServiceBase.cs:19), the many-to-many sibling: twoPostAsyncoverloads, one reading the created DTO back (:36) and one for an endpoint answering 204 (:52), plusDeleteByIdAsync(:70). A join row that is not there answers 404, which arrives as anErrorType.NotFoundfailure, so the caller can still separate "nothing to remove" from "the remove failed" (:62-66). It shares the bearer helper and the sameResultdispatch but deliberately issues its calls directly, outsideRetryPolicyand with no idempotency key. - Non-CRUD services take the root directly and reuse the same policy. Services whose endpoints are
not entity CRUD derive from
AuthenticatedServiceBaseitself and call the inheritedRetryPolicyby hand, minting a key where the endpoint is[Idempotent](for exampleMMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Services/SessionLive/LivePollUIService.cs:93,156and.../SessionQuestionUIService.cs:73). - UI state is per circuit, never process-wide. Blazor Server runs every user's circuit in one
process, so client-side state lives in scoped services: UI assemblies declare no mutable static
field and no settable static property, and no
*StateServiceor*StateContainertype is registered as a singleton, so one user's state cannot leak into another circuit. The rule is a fitness function authored once inMMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:18(reflection over theLayer.Uiassemblies,:29-64; a source scan for singleton stateful registrations,:67-79, pattern at:125). A subclass may exempt named members throughAllowedStaticMembers(:26, honored at:57). Common's subclass exempts exactly one,ErrorMessages._localizer(MMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:21-22; the field is atMMCA.Common/Source/Presentation/MMCA.Common.UI/Pages/Common/ErrorMessages.cs:26), a write-once localizer wiring rather than per-user state, and adds a singleton-scan test of its own. The rule runs inMMCA.Common/Tests/Architecture/MMCA.Common.Architecture.Tests/Ui/StateManagementConventionTests.cs:11,MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/Ui/StateManagementConventionTests.cs:9andMMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/Ui/StateManagementConventionTests.cs:10. MMCA.Helpdesk declares no subclass, so the rule is not enforced there.
Adoption inventory as of 2026-10-06, with every service filed under its aggregate folder.
Eighteen production services derive from EntityServiceBase: ten in ADC Conference
(MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.UI/Services/:
Activities/ActivityService.cs:11, Categories/CategoryItemService.cs:11,
Categories/ConferenceCategoryService.cs:11, Events/EventService.cs:27,
Partners/PartnerService.cs:11, Questions/QuestionService.cs:11, Rooms/RoomService.cs:15,
Sessions/SessionService.cs:11, Speakers/SpeakerService.cs:23, Sponsors/SponsorService.cs:11),
seven in Store
(Catalog.UI/Services/ProductService.cs:27, CategoryService.cs:24 and
Reviews/ReviewService.cs:24; Sales.UI/Services/Orders/OrderService.cs:19,
ShoppingCarts/ShoppingCartService.cs:18, Inventory/InventoryItemService.cs:20;
Identity.UI/Services/CustomerService.cs:25), and one inside
the framework itself
(MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Notifications/PushNotificationService.cs:20).
Four derive from ChildEntityServiceBase, all in ADC Conference and all in one file
(.../Services/Common/ChildEntityServices.cs:23,36,49,62). Twenty-two more take
AuthenticatedServiceBase directly: ten in ADC Engagement, five in ADC Conference (four files:
Services/Feedback/OrganizerFeedbackService.cs declares two of them, at :17 and :72, next to
Services/Speakers/SpeakerDashboardService.cs:16,
Services/Sessions/Selection/SessionSelectionService.cs:16 and
Services/SessionAssets/SessionAssetService.cs:27), ADC Identity's
Services/UserService.cs:22, four in the framework
(Services/Notifications/NotificationInboxService.cs:34,
Services/Legal/LegalAcceptanceUIService.cs:26,
Services/Administration/UserAdminService.cs:29 and
Services/Administration/RoleAdminService.cs:28), and two in Store: UserDataService
(MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.UI/Services/UserDataService.cs:20) and
CartStateService
(MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.UI/Services/ShoppingCarts/CartStateService.cs:39,
base at :45), which uses the inherited RetryPolicy (for example :98, :145, :264) and
NewIdempotencyKey() under the same rule: one key per user action, reused by every attempt (:91
per add-to-cart, :257 per checkout, shared by both checkout steps).
The list-page contract: DataGridListPageBase<TDto>
The consumer side of that data-access contract is equally uniform, and is recorded here rather than
separately because the two are used as a pair: a list page inherits the base and hands it a fetch
delegate that is almost always an EntityServiceBase.GetPagedAsync call.
DataGridListPageBase<TDto>
(MMCA.Common/Source/Presentation/MMCA.Common.UI/Pages/Common/DataGridListPageBase.cs:22) owns:
- Server-side paging through MudDataGrid
ServerData.LoadServerDataAsync(:521) flattens the grid's filter definitions into the one-filter-per-column dictionary the fetch delegate takes, with the newest row winning when the user stacks two filters on one column (ExtractGridFilters,:911-924), resolves sort fromGridState(ResolveSortParameters, called in the paged fetch at:599, defined at:938-950: it reads the grid's ownSortDefinitionthroughExtractSortParameters,:926-931, and falls back to the sort restored from the query string when the grid has not picked one up yet, which is the normal case on a first fetch with a URL-driven sort), and converts the grid's zero-based page to the API's one-basedpageNumber(:601). - Cancellation-token management. Each fetch swaps in a fresh source before tearing down the
previous one, tolerating the
ObjectDisposedExceptionrace a debounced reload after disposal would otherwise raise (ResetCancellationTokenAsync,:877-899); during SSR pre-render the token additionally times out afterPrerenderFetchTimeoutMs(5000 ms) so a cold backend cannot block the page load (:95,CreateFetchCts,:813-824). - A
LoadFailedflag that distinguishes error-with-retry from genuinely empty (:53). The grid and mobile paths share one fetch wrapper,RunFetchAsync(:717), which clears the flag (:726) and sets it on a thrown fetch (:752), whileFailedFetch(:770) sets it on a failedResult(:773). A failed fetch renders zero rows, which is visually identical to an empty list once the error snackbar expires, so pages branch on this flag inNoRecordsContentinstead of showing the "no records" state. - Viewport-driven mobile card state.
IsMobile(:57) flips from the browser-viewport observer (:318-331) below the 960 px sidebar-collapse threshold, and the card view has its own paged fetch path (MobileItems/MobileTotalItems/MobileCurrentPage/MobilePageSize,:60-63;LoadMobileDataAsync,:830). - List state persisted and restored three ways.
ListPageState(.../MMCA.Common.UI/Services/ListPageStateService.cs:9) carries page, page size, mobile page, sort, density, filters and scroll position;ListPageStateService(:63) holds it in memory and mirrors it tosessionStorage, andListPageQueryStateService(.../MMCA.Common.UI/Services/ListPageQueryStateService.cs:28) encodes it into the URL. The URL is the source of truth on initialization, with the in-memory entry as the fallback and scroll position read only from it (DataGridListPageBase.cs:217-254); writes go to all three (SaveCurrentState,:952-989). Deferred writes are dropped once the user has navigated away, because the route is pinned at initialization rather than read from the live URI at write time (_ownRoutePath, field at:1032, pinned at:215;IsOwnRouteCurrent,:1040-1041).
Twenty types inherit this base: thirteen in ADC (seven directly, six through the abstract
Pages/Common/EventFilteredListPageBase.cs:25) and seven in Store, nineteen of them routable list
pages plus ADC's non-routable AttendeeSearchPanel
(MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Pages/CheckIns/AttendeeSearchPanel.razor.cs:16).
ADR-056 owns the render-mode aspect of the same type (the PersistentComponentState pre-render
handoff and the InteractiveAuto registration) and inventories the same set of pages.
Rationale
- A hand-written typed base beats a generated client here because the surface is already generic. ADR-034 collapsed N entity endpoints into one shape, so there is exactly one client shape to write. A generator would emit N near-identical clients from an OpenAPI document, add a build step and a regeneration discipline, and still need hand-written policy for retry, idempotency and error extraction. The typed base gives compile-time DTO safety for the same cost as the generic call.
- User-facing retry belongs where the user is. The client base retries a browser-to-gateway call the user is waiting on, with second-scale backoff a person will tolerate; ADR-009's standard handler is tuned for server-to-server hops. Keeping them separate lets each move on its own.
- One key per logical operation is the only version of ADR-017 that works. The server dedups on the key; if the client minted a new one per attempt, a retried create would produce a second record, which is precisely the failure the filter exists to prevent. Setting the header on the client that serves every attempt makes the invariant structural rather than a rule to remember.
- Returning the domain error is what makes failures speakable. The API already returns a
specific business reason; a client that answered with a generic status-code exception would throw
that reason away. The original design extracted the reason and rethrew it, which preserved the
wording but kept the failure in the exception channel; returning a
Resultpreserves the wording and the category, so a page can turn a 404 into an empty state and a 401 into a redirect instead of pattern-matching on message text (ADR-013). - The list page is repeated twenty times, so it is worth a base class. Paging, cancellation, filter and sort extraction, viewport switching, error state and state restoration are identical across every list in both apps; twenty hand-rolled copies is twenty chances to get the cancellation race or the empty-versus-failed distinction wrong.
Trade-offs
- No generated client means drift is caught at runtime, not at build time. A server-side rename of
a query parameter or a DTO property does not fail the UI build; it fails the call. The mitigation is
that both halves live in one solution and share the DTO types, so the common case (a DTO change) is
a compile error anyway; the exposed case is the query-string vocabulary, which is asserted on each
side separately (
MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/ModelBinders/QueryFilterModelBinderTests.csfor the binder,EntityServiceBaseTests.csfor the emitter) rather than end to end. - Retry budgets stack across hops, and are deliberately bounded rather than eliminated. A host that
calls
AddServiceDefaultsapplies the standard resilience handler to every factory client throughConfigureHttpClientDefaults(MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:39-85), including"APIClient". Because the UI base already makes up to four attempts, the shared per-hop retry count is pinned to one (HttpResilienceDefaults.cs:21-30, applied atExtensions.cs:54), with the reason stated in both places: full budgets at every hop turned a backend brownout into an up-to-16x request storm. The cost is that the effective attempt count for a UI action is a product of two layers and cannot be read off either one alone. - The retry policy is
staticand not configurable.RetryPolicyis aprotected static readonlyfield (AuthenticatedServiceBase.cs:27), so its counts and delays are compile-time constants shared by every service in the process. A per-endpoint or per-environment retry profile would need a change to the framework, not configuration. - Only
AddAsyncgets an idempotency key automatically. Any non-CRUD write (a hand-written POST on a service deriving fromAuthenticatedServiceBase, such asCartStateService) has to mint and attach the key itself, and nothing fails the build if it does not. The omission costs resilience rather than correctness:RetryPolicynever replays a POST or PATCH that carries no key (IsReplaySafe,AuthenticatedServiceBase.cs:134-148), so a keyless write cannot create a duplicate, but a transient fault on it surfaces to the user instead of being absorbed. BothChildEntityServiceBase.PostAsyncoverloads (ChildEntityServiceBase.cs:36,:52) are such writes and send no key today. ChildEntityServiceBasecalls are not retried. Join add and remove operations get the bearer token and domain-error extraction but no transient-fault handling, so a blip surfaces to the user where the same blip on the parent entity would be absorbed.- The list-page base is deep. It coordinates render-mode-aware persistence, three state stores, JS
interop for scroll tracking, and two MudDataGrid v9 parameter-setter workarounds
(
DataGridListPageBase.cs:421-428,:472-488). That depth is the price of twenty pages behaving identically, but it makes the base itself the hardest type in the UI package to change safely.
Revision (2026-10-01)
The Blazor Server per-circuit state rule is recorded as a Decision bullet. It was already enforced
but not written down: UI assemblies carry no mutable static members outside a subclass's
AllowedStaticMembers list (Common exempts only ErrorMessages._localizer) and *StateService /
*StateContainer types are never registered as singletons, held by
MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/Ui/StateManagementConventionTestsBase.cs:18
and its subclasses in Common (StateManagementConventionTests.cs:11), ADC (:9) and Store
(:10); MMCA.Helpdesk has none. Store's CartStateService now derives from
AuthenticatedServiceBase (CartStateService.cs:45) and uses the inherited RetryPolicy and
NewIdempotencyKey() instead of local copies, so the direct-root count is twenty and the
Trade-offs bullet no longer calls it code outside the hierarchy. Store's EmailConfirmationService
no longer exists, so the EntityServiceBase count is eighteen. LoadFailed is now set in the
shared fetch wrapper RunFetchAsync (DataGridListPageBase.cs:643, set at :673) and its
FailedFetch helper (:690) rather than at four per-path sites. Line anchors into AuthenticatedServiceBase, EntityServiceBase, ResultUiExtensions,
the UI DependencyInjection, the Aspire Extensions, HttpResilienceDefaults,
OrganizerFeedbackService and DataGridListPageBase are refreshed. The client data-access
contract itself and its rationale are unchanged.
Revision (2026-10-06)
- The retry bullet now records that
IsRetryableResponseconsultsIsReplaySafe(AuthenticatedServiceBase.cs:134), so a POST or PATCH with noIdempotency-Keyis never replayed, and thatonRetrydisposes every retried response (:173). The idempotency-key trade-off is restated accordingly: a missing key now costs transient-fault absorption, not a duplicate record. - The read-cache bullet now records
InvalidateAfterWrite(EntityServiceBase.cs:300): a write invalidates the endpoint unless every error is a pre-write refusal (IsRejectedBeforeWrite,:309), so a 412, 404, 409, 5xx or transport failure invalidates like a success. The earlier "a rejected write invalidates nothing" held only for that pre-write set. GetPagedAsyncrecords thePagedIncludeFKsoverride (EntityServiceBase.cs:65), and bothSendRequestAsyncoverloads are recorded as taking the idempotency key andIf-Matchvalue.- The direct-root count is twenty-two, not twenty: Common's
LegalAcceptanceUIService(LegalAcceptanceUIService.cs:26) and Store'sUserDataService(UserDataService.cs:20) were missing. TheEntityServiceBase(eighteen) andChildEntityServiceBase(four) counts were recounted and hold. - Line anchors into
AuthenticatedServiceBase,EntityServiceBase,ResultUiExtensions, the UIDependencyInjection,EventService,SpeakerService,CartStateServiceandDataGridListPageBasewere re-verified against current source.
Related
ADR-034 (the server surface this contract calls, and the filter
grammar the client constructs), ADR-017 (the server-side filter whose
client half is specified here: who mints the key and what keeps it constant),
ADR-051 (how the bearer token this base attaches is obtained,
stored and refreshed across render modes), ADR-009 (the
server-to-server resilience handler whose budget interacts with, but does not replace, the client
retry), ADR-056 (the render-mode aspect of
DataGridListPageBase<TDto>, including the pre-render data handoff and the same inheritor
inventory), ADR-035 (the concurrency token whose If-Match carriage
this base owns), ADR-040 (the caching policy
whose client-side read cache this base consults).