14 KiB
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 inCerebellum.BlazorBlocks.Web10.3.33 andConfigureAuthServicescalls it inCerebellum.AuthBlocks.Web10.3.36; DeepDrftManager picked up 10.3.36 and removed its localEditModalSaveContextHolderstopgap. 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:
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— setsSaveContextHolder.Currentbefore opening the edit dialog and clears it in afinally(seeEditItem, lines 137–172).Web/Maintenance/Entities/EditModelModal.razor:40— readsSaveContextHolder.Currentto 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()(registersIDbExceptionClassifier; 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'sAddMudServices()(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.
// 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.EditItemwritesCurrentimmediately before opening the dialog and nulls it infinally;EditModelModalreads 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
ModelViewandEditModelModaldifferent 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 (theSaveContext is nullbranch inEditModelModal.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."
AddScopedin 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/EditModelModalto drop therequired/[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.
AddBlazorBlocksWebmust not callAddMudServices()— consumers configure MudBlazor (theme, snackbar options) themselves, and double-registration would clobber their config. DocumentAddMudServicesas a prerequisite in the extension's summary; do not absorb it. - Versioning: the
Webpackage packs/pushes viaC:\Development\BlazorBlocks\pack.ps1.Webis currentlyCerebellum.BlazorBlocks.Web10.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
- The
Webproject exposes a newIServiceCollectionextension (AddBlazorBlocksWeb()or the confirmed house name) that registersEditModalSaveContextHolderas scoped, with a summary documenting the scoped requirement and theAddMudServicesprerequisite. - 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. - 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
EditModelModalsees the contextModelViewset). A page that loads but silently no-ops the save (the transient-lifetime trap in §4) does not pass. - The
Webpackage 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
- Extension method name.
AddBlazorBlocksWeb()is proposed for consistency with the existingAddBlazorBlocksPostgres(). Confirm, or pick the preferred house name (e.g.AddBlazorBlocksMaintenance()if the team expects to split Web concerns further). Recommendation:AddBlazorBlocksWeb(). - 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. - Scope of the extension. Register only
EditModalSaveContextHoldernow (the only current gap), or pre-emptively makeAddBlazorBlocksWeb()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. - Other unregistered library-owned
[Inject]deps elsewhere? This brief scoped the audit to theWeb/Maintenancetree. If the team wants a clean bill of health, grep the wholeWebproject 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
Web/Maintenance/Entities/EditModalSaveContextHolder.cs— the unregistered service (and its scoped doc).Web/Maintenance/Entities/ModelView.razor.cs—[Inject]at line 42;EditItem(137–172) shows the holder being written/cleared around the dialog.Web/Maintenance/Entities/EditModelModal.razor—[Inject]at line 40;Submitshows the holder being read (and theSaveContext is nullfallback that masks the bug into a wrong-behavior path if the lifetime is botched).Data.Postgres/ServiceCollectionExtensions.cs— the existingAdd*convention to mirror.Web/Web.csproj— package idCerebellum.BlazorBlocks.Web, current version (10.3.32), where to add the newServiceCollectionExtensions.cs.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:
- BlazorBlocks (this team) ships first. Add
AddBlazorBlocksWeb(), bumpCerebellum.BlazorBlocks.Webfrom 10.3.32, pack/push, and report the new version number. - Then AuthBlocks bumps its
Cerebellum.BlazorBlocks.Webreference to the version this team just published and callsAddBlazorBlocksWeb()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.