-
-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Objective
Upgrade the legacy ISourceGenerator in Splat.DI.SourceGenerator to a high-performance IIncrementalGenerator using Roslyn 4.10.0. The focus is on performance, memory efficiency, and cacheability while strictly targeting netstandard2.0. This involves modernizing the .csproj file, implementing an efficient, cache-friendly incremental pipeline with chained steps and liberal use of records, and updating the README.md to reflect the changes.
1. Modernize the .csproj File
The current .csproj includes outdated practices (e.g., ILRepack) and unnecessary dependencies. Update it to align with modern source generator standards while maintaining netstandard2.0.
Actions
- Remove ILRepack and Related Targets: Replace with
<PrivateAssets>all</PrivateAssets>and<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>onPackageReferenceitems for efficient dependency management. - Target Framework: Strictly target
netstandard2.0for Roslyn compatibility. Avoid multi-targeting to simplify packaging. - Update CodeAnalysis Packages: Upgrade to
Microsoft.CodeAnalysis.CSharp4.10.0 andMicrosoft.CodeAnalysis.Analyzers3.3.4 (or latest stable). - Remove Unnecessary Packages: Eliminate
ReactiveMarbles.RoslynHelpers,Microsoft.CodeAnalysis.Common, andMicrosoft.CodeAnalysis.CSharp.Workspaces, as they are not needed. - Add PolySharp: Include
PolySharp(version 1.14.1 or latest) to enable modern C# features (e.g., records, init-only properties) innetstandard2.0. - Ensure Correct Packaging: Configure the project to output the DLL to the
analyzers/dotnet/csfolder in the NuGet package. Set<IsRoslynAnalyzer>true</IsRoslynAnalyzer>,<DevelopmentDependency>true</DevelopmentDependency>, and<IncludeBuildOutput>false</IncludeBuildOutput>. - Clean Up: Remove
<NoWarn>AD0001</NoWarn>,<DebugType>full</DebugType>, and custom targets likePackBuildOutputsif handled by SDK defaults. Set<LangVersion>12.0</LangVersion>for modern syntax.
Proposed .csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>12.0</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynAnalyzer>true</IsRoslynAnalyzer>
<DevelopmentDependency>true</DevelopmentDependency>
<IncludeBuildOutput>false</IncludeBuildOutput>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<PackageDescription>Produces DI registration for both property and constructor injection using the Splat locators.</PackageDescription>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
<PackageReference Include="PolySharp" Version="1.14.1" PrivateAssets="all" />
</ItemGroup>
</Project>Verification
- Build and pack the project to confirm the NuGet package places the analyzer DLL in
analyzers/dotnet/cs. - Test compatibility with Visual Studio 17.10+.
2. Implement Cache-Friendly Incremental Pipeline
Convert the ISourceGenerator.Execute logic to an IIncrementalGenerator pipeline with efficient chaining of steps and liberal use of records for data transfer. The pipeline must be cache-friendly, memory-efficient, and avoid LINQ in performance-critical paths.
Key Principles
- Immutable Data Models: Use C# record types extensively for data transfer between pipeline steps. Records ensure immutability and value-based equality, optimizing Roslyn’s caching.
- Efficient Chaining: Chain pipeline steps using
Select,SelectMany, andWhereto minimize intermediate collections and leverage Roslyn’s incremental execution. - Performance: Avoid LINQ in hot paths (e.g., no
ToList,Where, orSelectin loops). Useforeachloops and pre-sized collections (e.g.,List<T>with capacity). - Memory Efficiency: Filter invalid candidates early to reduce memory usage.
- Abstraction: Separate syntax collection, semantic extraction, and code emission into distinct, testable methods.
- Cache Friendliness: Ensure all transforms are pure functions (output depends only on input) to maximize Roslyn’s caching benefits.
- Attribute Support: Use
ForAttributeWithMetadataNamefor efficient attribute detection.
Pipeline Steps
Implement in the Initialize(IncrementalGeneratorInitializationContext context) method with chained providers:
-
Syntax Provider
- Use
context.SyntaxProvider.ForAttributeWithMetadataNameto efficiently findClassDeclarationSyntaxnodes with the fully qualifiedSplat.DI.Attributes.RegisterAttribute. - Record for this step:
public record AttributedClass( ClassDeclarationSyntax Syntax, string FullyQualifiedAttributeName );
- Chain directly to the transform step to avoid unnecessary allocations.
- Use
-
Transform Provider
- Chain a
Selecttransform to extract semantic information from eachAttributedClass. - For each class:
- Get the
SemanticModelusingcontext.CompilationProvider. - Extract class name, namespace, accessibility, and dependencies (constructor/property injections).
- Validate the class (e.g., not abstract, valid dependencies) and return
nullfor invalid candidates. - Return a record for valid classes.
- Get the
- Record for this step:
public record InjectionTarget( string ClassName, string Namespace, Accessibility Accessibility, ImmutableArray<DependencyInfo> Dependencies ); public record DependencyInfo( string Name, string Type, bool IsProperty // true for property injection, false for constructor );
- Chain a
Whereclause to filter outnullresults. - Use
foreachloops andList<T>with pre-allocated capacity for dependency collection to avoid LINQ allocations.
- Chain a
-
Aggregation Step
- Chain a
Collectstep to gather allInjectionTargetrecords into anImmutableArray<InjectionTarget>. - Record for this step:
public record RegistrationGroup( ImmutableArray<InjectionTarget> Targets );
- This step enables grouping all registrations into a single file (if needed) while maintaining cacheability.
- Chain a
-
Source Emitter
- Chain a
Selectto theRegistrationGroupfor code generation. - Use
StringBuilderto construct the source code efficiently. - Generate a single file (e.g.,
GeneratedRegistrations.cs) with all DI registrations. - Emit via
context.AddSource("GeneratedRegistrations.cs", generatedCode). - Record for this step (optional, for clarity):
public record GeneratedSource( string FileName, string SourceCode );
- Chain a
Example Pipeline Code
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Step 1: Syntax Provider
var attributedClasses = context.SyntaxProvider
.ForAttributeWithMetadataName(
"Splat.DI.Attributes.RegisterAttribute",
(node, _) => node is ClassDeclarationSyntax,
(ctx, _) => new AttributedClass((ClassDeclarationSyntax)ctx.Node, "Splat.DI.Attributes.RegisterAttribute")
);
// Step 2: Transform Provider
var injectionTargets = attributedClasses
.Select((attributedClass, ct) =>
{
var semanticModel = ctx.Compilation.GetSemanticModel(attributedClass.Syntax.SyntaxTree);
var symbol = semanticModel.GetDeclaredSymbol(attributedClass.Syntax, ct);
if (symbol == null || symbol.IsAbstract) return null;
var dependencies = new List<DependencyInfo>(capacity: 10); // Pre-size for efficiency
foreach (var member in symbol.GetMembers())
{
// Extract properties/constructors with attributes (logic omitted for brevity)
// Add to dependencies list
}
return new InjectionTarget(
symbol.Name,
symbol.ContainingNamespace.ToDisplayString(),
symbol.DeclaredAccessibility,
dependencies.ToImmutableArray()
);
})
.Where(target => target != null);
// Step 3: Aggregation
var registrationGroup = injectionTargets
.Collect()
.Select((targets, _) => new RegistrationGroup(targets));
// Step 4: Source Emitter
context.RegisterSourceOutput(registrationGroup, (spc, group) =>
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("namespace Splat.DI.Generated;");
sb.AppendLine("public static class Registrations");
sb.AppendLine("{");
foreach (var target in group.Targets)
{
// Generate registration code (logic omitted for brevity)
}
sb.AppendLine("}");
spc.AddSource("GeneratedRegistrations.cs", sb.ToString());
});
}Considerations
- Edge Cases: Handle partial classes, generics, or invalid attributes by filtering in the transform step.
- Performance: Pre-size collections (e.g.,
List<DependencyInfo>(capacity: 10)) and useforeachto avoid allocations. - Records: Use records at each step (
AttributedClass,InjectionTarget,DependencyInfo,RegistrationGroup,GeneratedSource) for immutability and caching. - Output: Ensure the generated code matches the legacy generator’s output functionally.
- PolySharp: Leverage records and modern C# features via PolySharp in
netstandard2.0.
3. Update README.md
Update the README.md to reflect the modernized generator and its benefits.
Actions
- Remove Obsolete Information: Eliminate references to the legacy generator’s behavior (e.g., full re-execution on every change).
- Add Performance Benefits: Include a section:
This generator uses Roslyn’s incremental model with efficient pipeline chaining and immutable records, significantly improving Visual Studio performance by caching intermediate results, reducing memory usage, and avoiding unnecessary re-computations during editing.
- Verify Usage Instructions: Confirm instructions for installing the NuGet package, applying
RegisterAttribute, and integrating with Splat locators are accurate for Visual Studio 17.10+. - Remove non-incremental pipeline - Only use incremental pipelines since we are just targetting VS 17.10
- Remove the SynfaxFactory - Use raw literals, and interpolated raw literals in more modern C# versions only. Can use StringBuilder only in small circumstances, but then just do like
sb.Append($$"""
the text {{data.Item}};
""");for multi line input.
Acceptance Criteria
- Generator converted to
IIncrementalGeneratorusing Roslyn 4.10.0. -
.csprojupdated: ILRepack removed, unnecessary packages eliminated, PolySharp added, strictly targetingnetstandard2.0. - Pipeline uses efficient chaining (
Select,Where,Collect), is cache-friendly (pure transforms, records), avoids LINQ in hot paths, and is memory-efficient (early filtering). - Liberal use of records (
AttributedClass,InjectionTarget,DependencyInfo,RegistrationGroup,GeneratedSource) for data transfer. - Logic abstracted into testable methods (syntax collection, semantic extraction, code emission).
-
README.mdupdated with modern architecture details and performance benefits. - Existing tests pass; new tests added for pipeline steps if necessary.
- Generated output functionally identical to the legacy version.
- Migrated the old non-incremental pipeline
- Build, pack, and test in a sample project with Visual Studio 17.10+.
References
- Roslyn Incremental Generators Overview
- Roslyn Incremental Generators Cookbook
- PolySharp
- Visual Studio Release History
Notes
- Test thoroughly in Visual Studio 17.10+ to verify IDE performance improvements.
- Validate NuGet package compatibility with downstream projects.
- Consider adding a sample project to demonstrate usage and performance gains.