diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md
index a2826991d..1f3e84633 100644
--- a/docs/guide/extensions.md
+++ b/docs/guide/extensions.md
@@ -246,10 +246,6 @@ await app.Services.ApplyAsyncWolverineExtensions();
## Wolverine Plugin Modules
-::: warning
-This functionality will likely be eliminated in Wolverine 3.0.
-:::
-
::: tip
Use this sparingly, but it might be advantageous for adding extra instrumentation or extra middleware
:::
@@ -269,11 +265,45 @@ with this attribute to automatically add that extension to Wolverine:
snippet source | anchor
+You can also use the non-generic `[assembly: WolverineModule]` form to mark an assembly purely as a *handler*
+module — its handlers are discovered, but no `IWolverineExtension` is applied.
+
+### How discovery works (source generator, not runtime scanning)
+
+::: tip
+Module discovery moved to a compile-time source generator in Wolverine 6.0 (GH-2902). Earlier versions
+probed the application's bin directory at startup (`AssemblyFinder`), which was slower and not AOT/trim-friendly.
+:::
+
+The `[WolverineModule]` and `[WolverineModule]` attributes both derive from JasperFx's `[JasperFxAssembly]`
+attribute. The **`JasperFx.SourceGenerator`** analyzer — which the `WolverineFx` package flows transitively to
+every project that references it (directly or through any Wolverine extension/transport package) — sees these
+assembly attributes at **compile time** and emits a small `JasperFx.Generated.DiscoveredExtensions` manifest into
+the assembly listing the declared extension type(s). At startup Wolverine reads those generated manifests from the
+loaded assemblies instead of scanning the filesystem and speculatively `Assembly.Load`-ing candidates.
+
+A few consequences worth knowing:
+
+* **Only the type declared in `[WolverineModule]` (or `[WolverineModule(typeof(T))]`) is auto-applied.** Other
+ `IWolverineExtension` implementations sitting in the same assembly are *not* activated automatically — register
+ those explicitly with `opts.Include()` or `services.AddWolverineExtension()`.
+* The extension assembly must be **compiled against WolverineFx 6.0+** so that the analyzer runs and emits its
+ manifest. A module built against an older Wolverine will not be auto-discovered; reference it and call its
+ configuration explicitly instead.
+* Wolverine walks the application's **reference graph** to make sure referenced module assemblies (for example
+ `WolverineFx.RuntimeCompilation`) are loaded before reading their manifests, so "reference the package and it
+ just activates" still works. This deliberately does *not* glob the bin directory the way the old scan did.
+
## Disabling Assembly Scanning
-Some Wolverine users have seen rare issues with the assembly scanning cratering an application with out of memory
-exceptions in the case of an application directory being the same as the root of a Docker container. *If* you experience
-that issue, or just want a faster start up time, you can disable the automatic extension discovery using this syntax:
+::: info
+As of Wolverine 6.0 there is no runtime bin-directory assembly scan for extensions — discovery is driven by the
+compile-time manifest described above. `ExtensionDiscovery.ManualOnly` remains the switch to turn automatic module
+discovery off entirely.
+:::
+
+If you want a marginally faster start up, or you simply want full control over which extensions are applied, you can
+disable automatic extension discovery (the reference-graph walk and manifest read) with this syntax:
diff --git a/src/Testing/CoreTests/Configuration/extension_discovery_via_manifest.cs b/src/Testing/CoreTests/Configuration/extension_discovery_via_manifest.cs
new file mode 100644
index 000000000..51e866feb
--- /dev/null
+++ b/src/Testing/CoreTests/Configuration/extension_discovery_via_manifest.cs
@@ -0,0 +1,58 @@
+using System.Linq;
+using JasperFx;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Module1;
+using Wolverine.ComplianceTests;
+using Wolverine.Tracking;
+using Xunit;
+
+namespace CoreTests.Configuration;
+
+// Auto-discovery of [WolverineModule] extensions now flows through the compile-time manifest
+// emitted by JasperFx.SourceGenerator (the JasperFx.Generated.DiscoveredExtensions class) instead
+// of the old runtime ExtensionLoader + AssemblyFinder filesystem scan. See GH-2902.
+public class extension_discovery_via_manifest
+{
+ private static Task startHostAsync()
+ {
+ // Default ExtensionDiscovery.Automatic; conventional *handler* discovery is off only to keep
+ // the host light — it does not affect extension discovery.
+ return Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
+ .UseWolverine(opts => opts.DisableConventionalDiscovery())
+ .StartAsync();
+ }
+
+ [Fact]
+ public async Task declared_module_extension_is_discovered_from_the_manifest_and_applied()
+ {
+ using var host = await startHostAsync();
+
+ // Module1 declares [assembly: WolverineModule]; Module1Extension.Configure
+ // registers IModuleService. Its presence proves the manifest-driven discovery applied it.
+ host.GetRuntime().Options.AppliedExtensions
+ .ShouldContain(x => x is Module1Extension);
+
+ host.Services.GetRequiredService()
+ .HasRegistrationFor().ShouldBeTrue();
+ }
+
+ [Fact]
+ public async Task framework_internal_extensions_are_never_auto_applied()
+ {
+ using var host = await startHostAsync();
+ var options = host.GetRuntime().Options;
+
+ // The manifest's marker-interface scan also lists Wolverine's own internal IWolverineExtension
+ // helpers (these are applied explicitly / via DI, never auto-discovered). Discovery is gated to
+ // each assembly's declared [WolverineModule] type, so none of them may leak in here. Auto-applying
+ // DisableExternalTransports in particular would silently stub every external transport.
+ options.ExternalTransportsAreStubbed.ShouldBeFalse();
+
+ var applied = options.AppliedExtensions.Select(x => x.GetType().Name).ToArray();
+ applied.ShouldNotContain("DisableExternalTransports");
+ applied.ShouldNotContain("DisablePersistence");
+ applied.ShouldNotContain("UseSoloDurabilityMode");
+ applied.ShouldNotContain("LambdaWolverineExtension");
+ }
+}
diff --git a/src/Wolverine/ExtensionLoader.cs b/src/Wolverine/ExtensionLoader.cs
deleted file mode 100644
index 1610ef709..000000000
--- a/src/Wolverine/ExtensionLoader.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Reflection;
-using JasperFx.Core;
-using JasperFx.Core.Reflection;
-using JasperFx.Core.TypeScanning;
-using Wolverine.Attributes;
-
-namespace Wolverine;
-
-internal static class ExtensionLoader
-{
- private static Assembly[]? _extensions;
- private static bool hasWarned = false;
-
- internal static bool IsModule(Assembly assembly)
- {
- try
- {
- if (assembly.HasAttribute()) return true;
- }
- catch (Exception)
- {
- if (!hasWarned)
- {
- Console.WriteLine("To disable automatic Wolverine extension finding, and stop these messages, see:");
- Console.WriteLine("https://wolverinefx.net/guide/extensions.html#disabling-assembly-scanning");
- }
- }
-
- return false;
- }
-
- [UnconditionalSuppressMessage("Trimming", "IL2026",
- Justification = "AssemblyFinder.FindAssemblies scans the application base directory for assemblies with [WolverineModule]. AOT-publishing apps should opt out of auto-extension-discovery (see https://wolverinefx.net/guide/extensions.html#disabling-assembly-scanning) and register extensions explicitly.")]
- internal static Assembly[] FindExtensionAssemblies()
- {
- if (_extensions != null)
- {
- return _extensions;
- }
-
- Action logFailure = msg =>
- {
- if (!hasWarned)
- {
- Console.WriteLine("To disable automatic Wolverine extension finding, and stop these messages, see:");
- Console.WriteLine("https://wolverinefx.net/guide/extensions.html#disabling-assembly-scanning");
- }
-
- Console.WriteLine(msg);
- };
-
- _extensions = AssemblyFinder
- .FindAssemblies(logFailure, a => a.HasAttribute(), false)
- .Concat(AppDomain.CurrentDomain.GetAssemblies())
- .Distinct()
- .Where(a => a.HasAttribute())
- .ToArray();
-
- var names = _extensions.Select(x => x.GetName().Name);
-
- Assembly[] FindDependencies(Assembly a)
- {
- return _extensions!.Where(x => names.Contains(x.GetName().Name)).ToArray();
- }
-
- _extensions = _extensions.TopologicalSort(FindDependencies, false).ToArray();
-
- return _extensions;
- }
-
- // Activator.CreateInstance(x!) on a Type-typed lambda parameter loses the
- // [DAM(PublicConstructors)] annotation that's present on the source field
- // (WolverineModuleAttribute.WolverineExtensionType — annotated in chunk G
- // #2760). The IL flow analyzer can't track DAM through a Select projection.
- // Suppression here is honest: the constructor preservation requirement IS
- // met at the source, IL just can't see it. AOT-clean apps register
- // extensions explicitly via WolverineOptions.AddExtension and avoid
- // assembly scanning entirely (see AOT publishing guide).
- [UnconditionalSuppressMessage("Trimming", "IL2067",
- Justification = "Constructor preservation requirement is met at the source (WolverineExtensionType is [DAM(PublicConstructors)]); IL flow analyzer can't see through Select projection. AOT consumers register explicitly via WolverineOptions.AddExtension. See AOT guide.")]
- internal static void ApplyExtensions(WolverineOptions options)
- {
- var assemblies = FindExtensionAssemblies();
-
- if (assemblies.Length == 0)
- {
- return;
- }
-
- options.IncludeExtensionAssemblies(assemblies);
-
- var extensions = assemblies.Select(x => x.GetAttribute()!.WolverineExtensionType)
- .Where(x => x != null)
- .Select(x => Activator.CreateInstance(x!)!.As())
- .ToArray();
-
- options.ApplyExtensions(extensions);
- }
-}
diff --git a/src/Wolverine/HostBuilderExtensions.cs b/src/Wolverine/HostBuilderExtensions.cs
index 917a8c69b..c106e5366 100644
--- a/src/Wolverine/HostBuilderExtensions.cs
+++ b/src/Wolverine/HostBuilderExtensions.cs
@@ -251,7 +251,7 @@ internal static IServiceCollection AddWolverine(this IServiceCollection services
options.Services = services;
if (discovery == ExtensionDiscovery.Automatic)
{
- ExtensionLoader.ApplyExtensions(options);
+ options.DiscoverAndApplyExtensions();
}
if (options.ApplicationAssembly != null)
diff --git a/src/Wolverine/IWolverineExtension.cs b/src/Wolverine/IWolverineExtension.cs
index 01489e3b4..2a1fb7ae8 100644
--- a/src/Wolverine/IWolverineExtension.cs
+++ b/src/Wolverine/IWolverineExtension.cs
@@ -1,10 +1,12 @@
+using JasperFx;
+
namespace Wolverine;
#region sample_iwolverineextension
///
/// Use to create loadable extensions to Wolverine applications
///
-public interface IWolverineExtension
+public interface IWolverineExtension : IJasperFxExtension
{
///
/// Make any alterations to the WolverineOptions for the application
@@ -19,7 +21,7 @@ public interface IWolverineExtension
/// Loadable extensions to Wolverine applications that may require
/// IoC services or asynchronous operations
///
-public interface IAsyncWolverineExtension
+public interface IAsyncWolverineExtension : IJasperFxExtension
{
ValueTask Configure(WolverineOptions options);
}
diff --git a/src/Wolverine/Wolverine.csproj b/src/Wolverine/Wolverine.csproj
index 7345803ac..5819f852d 100644
--- a/src/Wolverine/Wolverine.csproj
+++ b/src/Wolverine/Wolverine.csproj
@@ -58,9 +58,17 @@
-
+
+
diff --git a/src/Wolverine/WolverineOptions.Extensions.cs b/src/Wolverine/WolverineOptions.Extensions.cs
index 6dd88d7a3..e9ddb3d30 100644
--- a/src/Wolverine/WolverineOptions.Extensions.cs
+++ b/src/Wolverine/WolverineOptions.Extensions.cs
@@ -1,4 +1,9 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using JasperFx;
using JasperFx.Core;
+using JasperFx.Core.Reflection;
+using Wolverine.Attributes;
namespace Wolverine;
@@ -7,7 +12,209 @@ public sealed partial class WolverineOptions
private readonly IList _extensionTypes = new List();
internal List AppliedExtensions { get; } = [];
-
+ ///
+ /// Discover and apply types from the compile-time
+ /// manifest emitted by JasperFx.SourceGenerator (the JasperFx.Generated.DiscoveredExtensions
+ /// class in each eligible assembly). This replaces the previous runtime assembly scan
+ /// (ExtensionLoader + the AssemblyFinder.FindAssemblies bin-directory probe). See GH-2902.
+ ///
+ internal void DiscoverAndApplyExtensions()
+ {
+ // The manifest reader only sees assemblies that are already loaded, but a referenced
+ // module (e.g. WolverineFx.RuntimeCompilation) may not be loaded yet at bootstrap. Load the
+ // deployed [WolverineModule] assemblies so "reference the package and it auto-activates"
+ // still works — using the runtime's resolved deployment list, not the old bin-directory
+ // glob (Directory.EnumerateFiles) that AssemblyFinder.FindAssemblies used.
+ ensureReferencedModuleAssembliesAreLoaded();
+
+ // Include any [WolverineModule]-marked assembly that is now loaded so its handlers
+ // participate in discovery, exactly as the old ExtensionLoader.IncludeExtensionAssemblies
+ // path did. This also covers pure handler modules declared with a bare
+ // [assembly: WolverineModule] that contribute no IWolverineExtension (and so emit no
+ // manifest entry).
+ var moduleAssemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => !a.IsDynamic && a.HasAttribute())
+ .ToArray();
+
+ if (moduleAssemblies.Length > 0)
+ {
+ IncludeExtensionAssemblies(moduleAssemblies);
+ }
+
+ // Apply the extensions the JasperFx.SourceGenerator ExtensionDiscoveryGenerator found at
+ // compile time. We keep only the types explicitly declared via [WolverineModule] on
+ // their own assembly — the exact set the old runtime scan applied. The manifest's
+ // marker-interface scan also lists framework-internal IWolverineExtension helpers
+ // (DisableExternalTransports, the ConfigureWolverine() lambda extension, …) that are
+ // applied explicitly elsewhere and must never be auto-discovered; gating on the declaring
+ // assembly's declared [WolverineModule] type excludes them.
+ //
+ // Applied now — while the IServiceCollection is still mutable — so an extension may still
+ // register IoC services (e.g. Module1Extension), matching the historical timing.
+ var extensions = GeneratedExtensionManifest.ReadFromLoadedAssemblies()
+ .Where(type => type.IsConcrete() && type.CanBeCastTo())
+ .Where(isDeclaredModuleExtension)
+ .Select(buildExtension)
+ .ToArray();
+
+ ApplyExtensions(extensions);
+ }
+
+ // True only when the type is the extension declared by its own assembly's [WolverineModule]
+ // (or [WolverineModule(typeof(T))]) attribute. This is what restricts manifest discovery to the
+ // same opt-in set the old ExtensionLoader applied, rather than every IWolverineExtension the
+ // generator's marker scan happens to list.
+ private static bool isDeclaredModuleExtension(Type type)
+ {
+ return type == type.Assembly.GetAttribute()?.WolverineExtensionType;
+ }
+
+ // Framework assembly name prefixes we never need to walk into. No Wolverine extension/module
+ // assembly uses these prefixes, so skipping them keeps the reference-graph walk from loading
+ // the whole BCL closure.
+ private static readonly string[] _nonModulePrefixes =
+ ["System.", "System,", "Microsoft.", "netstandard", "mscorlib", "WindowsBase", "Newtonsoft."];
+
+ // Make sure every deployed [WolverineModule] assembly is loaded so its source-generated manifest
+ // can be read below. This intentionally replaces AssemblyFinder.FindAssemblies'
+ // Directory.EnumerateFiles bin-directory probe.
+ //
+ // It does NOT just walk Assembly.GetReferencedAssemblies(): the C# compiler prunes a referenced
+ // assembly whose types the application never uses directly, so a "reference the package and it
+ // auto-activates" module like WolverineFx.RuntimeCompilation is absent from the metadata
+ // reference graph even though it is deployed. Instead we use the runtime's resolved deployment
+ // list (TRUSTED_PLATFORM_ASSEMBLIES) - not a recursive filesystem scan - which DOES include those
+ // pruned-but-deployed assemblies. The reference-graph walk is kept as a fallback for hosts that
+ // don't surface that list (e.g. single-file deployments).
+ [UnconditionalSuppressMessage("Trimming", "IL2026",
+ Justification = "Loads assemblies from the runtime's resolved deployment list / declared reference graph (Assembly.Load), not a filesystem probe. AOT/trim-published apps reference their extension assemblies statically and register extensions explicitly (ExtensionDiscovery.ManualOnly); see GH-2902 / AOT guide.")]
+ private void ensureReferencedModuleAssembliesAreLoaded()
+ {
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic))
+ {
+ var name = assembly.GetName().Name;
+ if (name != null)
+ {
+ seen.Add(name);
+ }
+ }
+
+ if (!tryLoadFromDeploymentList(seen))
+ {
+ walkReferenceGraph(seen);
+ }
+ }
+
+ // Load the non-framework assemblies the runtime resolved for this app (the deployment closure),
+ // which includes referenced module packages whose types the app doesn't use directly. Returns
+ // false when the host doesn't expose TRUSTED_PLATFORM_ASSEMBLIES so the caller can fall back.
+ private bool tryLoadFromDeploymentList(HashSet seen)
+ {
+ if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is not string list || list.Length == 0)
+ {
+ return false;
+ }
+
+ foreach (var path in list.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
+ {
+ var name = Path.GetFileNameWithoutExtension(path);
+ if (name.IsEmpty() || isNonModuleAssembly(name) || !seen.Add(name))
+ {
+ continue;
+ }
+
+ try
+ {
+ Assembly.Load(new AssemblyName(name));
+ }
+ catch (Exception)
+ {
+ // Not loadable by simple name (native image, resource/satellite assembly, …); skip.
+ }
+ }
+
+ return true;
+ }
+
+ // Fallback: walk the reference graph from the application assembly plus everything already loaded,
+ // loading each non-framework referenced assembly. Misses compiler-pruned references, but better
+ // than nothing when no deployment list is available.
+ [UnconditionalSuppressMessage("Trimming", "IL2026",
+ Justification = "Reference-graph fallback (Assembly.GetReferencedAssemblies/Assembly.Load) used only when the runtime deployment list is unavailable. AOT/trim-published apps register extensions explicitly (ExtensionDiscovery.ManualOnly); see GH-2902 / AOT guide.")]
+ private void walkReferenceGraph(HashSet seen)
+ {
+ var queue = new Queue();
+
+ void enqueue(Assembly assembly)
+ {
+ var name = assembly.GetName().Name;
+ if (name != null && seen.Add(name))
+ {
+ queue.Enqueue(assembly);
+ }
+ }
+
+ foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).ToArray())
+ {
+ // Already in `seen` from the caller; enqueue for walking without re-adding.
+ queue.Enqueue(assembly);
+ }
+
+ if (ApplicationAssembly != null)
+ {
+ enqueue(ApplicationAssembly);
+ }
+
+ while (queue.Count > 0)
+ {
+ foreach (var reference in queue.Dequeue().GetReferencedAssemblies())
+ {
+ var name = reference.Name;
+ if (name == null || seen.Contains(name) || isNonModuleAssembly(name))
+ {
+ continue;
+ }
+
+ try
+ {
+ enqueue(Assembly.Load(reference));
+ }
+ catch (Exception)
+ {
+ // A reference we can't resolve can't be a discoverable module; ignore it.
+ seen.Add(name);
+ }
+ }
+ }
+ }
+
+ private static bool isNonModuleAssembly(string name)
+ {
+ foreach (var prefix in _nonModulePrefixes)
+ {
+ if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // The manifest stores extension types as a DAM-less Type[] (typeof(...) literals emitted by the
+ // generator), so the IL flow analyzer can't see that constructors are preserved. They are: real
+ // extensions are declared via [WolverineModule], whose attribute constructor takes a
+ // [DAM(PublicConstructors)] Type, and the generator emits the type with a typeof() literal that
+ // survives trimming. AOT-clean apps that trim aggressively register extensions explicitly.
+ [UnconditionalSuppressMessage("Trimming", "IL2067",
+ Justification = "Extension types come from the source-generated DiscoveredExtensions manifest (typeof literals); public constructors are preserved via the [WolverineModule] attribute's [DAM(PublicConstructors)] Type parameter. See GH-2902 / AOT guide.")]
+ private static IWolverineExtension buildExtension(Type type)
+ {
+ return (IWolverineExtension)Activator.CreateInstance(type)!;
+ }
+
+
///
/// Applies the extension to this application
///