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

14 KiB
Raw Permalink Blame History

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:

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.csAddBlazorBlocksPostgres() (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.

// 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.