Skip to content

Eliminate ExtensionLoader assembly scanning via source-generated extension manifest (GH-2902)#2918

Merged
jeremydmiller merged 3 commits into
mainfrom
eliminate-extensionloader-2902
May 27, 2026
Merged

Eliminate ExtensionLoader assembly scanning via source-generated extension manifest (GH-2902)#2918
jeremydmiller merged 3 commits into
mainfrom
eliminate-extensionloader-2902

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes #2902.

What

Replaces Wolverine's runtime assembly scan for IWolverineExtension types with the compile-time extension manifest from JasperFx (jasperfx#371), and removes ExtensionLoader entirely. Startup no longer probes the application's bin directory (AssemblyFinder.FindAssemblies) or speculatively Assembly.Loads candidates.

Changes

  • IWolverineExtension : IJasperFxExtension (and IAsyncWolverineExtension too) so implementers are seen by the generator's marker scan.
  • JasperFx.SourceGenerator flows transitively from the WolverineFx package (PrivateAssets="none"), so the app's own assembly and every extension/transport assembly emit a JasperFx.Generated.DiscoveredExtensions manifest. The generator self-gates to eligible assemblies ([JasperFxAssembly]/[WolverineModule]-marked or executable), so it is a no-op where it has nothing to emit.
  • ExtensionLoader deleted. New WolverineOptions.DiscoverAndApplyExtensions() reads the manifest from loaded assemblies and applies extensions early (while the IServiceCollection is still mutable, matching historical timing).
  • [WolverineModule] assemblies are still added to handler discovery via IncludeExtensionAssemblies (covers pure handler modules like OrderExtension that declare a bare [assembly: WolverineModule] and emit no manifest entry).

Two design points worth a look

  1. Discovery is scoped to each assembly's declared [WolverineModule<T>] type — not every IWolverineExtension the marker scan lists. The manifest's marker scan also picks up framework-internal helpers in the [JasperFxAssembly]-marked core (DisableExternalTransports, DisablePersistence, UseSoloDurabilityMode, the ConfigureWolverine lambda extension). Those are applied explicitly/via DI and must never be auto-activated — auto-applying DisableExternalTransports would stub every transport, and LambdaWolverineExtension has no parameterless ctor. Gating on the declaring assembly's declared module type reproduces the exact set the old scan applied.

  2. Reference-graph module loading instead of the bin-directory glob. The manifest reader only sees loaded assemblies, but a referenced module (e.g. WolverineFx.RuntimeCompilation) may not be loaded at bootstrap. Discovery first walks the application's reference graph (Assembly.GetReferencedAssemblies, skipping framework prefixes) and loads the [WolverineModule] assemblies it reaches — not the old Directory.EnumerateFiles probe — so "reference the package and it auto-activates" still works.

Acceptance criteria

  • IWolverineExtension extends IJasperFxExtension; extensions discovered via the generated manifest are applied identically to today (same set, same Configure ordering/idempotency).
  • No AssemblyFinder.FindAssemblies bin-directory probe on startup.
  • The JasperFx.SourceGenerator analyzer is referenced so app + extension assemblies emit manifests.
  • AOT/trim: the [RequiresUnreferencedCode] AssemblyFinder scan is gone from the extension path.
  • Regression coverage for modular-monolith / multi-extension-assembly discovery (extension_discovery_via_manifest, plus the existing Module1/OrderExtension/RuntimeCompilation fixtures via the default-app suites).

Verification

  • Full wolverine.slnx Release build: 0 warnings / 0 errors.
  • All 1713 CoreTests pass (net9.0).

🤖 Generated with Claude Code

jeremydmiller and others added 3 commits May 27, 2026 08:09
…H-2902)

Wolverine no longer probes the application's bin directory (AssemblyFinder.
FindAssemblies) and speculatively Assembly.Loads candidates to find
[WolverineModule] extensions at startup. Discovery now reads the compile-time
JasperFx.Generated.DiscoveredExtensions manifest emitted by the
JasperFx.SourceGenerator ExtensionDiscoveryGenerator.

- IWolverineExtension (and IAsyncWolverineExtension) now extend
  JasperFx.IJasperFxExtension so implementers are seen by the generator's
  marker scan.
