docs: split ModelView DI brief into per-team BlazorBlocks and AuthBlocks briefs

Two self-contained team briefs with explicit ship-ordering; original trimmed to an index pointing to both.
This commit is contained in:
daniel-c-harvey
2026-06-19 23:26:56 -04:00
parent b90604d311
commit 4bec507aab
3 changed files with 487 additions and 335 deletions
@@ -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 **Status:** scoped, not yet started. Confirmed against `Cerebellum.BlazorBlocks.Web` 10.3.32 /
`C:\Development\BlazorBlocks` and/or the **AuthBlocks** repository at `C:\Development\AuthBlocks`. `Cerebellum.AuthBlocks.Web` 10.3.33. Author: product-designer. Date: 2026-06-19.
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 request, not yet started. Confirmed at runtime against `Cerebellum.AuthBlocksWeb` ## The defect
10.3.33 / `Cerebellum.BlazorBlocks.Web` 10.3.32. Author: product-designer (for a downstream consumer team).
Date: 2026-06-19.
--- 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]` 1. **BlazorBlocks ships first.** Add a Web-side `AddBlazorBlocksWeb()` extension that registers the holder
dependency on `Web.Maintenance.Entities.EditModalSaveContextHolder`, but **no BlazorBlocks setup via `TryAddScoped` (scoped is required). Bump `Cerebellum.BlazorBlocks.Web` from 10.3.32; report the new
extension and no AuthBlocks setup extension registers that service** — so any consumer that surfaces a version.
page built on `ModelView` gets an unhandled `InvalidOperationException` that **terminates the Blazor 2. **AuthBlocks ships second.** Bump its `Cerebellum.BlazorBlocks.Web` reference to that new version, call
circuit on navigation**, unless the consumer manually hand-registers the internal service in its own `AddBlazorBlocksWeb()` from `ConfigureAuthServices`, bump `Cerebellum.AuthBlocks.Web` from 10.3.33.
`Program.cs`.
The fix is an **upstream library fix**, delivered as a version bump. The goal: a consumer that calls AuthBlocks is blocked until BlazorBlocks' new version is published. Registration lives with its owner
AuthBlocks' single DI entry point `AuthBlocksWeb.Startup.ConfigureAuthServices(...)` gets a fully (BlazorBlocks); AuthBlocks stays self-contained by composing it. MudBlazor (`AddMudServices`) stays a
working user-admin surface with **zero** manual service registrations. 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) - **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)
System.InvalidOperationException: Cannot provide a value for property 'SaveContextHolder' on type (the blocking BlazorBlocks prerequisite, the `ConfigureAuthServices` call, version bump, acceptance
'Web.Maintenance.Entities.ModelView`5[[AuthBlocksModels.InputModels.UserInputModel, ...], criteria).
[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 `<ModelView>`:
- `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<Web.Maintenance.Entities.EditModalSaveContextHolder>();
```
(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 137172).
- `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<TModel>` | `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
{
/// <summary>
/// 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.
/// </summary>
public static IServiceCollection AddBlazorBlocksWeb(this IServiceCollection services)
{
// Per-circuit slot bridging ModelView's save callback into EditModelModal.
services.AddScoped<EditModalSaveContextHolder>();
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<EditModalSaveContextHolder>()`, 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<Web.Maintenance.Entities.EditModalSaveContextHolder>();`) 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` (137172) 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 `<ModelView>` and trigger
the defect.
3. The AuthBlocks `pack.ps1` / packaging script — to bump AuthBlocks Web after it references the new
BlazorBlocks version.
@@ -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'
`<ModelView>` 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 `<ModelView>`:
- `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<Web.Maintenance.Entities.EditModalSaveContextHolder>()`) 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 `<ModelView>`; triggers the
defect on navigation.
3. `AuthBlocksWeb/Components/Pages/UserAdmin/Registrations/Registrations.razor` — the second page that
renders `<ModelView>`.
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.
@@ -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<Web.Maintenance.Entities.EditModalSaveContextHolder>()`) 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 137172).
- `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<TModel>` | `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
{
/// <summary>
/// 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.
/// </summary>
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<EditModalSaveContextHolder>();
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` (137172) 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.