diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index a48ddd7..74f3b7c 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -23,11 +23,11 @@ jobs:
- name: Setup dotnet ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Run benchmark
run: cd Benchmarking/Benchmarks && dotnet run -c Release --exporters json --filter '*'
- - name: Store benchmark result
+ - name: Store runtime benchmark result
uses: rhysd/github-action-benchmark@v1
with:
name: Benchmark.Net Benchmark
@@ -39,3 +39,15 @@ jobs:
# Show alert with commit comment on detecting possible performance regression
alert-threshold: '200%'
comment-on-alert: true
+
+ - name: Store generator benchmark result
+ uses: rhysd/github-action-benchmark@v1
+ with:
+ name: Generator Benchmark.Net Benchmark
+ tool: 'benchmarkdotnet'
+ output-file-path: Benchmarking/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.GeneratorBenchmarks-report-full-compressed.json
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ auto-push: true
+ summary-always: true
+ alert-threshold: '200%'
+ comment-on-alert: true
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 02c182e..cb76aff 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -28,13 +28,13 @@ jobs:
- name: Setup dotnet ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Restore
run: dotnet restore FactoryGenerator.sln
- name: Build
run: dotnet build FactoryGenerator.sln --no-restore
- name: Test
- run: dotnet test FactoryGenerator.sln --no-build --no-restore
+ run: dotnet test --no-build --no-restore --solution FactoryGenerator.sln
pack:
runs-on: ubuntu-latest
@@ -45,7 +45,7 @@ jobs:
- name: Setup dotnet ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Pack Generator
run: dotnet pack FactoryGenerator/FactoryGenerator.csproj -o "${{ env.NuGetDirectory }}" --property:RepositoryCommit="${{ env.COMMIT_SHA }}" --property:InformationalVersion="UNRELEASED" --property:AssemblyVersion="0.0.0" --property:FileVersion="0.0.0" --property:Version="0.0.0"
- name: Pack Attributes
@@ -68,7 +68,7 @@ jobs:
- name: Setup dotnet ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Pack Generator
run: dotnet pack FactoryGenerator/FactoryGenerator.csproj -o "${{ env.NuGetDirectory }}" --property:RepositoryCommit="${{ env.COMMIT_SHA }}" --property:InformationalVersion="${{ github.ref_name }}" --property:AssemblyVersion="${{ github.ref_name }}" --property:FileVersion="${{ github.ref_name }}" --property:Version="${{ github.ref_name }}"
- name: Pack Attributes
@@ -94,7 +94,7 @@ jobs:
- name: Setup dotnet ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Publish Nuget packages
run: |
for file in $(find "${{ env.NuGetDirectory }}" -type f -name "*.nupkg"); do
@@ -110,11 +110,11 @@ jobs:
- name: Setup dotnet ${{ matrix.dotnet-version }}
uses: actions/setup-dotnet@v4
with:
- dotnet-version: "9.0.x"
+ dotnet-version: "10.0.x"
- name: Run benchmark
run: cd Benchmarking/Benchmarks && dotnet run -c Release --exporters json --filter '*'
- - name: Store benchmark result
+ - name: Store runtime benchmark result
uses: rhysd/github-action-benchmark@v1
with:
name: Benchmark.Net Benchmark
@@ -124,5 +124,17 @@ jobs:
summary-always: true
# Show alert with commit comment on detecting possible performance regression
alert-threshold: '200%'
- comment-on-alert: true
- fail-on-alert: true
+ comment-on-alert: false
+ fail-on-alert: false
+
+ - name: Store generator benchmark result
+ uses: rhysd/github-action-benchmark@v1
+ with:
+ name: Generator Benchmark.Net Benchmark
+ tool: 'benchmarkdotnet'
+ output-file-path: Benchmarking/Benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.GeneratorBenchmarks-report-full-compressed.json
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ summary-always: true
+ alert-threshold: '200%'
+ comment-on-alert: false
+ fail-on-alert: false
diff --git a/.gitignore b/.gitignore
index 83371ec..45b9c53 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,7 +21,6 @@
[Rr]elease-x86/
[Dd]ebug-x86/
x64/
-build/
bld/
[Bb]in/
[Oo]bj/
diff --git a/Benchmarking/Benchmarks/Benchmarks.csproj b/Benchmarking/Benchmarks/Benchmarks.csproj
index 7da39a4..78c4269 100644
--- a/Benchmarking/Benchmarks/Benchmarks.csproj
+++ b/Benchmarking/Benchmarks/Benchmarks.csproj
@@ -2,17 +2,20 @@
Exe
- net9.0
+ net10.0
+ preview
enable
enable
- true
+
+
+
diff --git a/Benchmarking/Benchmarks/GeneratorBenchmarks.cs b/Benchmarking/Benchmarks/GeneratorBenchmarks.cs
new file mode 100644
index 0000000..14a136a
--- /dev/null
+++ b/Benchmarking/Benchmarks/GeneratorBenchmarks.cs
@@ -0,0 +1,751 @@
+using System.Collections.Immutable;
+using System.IO;
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using FactoryGenerator.Attributes;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Benchmarks;
+
+[MemoryDiagnoser]
+[ShortRunJob]
+[JsonExporterAttribute.Full]
+[JsonExporterAttribute.FullCompressed]
+public class GeneratorBenchmarks
+{
+ private ColdGeneratorScenario m_constructorGraph = null!;
+ private ColdGeneratorScenario m_noiseHeavyProject = null!;
+ private ColdGeneratorScenario m_featureRichStaticExtensionsDisabled = null!;
+ private ColdGeneratorScenario m_featureRichStaticExtensionsEnabled = null!;
+ private ColdGeneratorScenario m_multiAssemblyOverrideGraph = null!;
+ private FeatureRichIncrementalScenario m_featureRichIncremental = null!;
+ private IncrementalGeneratorScenario m_referenceAssemblyIncremental = null!;
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ m_constructorGraph = GeneratorBenchmarkScenarioFactory.CreateConstructorGraph(serviceCount: 250);
+ m_noiseHeavyProject = GeneratorBenchmarkScenarioFactory.CreateNoiseHeavyProject(serviceCount: 64, noiseTypeCount: 2000);
+ m_featureRichStaticExtensionsDisabled = GeneratorBenchmarkScenarioFactory.CreateFeatureRichGraph(emitStaticExtensions: false);
+ m_featureRichStaticExtensionsEnabled = GeneratorBenchmarkScenarioFactory.CreateFeatureRichGraph(emitStaticExtensions: true);
+ m_multiAssemblyOverrideGraph = GeneratorBenchmarkScenarioFactory.CreateMultiAssemblyOverrideGraph(baseServiceCount: 128, overrideCount: 16);
+ m_featureRichIncremental = GeneratorBenchmarkScenarioFactory.CreateFeatureRichIncrementalScenario();
+ m_referenceAssemblyIncremental = GeneratorBenchmarkScenarioFactory.CreateReferenceAssemblyIncrementalScenario();
+
+ GeneratorBenchmarkHarness.Validate(m_constructorGraph);
+ GeneratorBenchmarkHarness.Validate(m_noiseHeavyProject);
+ GeneratorBenchmarkHarness.Validate(m_featureRichStaticExtensionsDisabled);
+ GeneratorBenchmarkHarness.Validate(m_featureRichStaticExtensionsEnabled);
+ GeneratorBenchmarkHarness.Validate(m_multiAssemblyOverrideGraph);
+ }
+
+ [Benchmark]
+ public int Cold_ConstructorGraph() => GeneratorBenchmarkHarness.RunCold(m_constructorGraph);
+
+ [Benchmark]
+ public int Cold_NoiseHeavyProject() => GeneratorBenchmarkHarness.RunCold(m_noiseHeavyProject);
+
+ [Benchmark]
+ public int Cold_FeatureRichGraph_StaticExtensionsDisabled() => GeneratorBenchmarkHarness.RunCold(m_featureRichStaticExtensionsDisabled);
+
+ [Benchmark]
+ public int Cold_FeatureRichGraph_StaticExtensionsEnabled() => GeneratorBenchmarkHarness.RunCold(m_featureRichStaticExtensionsEnabled);
+
+ [Benchmark]
+ public int Cold_MultiAssemblyOverrideGraph() => GeneratorBenchmarkHarness.RunCold(m_multiAssemblyOverrideGraph);
+}
+
+internal sealed class ColdGeneratorScenario(CSharpCompilation compilation, AnalyzerConfigOptionsProvider optionsProvider)
+{
+ public CSharpCompilation Compilation { get; } = compilation;
+ public AnalyzerConfigOptionsProvider OptionsProvider { get; } = optionsProvider;
+}
+
+internal sealed class FeatureRichIncrementalScenario(
+ GeneratorDriver warmDriver,
+ CSharpCompilation baselineCompilation,
+ CSharpCompilation unrelatedEditCompilation,
+ CSharpCompilation injectedSignatureEditCompilation,
+ CSharpCompilation addInjectCompilation)
+{
+ public GeneratorDriver WarmDriver { get; } = warmDriver;
+ public CSharpCompilation BaselineCompilation { get; } = baselineCompilation;
+ public CSharpCompilation UnrelatedEditCompilation { get; } = unrelatedEditCompilation;
+ public CSharpCompilation InjectedSignatureEditCompilation { get; } = injectedSignatureEditCompilation;
+ public CSharpCompilation AddInjectCompilation { get; } = addInjectCompilation;
+}
+
+internal sealed class IncrementalGeneratorScenario(GeneratorDriver warmDriver, CSharpCompilation changedCompilation)
+{
+ public GeneratorDriver WarmDriver { get; } = warmDriver;
+ public CSharpCompilation ChangedCompilation { get; } = changedCompilation;
+}
+
+internal static class GeneratorBenchmarkHarness
+{
+ public static int RunCold(ColdGeneratorScenario scenario)
+ {
+ var driver = CreateDriver(scenario.Compilation, scenario.OptionsProvider);
+ return RunAndSummarize(driver, scenario.Compilation);
+ }
+
+ public static int RunIncremental(GeneratorDriver warmDriver, CSharpCompilation compilation)
+ {
+ return RunAndSummarize(warmDriver, compilation);
+ }
+
+ public static GeneratorDriver WarmAndValidate(ColdGeneratorScenario scenario)
+ {
+ var driver = CreateDriver(scenario.Compilation, scenario.OptionsProvider);
+ return RunAndValidate(driver, scenario.Compilation);
+ }
+
+ public static void Validate(ColdGeneratorScenario scenario)
+ {
+ _ = WarmAndValidate(scenario);
+ }
+
+ public static void Validate(GeneratorDriver warmDriver, CSharpCompilation compilation)
+ {
+ _ = RunAndValidate(warmDriver, compilation);
+ }
+
+ private static GeneratorDriver CreateDriver(CSharpCompilation compilation, AnalyzerConfigOptionsProvider optionsProvider)
+ {
+ return CSharpGeneratorDriver.Create(
+ [new global::FactoryGenerator.FactoryGenerator().AsSourceGenerator()],
+ parseOptions: (CSharpParseOptions) compilation.SyntaxTrees.First().Options,
+ optionsProvider: optionsProvider);
+ }
+
+ private static GeneratorDriver RunAndValidate(GeneratorDriver driver, CSharpCompilation compilation)
+ {
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _);
+ var runResult = driver.GetRunResult();
+ var exception = runResult.Results
+ .Select(result => result.Exception)
+ .FirstOrDefault(resultException => resultException is not null);
+
+ if (exception is not null)
+ throw exception;
+
+ var errors = outputCompilation.GetDiagnostics()
+ .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
+ .Select(diagnostic => diagnostic.ToString())
+ .ToArray();
+
+ if (errors.Length != 0)
+ throw new InvalidOperationException(string.Join(Environment.NewLine, errors));
+
+ return driver;
+ }
+
+ private static int RunAndSummarize(GeneratorDriver driver, CSharpCompilation compilation)
+ {
+ driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out _, out _);
+ var runResult = driver.GetRunResult();
+
+ return runResult.Results.Sum(result => result.GeneratedSources.Sum(source => source.SourceText.Length));
+ }
+}
+
+internal static class GeneratorBenchmarkScenarioFactory
+{
+ private static readonly ImmutableArray s_metadataReferences = CreateMetadataReferences();
+ private static readonly AnalyzerConfigOptionsProvider s_staticExtensionsEnabledOptions = new BenchmarkAnalyzerConfigOptionsProvider(true);
+ private static readonly AnalyzerConfigOptionsProvider s_staticExtensionsDisabledOptions = new BenchmarkAnalyzerConfigOptionsProvider(false);
+
+ public static ColdGeneratorScenario CreateConstructorGraph(int serviceCount)
+ {
+ var compilation = CreateCompilation(
+ "GeneratorConstructorGraphBenchmarks",
+ new BenchmarkSourceDocument("ConstructorGraph.cs", BuildConstructorGraphSource("GeneratorConstructorGraphInput", serviceCount)));
+
+ return new ColdGeneratorScenario(compilation, s_staticExtensionsDisabledOptions);
+ }
+
+ public static ColdGeneratorScenario CreateNoiseHeavyProject(int serviceCount, int noiseTypeCount)
+ {
+ var compilation = CreateCompilation(
+ "GeneratorNoiseHeavyBenchmarks",
+ new BenchmarkSourceDocument("ConstructorGraph.cs", BuildConstructorGraphSource("GeneratorNoiseInput", serviceCount)),
+ new BenchmarkSourceDocument("Noise.cs", BuildNoiseSource("GeneratorNoiseInput", noiseTypeCount)));
+
+ return new ColdGeneratorScenario(compilation, s_staticExtensionsDisabledOptions);
+ }
+
+ public static ColdGeneratorScenario CreateFeatureRichGraph(bool emitStaticExtensions)
+ {
+ var compilation = CreateCompilation(
+ emitStaticExtensions ? "GeneratorFeatureRichStaticExtensionsBenchmarks" : "GeneratorFeatureRichBenchmarks",
+ new BenchmarkSourceDocument(
+ "FeatureGraph.cs",
+ BuildFeatureRichSource("GeneratorFeatureRichInput", includeAdditionalExternalParameter: false, includeExtraWidgetInjection: false, labelDefault: "default", retryCountDefault: 3)),
+ new BenchmarkSourceDocument("Utilities.cs", BuildUtilitySource("GeneratorFeatureRichInput", utilitySuffix: "Baseline")));
+
+ return new ColdGeneratorScenario(compilation, emitStaticExtensions ? s_staticExtensionsEnabledOptions : s_staticExtensionsDisabledOptions);
+ }
+
+ public static ColdGeneratorScenario CreateMultiAssemblyOverrideGraph(int baseServiceCount, int overrideCount)
+ {
+ const string baseAssemblyName = "GeneratorOverrideBase";
+ const string derivedAssemblyName = "GeneratorOverrideDerived";
+
+ var baseCompilation = CreateCompilation(
+ baseAssemblyName,
+ new BenchmarkSourceDocument("BaseServices.cs", BuildOverrideBaseSource(baseAssemblyName, baseServiceCount)));
+ var baseReference = EmitReference(baseCompilation);
+ var derivedCompilation = CreateCompilation(
+ derivedAssemblyName,
+ baseReference,
+ new BenchmarkSourceDocument("DerivedServices.cs", BuildOverrideDerivedSource(baseAssemblyName, derivedAssemblyName, baseServiceCount, overrideCount)));
+
+ return new ColdGeneratorScenario(derivedCompilation, s_staticExtensionsDisabledOptions);
+ }
+
+ public static FeatureRichIncrementalScenario CreateFeatureRichIncrementalScenario()
+ {
+ const string assemblyName = "GeneratorFeatureRichIncremental";
+
+ var baselineCompilation = CreateCompilation(
+ assemblyName,
+ new BenchmarkSourceDocument(
+ "FeatureGraph.cs", BuildFeatureRichSource(assemblyName, includeAdditionalExternalParameter: false, includeExtraWidgetInjection: false, labelDefault: "default", retryCountDefault: 3)),
+ new BenchmarkSourceDocument("Utilities.cs", BuildUtilitySource(assemblyName, utilitySuffix: "Baseline")));
+ var unrelatedEditCompilation = CreateCompilation(
+ assemblyName,
+ new BenchmarkSourceDocument(
+ "FeatureGraph.cs", BuildFeatureRichSource(assemblyName, includeAdditionalExternalParameter: false, includeExtraWidgetInjection: false, labelDefault: "default", retryCountDefault: 3)),
+ new BenchmarkSourceDocument("Utilities.cs", BuildUtilitySource(assemblyName, utilitySuffix: "Edited")));
+ var injectedSignatureEditCompilation = CreateCompilation(
+ assemblyName,
+ new BenchmarkSourceDocument(
+ "FeatureGraph.cs", BuildFeatureRichSource(assemblyName, includeAdditionalExternalParameter: true, includeExtraWidgetInjection: false, labelDefault: "edited", retryCountDefault: 5)),
+ new BenchmarkSourceDocument("Utilities.cs", BuildUtilitySource(assemblyName, utilitySuffix: "Baseline")));
+ var addInjectCompilation = CreateCompilation(
+ assemblyName,
+ new BenchmarkSourceDocument(
+ "FeatureGraph.cs", BuildFeatureRichSource(assemblyName, includeAdditionalExternalParameter: false, includeExtraWidgetInjection: true, labelDefault: "default", retryCountDefault: 3)),
+ new BenchmarkSourceDocument("Utilities.cs", BuildUtilitySource(assemblyName, utilitySuffix: "Baseline")));
+
+ var baselineScenario = new ColdGeneratorScenario(baselineCompilation, s_staticExtensionsEnabledOptions);
+ var warmDriver = GeneratorBenchmarkHarness.WarmAndValidate(baselineScenario);
+ GeneratorBenchmarkHarness.Validate(GeneratorBenchmarkHarness.WarmAndValidate(baselineScenario), unrelatedEditCompilation);
+ GeneratorBenchmarkHarness.Validate(GeneratorBenchmarkHarness.WarmAndValidate(baselineScenario), injectedSignatureEditCompilation);
+ GeneratorBenchmarkHarness.Validate(GeneratorBenchmarkHarness.WarmAndValidate(baselineScenario), addInjectCompilation);
+
+ return new FeatureRichIncrementalScenario(
+ warmDriver,
+ baselineCompilation,
+ unrelatedEditCompilation,
+ injectedSignatureEditCompilation,
+ addInjectCompilation);
+ }
+
+ public static IncrementalGeneratorScenario CreateReferenceAssemblyIncrementalScenario()
+ {
+ const string baseAssemblyName = "GeneratorReferenceBase";
+ const string derivedAssemblyName = "GeneratorReferenceDerived";
+
+ var baseCompilation = CreateCompilation(
+ baseAssemblyName,
+ new BenchmarkSourceDocument("BaseServices.cs", BuildReferenceBaseSource(baseAssemblyName, includeSecondBasePart: false)));
+ var changedBaseCompilation = CreateCompilation(
+ baseAssemblyName,
+ new BenchmarkSourceDocument("BaseServices.cs", BuildReferenceBaseSource(baseAssemblyName, includeSecondBasePart: true)));
+
+ var baselineCompilation = CreateCompilation(
+ derivedAssemblyName,
+ EmitReference(baseCompilation),
+ new BenchmarkSourceDocument("DerivedServices.cs", BuildReferenceDerivedSource(baseAssemblyName, derivedAssemblyName)));
+ var changedCompilation = CreateCompilation(
+ derivedAssemblyName,
+ EmitReference(changedBaseCompilation),
+ new BenchmarkSourceDocument("DerivedServices.cs", BuildReferenceDerivedSource(baseAssemblyName, derivedAssemblyName)));
+
+ var baselineScenario = new ColdGeneratorScenario(baselineCompilation, s_staticExtensionsEnabledOptions);
+ var warmDriver = GeneratorBenchmarkHarness.WarmAndValidate(baselineScenario);
+ GeneratorBenchmarkHarness.Validate(GeneratorBenchmarkHarness.WarmAndValidate(baselineScenario), changedCompilation);
+
+ return new IncrementalGeneratorScenario(warmDriver, changedCompilation);
+ }
+
+ private static CSharpCompilation CreateCompilation(string assemblyName, params BenchmarkSourceDocument[] documents)
+ {
+ return CreateCompilation(assemblyName, s_metadataReferences, documents);
+ }
+
+ private static CSharpCompilation CreateCompilation(
+ string assemblyName,
+ MetadataReference additionalReference,
+ params BenchmarkSourceDocument[] documents)
+ {
+ return CreateCompilation(assemblyName, s_metadataReferences.Add(additionalReference), documents);
+ }
+
+ private static CSharpCompilation CreateCompilation(
+ string assemblyName,
+ ImmutableArray references,
+ params BenchmarkSourceDocument[] documents)
+ {
+ var syntaxTrees = documents
+ .Select(document => CSharpSyntaxTree.ParseText(
+ document.Source,
+ new CSharpParseOptions(LanguageVersion.Preview),
+ path: document.FileName))
+ .ToArray();
+
+ return CSharpCompilation.Create(
+ assemblyName,
+ syntaxTrees,
+ references,
+ new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+ }
+
+ private static MetadataReference EmitReference(Compilation compilation)
+ {
+ using var stream = new MemoryStream();
+ var result = compilation.Emit(stream);
+ if (!result.Success)
+ {
+ throw new InvalidOperationException(
+ string.Join(Environment.NewLine, result.Diagnostics.Select(diagnostic => diagnostic.ToString())));
+ }
+
+ return MetadataReference.CreateFromImage(stream.ToArray());
+ }
+
+ private static ImmutableArray CreateMetadataReferences()
+ {
+ var excludedAssemblies = new HashSet(StringComparer.Ordinal)
+ {
+ "Benchmarks",
+ "FactoryGenerator",
+ "FactoryGenerator.Attributes",
+ "FactoryGenerator.Extensions.AspNetCore",
+ "FactoryGenerator.Extensions.AspNetCore.Tests",
+ "FactoryGenerator.Tests",
+ "Inherited",
+ "Inheritor",
+ "TestWebApp"
+ };
+
+ return
+ [
+ .. ((string?) AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))!
+ .Split(Path.PathSeparator)
+ .Where(path => !excludedAssemblies.Contains(Path.GetFileNameWithoutExtension(path)))
+ .Select(path => (MetadataReference) MetadataReference.CreateFromFile(path)),
+
+ MetadataReference.CreateFromFile(typeof(InjectAttribute).Assembly.Location)
+ ];
+ }
+
+ private static string BuildConstructorGraphSource(string namespaceName, int serviceCount)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("using FactoryGenerator.Attributes;");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {namespaceName}");
+ sb.AppendLine("{");
+
+ for (var i = 0; i < serviceCount; i++)
+ {
+ sb.AppendLine($"public interface IService{i}");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine();
+ sb.AppendLine("[Inject]");
+ if (i == 0)
+ {
+ sb.AppendLine($"public sealed class Service{i} : IService{i}");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ }
+ else
+ {
+ sb.AppendLine($"public sealed class Service{i}(IService{i - 1} previous) : IService{i}");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ }
+
+ sb.AppendLine();
+ }
+
+ sb.AppendLine("[Inject]");
+ sb.AppendLine($"public sealed class RootConsumer(IService{serviceCount - 1} root)");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private static string BuildNoiseSource(string namespaceName, int noiseTypeCount)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("using System;");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {namespaceName}");
+ sb.AppendLine("{");
+
+ for (var i = 0; i < noiseTypeCount; i++)
+ {
+ sb.AppendLine($"public sealed class NoiseType{i}");
+ sb.AppendLine("{");
+ sb.AppendLine($" public int Compute(int value) => value + {i};");
+ sb.AppendLine($" public string Name => \"NoiseType{i}\";");
+ sb.AppendLine(" public DateTime Timestamp => DateTime.UnixEpoch;");
+ sb.AppendLine("}");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static string BuildFeatureRichSource(
+ string namespaceName,
+ bool includeAdditionalExternalParameter,
+ bool includeExtraWidgetInjection,
+ string labelDefault,
+ int retryCountDefault)
+ {
+ var additionalExternalParameter = includeAdditionalExternalParameter ? ", AdditionalExternalDependency additional" : string.Empty;
+ var widgetCAttribute = includeExtraWidgetInjection ? "[Inject]\n" : string.Empty;
+
+ return $$"""
+ using System.Collections.Generic;
+ using FactoryGenerator.Attributes;
+
+ namespace {{namespaceName}}
+ {
+ public sealed class ExternalDependency
+ {
+ }
+
+ public sealed class AdditionalExternalDependency
+ {
+ }
+
+ public interface IFlaggedFeature
+ {
+ }
+
+ [Inject, Boolean("feature_enabled")]
+ public sealed class EnabledFeature : IFlaggedFeature
+ {
+ }
+
+ [Inject]
+ public sealed class FallbackFeature : IFlaggedFeature
+ {
+ }
+
+ public interface IWidget
+ {
+ }
+
+ [Inject]
+ public sealed class WidgetA : IWidget
+ {
+ }
+
+ [Inject]
+ public sealed class WidgetB : IWidget
+ {
+ }
+
+ {{widgetCAttribute}}public sealed class WidgetC : IWidget
+ {
+ }
+
+ public interface IPropertyResult
+ {
+ }
+
+ public sealed class PropertyResult(IFlaggedFeature feature) : IPropertyResult
+ {
+ public IFlaggedFeature Feature { get; } = feature;
+ }
+
+ public interface IPropertyFactory
+ {
+ [Inject]
+ IPropertyResult Value { get; }
+ }
+
+ [Inject]
+ public sealed class PropertyFactory(IFlaggedFeature feature) : IPropertyFactory
+ {
+ public IPropertyResult Value => new PropertyResult(feature);
+ }
+
+ public interface IMethodResult
+ {
+ }
+
+ public sealed class MethodResult(
+ IFlaggedFeature feature,
+ IEnumerable widgets,
+ ExternalDependency external,
+ string label,
+ int retryCount) : IMethodResult
+ {
+ public IFlaggedFeature Feature { get; } = feature;
+ public IEnumerable Widgets { get; } = widgets;
+ public ExternalDependency External { get; } = external;
+ public string Label { get; } = label;
+ public int RetryCount { get; } = retryCount;
+ }
+
+ public interface IFeatureFactory
+ {
+ [Inject]
+ IMethodResult Create(
+ ExternalDependency external{{additionalExternalParameter}},
+ string label = "{{labelDefault}}",
+ int retryCount = {{retryCountDefault}},
+ params IWidget[] widgets);
+ }
+
+ [Inject]
+ public sealed class FeatureFactory(IFlaggedFeature feature) : IFeatureFactory
+ {
+ public IMethodResult Create(
+ ExternalDependency external{{additionalExternalParameter}},
+ string label = "{{labelDefault}}",
+ int retryCount = {{retryCountDefault}},
+ params IWidget[] widgets)
+ {
+ return new MethodResult(feature, widgets, external, label, retryCount);
+ }
+ }
+
+ [Inject]
+ public sealed class FeatureGraphConsumer(
+ IMethodResult methodResult,
+ IPropertyResult propertyResult,
+ IEnumerable widgets,
+ IFlaggedFeature feature)
+ {
+ public IMethodResult MethodResult { get; } = methodResult;
+ public IPropertyResult PropertyResult { get; } = propertyResult;
+ public IEnumerable Widgets { get; } = widgets;
+ public IFlaggedFeature Feature { get; } = feature;
+ }
+ }
+ """;
+ }
+
+ private static string BuildUtilitySource(string namespaceName, string utilitySuffix)
+ {
+ return $$"""
+ namespace {{namespaceName}}
+ {
+ public static class UtilityValues
+ {
+ public const string Marker = "{{utilitySuffix}}";
+
+ public static string Combine(string prefix)
+ {
+ return prefix + Marker;
+ }
+ }
+ }
+ """;
+ }
+
+ private static string BuildOverrideBaseSource(string assemblyName, int baseServiceCount)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("using FactoryGenerator.Attributes;");
+ sb.AppendLine();
+ sb.AppendLine("[assembly: InjectionPriority(9)]");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {assemblyName}");
+ sb.AppendLine("{");
+ sb.AppendLine("public interface ISharedService");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine();
+ sb.AppendLine("[Inject]");
+ sb.AppendLine("public sealed class BaseSharedService : ISharedService");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine();
+
+ for (var i = 0; i < baseServiceCount; i++)
+ {
+ sb.AppendLine($"public interface INode{i}");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine();
+ sb.AppendLine("[Inject]");
+ if (i == 0)
+ {
+ sb.AppendLine($"public sealed class BaseNode{i}(ISharedService sharedService) : INode{i}");
+ }
+ else
+ {
+ sb.AppendLine($"public sealed class BaseNode{i}(INode{i - 1} previous) : INode{i}");
+ }
+
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static string BuildOverrideDerivedSource(string baseAssemblyName, string derivedAssemblyName, int baseServiceCount, int overrideCount)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine($"using {baseAssemblyName};");
+ sb.AppendLine("using FactoryGenerator.Attributes;");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {derivedAssemblyName}");
+ sb.AppendLine("{");
+
+ for (var i = 0; i < overrideCount; i++)
+ {
+ var serviceIndex = i * Math.Max(1, baseServiceCount / overrideCount);
+ sb.AppendLine("[Inject]");
+ if (serviceIndex == 0)
+ {
+ sb.AppendLine($"public sealed class DerivedNode{serviceIndex}(ISharedService sharedService) : INode{serviceIndex}");
+ }
+ else
+ {
+ sb.AppendLine($"public sealed class DerivedNode{serviceIndex}(INode{serviceIndex - 1} previous) : INode{serviceIndex}");
+ }
+
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine("[Inject]");
+ sb.AppendLine($"public sealed class DerivedRoot(ISharedService sharedService, INode0 firstNode, INode{baseServiceCount - 1} lastNode)");
+ sb.AppendLine("{");
+ sb.AppendLine("}");
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static string BuildReferenceBaseSource(string assemblyName, bool includeSecondBasePart)
+ {
+ var secondPart = includeSecondBasePart
+ ? """
+
+ [Inject]
+ public sealed class BasePartTwo : IBasePart
+ {
+ }
+ """
+ : string.Empty;
+
+ return $$"""
+ using System.Collections.Generic;
+ using FactoryGenerator.Attributes;
+
+ namespace {{assemblyName}}
+ {
+ public interface IBasePart
+ {
+ }
+
+ [Inject]
+ public sealed class BasePartOne : IBasePart
+ {
+ }
+ {{secondPart}}
+
+ public interface IBaseService
+ {
+ }
+
+ [Inject]
+ public sealed class BaseService(IEnumerable parts) : IBaseService
+ {
+ public IEnumerable Parts { get; } = parts;
+ }
+ }
+ """;
+ }
+
+ private static string BuildReferenceDerivedSource(string baseAssemblyName, string derivedAssemblyName)
+ {
+ return $$"""
+ using System.Collections.Generic;
+ using FactoryGenerator.Attributes;
+ using {{baseAssemblyName}};
+
+ namespace {{derivedAssemblyName}}
+ {
+ [Inject]
+ public sealed class DerivedPart : IBasePart
+ {
+ }
+
+ [Inject]
+ public sealed class DerivedConsumer(IBaseService service, IEnumerable parts)
+ {
+ public IBaseService Service { get; } = service;
+ public IEnumerable Parts { get; } = parts;
+ }
+ }
+ """;
+ }
+}
+
+internal sealed class BenchmarkSourceDocument(string fileName, string source)
+{
+ public string FileName { get; } = fileName;
+ public string Source { get; } = source;
+}
+
+internal sealed class BenchmarkAnalyzerConfigOptionsProvider(bool emitStaticExtensions) : AnalyzerConfigOptionsProvider
+{
+ private readonly AnalyzerConfigOptions m_globalOptions = new DictionaryAnalyzerConfigOptions(
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["build_property.FactoryGenerator_EmitStaticExtensions"] = emitStaticExtensions ? "true" : "false"
+ });
+
+ public override AnalyzerConfigOptions GlobalOptions => m_globalOptions;
+
+ public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => EmptyAnalyzerConfigOptions.Instance;
+
+ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => EmptyAnalyzerConfigOptions.Instance;
+}
+
+internal sealed class DictionaryAnalyzerConfigOptions(IReadOnlyDictionary values) : AnalyzerConfigOptions
+{
+ public override bool TryGetValue(string key, out string value)
+ {
+ if (values.TryGetValue(key, out var foundValue))
+ {
+ value = foundValue;
+ return true;
+ }
+
+ value = string.Empty;
+ return false;
+ }
+}
+
+internal sealed class EmptyAnalyzerConfigOptions : AnalyzerConfigOptions
+{
+ public static EmptyAnalyzerConfigOptions Instance { get; } = new();
+
+ public override bool TryGetValue(string key, out string value)
+ {
+ value = string.Empty;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Benchmarking/Benchmarks/Program.cs b/Benchmarking/Benchmarks/Program.cs
index ed35ed6..5d93083 100644
--- a/Benchmarking/Benchmarks/Program.cs
+++ b/Benchmarking/Benchmarks/Program.cs
@@ -7,6 +7,8 @@
namespace Benchmarks;
+// ── Dictionary-based resolution (existing path) ──────────────────────────────
+
[MemoryDiagnoser]
[JsonExporterAttribute.Full]
[JsonExporterAttribute.FullCompressed]
@@ -33,13 +35,44 @@ public class ResolveBenchmarks
public IContainer Create() => new DependencyInjectionContainer(default, default, default!);
[Benchmark]
- public IContainer CreateFromSelf() => new DependencyInjectionContainer(m_container);
+ public void CreateFromSelf()
+ {
+ // Child containers attach to their base until disposed, so each benchmark
+ // invocation must clean up or the inheritor chain grows across operations.
+ using var child = new DependencyInjectionContainer(m_container);
+ }
+
+ // ── Static-extension resolution (C# 14 / .NET 10+ path) ─────────────────────
+ //
+ // Each Resolve(container?) call inlines the full construction chain directly —
+ // no dictionary lookup, no factory-method indirection.
+ //
+ // Null-container variants bypass the singleton cache entirely and perform a
+ // fresh allocation on every call, exposing the raw construction cost.
+ [Benchmark]
+ public ISingleton ExtensionResolveSingleton() => ISingleton.Resolve(m_container);
+
+ [Benchmark]
+ public ISingleton ExtensionResolveSingletonNullContainer() => ISingleton.Resolve(null);
+
+ [Benchmark]
+ public IOverridable ExtensionResolveTransient() => IOverridable.Resolve(m_container);
+
+ [Benchmark]
+ public ChainA ExtensionResolveChain() => ChainA.Resolve(m_container);
+
+ [Benchmark]
+ public ChainA ExtensionResolveChainNullContainer() => ChainA.Resolve(null);
+
+ [Benchmark]
+ public ArrayConsumer ExtensionResolveWithCollection() => ArrayConsumer.Resolve(m_container);
+
+ [Benchmark]
+ public ArrayConsumer ExtensionResolveWithCollectionNullContainer() => ArrayConsumer.Resolve(null);
}
internal static class Program
{
- private static void Main(string[] args)
- {
- var summary = BenchmarkRunner.Run();
- }
+ private static void Main(string[] args) =>
+ BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 70bdb68..0291d13 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,33 +1,26 @@
-
- true
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
\ No newline at end of file
diff --git a/FactoryGenerator.Attributes/Attributes/InjectionPriorityAttribute.cs b/FactoryGenerator.Attributes/Attributes/InjectionPriorityAttribute.cs
new file mode 100644
index 0000000..5c5d606
--- /dev/null
+++ b/FactoryGenerator.Attributes/Attributes/InjectionPriorityAttribute.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace FactoryGenerator.Attributes;
+
+[AttributeUsage(AttributeTargets.Assembly)]
+public class InjectionPriorityAttribute(int priority) : Attribute
+{
+ public int Priority { get; } = priority;
+}
diff --git a/FactoryGenerator.Attributes/ContainerRegistry.cs b/FactoryGenerator.Attributes/ContainerRegistry.cs
index 65e43c6..6031c61 100644
--- a/FactoryGenerator.Attributes/ContainerRegistry.cs
+++ b/FactoryGenerator.Attributes/ContainerRegistry.cs
@@ -42,9 +42,12 @@ public static IContainer BuildChain(IContainer baseContainer)
snapshot = s_registrations.OrderBy(r => r.Priority).ToList();
}
+ var existingAssemblies = GetAssemblyNames(baseContainer);
var current = baseContainer;
foreach (var registration in snapshot)
{
+ if (!existingAssemblies.Add(registration.AssemblyName))
+ continue;
current = registration.Factory(current);
}
@@ -65,9 +68,13 @@ public static IContainer BuildChain(IContainer baseContainer, IEnumerable(s_registrations);
}
+ var existingAssemblies = GetAssemblyNames(baseContainer);
var current = baseContainer;
foreach (var name in assemblyNames)
{
+ if (!existingAssemblies.Add(name))
+ continue;
+
var registration = snapshot.Find(r => r.AssemblyName == name);
if (registration == null)
{
@@ -82,6 +89,25 @@ public static IContainer BuildChain(IContainer baseContainer, IEnumerable GetAssemblyNames(IContainer container)
+ {
+ var names = new HashSet(StringComparer.Ordinal);
+
+ for (var current = container; current is not null; current = current.Base)
+ {
+ if (current is IContainerRegistrationMetadata metadata)
+ names.Add(metadata.AssemblyName);
+ }
+
+ for (var current = container.Inheritor; current is not null; current = current.Inheritor)
+ {
+ if (current is IContainerRegistrationMetadata metadata)
+ names.Add(metadata.AssemblyName);
+ }
+
+ return names;
+ }
+
///
/// Returns the names of all currently registered container assemblies.
///
diff --git a/FactoryGenerator.Attributes/FactoryGenerator.Attributes.csproj b/FactoryGenerator.Attributes/FactoryGenerator.Attributes.csproj
index 4c79949..b0de705 100644
--- a/FactoryGenerator.Attributes/FactoryGenerator.Attributes.csproj
+++ b/FactoryGenerator.Attributes/FactoryGenerator.Attributes.csproj
@@ -6,4 +6,8 @@
latest
+
+
+
+
diff --git a/FactoryGenerator.Attributes/IContainer.cs b/FactoryGenerator.Attributes/IContainer.cs
index b367ce9..b0a8f33 100644
--- a/FactoryGenerator.Attributes/IContainer.cs
+++ b/FactoryGenerator.Attributes/IContainer.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections;
using System.Collections.Generic;
namespace FactoryGenerator;
@@ -25,4 +24,28 @@ public interface IContainer : ILifetimeScope
{
IContainer? Base { get; }
IContainer? Inheritor { get; set; }
+}
+
+public interface IContainerScopeFactory
+{
+ ILifetimeScope BeginLifetimeScope(IContainer? baseContainer);
+}
+
+public interface IContainerRegistrationMetadata
+{
+ string AssemblyName { get; }
+}
+
+public interface IContainerCacheInvalidator
+{
+ void InvalidateCollectionCaches();
+}
+
+public interface IContainerLocalCollectionResolver
+{
+ bool TryResolveLocalCollection(Type type, out object? resolved);
+}
+
+public interface IServiceProviderBackedContainer
+{
}
\ No newline at end of file
diff --git a/FactoryGenerator.Attributes/LifetimeScopeDisposalExtensions.cs b/FactoryGenerator.Attributes/LifetimeScopeDisposalExtensions.cs
new file mode 100644
index 0000000..3945d42
--- /dev/null
+++ b/FactoryGenerator.Attributes/LifetimeScopeDisposalExtensions.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Threading.Tasks;
+
+namespace FactoryGenerator;
+
+public static class LifetimeScopeDisposalExtensions
+{
+ public static ValueTask DisposeAsync(this ILifetimeScope scope)
+ {
+ if (scope is null)
+ throw new ArgumentNullException(nameof(scope));
+
+ if (scope is IAsyncDisposable asyncDisposable)
+ return asyncDisposable.DisposeAsync();
+
+ scope.Dispose();
+ return default;
+ }
+}
diff --git a/FactoryGenerator.Attributes/ResolvedInstanceTracker.cs b/FactoryGenerator.Attributes/ResolvedInstanceTracker.cs
new file mode 100644
index 0000000..d5d65ea
--- /dev/null
+++ b/FactoryGenerator.Attributes/ResolvedInstanceTracker.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace FactoryGenerator;
+
+#nullable enable
+
+public sealed class ResolvedInstanceTracker : IDisposable, IAsyncDisposable
+{
+ private enum DisposalMode
+ {
+ Active = 0,
+ Synchronous = 1,
+ Asynchronous = 2
+ }
+
+ private readonly object m_lock = new object();
+ private List>? m_instances = new List>();
+ private DisposalMode m_disposalMode;
+
+ public void Track(object? instance)
+ {
+ if (instance is null)
+ return;
+
+ if (instance is not IDisposable && instance is not IAsyncDisposable)
+ return;
+
+ DisposalMode disposalMode;
+ lock (m_lock)
+ {
+ disposalMode = m_disposalMode;
+ if (disposalMode == DisposalMode.Active)
+ {
+ m_instances!.Add(new WeakReference