# Team Brief — BlazorBlocks: Register the Missing `EditModalSaveContextHolder` DI Dependency **Audience:** an orchestrator (and its implementers) working **only** in the BlazorBlocks repository at `C:\Development\BlazorBlocks`. You do not need, and should not assume, any knowledge of AuthBlocks or of any product that consumes BlazorBlocks. Everything you need is in this brief or in that one repo. **Status:** ✅ RESOLVED — shipped in `Cerebellum.BlazorBlocks.Web` 10.3.33 + `Cerebellum.AuthBlocks.Web` 10.3.36 (2026-06-20). ~~scoped request, not yet started. Confirmed at runtime against `Cerebellum.BlazorBlocks.Web` 10.3.32. Author: product-designer (for a downstream consumer team). Date: 2026-06-19.~~ > **Resolution (2026-06-20):** `AddBlazorBlocksWeb()` landed in `Cerebellum.BlazorBlocks.Web` 10.3.33 and `ConfigureAuthServices` calls it in `Cerebellum.AuthBlocks.Web` 10.3.36; DeepDrftManager picked up 10.3.36 and removed its local `EditModalSaveContextHolder` stopgap. > This brief is retained as historical record — no further action required. **This is one half of a two-team, ordered fix. BlazorBlocks ships first — see §9.** --- ## 1. The defect in one sentence The BlazorBlocks `ModelView` component (and the `EditModelModal` it drives) has a `required [Inject]` dependency on `Web.Maintenance.Entities.EditModalSaveContextHolder`, but **the `Web` package ships no `IServiceCollection` registration extension at all** — so the dependency is never registered by the library, and any consumer that surfaces a page built on `ModelView` gets an unhandled `InvalidOperationException` that **terminates the Blazor circuit on navigation**, unless that consumer manually hand-registers the internal service in its own `Program.cs`. The fix is a BlazorBlocks library fix delivered as a version bump: add a Web-side `Add*` extension that registers the holder. --- ## 2. The confirmed failure (downstream symptom — evidence, not your concern to chase) The defect was discovered by a consumer navigating to an admin page built on `ModelView`. The captured stack trace is included here as confirming evidence of the unregistered-service failure mode; the consumer's identity and its pages are out of scope for this team — your job is to register the dependency the library requires. ``` System.InvalidOperationException: Cannot provide a value for property 'SaveContextHolder' on type 'Web.Maintenance.Entities.ModelView`5[[AuthBlocksModels.InputModels.UserInputModel, ...], [AuthBlocksModels.Models.UserModel, ...],[AuthBlocksWeb.Components.Pages.UserAdmin.Users.UserEditModal, ...], [AuthBlocksWeb.Components.Pages.UserAdmin.Users.UsersViewModel, ...], [AuthBlocksModels.Converters.UserModelToInputConverter, ...]]'. There is no registered service of type 'Web.Maintenance.Entities.EditModalSaveContextHolder'. at Microsoft.AspNetCore.Components.ComponentFactory...CreatePropertyInjector... ``` Because `SaveContextHolder` is declared `required` with `[Inject]`, Blazor's component factory throws during component activation. There is no try/catch around component instantiation in the render path, so the exception propagates and tears down the circuit. The end user sees a dead page / "An unhandled error has occurred" and must reload. The tell that this is a library gap and not a consumer mistake: **two independent downstream products** have each had to hand-register the *same* internal BlazorBlocks service (`AddScoped()`) in their own `Program.cs` to make `ModelView`-based pages work. When two unrelated consumers independently discover they must reach into a library's internal namespace (`Web.Maintenance.Entities`) and register a type the library never documented as a consumer responsibility, that is a leaked registration. The correct owner of the registration is the library — this team. --- ## 3. Root-cause findings (from reading the source) ### 3.1 The service and who needs it `EditModalSaveContextHolder` (`C:\Development\BlazorBlocks\Web\Maintenance\Entities\EditModalSaveContextHolder.cs`) is a tiny per-circuit slot: ```csharp public sealed class EditModalSaveContextHolder { public IEditModalSaveContext? Current { get; set; } } ``` It is injected as `required` in **two** library components: - `Web/Maintenance/Entities/ModelView.razor.cs:42` — sets `SaveContextHolder.Current` before opening the edit dialog and clears it in a `finally` (see `EditItem`, lines 137–172). - `Web/Maintenance/Entities/EditModelModal.razor:40` — reads `SaveContextHolder.Current` to obtain the typed save callback (`SaveContext`). The holder is deliberately the **per-circuit bridge** between these two components — it threads a save closure from the page-side `ModelView` into the generic `EditModelModal` without forcing a parameter through every per-page modal wrapper. So **both** components fail without it; the bug surfaces at `ModelView` activation simply because that component is constructed first. ### 3.2 Is there already a Web-side registration extension to fix? **No Web-side registration extension exists.** A full scan of BlazorBlocks for `IServiceCollection` extension methods finds only: - `Data.Postgres/ServiceCollectionExtensions.cs` → `AddBlazorBlocksPostgres()` (registers `IDbExceptionClassifier`; a data-layer concern, unrelated to the Web components). - `API/Errors/...AddResultMessagePolymorphism(...)` (a JSON resolver helper, not DI). There is **no** `AddBlazorBlocks()` / `AddBlazorBlocksWeb()` / `AddMaintenance()` method in the `Web` project (`Cerebellum.BlazorBlocks.Web`). So `EditModalSaveContextHolder` is not "missing from an existing extension" — there is no Web-side extension at all. The library ships components with a hard DI dependency and provides no entry point to register that dependency. ### 3.3 Is `EditModalSaveContextHolder` the *only* missing dependency? **It is the only library-owned missing registration.** Every `[Inject]` across the BlazorBlocks `Web/Maintenance` tree was enumerated: | Component | Injected types | |---|---| | `ModelView<...>` | `TViewModel` (consumer), `NavigationManager` (framework), `IDialogService` (MudBlazor), `ISnackbar` (MudBlazor), **`EditModalSaveContextHolder` (BlazorBlocks — UNREGISTERED)** | | `EditModelModal` | `ISnackbar` (MudBlazor), **`EditModalSaveContextHolder` (BlazorBlocks — UNREGISTERED)** | | `NewModelView<...>` | `TClient` (consumer), `NavigationManager` (framework), `IDialogService` (MudBlazor), `ISnackbar` (MudBlazor) | Everything else resolves through services consumers already register: - `NavigationManager` — Blazor framework. - `IDialogService`, `ISnackbar` — MudBlazor, registered by the consumer's `AddMudServices()` (a documented MudBlazor prerequisite). - `TViewModel` / `TClient` — the per-entity viewmodel/client, registered by the consumer (or by a higher-level library) for its own entities. So `EditModalSaveContextHolder` is the single library-owned gap. Registering the one holder closes the whole set, provided the consumer has already called `AddMudServices()`. --- ## 4. The fix Add a new `IServiceCollection` extension in the `Web` project that registers the maintenance components' library-owned dependencies. This is where `EditModalSaveContextHolder` belongs — it is BlazorBlocks' internal bridge, and any product using `ModelView` needs it. ```csharp // C:\Development\BlazorBlocks\Web\ServiceCollectionExtensions.cs (new) using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; // for TryAddScoped using Web.Maintenance.Entities; namespace Web; public static class ServiceCollectionExtensions { /// /// Registers the services required by the BlazorBlocks maintenance UI /// components (ModelView / EditModelModal / NewModelView). Call this in /// your application's DI setup when using those components. /// Note: MudBlazor (AddMudServices) is a separate, caller-owned prerequisite /// and is intentionally NOT registered here. /// public static IServiceCollection AddBlazorBlocksWeb(this IServiceCollection services) { // Per-circuit slot bridging ModelView's save callback into EditModelModal. // Scoped per circuit; see lifetime rationale. TryAddScoped => idempotent. services.TryAddScoped(); return services; } } ``` The method should be the single place the maintenance components' library-owned deps are registered, so any future `ModelView` dependency is added here once and every consumer picks it up on the next bump. ### Lifetime rationale — scoped, not singleton, not transient The extension must register the holder as **scoped** (`AddScoped` / `TryAddScoped`): - **Why not singleton:** the holder is per-circuit mutable state — `ModelView.EditItem` writes `Current` immediately before opening the dialog and nulls it in `finally`; `EditModelModal` reads it. A singleton would share one slot across all users/circuits and cross-contaminate concurrent edits — a correctness/security bug. - **Why not transient:** a transient would hand `ModelView` and `EditModelModal` *different* instances, so the modal would never see the context the view set. The bridge would silently no-op and edits would fall back to the legacy "close with model" path (the `SaveContext is null` branch in `EditModelModal.Submit`). This is the dangerous failure mode: the page loads fine, but saves quietly take the wrong path. - The type's own XML doc states it is "Scoped per circuit." `AddScoped` in Blazor Server = one instance per circuit, which is exactly the intended semantics. Document the scoped requirement in the extension's summary so it is not "tidied" to singleton later. --- ## 5. Constraints - **Use scoped** for `EditModalSaveContextHolder` — not singleton, not transient (§4). - **Do not change `ModelView` / `EditModelModal` to drop the `required`/`[Inject]`** — the holder is the intentional design (the bridge described in §3.1, and in the type's own doc comment). The fix is to *register* the dependency, not to remove it. - **MudBlazor remains a caller-owned prerequisite.** `AddBlazorBlocksWeb` must **not** call `AddMudServices()` — consumers configure MudBlazor (theme, snackbar options) themselves, and double-registration would clobber their config. Document `AddMudServices` as a prerequisite in the extension's summary; do not absorb it. - **Versioning:** the `Web` package packs/pushes via `C:\Development\BlazorBlocks\pack.ps1`. `Web` is currently `Cerebellum.BlazorBlocks.Web` **10.3.32**; bump to the next version and pack/push. **Record the new version number** — the AuthBlocks team needs it to reference (see §9). --- ## 6. Acceptance criteria 1. The `Web` project exposes a new `IServiceCollection` extension (`AddBlazorBlocksWeb()` or the confirmed house name) that registers `EditModalSaveContextHolder` as **scoped**, with a summary documenting the scoped requirement and the `AddMudServices` prerequisite. 2. A product that uses BlazorBlocks `ModelView` (with or without any higher-level library) can register the maintenance deps via a single call to the new extension and gets working behavior. 3. Working behavior means not just page load: opening an edit dialog and **saving a valid change succeeds** — i.e. the save bridge actually works (the holder is scoped correctly so `EditModelModal` sees the context `ModelView` set). A page that loads but silently no-ops the save (the transient-lifetime trap in §4) does **not** pass. 4. The `Web` package is published as a version bump from 10.3.32, and the new version number is recorded for the AuthBlocks team. --- ## 7. Open questions for the implementing team / its sponsor 1. **Extension method name.** `AddBlazorBlocksWeb()` is proposed for consistency with the existing `AddBlazorBlocksPostgres()`. Confirm, or pick the preferred house name (e.g. `AddBlazorBlocksMaintenance()` if the team expects to split Web concerns further). *Recommendation: `AddBlazorBlocksWeb()`.* 2. **Idempotency.** Use `TryAddScoped` (recommended) so a consumer that calls the extension directly *and* via a higher-level library's setup gets a no-op on the second call rather than a duplicate registration. 3. **Scope of the extension.** Register only `EditModalSaveContextHolder` now (the only current gap), or pre-emptively make `AddBlazorBlocksWeb()` the home for *all* future maintenance-component deps? *Recommendation: ship it with just the holder now, but frame it (name + doc) as the general home so future deps land in one place — no speculative registrations.* 4. **Other unregistered library-owned `[Inject]` deps elsewhere?** This brief scoped the audit to the `Web/Maintenance` tree. If the team wants a clean bill of health, grep the whole `Web` project for `[Inject]` of BlazorBlocks-owned types and fold any others into the same extension. *Recommendation: do the quick full-project grep while you are in here; cheap insurance.* --- ## 8. Suggested reading order in the repo 1. `Web/Maintenance/Entities/EditModalSaveContextHolder.cs` — the unregistered service (and its scoped doc). 2. `Web/Maintenance/Entities/ModelView.razor.cs` — `[Inject]` at line 42; `EditItem` (137–172) shows the holder being written/cleared around the dialog. 3. `Web/Maintenance/Entities/EditModelModal.razor` — `[Inject]` at line 40; `Submit` shows the holder being read (and the `SaveContext is null` fallback that masks the bug into a wrong-behavior path if the lifetime is botched). 4. `Data.Postgres/ServiceCollectionExtensions.cs` — the existing `Add*` convention to mirror. 5. `Web/Web.csproj` — package id `Cerebellum.BlazorBlocks.Web`, current version (10.3.32), where to add the new `ServiceCollectionExtensions.cs`. 6. `pack.ps1` — the pack/push flow for the version bump. --- ## 9. Cross-team ordering (important — you ship first) This fix is layered across two repos and **must land in order**: 1. **BlazorBlocks (this team) ships first.** Add `AddBlazorBlocksWeb()`, bump `Cerebellum.BlazorBlocks.Web` from 10.3.32, pack/push, and **report the new version number**. 2. **Then AuthBlocks** bumps its `Cerebellum.BlazorBlocks.Web` reference to the version this team just published and calls `AddBlazorBlocksWeb()` from its own setup entry point. The AuthBlocks team **cannot complete its part until this team's new version is published**. This team is not blocked by anyone — just ship the extension, bump the version, and hand off the new version number. You do not need to touch or know anything about AuthBlocks.