This document aims to be a guide to help the creation of source generators by providing a series of guidelines for common patterns. It also aims to set out what types of generators are possible under the current design, and what is expected to be explicitly out of scope in the final design of the shipping feature.
This document expands on the details in the full design document, please ensure you have read that first.
- Incremental Generators Cookbook
- Summary
- Table of contents
- Proposal
- Out of scope designs
- Conventions
- Pipeline model design
- Use
ForAttributeWithMetadataName - Use an indented text writer, not
SyntaxNodes, for generation - Put
Microsoft.CodeAnalysis.EmbeddedAttributeon generated marker types - Do not scan for types that indirectly implement interfaces, indirectly inherit from types, or are indirectly marked by an attribute from an interface or base type
- Designs
- Generated class
- Additional file transformation
- Augment user code
- Issue Diagnostics
- INotifyPropertyChanged
- Package a generator as a NuGet package
- Use functionality from NuGet packages
- Access Analyzer Config properties
- Consume MSBuild properties and metadata
- Unit Testing of Generators
- Auto interface implementation
- Breaking Changes:
- Open Issues
As a reminder, the high level design goals of source generators are:
- Generators produce one or more strings that represent C# source code to be added to the compilation.
- Explicitly additive only. Generators can add new source code to a compilation but may not modify existing user code.
- May access additional files, that is, non-C# source texts.
- Run un-ordered, each generator will see the same input compilation, with no access to files created by other source generators.
- A user specifies the generators to run via list of assemblies, much like analyzers.
- Generators create a pipeline, starting from base input sources and mapping them to the output they wish to produce. The more exposed, properly equatable states exist, the earlier the compiler will be able to cut off changes and reuse the same output.
We will briefly look at the non-solvable problems as examples of the kind of problems source generators are not designed to solve:
Source generators are not designed to replace new language features: for instance one could imagine records being implemented as a source generator that converts the specified syntax to a compilable C# representation.
We explicitly consider this to be an anti-pattern; the language will continue to evolve and add new features, and we don't expect source generators to be a way to enable this. Doing so would create new 'dialects' of C# that are incompatible with the compiler without generators. Further, because generators, by design, cannot interact with each other, language features implemented in this way would quickly become incompatible with other additions to the language.
There are many post-processing tasks that users perform on their assemblies today, which here we define broadly as 'code rewriting'. These include, but are not limited to:
- Optimization
- Logging injection
- IL Weaving
- Call site re-writing
While these techniques have many valuable use cases, they do not fit into the idea of source generation. They are, by definition, code altering operations which are explicitly ruled out by the source generators proposal.
There are already well supported tools and techniques for achieving these kinds of operations, and the source generators proposal is not aimed at replacing them. We are exploring approaches for call site rewriting (see interceptors.md), but those features are experimental and may change significantly or even be removed.
As a general guideline, source generator pipelines need to pass along models that are value equatable. This is critical to the incrementality of an
IIncrementalGenerator; as soon as a pipeline step returns the same information that it returned in the previous run, the generator driver can stop running
the generator and reuse the same cached data that the generator produced in the previous run. Most times a generator is triggered (particularly generators that
need to look at type or method definitions, using ForAttributeWithMetadataName) the edit that triggered the generator will not actually have affected the
things that your generator is looking at. However, because semantics can change on basically any edit, the generator driver must rerun your generator again
to ensure that this is the case. If your generator then produces a model with the same values as it did previously, this short-circuits the pipeline and allows
us to avoid a lot of work. Here are some general guidelines around designing your models to ensure that you maintain this equality:
- Use
records, rather thanclasses, so that value equality is generated for you. - Symbols (
ISymboland anything that inherits from that interface) are never equatable, and including them in your model can potentially root old compilations and force Roslyn to hold onto lots of memory that it could otherwise free. Never put these in your model types. Instead, extract the information you need from the symbols you inspect to an equatable representation:strings often work quite well here. SyntaxNodes are also usually not equatable between runs. They're not as strongly discouraged from the initial stages of a pipeline as symbols, and an example later down shows a case where you will need to include aSyntaxNodein model. They also don't potentially root as much memory as symbols will. However, any edit in a file will ensure that allSyntaxNodes from that file are no longer equatable, so they should be removed from your models as soon as possible.- The previous bullet applies to
Locations as well. - Be careful of collection types in your models. Most built-in collection types in .NET do not do value equality by default. Arrays,
ImmutableArray<T>andList<T>, for example, use reference equality, not value equality. We suggest that most generator authors use a wrapper type around arrays to augment them with value-based equality.
We highly recommend that all generator authors that need to inspect syntax do so by using a marker attribute to indicate the types or members that need to be inspected. This has multiple benefits, both for you as an author, and also for your users:
- As an author, you can use
SyntaxProvider.ForAttributeWithMetadataName. This utility method is at least 99x more efficient thanSyntaxProvider.CreateSyntaxProvider, and in many cases even more efficient. This will help you avoid causing performance issues for your users in editors. - Your users can clearly indicate that they intend to use your source generator. This intention is extremely helpful for designing a good user experience; it
means that you can author Roslyn analyzers to help your users when they intended to use your generator but violated your rules in some fashion. For example, if
you are generating some method body, and your generator requires that the user return a specific type, the presence of a
GenerateMeattribute means you can write an analyzer to tell the user if their method declaration returns something that it shouldn't.
We do not recommend generating SyntaxNodes when generating syntax for AddSource. Doing so can be complex, and it can be difficult to format it well; calling
NormalizeWhitespace is often quite expensive, and the API is not really designed for this use-case. Additionally, to ensure immutability guarantees, AddSource does
not accept SyntaxNodes. It instead requires getting the string representation and putting that into a SourceText. Instead of SyntaxNode, we recommend using a
wrapper around StringBuilder that will keep track of indent level and prepend the right amount of indentation when AppendLine is called. See
this conversation on the performance of NormalizeWhitespace for more examples, performance
measurements, and discussion on why we don't believe that SyntaxNodes are a good abstraction for this use case.
Users might depend on your generator in multiple projects in the same solution, and these projects will often have InternalsVisibleTo applied. This means that your
internal marker attributes may be defined in multiple projects, and the compiler will warn about this. While this doesn't block compilation, it can be irritating to
users. To avoid this, mark such attributes with Microsoft.CodeAnalysis.EmbeddedAttribute; when the compiler sees this attribute on a type from separate assembly or
project, it will not include that type in lookup results. To ensure that Microsoft.CodeAnalysis.EmbeddedAttribute is available in the compilation, call the
AddEmbeddedAttributeDefinition helper method in your RegisterPostInitializationOutput callback.
Another option is to provide an assembly in your nuget package that defines your marker attributes, but this can be more difficult to author. We recommend the
EmbeddedAttribute approach, unless you need to support versions of Roslyn lower than 4.14.
Do not scan for types that indirectly implement interfaces, indirectly inherit from types, or are indirectly marked by an attribute from an interface or base type
Using an interface/base type marker can be a very tempting and natural fit for generators. However, scanning for these types of markers is very expensive, and cannot be done incrementally. Doing so can have an outsized impact on IDE and command-line performance, even for fairly small consuming users. These scenarios are:
- A user implements an interface on
BaseModelType, and then the generator looks all derived types fromBaseModelType. Because the generator cannot know ahead of time whatBaseModelTypeactually is, it means that the generator has to fetchAllInterfaceson every single type in the compilation so it can scan for the marker interface. This will end up occurring either on every keystroke or every file save, depending on what mode the user is running generators in; either one is disastrous for IDE performance, even when trying to optimize by scoping down the scanning to only types with a base list. - A user inherits from a generator-defined
BaseSerializerType, and the generator looks for anything that inherits from that type, either directly or indirectly. Similar to the above scenario, the generator will need to scan all types with a base type in the entire compilation for the inheritedBaseSerializerType, which will heavily impact IDE performance. - A generator looks among all base types/implemented interfaces for a type that is attributed with a generator's marker attribute. This is effectively either scenario 1 or 2, just with a different search criteria.
- A generator leaves its marker attribute unsealed, and expects users to be able to derive their own attributes from that marker, as a source of parameter customization.
This has a couple of problems: first, every attributed type needs to be checked to see if the attribute inherits from the marker attribute. While not as performance
impacting as the first three scenarios, this isn't great for performance. Second, and more importantly, there is no good way to retrieve any customizations from the
inherited attribute. These attributes are not instantiated by the source generator, so any parameters passed to the
base()constructor call or values that are assigned to any properties of the base attribute are not visible to the generator. Prefer using FAWMN-driven development here, and using an analyzer to inform the user if they need to inherit from some base class for your generator to work correctly.
This section is broken down by user scenarios, with general solutions listed first, and more specific examples later on.
User scenario: As a generator author I want to be able to add a type to the compilation, that can be referenced by the user's code. Common use cases include creating an attribute that will be used to drive other source generator steps.
Solution: Have the user write the code as if the type was already present. Generate the missing type based on information available in the compilation using
the RegisterPostInitializationOutput step.
Example:
Given the following user code:
public partial class UserClass
{
[GeneratedNamespace.GeneratedAttribute]
public partial void UserMethod();
}Create a generator that will create the missing type when run:
[Generator]
public class CustomGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static postInitializationContext => {
postInitializationContext.AddEmbeddedAttributeDefinition();
postInitializationContext.AddSource("myGeneratedFile.cs", SourceText.From("""
using System;
using Microsoft.CodeAnalysis;
namespace GeneratedNamespace
{
[Embedded]
internal sealed class GeneratedAttribute : Attribute
{
}
}
""", Encoding.UTF8));
});
}
}Alternative Solution: If you are also providing a library to your users, in addition to a source generator, simply have that library include the attribute definition.
User scenario: As a generator author I want to be able to transform an external non-C# file into an equivalent C# representation.
Solution: Use the AdditionalTextsProvider to filter for and retrieve your files. Transform them into the code you care about, then
register that code with the solution.
Example:
[Generator]
public class FileTransformGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var pipeline = context.AdditionalTextsProvider
.Where(static (text) => text.Path.EndsWith(".xml"))
.Select(static (text, cancellationToken) =>
{
var name = Path.GetFileName(text.Path);
var code = MyXmlToCSharpCompiler.Compile(text.GetText(cancellationToken));
return (name, code);
});
context.RegisterSourceOutput(pipeline,
static (context, pair) =>
// Note: this AddSource is simplified. You will likely want to include the path in the name of the file to avoid
// issues with duplicate file names in different paths in the same project.
context.AddSource($"{pair.name}generated.cs", SourceText.From(pair.code, Encoding.UTF8)));
}
}Items need to be included in your csproj files by using the AdditionalFiles ItemGroup:
<ItemGroup>
<AdditionalFiles Include="file1.xml" />
<AdditionalFiles Include="file2.xml" />
<ItemGroup>User scenario: As a generator author I want to be able to inspect and augment a user's code with new functionality.
Solution: Require the user to make the class you want to augment be a partial class, and mark it with a unique attribute.
Provide that attribute in a RegisterPostInitializationOutput step. Register for callbacks on that attribute with
ForAttributeWithMetadataName to collect the information needed to generate code, and use tuples (or create an equatable model)
to pass along that information. That information should be extracted from syntax and symbols; do not put syntax or symbols into
your models.
Example:
public partial class UserClass
{
[GeneratedNamespace.Generated]
public partial void UserMethod();
}[Generator]
public class AugmentingGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(static postInitializationContext =>
postInitializationContext.AddEmbeddedAttributeDefinition();
postInitializationContext.AddSource("myGeneratedFile.cs", SourceText.From("""
using System;
using Microsoft.CodeAnalysis;
namespace GeneratedNamespace
{
[AttributeUsage(AttributeTargets.Method), Embedded]
internal sealed class GeneratedAttribute : Attribute
{
}
}
""", Encoding.UTF8)));
var pipeline = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "GeneratedNamespace.GeneratedAttribute",
predicate: static (syntaxNode, cancellationToken) => syntaxNode is BaseMethodDeclarationSyntax,
transform: static (context, cancellationToken) =>
{
var containingClass = context.TargetSymbol.ContainingType;
return new Model(
// Note: this is a simplified example. You will also need to handle the case where the type is in a global namespace, nested, etc.
Namespace: containingClass.ContainingNamespace?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)),
ClassName: containingClass.Name,
MethodName: context.TargetSymbol.Name);
}
);
context.RegisterSourceOutput(pipeline, static (context, model) =>
{
var sourceText = SourceText.From($$"""
namespace {{model.Namespace}};
partial class {{model.ClassName}}
{
partial void {{model.MethodName}}()
{
// generated code
}
}
""", Encoding.UTF8);
context.AddSource($"{model.ClassName}_{model.MethodName}.g.cs", sourceText);
});
}
private record Model(string Namespace, string ClassName, string MethodName);
}User Scenario: As a generator author I want to be able to add diagnostics to the user's compilation.
Solution: We do not recommend issuing diagnostics within generators. It is possible, but doing so without breaking incrementality is advanced topic beyond the scope of this cookbook. Instead, we suggest writing a separate analyzer for reporting diagnostics.
User scenario: As a generator author I want to be able to implement the INotifyPropertyChanged pattern automatically for a user.
Solution: The design tenant 'Explicitly additive only' seems to be at direct odds with the ability to implement this, and appears to call for user code modification. However we can instead take advantage of explicit fields and instead of editing the users properties, directly provide them for listed fields.
Example:
Given a user class such as:
using AutoNotify;
public partial class UserClass
{
[AutoNotify]
private bool _boolProp;
[AutoNotify(PropertyName = "Count")]
private int _intProp;
}A generator could produce the following:
using System;
using System.ComponentModel;
namespace AutoNotify
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
sealed class AutoNotifyAttribute : Attribute
{
public AutoNotifyAttribute()
{
}
public string PropertyName { get; set; }
}
}
public partial class UserClass : INotifyPropertyChanged
{
public bool BoolProp
{
get => _boolProp;
set
{
_boolProp = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("UserBool"));
}
}
public int Count
{
get => _intProp;
set
{
_intProp = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}User scenario: As a generator author I want to package my generator as a NuGet package for consumption.
Solution: Generators can be packaged using the same method as an Analyzer would.
Ensure the generator is placed in the analyzers\dotnet\cs folder of the package for it to be automatically added to the users project on install.
For example, to turn your generator project into a NuGet package at build, add the following to your project file:
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
</PropertyGroup>
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>User Scenario: As a generator author I want to rely on functionality provided in NuGet packages inside my generator.
Solution: It is possible to depend on NuGet packages inside of a generator, but special consideration has to be taken for distribution.
Any runtime dependencies, that is, code that the end users program will need to rely on, can simply be added as a dependency of the generator NuGet package via the usual referencing mechanism.
For example, consider a generator that creates code that relies on Newtonsoft.Json. The generator does not directly use the dependency, it just emits code that relies on the library being referenced in the users compilation. The author would add a reference to Newtonsoft.Json as a public dependency, and when the user adds the generator package it will be referenced automatically.
<Project>
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
</PropertyGroup>
<ItemGroup>
<!-- Take a public dependency on Json.Net. Consumers of this generator will get a reference to this package -->
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>However, any generation-time dependencies, that is, used by the generator while it is is running and generating code, must be packaged directly alongside the generator assembly inside the generator NuGet package. There are no automatic facilities for this, and you will need to manually specify the dependencies to include.
Consider a generator that uses Newtonsoft.Json to encode something to json during the generation pass, but does not emit any code the relies on it being present at runtime. The author would add a reference to Newtonsoft.Json but make all of its assets private; this ensures the consumer of the generator does not inherit a dependency on the library.
The author would then have to package the Newtonsoft.Json library alongside the generator inside of the NuGet package. This can be achieved in the following way: set the dependency to generate a path property by adding GeneratePathProperty="true". This will create a new MSBuild property of the format PKG<PackageName> where <PackageName> is the package name with . replaced by _. In our example there would be an MSBuild property called PKGNewtonsoft_Json with a value that points to the path on disk of the binary contents of the NuGet files. We can then use that to add the binaries to the resulting NuGet package as we do with the generator itself:
<Project>
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
</PropertyGroup>
<ItemGroup>
<!-- Take a private dependency on Newtonsoft.Json (PrivateAssets=all) Consumers of this generator will not reference it.
Set GeneratePathProperty=true so we can reference the binaries via the PKGNewtonsoft_Json property -->
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" PrivateAssets="all" GeneratePathProperty="true" />
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<!-- Package the Newtonsoft.Json dependency alongside the generator assembly -->
<None Include="$(PkgNewtonsoft_Json)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>[Generator]
public class JsonUsingGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var pipeline = context.AdditionalTextsProvider.Select(static (text, cancellationToken) =>
{
if (!text.Path.EndsWith("*.json"))
{
return default;
}
return (Name: Path.GetFileName(text.Path), Value: Newtonsoft.Json.JsonConvert.DeserializeObject<MyObject>(text.GetText(cancellationToken).ToString()));
})
.Where((pair) => pair is not ((_, null) or (null, _)));
context.RegisterSourceOutput(pipeline, static (context, pair) =>
{
var sourceText = SourceText.From($$"""
namespace GeneratedNamespace
{
internal sealed class GeneratedClass
{
public static const (int A, int B) SerializedContent = ({{pair.A}}, {{pair.B}});
}
}
""", Encoding.UTF8);
context.AddSource($"{pair.Name}generated.cs", sourceText)
});
}
record MyObject(int A, int B);
}User Scenarios:
- As a generator author I want to access the analyzer config properties for a syntax tree or additional file.
- As a generator author I want to access key-value pairs that customize the generator output.
- As a user of a generator I want to be able to customize the generated code and override defaults.
Solution: Generators can access analyzer config values via the AnalyzerConfigOptionsProvider. Analyzer config values can either be accessed in the context of a SyntaxTree, AdditionalFile or globally via GlobalOptions. Global options are 'ambient' in that they don't apply to any specific context, but will be included when requesting option within a specific context.
Note that this is one of the few cases that it is necessary to put a SyntaxNode into a pipeline, as you need the tree in order to get the generator option.
Try to get the SyntaxNode out of the pipeline as fast as possible to avoid making the model not correctly equatable.
A generator is free to use a global option to customize its output. For example, consider a generator that can optionally emit logging. The author may choose to check the value of a global analyzer config value in order to control whether or not to emit the logging code. A user can then choose to enable the setting per project via an .globalconfig file:
mygenerator_emit_logging = true
[Generator]
public class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var userCodePipeline = context.SyntaxProvider.ForAttributeWithMetadataName(... /* collect user code info */);
var emitLoggingPipeline = context.AnalyzerConfigOptionsProvider.Select(static (options, cancellationToken) =>
options.GlobalOptions.TryGetValue("mygenerator_emit_logging", out var emitLoggingSwitch)
? emitLoggingSwitch.Equals("true", StringComparison.InvariantCultureIgnoreCase)
: false); // Default
context.RegisterSourceOutput(userCodePipeline.Combine(emitLoggingPipeline), (context, pair) => /* emit code */);
}
}User Scenarios:
- As a generator author I want to make decisions based on the values contained in the project file
- As a user of a generator I want to be able to customize the generated code and override defaults.
Solution: MSBuild will automatically translate specified properties and metadata into a global analyzer config that can be read by a generator. A generator author specifies the properties and metadata they want to make available by adding items to the CompilerVisibleProperty and CompilerVisibleItemMetadata item groups. These can be added via a props or targets file when packaging the generator as a NuGet package.
For example, consider a generator that creates source based on additional files, and wants to allow a user to enable or disable logging via the project file. The author would specify in their props file that they want to make the specified MSBuild property visible to the compiler:
<ItemGroup>
<CompilerVisibleProperty Include="MyGenerator_EnableLogging" />
</ItemGroup>The value of MyGenerator_EnableLogging property will then be emitted to a generated analyzer config file before build, with a name of build_property.MyGenerator_EnableLogging. The generator is then able read this property from via the GlobalOptions property of the AnalyzerConfigOptionsProvider pipeline:
context.AnalyzerConfigOptionsProvider.Select((provider, ct) =>
provider.GlobalOptions.TryGetValue("build_property.MyGenerator_EnableLogging", out var emitLoggingSwitch)
? emitLoggingSwitch.Equals("true", StringComparison.InvariantCultureIgnoreCase) : false);A user can thus enable, or disable logging, by setting a property in their project file.
Now, consider that the generator author wants to optionally allow opting in/out of logging on a per-additional file basis. The author can request that MSBuild emit the value of metadata for the specified file, by adding to the CompilerVisibleItemMetadata item group. The author specifies both the MSBuild itemType they want to read the metadata from, in this case AdditionalFiles, and the name of the metadata that they want to retrieve for them.
<ItemGroup>
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="MyGenerator_EnableLogging" />
</ItemGroup>This value of MyGenerator_EnableLogging will be emitted to a generated analyzer config file, for each of the additional files in the compilation, with an item name of build_metadata.AdditionalFiles.MyGenerator_EnableLogging. The generator can read this value in the context of each additional file:
context.AdditionalTextsProvider
.Combine(context.AnalyzerConfigOptionsProvider)
.Select((pair, ctx) =>
pair.Right.GetOptions(pair.Left).TryGetValue("build_metadata.AdditionalFiles.MyGenerator_EnableLogging", out var perFileLoggingSwitch)
? perFileLoggingSwitch : false);In the users project file, the user can now annotate the individual additional files to say whether or not they want to enable logging:
<ItemGroup>
<AdditionalFiles Include="file1.txt" /> <!-- logging will be controlled by default, or global value -->
<AdditionalFiles Include="file2.txt" MyGenerator_EnableLogging="true" /> <!-- always enable logging for this file -->
<AdditionalFiles Include="file3.txt" MyGenerator_EnableLogging="false" /> <!-- never enable logging for this file -->
</ItemGroup>Note that MSBuild properties passed to source generators via CompilerVisibleProperty are written into and read from an editorconfig file, resulting in data loss for non-trivial property values. One possible workaround is to use a build task to apply a transport encoding preventing the data loss; as an example a semicolon separated list can be converted to a space separated list (; is the editorconfig comment character):
<Project>
<ItemGroup>
<CompilerVisibleProperty Include="_MyInterpolatorsNamespaces" />
</ItemGroup>
<Task Name="_MyInterpolatorsNamespaces" BeforeTargets="BeforeBuild">
<PropertyGroup>
<_MyInterpolatorsNamespaces>$([System.String]::Copy('$(InterpolatorsNamespaces)').Replace(';', ' '))</_MyInterpolatorsNamespaces>
</PropertyGroup>
</Task>
</Project>Full Example:
MyGenerator.props:
<Project>
<ItemGroup>
<CompilerVisibleProperty Include="MyGenerator_EnableLogging" />
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="MyGenerator_EnableLogging" />
</ItemGroup>
</Project>MyGenerator.csproj:
<Project>
<PropertyGroup>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput> <!-- Do not include the generator as a lib dependency -->
</PropertyGroup>
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<!-- Package the props file -->
<None Include="MyGenerator.props" Pack="true" PackagePath="build" Visible="false" />
</ItemGroup>
</Project>MyGenerator.cs:
[Generator]
public class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var emitLoggingPipeline = context.AdditionalTextsProvider
.Combine(context.AnalyzerConfigOptionsProvider)
.Select((pair, ctx) =>
pair.Right.GetOptions(pair.Left).TryGetValue("build_metadata.AdditionalFiles.MyGenerator_EnableLogging", out var perFileLoggingSwitch)
? perFileLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase)
: pair.Right.GlobalOptions.TryGetValue("build_property.MyGenerator_EnableLogging", out var emitLoggingSwitch)
? emitLoggingSwitch.Equals("true", StringComparison.OrdinalIgnoreCase)
: false);
var sourcePipeline = context.AdditionalTextsProvider.Select((file, ctx) => /* Gather build info */);
context.RegisterSourceOutput(sourcePipeline.Combine(emitLoggingPipeline), (context, pair) => /* Add source */);
}
}User scenario: As a generator author, I want to be able to unit test my generators to make development easier and ensure correctness.
Solution A:
The recommended approach is to use Microsoft.CodeAnalysis.Testing packages:
Microsoft.CodeAnalysis.CSharp.SourceGenerators.TestingMicrosoft.CodeAnalysis.VisualBasic.SourceGenerators.Testing
TODO: #72149
User scenario: As a generator author I want to be able to implement the properties of interfaces passed as arguments to a decorator of a class automatically for a user
Solution: Require the user to decorate the class with the [AutoImplement] Attribute and pass as arguments the types of the interfaces they want to self-implement themselves; The classes that implement the attribute have to be partial class.
Provide that attribute in a RegisterPostInitializationOutput step. Register for callbacks on the classes with
ForAttributeWithMetadataName using the fullyQualifiedMetadataName FullyQualifiedAttributeName, and use tuples (or create an equatable model) to pass along that information.
The attribute could work for structs too, the example was kept simple on purpose for the workbook sample.
Example:
public interface IUserInterface
{
int InterfaceProperty { get; set; }
}
public interface IUserInterface2
{
float InterfacePropertyOnlyGetter { get; }
}
[AutoImplementProperties(typeof(IUserInterface), typeof(IUserInterface2))]
public partial class UserClass
{
public string UserProp { get; set; }
}#nullable enable
[Generator]
public class AutoImplementGenerator : IIncrementalGenerator
{
private const string AttributeNameSpace = "AttributeGenerator";
private const string AttributeName = "AutoImplementProperties";
private const string AttributeClassName = $"{AttributeName}Attribute";
private const string FullyQualifiedAttributeName = $"{AttributeNameSpace}.{AttributeClassName}";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddEmbeddedAttributeDefinition();
//Generate the AutoImplementProperties Attribute
const string autoImplementAttributeDeclarationCode = $$"""
// <auto-generated/>
using System;
using Microsoft.CodeAnalysis;
namespace {{AttributeNameSpace}};
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false), Embedded]
internal sealed class {{AttributeClassName}} : Attribute
{
public Type[] InterfacesTypes { get; }
public {{AttributeClassName}}(params Type[] interfacesTypes)
{
InterfacesTypes = interfacesTypes;
}
}
""";
ctx.AddSource($"{AttributeClassName}.g.cs", autoImplementAttributeDeclarationCode);
});
IncrementalValuesProvider<ClassModel> provider = context.SyntaxProvider.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: FullyQualifiedAttributeName,
predicate: static (node, cancellationToken_) => node is ClassDeclarationSyntax,
transform: static (ctx, cancellationToken) =>
{
ISymbol classSymbol = ctx.TargetSymbol;
return new ClassModel(
classSymbol.Name,
classSymbol.ContainingNamespace.ToDisplayString(),
GetInterfaceModels(ctx.Attributes[0])
);
});
context.RegisterSourceOutput(provider, static (context, classModel) =>
{
foreach (InterfaceModel interfaceModel in classModel.Interfaces)
{
StringBuilder sourceBuilder = new($$"""
// <auto-generated/>
namespace {{classModel.NameSpace}};
public partial class {{classModel.Name}} : {{interfaceModel.FullyQualifiedName}}
{
""");
foreach (string property in interfaceModel.Properties)
{
sourceBuilder.AppendLine(property);
}
sourceBuilder.AppendLine("""
}
""");
//Concat class name and interface name to have unique file name if a class implements two interfaces with AutoImplement Attribute
string generatedFileName = $"{classModel.Name}_{interfaceModel.FullyQualifiedName}.g.cs";
context.AddSource(generatedFileName, sourceBuilder.ToString());
}
});
}
private static EquatableList<InterfaceModel> GetInterfaceModels(AttributeData attribute)
{
EquatableList<InterfaceModel> ret = [];
if (attribute.ConstructorArguments.Length == 0)
return ret;
foreach(TypedConstant constructorArgumentValue in attribute.ConstructorArguments[0].Values)
{
if (constructorArgumentValue.Value is INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol)
{
EquatableList<string> properties = new();
foreach (IPropertySymbol interfaceProperty in interfaceSymbol
.GetMembers()
.OfType<IPropertySymbol>())
{
string type = interfaceProperty.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
//Check if property has a setter
string setter = interfaceProperty.SetMethod is not null
? "set; "
: string.Empty;
properties.Add($$"""
public {{type}} {{interfaceProperty.Name}} { get; {{setter}}}
""");
}
ret.Add(new InterfaceModel(interfaceSymbol.ToDisplayString(), properties));
}
}
return ret;
}
private record ClassModel(string Name, string NameSpace, EquatableList<InterfaceModel> Interfaces);
private record InterfaceModel(string FullyQualifiedName, EquatableList<string> Properties);
private class EquatableList<T> : List<T>, IEquatable<EquatableList<T>>
{
public bool Equals(EquatableList<T>? other)
{
// If the other list is null or a different size, they're not equal
if (other is null || Count != other.Count)
{
return false;
}
// Compare each pair of elements for equality
for (int i = 0; i < Count; i++)
{
if (!EqualityComparer<T>.Default.Equals(this[i], other[i]))
{
return false;
}
}
// If we got this far, the lists are equal
return true;
}
public override bool Equals(object obj)
{
return Equals(obj as EquatableList<T>);
}
public override int GetHashCode()
{
return this.Select(item => item?.GetHashCode() ?? 0).Aggregate((x, y) => x ^ y);
}
public static bool operator ==(EquatableList<T> list1, EquatableList<T> list2)
{
return ReferenceEquals(list1, list2)
|| list1 is not null && list2 is not null && list1.Equals(list2);
}
public static bool operator !=(EquatableList<T> list1, EquatableList<T> list2)
{
return !(list1 == list2);
}
}
}- None currently
This section track other miscellaneous TODO items:
Framework targets: May want to mention if we have framework requirements for the generators, e.g. they must target netstandard2.0 or similar.
Conventions: (See TODO in conventions section above). What standard conventions are we suggesting to users?
Feature detection: Show how to create a generator that relies on specific target framework features, without depending on the TargetFramework property.