diff --git a/product-notes/blazorblocks-modelview-di-registration-brief.md b/product-notes/blazorblocks-modelview-di-registration-brief.md index 52dfa30..b8670c8 100644 --- a/product-notes/blazorblocks-modelview-di-registration-brief.md +++ b/product-notes/blazorblocks-modelview-di-registration-brief.md @@ -1,344 +1,36 @@ -# Team Brief — Fix the Missing `EditModalSaveContextHolder` DI Registration in BlazorBlocks / AuthBlocks +# Index — `EditModalSaveContextHolder` Missing DI Registration (BlazorBlocks / AuthBlocks) -**Audience:** an orchestrator (and its implementers) working in the **BlazorBlocks** repository at -`C:\Development\BlazorBlocks` and/or the **AuthBlocks** repository at `C:\Development\AuthBlocks`. -You do not need, and should not assume, any knowledge of the products that consume these libraries. -Everything you need is in this brief or in those two repos. +**Status:** scoped, not yet started. Confirmed against `Cerebellum.BlazorBlocks.Web` 10.3.32 / +`Cerebellum.AuthBlocks.Web` 10.3.33. Author: product-designer. Date: 2026-06-19. -**Status:** scoped request, not yet started. Confirmed at runtime against `Cerebellum.AuthBlocksWeb` -10.3.33 / `Cerebellum.BlazorBlocks.Web` 10.3.32. Author: product-designer (for a downstream consumer team). -Date: 2026-06-19. +## The defect ---- +BlazorBlocks' `ModelView` / `EditModelModal` components have a `required [Inject]` dependency on +`Web.Maintenance.Entities.EditModalSaveContextHolder` (a per-circuit save bridge), but the BlazorBlocks +`Web` package ships no registration extension for it and AuthBlocks' `ConfigureAuthServices` never registers +it either. Any consumer of a `ModelView`-based page (e.g. AuthBlocks' `Users.razor` / +`Registrations.razor`) crashes the Blazor circuit on navigation with an unregistered-service +`InvalidOperationException`. Two independent downstream products have each hand-registered the internal +service as a stopgap — the tell that this is a leaked library registration. -## 1. The defect in one sentence +## The two-team layered fix (ordered — do not reorder) -The BlazorBlocks `ModelView` component (and the `EditModelModal` it drives) has a `required [Inject]` -dependency on `Web.Maintenance.Entities.EditModalSaveContextHolder`, but **no BlazorBlocks setup -extension and no AuthBlocks setup extension registers that service** — so any consumer that surfaces a -page built on `ModelView` gets an unhandled `InvalidOperationException` that **terminates the Blazor -circuit on navigation**, unless the consumer manually hand-registers the internal service in its own -`Program.cs`. +1. **BlazorBlocks ships first.** Add a Web-side `AddBlazorBlocksWeb()` extension that registers the holder + via `TryAddScoped` (scoped is required). Bump `Cerebellum.BlazorBlocks.Web` from 10.3.32; report the new + version. +2. **AuthBlocks ships second.** Bump its `Cerebellum.BlazorBlocks.Web` reference to that new version, call + `AddBlazorBlocksWeb()` from `ConfigureAuthServices`, bump `Cerebellum.AuthBlocks.Web` from 10.3.33. -The fix is an **upstream library fix**, delivered as a version bump. The goal: a consumer that calls -AuthBlocks' single DI entry point `AuthBlocksWeb.Startup.ConfigureAuthServices(...)` gets a fully -working user-admin surface with **zero** manual service registrations. +AuthBlocks is blocked until BlazorBlocks' new version is published. Registration lives with its owner +(BlazorBlocks); AuthBlocks stays self-contained by composing it. MudBlazor (`AddMudServices`) stays a +caller-owned prerequisite throughout. ---- +## The detail lives in the two team briefs -## 2. The confirmed failure +Each is fully self-contained for an orchestrator working in only that one repo: -### Stack trace (captured from a consuming app on navigation to the Users admin page) - -``` -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 user sees a dead page / "An unhandled error -has occurred" and must reload. - -### Which AuthBlocks pages trigger it - -`AuthBlocksWeb` ships two user-admin pages that render ``: - -- `AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor` -- `AuthBlocksWeb/Components/Pages/UserAdmin/Registrations/Registrations.razor` - -Any consumer that routes to either page hits the defect on first navigation. - ---- - -## 3. Why this is an upstream gap, not a consumer mistake (the two-consumer tell) - -Two **independent** downstream products have each had to hand-register the *same* internal BlazorBlocks -service to make AuthBlocks' shipped pages work: - -- **SkipperHaven** — `C:\Development\skipper\SkipperHaven\SkipperHaven\Program.cs:129`: - ```csharp - builderServices.AddScoped(); - ``` - (with a comment explaining it bridges the per-page save callback from `ModelView` into the generic - `EditModelModal`.) -- **A second consumer** added the identical `AddScoped<...EditModalSaveContextHolder>()` workaround as a - stopgap. - -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. A library that ships components requiring a service its -own setup extension doesn't register is pushing its wiring onto every consumer. The correct owner of the -registration is the library, not the application. - ---- - -## 4. Root-cause findings (from reading the source) - -### 4.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 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. - -### 4.2 Is there already a BlazorBlocks 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. - -### 4.3 Is `EditModalSaveContextHolder` the *only* missing dependency, or will the next page hit a second? - -**It is the only library-owned missing registration.** I enumerated every `[Inject]` across the -BlazorBlocks `Web/Maintenance` tree: - -| 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()` (SkipperHaven - calls it at `Program.cs:36`; it is a documented MudBlazor prerequisite). -- `TViewModel` / `TClient` — the per-entity viewmodel/client, registered by AuthBlocks' - `ConfigureAuthServices` (e.g. `UsersViewModel`, `RegistrationsViewModel`) or by the consumer for its own - entities. - -So `EditModalSaveContextHolder` is the single gap. The next user-admin page will **not** hit a second -missing registration, provided the consumer has already called `AddMudServices()` (which AuthBlocks -already implicitly requires for all its MudBlazor-based pages). Registering the one holder closes the -whole set. - -### 4.4 The AuthBlocks side - -`AuthBlocksWeb/Startup.cs` exposes `ConfigureAuthServices(IServiceCollection, string apiBaseUrl)` — the -**single DI entry point** consumers call to light up the AuthBlocks Web surface. It registers all the -user-admin viewmodels and clients (`UsersViewModel`, `RegistrationsViewModel`, `PermissionsViewModel`, -their clients, auth state, hierarchical authorization, etc.). It does **not** register -`EditModalSaveContextHolder`, and it does **not** call any BlazorBlocks Web extension (there is none to -call). So AuthBlocks ships pages that depend on `ModelView` while leaving one of `ModelView`'s required -services unregistered — the consumer is silently expected to fill the gap, which is exactly what the two -consumers discovered the hard way. - ---- - -## 5. Recommended fix - -**Do both, layered — the registration *lives* in BlazorBlocks; AuthBlocks *calls* it.** - -### 5.1 BlazorBlocks: add a Web-side registration extension (the owner of the type registers the type) - -Add an `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` (with or without AuthBlocks) needs it. - -```csharp -// C:\Development\BlazorBlocks\Web\ServiceCollectionExtensions.cs (new) -using Microsoft.Extensions.DependencyInjection; -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. - /// - public static IServiceCollection AddBlazorBlocksWeb(this IServiceCollection services) - { - // Per-circuit slot bridging ModelView's save callback into EditModelModal. - services.AddScoped(); - return services; - } -} -``` - -Pick the exact method name to match the house convention (`AddBlazorBlocksPostgres` already exists in the -data package, so `AddBlazorBlocksWeb` is the consistent sibling). 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. - -### 5.2 AuthBlocks: call the BlazorBlocks extension from `ConfigureAuthServices` - -So that AuthBlocks stays self-contained for *its* consumers (the whole value of a single -`ConfigureAuthServices` entry point), have `ConfigureAuthServices` call `AddBlazorBlocksWeb()`: - -```csharp -// AuthBlocksWeb/Startup.cs, inside ConfigureAuthServices(...) -services.AddBlazorBlocksWeb(); // registers EditModalSaveContextHolder for the ModelView-based pages -``` - -Place it near the top, before/after the existing registrations — order does not matter for this scoped -service. This means a consumer that calls only `ConfigureAuthServices` (plus the already-required -`AddMudServices`) gets a fully working Users/Registrations admin surface with no manual registrations. - -### 5.3 Why both, and not just one - -- **BlazorBlocks-only** would fix it for direct `ModelView` consumers, but AuthBlocks consumers who only - call `ConfigureAuthServices` would still have to *also* call `AddBlazorBlocksWeb()` themselves — the gap - just moves one layer up and stays a leaked registration. -- **AuthBlocks-only** (register the holder directly inside `ConfigureAuthServices`) would fix the - AuthBlocks consumers, but a product using BlazorBlocks `ModelView` *without* AuthBlocks would still hit - the bug, and AuthBlocks would be reaching into BlazorBlocks' internal namespace to register a type it - doesn't own — the same smell the two consumers exhibited, just relocated. -- **Both** puts the registration with its owner (BlazorBlocks) and makes AuthBlocks self-contained by - composing the owner's extension. This is the standard ASP.NET Core layering: each library exposes an - `Add*` that registers its own services; a higher-level library's `Add*` calls the lower one's. - ---- - -## 6. Lifetime note (confirm and call out) - -Both current consumers register the holder as `AddScoped` (per-circuit). **Scoped is correct** and the -extension above must use `AddScoped`, not singleton or transient: - -- 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. -- 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 (see `EditModelModal.Submit`, the `SaveContext is null` branch). -- 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. - -So: `AddScoped()`, and document the scoped requirement in the extension's -summary so it is not "tidied" to singleton later. - ---- - -## 7. Downstream cleanup (after the fix ships) - -Once BlazorBlocks `Web` and AuthBlocks `Web` are fixed and version-bumped, and consumers pin the new -versions: - -- **SkipperHaven** can delete the manual line at `Program.cs:129` - (`builderServices.AddScoped();`) and its comment. -- **The second consumer** can delete its identical stopgap line. - -These deletions are out of scope for *this* brief (they are consumer-side, done by each consumer team -after picking up the bump) but are listed so the fix's "done" state is unambiguous: the workaround should -no longer exist anywhere. Verify nothing else in either consumer references the holder directly before -removing. - ---- - -## 8. Constraints - -- **Use `AddScoped`** for `EditModalSaveContextHolder` — not singleton, not transient (§6). -- **Do not change `ModelView` / `EditModelModal` to drop the `required`/`[Inject]`** — the holder is the - intentional design (the bridge described in §4.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. -- **Keep `ConfigureAuthServices` the single AuthBlocks entry point.** Do not introduce a second method - consumers must remember to call; fold the BlazorBlocks call into the existing one. -- **Versioning:** both libraries pack/push via `pack.ps1` (`C:\Development\BlazorBlocks\pack.ps1` and the - AuthBlocks equivalent). BlazorBlocks `Web` is currently `Cerebellum.BlazorBlocks.Web` 10.3.32; AuthBlocks - Web is 10.3.33. Bump BlazorBlocks first, then bump AuthBlocks to reference the new BlazorBlocks version - and add the `AddBlazorBlocksWeb()` call. Record both new versions so consumers can pin. -- **Order of operations:** BlazorBlocks bump must land and be referenced by AuthBlocks before the - AuthBlocks bump can call `AddBlazorBlocksWeb()`. - ---- - -## 9. Acceptance criteria - -1. A fresh consumer that calls `AddMudServices()` and `AuthBlocksWeb.Startup.ConfigureAuthServices(...)` — - and **registers nothing else by hand** — can navigate to the Users admin page and the Registrations - admin page with **no `InvalidOperationException`** and **no circuit teardown**. -2. Opening the edit dialog on a user, saving a valid change, succeeds — i.e. the save bridge actually - works (the holder is scoped correctly so `EditModelModal` sees the context `ModelView` set), not merely - "the page loads." -3. A product that uses BlazorBlocks `ModelView` **without** AuthBlocks can register the maintenance deps - via the new `AddBlazorBlocksWeb()` (single call) and gets the same working behavior. -4. The new BlazorBlocks `Web` extension registers `EditModalSaveContextHolder` as **scoped**, with a - summary documenting the scoped requirement and the `AddMudServices` prerequisite. -5. `ConfigureAuthServices` calls the BlazorBlocks extension; no AuthBlocks consumer needs to touch - `Web.Maintenance.Entities` directly. -6. Both libraries are published as version bumps; the new versions are recorded. -7. (Verification, not a code change) The two consumer stopgaps can be deleted and the pages still work. - ---- - -## 10. 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. **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.* -3. **Should `AddBlazorBlocksWeb` be idempotent / safe to double-call?** If a consumer calls it directly - *and* via `ConfigureAuthServices`, a plain `AddScoped` registers the service twice (last wins; harmless - for this slot, but slightly untidy). Consider `TryAddScoped` to make it idempotent. - *Recommendation: use `TryAddScoped` so double-registration is a no-op.* -4. **Are there other BlazorBlocks components elsewhere (outside `Web/Maintenance`) with unregistered - library-owned `[Inject]` deps?** This brief scoped the audit to the maintenance tree that AuthBlocks' - pages use. 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.* - ---- - -## 11. Suggested reading order in the repos - -**BlazorBlocks (`C:\Development\BlazorBlocks`):** -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, where to add the new - `ServiceCollectionExtensions.cs`. -6. `pack.ps1` — the pack/push flow for the version bump. - -**AuthBlocks (`C:\Development\AuthBlocks`):** -1. `AuthBlocksWeb/Startup.cs` — `ConfigureAuthServices`, the single entry point; add the - `AddBlazorBlocksWeb()` call here. -2. `AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor` and - `.../UserAdmin/Registrations/Registrations.razor` — the two pages that render `` and trigger - the defect. -3. The AuthBlocks `pack.ps1` / packaging script — to bump AuthBlocks Web after it references the new - BlazorBlocks version. +- **BlazorBlocks team** → [`team-brief-blazorblocks-modelview-di.md`](./team-brief-blazorblocks-modelview-di.md) + (root cause, `[Inject]` audit, lifetime rationale, the new extension, version bump, acceptance criteria). +- **AuthBlocks team** → [`team-brief-authblocks-modelview-di.md`](./team-brief-authblocks-modelview-di.md) + (the blocking BlazorBlocks prerequisite, the `ConfigureAuthServices` call, version bump, acceptance + criteria). diff --git a/product-notes/team-brief-authblocks-modelview-di.md b/product-notes/team-brief-authblocks-modelview-di.md new file mode 100644 index 0000000..2d0aa32 --- /dev/null +++ b/product-notes/team-brief-authblocks-modelview-di.md @@ -0,0 +1,204 @@ +# Team Brief — AuthBlocks: Register `ModelView`'s Missing Dependency via `ConfigureAuthServices` + +**Audience:** an orchestrator (and its implementers) working **only** in the AuthBlocks repository at +`C:\Development\AuthBlocks`. You do not need, and should not assume, any knowledge of the products that +consume AuthBlocks. Everything you need is in this brief or in that one repo (plus a single new BlazorBlocks +package version — see §2, the blocking prerequisite). + +**Status:** scoped request, blocked on a BlazorBlocks publish (see §2). Confirmed at runtime against +`Cerebellum.AuthBlocks.Web` 10.3.33 / `Cerebellum.BlazorBlocks.Web` 10.3.32. Author: product-designer (for a +downstream consumer team). Date: 2026-06-19. + +**This is one half of a two-team, ordered fix. AuthBlocks ships second — see §2 and §9.** + +--- + +## 1. The defect in one sentence + +`AuthBlocksWeb` ships `Users.razor` and `Registrations.razor`, both of which render BlazorBlocks' +`` component — but `ConfigureAuthServices` (the single DI entry point consumers call to light up +the AuthBlocks Web surface) does **not** ensure `ModelView`'s required service +`Web.Maintenance.Entities.EditModalSaveContextHolder` is registered. So a consumer that wires up AuthBlocks +the normal way gets an unhandled `InvalidOperationException` that **terminates the Blazor circuit on +navigation** to either page, unless that consumer manually hand-registers an internal BlazorBlocks service +in its own `Program.cs`. + +The fix: have `ConfigureAuthServices` call BlazorBlocks' new `AddBlazorBlocksWeb()` extension (which +registers the holder), so AuthBlocks stays self-contained for *its* consumers. + +--- + +## 2. Blocking prerequisite — BlazorBlocks must ship first + +This fix **cannot land until BlazorBlocks publishes a new `Cerebellum.BlazorBlocks.Web` version that +exposes a Web-side registration extension** (working name `AddBlazorBlocksWeb()`). That extension is what +actually registers `EditModalSaveContextHolder`; AuthBlocks' job is only to *call* it. + +- AuthBlocks is currently on `Cerebellum.BlazorBlocks.Web` **10.3.32**, which has **no** such extension. +- The BlazorBlocks team is shipping the extension and bumping the package as the first half of this fix. +- **Reference the specific new version BlazorBlocks publishes for this fix** — fill in the exact version + number once the BlazorBlocks team reports it. Do not proceed against 10.3.32; the method will not exist. + +If you reach this work before the BlazorBlocks version is available, stop and wait for the published version +number. The AuthBlocks change is small once the prerequisite is in hand. + +--- + +## 3. The confirmed failure + +### Stack trace (captured from a consuming app on navigation to the Users admin page) + +``` +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... +``` + +`ModelView` declares `SaveContextHolder` as `required` with `[Inject]`, so 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 user sees a dead page / "An unhandled error has +occurred" and must reload. + +### Which AuthBlocks pages trigger it + +`AuthBlocksWeb` ships two user-admin pages that render ``: + +- `AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor` +- `AuthBlocksWeb/Components/Pages/UserAdmin/Registrations/Registrations.razor` + +Any consumer that routes to either page hits the defect on first navigation. + +--- + +## 4. Why this is AuthBlocks' gap to close (not the consumer's) + +`AuthBlocksWeb/Startup.cs` exposes `ConfigureAuthServices(IServiceCollection, string apiBaseUrl)` — the +**single DI entry point** consumers call to light up the AuthBlocks Web surface. It already registers all +the user-admin viewmodels and clients (`UsersViewModel`, `RegistrationsViewModel`, `PermissionsViewModel`, +their clients, auth state, hierarchical authorization, etc.). It does **not** register +`EditModalSaveContextHolder` and does **not** call any BlazorBlocks Web extension. + +So AuthBlocks ships pages that depend on `ModelView` while leaving one of `ModelView`'s required services +unregistered. The consumer is silently expected to fill the gap — and that is exactly what happened: +**two independent downstream products** each had to hand-register the internal BlazorBlocks service +(`AddScoped()`) in their own `Program.cs` to make +AuthBlocks' shipped pages work. The whole value of a single `ConfigureAuthServices` entry point is that a +consumer calling it (plus the already-required `AddMudServices`) gets a working surface with zero manual +registrations. Today they don't. Closing this gap inside `ConfigureAuthServices` restores that promise. + +Note: `IDialogService` / `ISnackbar` (also injected by `ModelView`) come from MudBlazor's +`AddMudServices()`, which every AuthBlocks Web consumer already calls as a documented prerequisite — those +are not the gap. The single library-owned gap is `EditModalSaveContextHolder`. + +--- + +## 5. The fix + +### 5.1 Bump the BlazorBlocks reference + +Update the `Cerebellum.BlazorBlocks.Web` package reference (currently 10.3.32) to the new version +BlazorBlocks publishes for this fix (see §2 — fill in the exact version). This is what makes +`AddBlazorBlocksWeb()` available. + +### 5.2 Call the extension from `ConfigureAuthServices` + +```csharp +// AuthBlocksWeb/Startup.cs, inside ConfigureAuthServices(...) +services.AddBlazorBlocksWeb(); // registers EditModalSaveContextHolder for the ModelView-based pages +``` + +Order does not matter for this scoped service; place it near the other registrations. The +`AddBlazorBlocksWeb()` extension registers `EditModalSaveContextHolder` as scoped per circuit (its correct +lifetime — the holder is per-circuit mutable state that `ModelView` writes and `EditModelModal` reads). + +### 5.3 Why this belongs in `ConfigureAuthServices`, not pushed onto consumers + +- `ConfigureAuthServices` is AuthBlocks' **single DI entry point**. A consumer calling only + `AddMudServices()` + `ConfigureAuthServices(...)` should get a fully working user-admin surface. Folding + the BlazorBlocks call into the existing entry point keeps that promise; introducing a second method the + consumer must remember to call (or expecting them to call `AddBlazorBlocksWeb()` themselves) just relocates + the leaked registration one layer up. +- AuthBlocks must **not** register `EditModalSaveContextHolder` directly (reaching into BlazorBlocks' + internal `Web.Maintenance.Entities` namespace) — that is the same smell the two consumers exhibited, + merely relocated. Compose BlazorBlocks' own `Add*` extension instead; the registration lives with its + owner, and AuthBlocks stays self-contained by calling it. This is the standard ASP.NET Core layering: + each library exposes an `Add*` for its own services, and a higher-level library's `Add*` calls the lower + one's. + +--- + +## 6. Constraints + +- **Do not register `EditModalSaveContextHolder` directly** in AuthBlocks. Call `AddBlazorBlocksWeb()`; + let BlazorBlocks own its type (§5.3). +- **Keep `ConfigureAuthServices` the single AuthBlocks entry point.** Do not introduce a second method + consumers must remember to call; fold the BlazorBlocks call into the existing one. +- **MudBlazor remains a caller-owned prerequisite.** AuthBlocks already relies on consumers calling + `AddMudServices()` for all its MudBlazor-based pages; do not absorb it into `ConfigureAuthServices`. +- **Versioning:** AuthBlocks Web packs/pushes via its `pack.ps1` / packaging script. `AuthBlocksWeb` is + currently `Cerebellum.AuthBlocks.Web` **10.3.33**; bump to the next version after referencing the new + BlazorBlocks version and adding the `AddBlazorBlocksWeb()` call. **Record the new version** so consumers + can pin. + +--- + +## 7. Acceptance criteria + +1. The `Cerebellum.BlazorBlocks.Web` package reference is bumped to the new version BlazorBlocks published + for this fix, and `ConfigureAuthServices` calls `AddBlazorBlocksWeb()`. +2. A fresh consumer that calls **only** `AddMudServices()` and + `AuthBlocksWeb.Startup.ConfigureAuthServices(...)` — and **registers nothing else by hand** — can + navigate to the Users admin page and the Registrations admin page with **no `InvalidOperationException`** + and **no circuit teardown**. +3. Working behavior means not just page load: opening the edit dialog on a user, saving a valid change, + **succeeds** — i.e. the save bridge actually works end-to-end through AuthBlocks' pages. +4. No AuthBlocks consumer needs to touch `Web.Maintenance.Entities` directly; no manual registrations are + required beyond the documented `AddMudServices`. +5. `AuthBlocksWeb` is published as a version bump from 10.3.33, and the new version number is recorded. + +--- + +## 8. Open questions for the implementing team / its sponsor + +1. **Exact BlazorBlocks version to reference.** Pending the BlazorBlocks team's publish (§2). Confirm the + published version number and the exact extension method name (proposed `AddBlazorBlocksWeb()`) before + landing the call. +2. **Placement within `ConfigureAuthServices`.** Anywhere in the method works (scoped service, order- + independent). Confirm there is no existing convention in `Startup.cs` for grouping third-party `Add*` + calls that this should follow. +3. **Any other AuthBlocks pages built on `ModelView`/`NewModelView`?** This brief identified Users and + Registrations. If the team expects to add more maintenance pages, note that calling `AddBlazorBlocksWeb()` + once covers them all (it is the single home for the maintenance-component deps). + +--- + +## 9. Suggested reading order in the repo + +1. `AuthBlocksWeb/Startup.cs` — `ConfigureAuthServices`, the single entry point; add the + `AddBlazorBlocksWeb()` call here. +2. `AuthBlocksWeb/Components/Pages/UserAdmin/Users/Users.razor` — renders ``; triggers the + defect on navigation. +3. `AuthBlocksWeb/Components/Pages/UserAdmin/Registrations/Registrations.razor` — the second page that + renders ``. +4. The AuthBlocks Web `.csproj` — the `Cerebellum.BlazorBlocks.Web` package reference to bump. +5. The AuthBlocks `pack.ps1` / packaging script — to bump and publish `AuthBlocksWeb` after referencing the + new BlazorBlocks version. + +--- + +## 10. Cross-team ordering (important — you ship second) + +This fix is layered across two repos and **must land in order**: + +1. **BlazorBlocks ships first.** It adds `AddBlazorBlocksWeb()`, bumps `Cerebellum.BlazorBlocks.Web` from + 10.3.32, packs/pushes, and reports the new version number. +2. **Then AuthBlocks (this team)** bumps its `Cerebellum.BlazorBlocks.Web` reference to that published + version, calls `AddBlazorBlocksWeb()` from `ConfigureAuthServices`, bumps `Cerebellum.AuthBlocks.Web` from + 10.3.33, and publishes. + +This team **cannot complete its part until the BlazorBlocks version is published** (§2). Confirm that +version number is in hand before starting. diff --git a/product-notes/team-brief-blazorblocks-modelview-di.md b/product-notes/team-brief-blazorblocks-modelview-di.md new file mode 100644 index 0000000..f615ff9 --- /dev/null +++ b/product-notes/team-brief-blazorblocks-modelview-di.md @@ -0,0 +1,256 @@ +# 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:** 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. + +**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.