- The JasperFx.SourceGenerator analyzer is referenced from WolverineFx with
  PrivateAssets="none" so it flows transitively to the app's own assembly and
  every extension/transport assembly; each emits its DiscoveredExtensions
  manifest. The generator self-gates to eligible ([JasperFxAssembly]/
  [WolverineModule]-marked or executable) assemblies.
- ExtensionLoader is deleted. WolverineOptions.DiscoverAndApplyExtensions()
  replaces it: it applies ONLY each assembly's declared [WolverineModule<T>]
  type (exact parity with the old scan), which excludes the framework-internal
  IWolverineExtension helpers (DisableExternalTransports, the ConfigureWolverine
  lambda extension, ...) that the manifest's marker scan also lists and that
  must never be auto-activated.
- Because the manifest reader only sees loaded assemblies, discovery first
  walks the application's reference graph (Assembly.GetReferencedAssemblies)
  and loads the [WolverineModule] assemblies it reaches — NOT the old
  bin-directory glob — so reference-and-go modules like
  WolverineFx.RuntimeCompilation still auto-activate. [WolverineModule]
  assemblies are still added to handler discovery via IncludeExtensionAssemblies.

Adds regression coverage (extension_discovery_via_manifest) asserting the
declared module extension is applied and framework-internal helpers never are,
and updates docs/guide/extensions.md to describe the [WolverineModule]/
[JasperFxAssembly] attributes and the source-generator-based discovery.

Full wolverine.slnx Release build clean; all 1713 CoreTests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…st (GH-2902 CI fix)

The reference-graph walk (Assembly.GetReferencedAssemblies) could not see a referenced
[WolverineModule] assembly whose types the application never uses directly: the C#
compiler prunes such references from assembly metadata. WolverineFx.RuntimeCompilation
is exactly that case — you reference the package for its [WolverineModule] auto-activation,
not to use its types — so its source-generated DiscoveredExtensions manifest was never
read, and TypeLoadMode.Dynamic apps failed at startup with "no IAssemblyGenerator
(Roslyn) is registered". That broke MessageRoutingTests and other suites in CI (and
would affect real reference-and-auto-activate apps too, not just tests).

Load the non-framework assemblies from the runtime's resolved deployment list
(TRUSTED_PLATFORM_ASSEMBLIES) instead, which includes those pruned-but-deployed
assemblies. This is still NOT the old AssemblyFinder Directory.EnumerateFiles bin probe
— it reads the runtime's own resolved assembly list rather than recursively scanning the
filesystem. The reference-graph walk is kept as a fallback for hosts that do not surface
that list (e.g. single-file deployments).

Validated: previously-failing MessageRoutingTests (25) now pass; CoreTests (1713) pass;
full wolverine.slnx Release build clean (0 warnings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller
Copy link
Copy Markdown
Member Author

Brought the branch up to date with main and fixed the CI failures.

Note on history: branch protection blocks force-push, so I merged main in (commit 21280ef) rather than rebasing.

CI failure root cause: every failing suite (e.g. MessageRoutingTests) threw no IAssemblyGenerator (Roslyn) is registered. WolverineFx.RuntimeCompilation is referenced for its [WolverineModule] auto-activation but its types are never used directly, so the C# compiler prunes it from assembly metadata — the reference-graph walk (Assembly.GetReferencedAssemblies) couldn't see it even though it's deployed, so its DiscoveredExtensions manifest was never read. (This would affect real reference-and-auto-activate apps too, not just tests.)

Fix (a0bd68c): load the non-framework assemblies from the runtime's resolved deployment list (TRUSTED_PLATFORM_ASSEMBLIES), which includes pruned-but-deployed assemblies. Still not the old Directory.EnumerateFiles bin probe — it reads the runtime's own assembly list. The reference-graph walk remains as a fallback for hosts that don't surface that list (single-file deployments).

Validated locally: previously-failing MessageRoutingTests (25) now pass, CoreTests (1713) pass, full wolverine.slnx Release build clean.

@jeremydmiller jeremydmiller merged commit 7878886 into main May 27, 2026
20 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Eliminate ExtensionLoader assembly scanning via JasperFx source-generated extension manifest

1 participant