Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
JasperFx.Events, JasperFx.Events.SourceGenerator) set
<Version>$(JasperFxVersion)</Version> so they always release together. Bump this one
value to release the whole set. -->
<JasperFxVersion>2.2.7</JasperFxVersion>
<JasperFxVersion>2.3.0</JasperFxVersion>
<LangVersion>13</LangVersion>
<NoWarn>1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618</NoWarn>
<Authors>Jeremy D. Miller;Jaedyn Tonee</Authors>
Expand Down
51 changes: 48 additions & 3 deletions src/CodegenTests/DynamicCodeBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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));
Expand All @@ -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<ICodeFile> BuildFiles() => new[] { _file };
public IReadOnlyList<ICodeFile> BuildFiles() => _files;
}

private sealed class ServicelessCollection : ICodeFileCollection
Expand All @@ -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<string> 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<ServiceLocationReport>();
Expand Down
27 changes: 27 additions & 0 deletions src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ public void WriteGeneratedCode(Action<string> 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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -208,6 +217,9 @@ public void TryBuildAndCompileAll(Action<GeneratedAssembly, IServiceVariableSour
var generatedAssembly = collection.StartAssembly(collection.Rules);
file.AssembleTypes(generatedAssembly);

// #2991: see WriteGeneratedCode.
applyServiceProviderOverride(file, services);

try
{
withAssembly(generatedAssembly, services);
Expand All @@ -222,6 +234,21 @@ public void TryBuildAndCompileAll(Action<GeneratedAssembly, IServiceVariableSour
}
}

// GH-2991: honor a per-file ServiceProviderSource override (e.g. HTTP's httpContext.RequestServices)
// in the CLI codegen paths, matching DynamicTypeLoader.Initialize/CompileAndAttach. Because the CLI
// reuses a single shared IServiceVariableSource across every file, reset any prior override first so
// it cannot leak into a following file that should keep the default isolated-and-scoped provider.
private static void applyServiceProviderOverride(ICodeFile file, IServiceVariableSource? services)
{
if (services == null) return;

services.ResetServiceProvider();
if (file.TryReplaceServiceProvider(out var serviceProvider))
{
services.ReplaceServiceProvider(serviceProvider);
}
}

private void assertServiceLocationsAllowed(ICodeFile file, IServiceVariableSource? services)
{
if (services == null) return;
Expand Down
7 changes: 7 additions & 0 deletions src/JasperFx/CodeGeneration/Model/IServiceVariableSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ void ReplaceServiceProvider(Variable serviceProvider)
// Nothing, but implement for realsies!
}

// GH-2991: undo a prior per-file ReplaceServiceProvider so a shared source instance (the CLI
// codegen path reuses one across all files) can isolate the override per ICodeFile.
void ResetServiceProvider()
{
// Nothing by default.
}

ServiceLocationReport[] ServiceLocations() => [];


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading