Files
deepdrft/product-notes/team-brief-blazorblocks-modelview-di.md

259 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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.