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:
@@ -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 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<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` (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 `<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 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.
|
||||||
Reference in New Issue
Block a user