diff --git a/jasperfx.slnx b/jasperfx.slnx
index bc69704..4869e16 100644
--- a/jasperfx.slnx
+++ b/jasperfx.slnx
@@ -41,6 +41,7 @@
+
diff --git a/src/CoreTests/CoreTests.csproj b/src/CoreTests/CoreTests.csproj
index 2d184b5..58b5793 100644
--- a/src/CoreTests/CoreTests.csproj
+++ b/src/CoreTests/CoreTests.csproj
@@ -28,6 +28,7 @@
+
diff --git a/src/CoreTests/JasperFxOptionsTests.cs b/src/CoreTests/JasperFxOptionsTests.cs
index 91d9c22..d2b2a9a 100644
--- a/src/CoreTests/JasperFxOptionsTests.cs
+++ b/src/CoreTests/JasperFxOptionsTests.cs
@@ -1,3 +1,4 @@
+using ExtensionStandIn;
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.CommandLine.Descriptions;
@@ -395,4 +396,97 @@ public void never_adopts_a_test_runner_as_the_calling_assembly()
assembly.ShouldNotBeNull();
JasperFxOptions.IsTestRunnerAssembly(assembly.GetName().Name!).ShouldBeFalse();
}
+
+ // GH-601: UseWolverine() and AddMarten() both call services.AddJasperFx() from inside their own
+ // assembly, so the first frame outside JasperFx belongs to the extension rather than the application.
+
+ [Theory]
+ [InlineData("JasperFx")]
+ [InlineData("JasperFx.Events")]
+ [InlineData("JasperFx.RuntimeCompiler")]
+ [InlineData("Wolverine")]
+ [InlineData("Wolverine.SqlServer")]
+ [InlineData("WolverineFx.RabbitMQ")]
+ [InlineData("Marten")]
+ [InlineData("Marten.AspNetCore")]
+ [InlineData("Weasel.Postgresql")]
+ [InlineData("Polecat")]
+ [InlineData("CritterWatch.Server")]
+ [InlineData("Oakton")]
+ public void recognizes_critter_stack_framework_assemblies(string assemblyName)
+ {
+ JasperFxOptions.IsCritterStackAssembly(assemblyName).ShouldBeTrue();
+ }
+
+ [Theory]
+ // An application is not framework code just because it is named for a product. Matching is by exact
+ // name or dotted prefix, never a bare StartsWith.
+ [InlineData("MartenPlayground")]
+ [InlineData("WolverineDemo")]
+ [InlineData("JasperFxSamples")]
+ [InlineData("MyApp")]
+ [InlineData("CoreTests")]
+ // And the Critter Stack repos' own test assemblies ARE the application under test -- their handlers
+ // and documents are the types discovery has to find.
+ [InlineData("Wolverine.RabbitMQ.Tests")]
+ [InlineData("Marten.Testing")]
+ [InlineData("JasperFx.Events.Tests")]
+ public void does_not_mistake_applications_or_suites_for_framework_assemblies(string assemblyName)
+ {
+ JasperFxOptions.IsCritterStackAssembly(assemblyName).ShouldBeFalse();
+ }
+
+ [Fact]
+ public async Task registration_is_attributed_to_the_app_not_the_extension_that_registered_for_it()
+ {
+ var original = JasperFxOptions.RememberedApplicationAssembly;
+ try
+ {
+ JasperFxOptions.RememberedApplicationAssembly = null;
+
+ // AddSomeCritterStackTool lives in an assembly named "Wolverine.StackWalkStandIn" and calls
+ // AddJasperFx() on our behalf, which is exactly what UseWolverine()/AddMarten() do.
+ using var host = await Host.CreateDefaultBuilder()
+ .ConfigureServices(s => s.AddSomeCritterStackTool())
+ .UseEnvironment("Development")
+ .StartAsync(TestContext.Current.CancellationToken);
+
+ var options = host.Services.GetRequiredService();
+
+ options.RegistrationCallingAssembly.ShouldBe(GetType().Assembly);
+
+ // ...and because registration is now attributed correctly, the GH-3521 divergence warning
+ // stays quiet on a host where nothing is actually wrong.
+ options.ApplicationAssemblyReuseWarning.ShouldBeNull();
+ }
+ finally
+ {
+ JasperFxOptions.RememberedApplicationAssembly = original;
+ }
+ }
+
+ [Fact]
+ public void an_extension_registration_does_not_pin_the_extension_as_the_application_assembly()
+ {
+ var original = JasperFxOptions.RememberedApplicationAssembly;
+ try
+ {
+ JasperFxOptions.RememberedApplicationAssembly = null;
+
+ // No meaningful IHostEnvironment.ApplicationName here, so establishApplicationAssembly falls
+ // through to the process-wide pin that AddJasperFx seeded from the same stack walk. Before the
+ // fix that pin -- and therefore type discovery -- was the extension assembly.
+ var services = new ServiceCollection();
+ services.AddSomeCritterStackTool();
+
+ var options = services.BuildServiceProvider().GetRequiredService();
+
+ options.ApplicationAssembly.ShouldBe(GetType().Assembly);
+ JasperFxOptions.RememberedApplicationAssembly.ShouldBe(GetType().Assembly);
+ }
+ finally
+ {
+ JasperFxOptions.RememberedApplicationAssembly = original;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/ExtensionStandIn/CritterStackExtension.cs b/src/ExtensionStandIn/CritterStackExtension.cs
new file mode 100644
index 0000000..3244526
--- /dev/null
+++ b/src/ExtensionStandIn/CritterStackExtension.cs
@@ -0,0 +1,18 @@
+using JasperFx;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace ExtensionStandIn;
+
+///
+/// Stands in for a Critter Stack extension in the one respect that matters to
+/// JasperFxOptions.DetermineCallingAssembly: it calls AddJasperFx() from inside its own
+/// assembly on the application's behalf, exactly the way UseWolverine() and AddMarten() do.
+/// This assembly is named "Wolverine.StackWalkStandIn" so the walk sees it as framework code. See GH-601.
+///
+public static class CritterStackExtension
+{
+ public static IServiceCollection AddSomeCritterStackTool(this IServiceCollection services)
+ {
+ return services.AddJasperFx();
+ }
+}
diff --git a/src/ExtensionStandIn/ExtensionStandIn.csproj b/src/ExtensionStandIn/ExtensionStandIn.csproj
new file mode 100644
index 0000000..7697f9f
--- /dev/null
+++ b/src/ExtensionStandIn/ExtensionStandIn.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Wolverine.StackWalkStandIn
+ false
+
+
+
+
+
+
diff --git a/src/JasperFx/JasperFxOptions.cs b/src/JasperFx/JasperFxOptions.cs
index 9d547dd..72617c7 100644
--- a/src/JasperFx/JasperFxOptions.cs
+++ b/src/JasperFx/JasperFxOptions.cs
@@ -241,7 +241,7 @@ private void checkForDivergentApplicationAssembly(Assembly adopted)
}
if (assemblyName.StartsWith("System") || assemblyName.StartsWith("Microsoft") ||
- IsTestRunnerAssembly(assemblyName))
+ IsTestRunnerAssembly(assemblyName) || IsCritterStackAssembly(assemblyName))
{
continue;
}
@@ -273,6 +273,57 @@ internal static bool IsTestRunnerAssembly(string assemblyName)
|| assemblyName.StartsWith("NCrunch.", StringComparison.OrdinalIgnoreCase);
}
+ // GH-601: a Critter Stack extension registers JasperFx on the application's behalf --
+ // UseWolverine() and AddMarten() both call services.AddJasperFx() from inside their own assembly --
+ // so the first frame outside JasperFx belongs to the EXTENSION, not the app. RegistrationCallingAssembly
+ // then stopped meaning what its name says, and checkForDivergentApplicationAssembly compared the
+ // extension against the (correctly resolved) application assembly and warned on healthy hosts. That
+ // inverts the warning's whole value: it trains readers to ignore it, and its absence stops being
+ // evidence -- on JasperFx/wolverine#3776 "zero occurrences of the warning" was explicitly recorded as
+ // grounds for ruling out an application-assembly problem, which was exactly the bug.
+ //
+ // Matched as an exact name or a dotted prefix rather than a bare StartsWith, so an application named
+ // "MartenPlayground" or "WolverineDemo" is left alone.
+ private static readonly string[] _critterStackAssemblies =
+ [
+ "JasperFx",
+ "Wolverine",
+ "WolverineFx",
+ "Marten",
+ "Weasel",
+ "Polecat",
+ "CritterWatch",
+ "Oakton"
+ ];
+
+ internal static bool IsCritterStackAssembly(string assemblyName)
+ {
+ // The test assemblies inside the Critter Stack repos themselves -- Wolverine.RabbitMQ.Tests,
+ // Marten.Testing and friends -- ARE the application as far as discovery is concerned, because the
+ // handlers and documents under test live in them. Skipping those would reintroduce GH-600's defect
+ // from the other direction.
+ if (assemblyName.EndsWith("Tests", StringComparison.OrdinalIgnoreCase)
+ || assemblyName.EndsWith("Testing", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ foreach (var name in _critterStackAssemblies)
+ {
+ if (assemblyName.Equals(name, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ if (assemblyName.StartsWith(name + ".", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
/// Attempts to resolve the project root directory by climbing up from the current path
/// looking for .csproj or .sln files. This is useful for Console apps where