diff --git a/Directory.Build.props b/Directory.Build.props
index 86d4457..772abf1 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,7 +7,7 @@
JasperFx.Events, JasperFx.Events.SourceGenerator) set
$(JasperFxVersion) so they always release together. Bump this one
value to release the whole set. -->
- 2.2.7
+ 2.3.0
13
1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618
Jeremy D. Miller;Jaedyn Tonee
diff --git a/src/CodegenTests/DynamicCodeBuilderTests.cs b/src/CodegenTests/DynamicCodeBuilderTests.cs
index 8b5af54..1ebffbb 100644
--- a/src/CodegenTests/DynamicCodeBuilderTests.cs
+++ b/src/CodegenTests/DynamicCodeBuilderTests.cs
@@ -83,6 +83,31 @@ public void no_assertion_when_collection_does_not_use_services()
recorder.AssertionCalls.ShouldBe(0);
}
+ // #2991: across files sharing one IServiceVariableSource (the CLI codegen path), a per-file
+ // ServiceProviderSource override (TryReplaceServiceProvider -> true, e.g. HTTP's
+ // httpContext.RequestServices) must be applied for that file and RESET before the next file so it
+ // cannot leak into a file that should keep the default isolated-and-scoped provider.
+ [Fact]
+ public void per_file_service_provider_override_is_isolated_to_its_own_file()
+ {
+ var httpStyle = new RecordingFile("HttpStyle") { ReplacesProvider = true };
+ var plain = new RecordingFile("Plain");
+ var source = new FakeServiceVariableSource(reportName: null);
+
+ var builder = new DynamicCodeBuilder(
+ new ServiceCollection().BuildServiceProvider(),
+ new ICodeFileCollection[] { new RecordingCollection(httpStyle, plain) })
+ {
+ ServiceVariableSource = source
+ };
+
+ builder.GenerateAllCode();
+
+ // Reset runs once per file; ReplaceServiceProvider runs only for the file that opts in — and the
+ // reset between files prevents the override from leaking into the plain file.
+ source.Calls.ShouldBe(new[] { "reset", "replace:httpContext.RequestServices", "reset" });
+ }
+
private static DynamicCodeBuilder BuildWithCollection(RecordingFile file, IServiceVariableSource source)
{
return new DynamicCodeBuilder(
@@ -101,6 +126,18 @@ private sealed class RecordingFile : ICodeFile
public int AssertionCalls { get; private set; }
public ServiceLocationReport[]? LastReports { get; private set; }
+ // When true, mimics an HTTP endpoint configured with ServiceProviderSource.FromHttpContextRequestServices.
+ public bool ReplacesProvider { get; init; }
+
+ public bool TryReplaceServiceProvider(out JasperFx.CodeGeneration.Model.Variable serviceProvider)
+ {
+ serviceProvider = default!;
+ if (!ReplacesProvider) return false;
+
+ serviceProvider = new JasperFx.CodeGeneration.Model.Variable(typeof(IServiceProvider), "httpContext.RequestServices");
+ return true;
+ }
+
public void AssembleTypes(GeneratedAssembly assembly)
{
assembly.AddType(FileName + "Type", typeof(object));
@@ -121,11 +158,11 @@ public void AssertServiceLocationsAreAllowed(ServiceLocationReport[] reports, IS
private sealed class RecordingCollection : ICodeFileCollectionWithServices
{
- private readonly ICodeFile _file;
- public RecordingCollection(ICodeFile file) { _file = file; }
+ private readonly ICodeFile[] _files;
+ public RecordingCollection(params ICodeFile[] files) { _files = files; }
public string ChildNamespace => "RecordedNamespace";
public GenerationRules Rules { get; } = new("RecordedNamespace");
- public IReadOnlyList BuildFiles() => new[] { _file };
+ public IReadOnlyList BuildFiles() => _files;
}
private sealed class ServicelessCollection : ICodeFileCollection
@@ -151,6 +188,14 @@ public void ReplaceVariables(IMethodVariables method) { }
public void StartNewType() { }
public void StartNewMethod() { }
+ // #2991: record the per-file override lifecycle so the test can assert isolation.
+ public List Calls { get; } = new();
+
+ public void ReplaceServiceProvider(JasperFx.CodeGeneration.Model.Variable serviceProvider)
+ => Calls.Add($"replace:{serviceProvider.Usage}");
+
+ public void ResetServiceProvider() => Calls.Add("reset");
+
public ServiceLocationReport[] ServiceLocations()
{
if (_reportName == null) return Array.Empty();
diff --git a/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs b/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
index 8addcb3..a4cee1b 100644
--- a/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
+++ b/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
@@ -122,6 +122,11 @@ public void WriteGeneratedCode(Action onFileWritten)
var generatedAssembly = collection.StartAssembly(collection.Rules);
file.AssembleTypes(generatedAssembly);
+ // #2991: apply any per-file service-provider override (e.g. HTTP's
+ // httpContext.RequestServices), matching the runtime DynamicTypeLoader path.
+ // `services` is shared across every file here, so reset the prior override first.
+ applyServiceProviderOverride(file, services);
+
var code = generatedAssembly.GenerateCode(services);
// #227: enforce ServiceLocationPolicy per-file, matching the
@@ -176,6 +181,10 @@ private string generateCode(ICodeFileCollection collection)
throw new CodeGenerationException(file, e);
}
+ // #2991: see WriteGeneratedCode — honor a per-file service-provider override on the
+ // shared source.
+ applyServiceProviderOverride(file, services);
+
var code = generatedAssembly.GenerateCode(services);
assertServiceLocationsAllowed(file, services);
@@ -208,6 +217,9 @@ public void TryBuildAndCompileAll(Action [];
diff --git a/src/JasperFx/CodeGeneration/Services/ServiceCollectionServerVariableSource.cs b/src/JasperFx/CodeGeneration/Services/ServiceCollectionServerVariableSource.cs
index 8c23a26..2d1edd2 100644
--- a/src/JasperFx/CodeGeneration/Services/ServiceCollectionServerVariableSource.cs
+++ b/src/JasperFx/CodeGeneration/Services/ServiceCollectionServerVariableSource.cs
@@ -102,6 +102,19 @@ public void ReplaceServiceProvider(Variable serviceProvider)
_scoped = serviceProvider;
}
+ // GH-2991: the runtime (DynamicTypeLoader) resolves a fresh transient IServiceVariableSource per
+ // ICodeFile, so a ReplaceServiceProvider() call there is naturally isolated to one file. The CLI
+ // codegen paths (DynamicCodeBuilder write/preview/test) reuse a SINGLE shared instance across every
+ // file, and ReplaceServiceProvider latches _replacedServiceProvider = true permanently (StartNewMethod
+ // only re-creates the default scope when it is false). Reset between files so a per-file
+ // ServiceProviderSource override (e.g. HTTP's httpContext.RequestServices) does not leak into the
+ // following files.
+ public void ResetServiceProvider()
+ {
+ _replacedServiceProvider = false;
+ _scoped = new ScopedContainerCreation().Scoped;
+ }
+
public ServiceLocationReport[] ServiceLocations()
{
return _serviceLocations.ToArray();