## Summary of `CShells` Changes There were two separate runtime problems I was addressing in `CShells`: 1. **Duplicate endpoint registration during startup** - This surfaced as: `System.InvalidOperationException: Not allowed to configure endpoints after startup! Culprit: [Verbs()]` - The root cause was that shell endpoints were getting mapped once during per-shell activation and then mapped again during the startup-wide `ShellsReloaded` flow. 2. **Ambiguous activation of `WebRoutingShellResolver`** - This surfaced as: `Unable to activate type 'CShells.AspNetCore.Resolution.WebRoutingShellResolver'. The following constructors are ambiguous: ...` - The root cause was that dependency injection could satisfy more than one constructor, so type-based activation was ambiguous. --- ## `src/CShells.AspNetCore/Configuration/CShellsBuilderExtensions.cs` ### What changed I made two important changes here. #### 1. Switched web-routing resolver registration from type-based to factory-based creation Instead of registering `WebRoutingShellResolver` by type alone, I changed the resolver pipeline registration to explicitly construct it with: - `IShellHost` - `WebRoutingShellResolverOptions` #### 2. Changed endpoint-registration notification wiring to one shared concrete handler plus per-notification forwarders Previously, `ShellEndpointRegistrationHandler` was registered separately as: - `INotificationHandler` - `INotificationHandler` - `INotificationHandler` - `INotificationHandler` I changed that to: - register one singleton `ShellEndpointRegistrationHandler` - register small forwarders for each notification type: - `ShellActivatedEndpointRegistrationForwarder` - `ShellDeactivatingEndpointRegistrationForwarder` - `ShellRemovedEndpointRegistrationForwarder` - `ShellsReloadedEndpointRegistrationForwarder` ### Why I did that The factory-based resolver registration was to fix the `WebRoutingShellResolver` constructor ambiguity. Since DI could resolve multiple constructors, it was not safe to rely on type activation. Explicit construction forces one intended activation path. The forwarder-based notification registration was needed because the endpoint deduplication logic depends on shared in-memory state. Registering the same implementation separately for multiple closed generic interfaces can lead to different singleton instances being used per interface type. That breaks state sharing. By routing all notifications through one shared concrete `ShellEndpointRegistrationHandler`, the deduplication state is preserved across `ShellActivated`, `ShellDeactivating`, `ShellRemoved`, and `ShellsReloaded`. --- ## `src/CShells.AspNetCore/Notifications/ShellEndpointRegistrationHandler.cs` ### What changed This file contains the main startup-endpoint fix. I added: - an optional `IShellRuntimeStateAccessor` - a lock-protected dictionary of tracked applied generations - helper methods to: - track generations - forget generations - reset tracked generations - an `IsEndpointGraphCurrent(...)` guard I also changed notification handling so that: - `ShellActivated` registers endpoints and tracks the applied generation - `ShellDeactivating` removes endpoints and forgets the generation - `ShellRemoved` removes endpoints and forgets the generation - `ShellsReloaded` now: - filters to routable shells - checks whether the current endpoint graph is already current - skips rebuild if nothing changed - otherwise clears and rebuilds the full endpoint graph and retracks generations Finally, I added the forwarder classes at the bottom of the file. ### Why I did that This was the core fix for the duplicate startup endpoint-registration path. The failure mode was effectively: 1. A shell becomes active 2. Its endpoints are mapped 3. A startup-wide `ShellsReloaded` is published 4. All shell endpoints are rebuilt again That second pass was a problem for `FastEndpoints`, which rejects endpoint configuration after startup has progressed beyond its expected window. To fix that, I made `ShellEndpointRegistrationHandler` state-aware. It now checks whether the currently mapped shell IDs and applied generations already match runtime state. If so, it skips the aggregate rebuild instead of remapping the same endpoints again. That preserves the ability to rebuild endpoints when shells truly change, while avoiding no-op startup remaps. --- ## `src/CShells/Hosting/ShellStartupHostedService.cs` ### What changed I changed startup publication of `ShellsReloaded` to use a custom notification strategy: - `startupShellsReloadedStrategy` I also added a private `StartupShellsReloadedNotificationStrategy` implementation that filters out ASP.NET Core notification handlers during the startup `ShellsReloaded` publication. ### Why I did that This was a startup-specific safety measure. Even after adding deduplication logic, I wanted to prevent the startup-wide `ShellsReloaded` from immediately driving the ASP.NET Core endpoint-remapping path in the same startup cycle. The intent was: - keep publishing `ShellsReloaded` as a meaningful system event - avoid redundant ASP.NET Core endpoint registration work during startup - preserve other non-ASP.NET-Core `ShellsReloaded` observers So this was not a general suppression of `ShellsReloaded`; it was a targeted startup filter to avoid duplicate endpoint mapping during application bootstrap. --- ## `src/CShells/Resolution/ResolverPipelineBuilder.cs` ### What changed I added support for registering resolver strategies with a factory: - `Use(Func factory, int? order = null)` I also updated internal strategy registration so each strategy registration can now hold: - a type - an instance - or a factory During pipeline build, the resolver strategy can now be registered using that explicit factory. ### Why I did that This was necessary to support the explicit-construction fix for `WebRoutingShellResolver` in `CShellsBuilderExtensions`. Once I decided not to rely on DI constructor selection for that resolver, the pipeline needed a way to say: > register this resolver strategy, but instantiate it exactly this way Without factory support in `ResolverPipelineBuilder`, there was no clean way to do that. --- ## Supporting Test Changes These test changes matter because they encode the design intent behind the production changes. ### `tests/CShells.Tests/Integration/AspNetCore/ApplicationBuilderExtensionsTests.cs` #### What changed I added a regression test that verifies: - after a shell is activated and endpoints are mapped, - a subsequent `ShellsReloaded` with the same applied generation state - does **not** remap the endpoints again I also added: - a counting web feature - a simple endpoint mapping counter #### Why I did that Because the production bug was about duplicate endpoint registration, I wanted a test that directly proves the bad sequence no longer happens. The test protects the scenario that previously triggered the `FastEndpoints` startup exception. --- ### `tests/CShells.Tests/Integration/AspNetCore/ServiceCollectionExtensionsTests.cs` #### What changed I added a regression test that verifies the DI registration shape now consists of: - one concrete `ShellEndpointRegistrationHandler` - forwarders registered for each notification interface #### Why I did that The shared-state design only works if all notification types ultimately delegate to the same concrete handler instance. This test protects that registration shape so it is not accidentally simplified back into a broken form later. --- ## Overall Rationale The `CShells` changes were meant to make startup behavior more correct and deterministic. ### For endpoint registration I was trying to prevent this sequence: - shell activates - endpoints are mapped - startup-wide reload fires - endpoints are rebuilt again - `FastEndpoints` rejects the second configuration pass So I: - added endpoint graph/generation tracking - ensured the tracking state is actually shared across notification types - skipped unnecessary aggregate rebuilds - added a startup-specific filter as a safety measure ### For web routing I was trying to prevent DI from making an ambiguous constructor choice when building `WebRoutingShellResolver`. So I: - stopped relying on type-based activation - added factory-based resolver registration support - explicitly constructed the resolver with the intended dependencies --- ## Net Effect These changes were intended to move `CShells` away from startup behavior that depended on: - duplicate lifecycle-triggered endpoint registration - DI constructor-selection heuristics and toward behavior that is: - state-aware - deduplicated - explicitly constructed where necessary That is why each of those `CShells` files changed the way they did.