259 lines
14 KiB
Markdown
259 lines
14 KiB
Markdown
# Team Brief — BlazorBlocks: Register the Missing `EditModalSaveContextHolder` DI Dependency
|
||
|
||
**Audience:** an orchestrator (and its implementers) working **only** in the BlazorBlocks repository at
|
||
`C:\Development\BlazorBlocks`. You do not need, and should not assume, any knowledge of AuthBlocks or of
|
||
any product that consumes BlazorBlocks. Everything you need is in this brief or in that one repo.
|
||
|
||
**Status:** ✅ RESOLVED — shipped in `Cerebellum.BlazorBlocks.Web` 10.3.33 + `Cerebellum.AuthBlocks.Web` 10.3.36 (2026-06-20). ~~scoped request, not yet started. Confirmed at runtime against `Cerebellum.BlazorBlocks.Web` 10.3.32. Author: product-designer (for a downstream consumer team). Date: 2026-06-19.~~
|
||
|
||
> **Resolution (2026-06-20):** `AddBlazorBlocksWeb()` landed in `Cerebellum.BlazorBlocks.Web` 10.3.33 and `ConfigureAuthServices` calls it in `Cerebellum.AuthBlocks.Web` 10.3.36; DeepDrftManager picked up 10.3.36 and removed its local `EditModalSaveContextHolder` stopgap.
|
||
> This brief is retained as historical record — no further action required.
|
||
|
||
**This is one half of a two-team, ordered fix. BlazorBlocks ships first — see §9.**
|
||
|
||
---
|
||
|
||
## 1. The defect in one sentence
|
||
|
||
The BlazorBlocks `ModelView` component (and the `EditModelModal` it drives) has a `required [Inject]`
|
||
dependency on `Web.Maintenance.Entities.EditModalSaveContextHolder`, but **the `Web` package ships no
|
||
`IServiceCollection` registration extension at all** — so the dependency is never registered by the
|
||
library, and any consumer that surfaces a page built on `ModelView` gets an unhandled
|
||
`InvalidOperationException` that **terminates the Blazor circuit on navigation**, unless that consumer
|
||
manually hand-registers the internal service in its own `Program.cs`.
|
||
|
||
The fix is a BlazorBlocks library fix delivered as a version bump: add a Web-side `Add*` extension that
|
||
registers the holder.
|
||
|
||
---
|
||
|
||
## 2. The confirmed failure (downstream symptom — evidence, not your concern to chase)
|
||
|
||
The defect was discovered by a consumer navigating to an admin page built on `ModelView`. The captured
|
||
stack trace is included here as confirming evidence of the unregistered-service failure mode; the
|
||
consumer's identity and its pages are out of scope for this team — your job is to register the dependency
|
||
the library requires.
|
||
|
||
```
|
||
System.InvalidOperationException: Cannot provide a value for property 'SaveContextHolder' on type
|
||
'Web.Maintenance.Entities.ModelView`5[[AuthBlocksModels.InputModels.UserInputModel, ...],
|
||
[AuthBlocksModels.Models.UserModel, ...],[AuthBlocksWeb.Components.Pages.UserAdmin.Users.UserEditModal, ...],
|
||
[AuthBlocksWeb.Components.Pages.UserAdmin.Users.UsersViewModel, ...],
|
||
[AuthBlocksModels.Converters.UserModelToInputConverter, ...]]'.
|
||
There is no registered service of type 'Web.Maintenance.Entities.EditModalSaveContextHolder'.
|
||
at Microsoft.AspNetCore.Components.ComponentFactory...CreatePropertyInjector...
|
||
```
|
||
|
||
Because `SaveContextHolder` is declared `required` with `[Inject]`, Blazor's component factory throws
|
||
during component activation. There is no try/catch around component instantiation in the render path, so
|
||
the exception propagates and tears down the circuit. The end user sees a dead page / "An unhandled error
|
||
has occurred" and must reload.
|
||
|
||
The tell that this is a library gap and not a consumer mistake: **two independent downstream products**
|
||
have each had to hand-register the *same* internal BlazorBlocks service
|
||
(`AddScoped<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 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<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` (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.
|