序
本文概述了利用.NET Compiler Platform(“Roslyn”)SDK 附带的源生成器(Source Generator)自动生成机械重复的代码。关于这部分的基础入门知识可以在MSDN[1]学到。
本文默认已经有一个解决方案,包含两个项目。一个是普通C#项目,依赖于另一个源生成器项目。
注:红色加粗表示重点;绿色表示次重点;蓝色表示参考资料及链接,在文章最后显示。
创建及使用Attribute
此处以DependencyPropertyAttribute为例,可以为拥有本Attribute的类,添加一条DependencyProperty的属性。
本DependencyProperty的名称、类型、属性改变处理函数都是必须指定的,可选指定内容是属性setter的公共性、该类型的null性、和默认值。可选内容有默认值。
以下是DependencyPropertyAttribute的实现:
using System;
namespace Attributes;
/// <summary>
/// 生成如下代码
/// <code>
/// public static readonly DependencyProperty Property = DependencyProperty.Register("Field", typeof(Type), typeof(TClass), new PropertyMetadata(DefaultValue, OnPropertyChanged));
/// public Type Field { get => (Type)GetValue(Property); set => SetValue(Property, value); }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class DependencyPropertyAttribute : Attribute
{
public DependencyPropertyAttribute(string name, Type type, string propertyChanged = "")
{
Name = name;
Type = type;
PropertyChanged = propertyChanged;
}
public string Name { get; }
public Type Type { get; }
public string PropertyChanged { get; }
public bool IsSetterPublic { get; init; } = true;
public bool IsNullable { get; init; } = true;
public string DefaultValue { get; init; } = "DependencyProperty.UnsetValue";
}
以下是使用示例:
namespace Controls.IconButton;
[DependencyProperty("Text", typeof(string), nameof(OnTextChanged))]
[DependencyProperty("Icon", typeof(IconElement), nameof(OnIconChanged))]
public partial class IconButton : Button
{
...
}
这将会生成如下代码:
using Microsoft.UI.Xaml;
using System;
using Microsoft.UI.Xaml.Controls;
#nullable enable
namespace Controls.IconButton
{
partial class IconButton
{
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnTextChanged));
public string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); }
public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(IconElement), typeof(IconButton), new PropertyMetadata(DependencyProperty.UnsetValue, OnIconChanged));
public IconElement Icon { get => (IconElement)GetValue(IconProperty); set => SetValue(IconProperty, value); }
}
}
注:DependencyPropertyAttribute中建议只使用基本类型的常量,因为复杂类型不方便获取。
注:被添加Attribute的类(如IconButton)要加partial关键字,否则会出重定义错误。
注:DependencyPropertyAttribute中,只会用到构造函数和可选指定内容的属性,这说明实现可以简化为:
using System;
namespace Attributes;
///...
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class DependencyPropertyAttribute : Attribute
{
public DependencyPropertyAttribute(string name, Type type, string propertyChanged = "") { }
public bool IsSetterPublic { get; init; }
public bool IsNullable { get; init; }
public string DefaultValue { get; init; }
}
因为当源生成器分析的时候,分析的是被捕获的类(如IconButton)及其上下文,而非DependencyPropertyAttribute的,所以其他内容实际上用不上。
但原来的写法方便将来可能需要反射本Attribute的操作,也方便阅读,所以建议保留。
创建通用AttributeReceiver
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
internal class AttributeReceiver : ISyntaxContextReceiver
{
private readonly string _attributeName;
private readonly List<TypeDeclarationSyntax> _candidateTypes = new();
private INamedTypeSymbol? _attributeSymbol;
public AttributeReceiver(string attributeName)
{
_attributeName = attributeName;
}
public IReadOnlyList<TypeDeclarationSyntax> CandidateTypes => _candidateTypes;
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
_attributeSymbol ??= context.SemanticModel.Compilation.GetTypeByMetadataName(_attributeName);
if (_attributeSymbol is null) return;
if (context.Node is TypeDeclarationSyntax typeDeclaration && typeDeclaration.AttributeLists
.SelectMany(l => l.Attributes, (_, attribute) => context.SemanticModel.GetSymbolInfo(attribute))
.Any(symbolInfo => SymbolEqualityComparer.Default.Equals(symbolInfo.Symbol?.ContainingType, _attributeSymbol)))
_candidateTypes.Add(typeDeclaration);
}
}
本类实现接受语法上下文(ISyntaxContextReceiver)接口,它会将所有拥有_attributeName的类型声明节点(TypeDeclarationSyntax)(例如class、struct、record、enum等)捕获下来(此处也可以捕获property、field、method等,但没有做)。
创建通用基类GetAttributeGenerator
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Utilities;
internal abstract class GetAttributeGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new AttributeReceiver(AttributePath));
}
public void Execute(GeneratorExecutionContext context)
{
if (context.Compilation.GetTypeByMetadataName(AttributePath) is not { } attributeType)
{
return;
}
foreach (var typeDeclaration in ((AttributeReceiver) context.SyntaxContextReceiver!).CandidateTypes)
{
var semanticModel = context.Compilation.GetSemanticModel(typeDeclaration.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(typeDeclaration) is not { } specificType)
{
continue;
}
ExecuteForEach(context, attributeType, typeDeclaration, specificType);
}
}
protected abstract string AttributePathGetter();
/// <summary>
/// 获取包含指定attribute的类型
/// </summary>
/// <param name="context">生成器运行上下文</param>
/// <param name="attributeType">指定的attribute类型</param>
/// <param name="typeDeclaration">该类型的声明</param>
/// <param name="specificType">该类型</param>
protected abstract void ExecuteForEach(GeneratorExecutionContext context, INamedTypeSymbol attributeType, TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol specificType);
private string AttributePath => AttributePathGetter();
}
本类继承于ISourceGenerator接口,如果拥有GeneratorAttribute则会被认为是源生成器了。它通过调用上面的AttributeReceiver获取所有符合的语法节点,AttributePathGetter()由子类实现,这样可以保证子类的Attribute名被本基类读取。
对于每个被捕获的类型分别用ExecuteForEach()处理。
创建具体的子类DependencyPropertyGenerator
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Linq;
using System.Text;
[Generator]
internal class DependencyPropertyGenerator : Utilities.GetAttributeGenerator
{
protected override string AttributePathGetter() => "Attributes.DependencyPropertyAttribute";
protected override void ExecuteForEach(GeneratorExecutionContext context, INamedTypeSymbol attributeType, TypeDeclarationSyntax typeDeclaration, INamedTypeSymbol specificClass)
{
var members = new List<MemberDeclarationSyntax>();
var namespaces = new HashSet<string> { specificClass.ContainingNamespace.ToDisplayString(), "Microsoft.UI.Xaml" };
var usedTypes = new HashSet<ITypeSymbol>(SymbolEqualityComparer.Default);
foreach (var attribute in specificClass.GetAttributes().Where(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, attributeType)))
{
if (attribute.ConstructorArguments[0].Value is not string propertyName || attribute.ConstructorArguments[1].Value is not INamedTypeSymbol type)
{
continue;
}
if (attribute.ConstructorArguments.Length < 3 || attribute.ConstructorArguments[2].Value is not string propertyChanged)
{
continue;
}
var isSetterPublic = true;
var defaultValue = "DependencyProperty.UnsetValue";
var isNullable = false;
foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Value.Value is { } value)
{
switch (namedArgument.Key)
{
case "IsSetterPublic":
isSetterPublic = (bool) value;
break;
case "DefaultValue":
defaultValue = (string) value;
break;
case "IsNullable":
isNullable = (bool) value;
break;
}
}
}
var fieldName = propertyName + "Property";
namespaces.UseNamespace(usedTypes, type);
var defaultValueExpression = SyntaxFactory.ParseExpression(defaultValue);
var metadataCreation = GetObjectCreationExpression(defaultValueExpression);
if (propertyChanged is not "")
{
metadataCreation = GetMetadataCreation(metadataCreation, propertyChanged);
}
var registration = GetRegistration(propertyName, type, specificClass, metadataCreation);
var staticFieldDeclaration = GetStaticFieldDeclaration(fieldName, registration);
var getter = GetGetter(fieldName, isNullable, type, context);
var setter = GetSetter(fieldName, isSetterPublic);
var propertyDeclaration = GetPropertyDeclaration(propertyName, isNullable, type, getter, setter);
members.Add(staticFieldDeclaration);
members.Add(propertyDeclaration);
}
if (members.Count > 0)
{
var generatedClass = GetClassDeclaration(specificClass, members);
var generatedNamespace = GetNamespaceDeclaration(specificClass, generatedClass);
var compilationUnit = GetCompilationUnit(generatedNamespace, namespaces.Skip(1));
var fileName = specificClass.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)) + ".g.cs";
context.AddSource(fileName, SyntaxFactory.SyntaxTree(compilationUnit, encoding: Encoding.UTF8).GetText());
}
}
/// <summary>
/// 生成如下代码
/// <code>
/// new PropertyMetadata(<paramref name="defaultValueExpression" />);
/// </code>
/// </summary>
/// <returns>ObjectCreationExpression</returns>
private static ObjectCreationExpressionSyntax GetObjectCreationExpression(ExpressionSyntax defaultValueExpression)
{
return SyntaxFactory.ObjectCreationExpression(SyntaxFactory.IdentifierName("PropertyMetadata"))
.AddArgumentListArguments(SyntaxFactory.Argument(defaultValueExpression));
}
/// <summary>
/// 生成如下代码
/// <code>
/// new PropertyMetadata(<paramref name="metadataCreation" />, <paramref name="partialMethodName" />)
/// </code>
/// </summary>
/// <returns>MetadataCreation</returns>
private static ObjectCreationExpressionSyntax GetMetadataCreation(ObjectCreationExpressionSyntax metadataCreation, string partialMethodName)
{
return metadataCreation.AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.IdentifierName(partialMethodName)));
}
/// <summary>
/// 生成如下代码
/// <code>
/// DependencyProperty.Register("<paramref name="propertyName" />", typeof(<paramref name="type" />), typeof(<paramref
/// name="specificClass" />), <paramref name="metadataCreation" />);
/// </code>
/// </summary>
/// <returns>Registration</returns>
private static InvocationExpressionSyntax GetRegistration(string propertyName, ITypeSymbol type, ITypeSymbol specificClass, ExpressionSyntax metadataCreation)
{
return SyntaxFactory.InvocationExpression(SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression, SyntaxFactory.IdentifierName("DependencyProperty"), SyntaxFactory.IdentifierName("Register")))
.AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(propertyName))), SyntaxFactory.Argument(SyntaxFactory.TypeOfExpression(type.GetTypeSyntax(false))), SyntaxFactory.Argument(SyntaxFactory.TypeOfExpression(specificClass.GetTypeSyntax(false))), SyntaxFactory.Argument(metadataCreation));
}
/// <summary>
/// 生成如下代码
/// <code>
/// public static readonly DependencyProperty <paramref name="fieldName" /> = <paramref name="registration" />;
/// </code>
/// </summary>
/// <returns>StaticFieldDeclaration</returns>
private static FieldDeclarationSyntax GetStaticFieldDeclaration(string fieldName, ExpressionSyntax registration)
{
return SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(SyntaxFactory.IdentifierName("DependencyProperty")))
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword))
.AddDeclarationVariables(SyntaxFactory.VariableDeclarator(fieldName).WithInitializer(SyntaxFactory.EqualsValueClause(registration)));
}
/// <summary>
/// 生成如下代码
/// <code>
/// get => (<paramref name="type" /><<paramref name="isNullable" />>)GetValue(<paramref name="fieldName" />);
/// </code>
/// </summary>
/// <returns>Getter</returns>
private static AccessorDeclarationSyntax GetGetter(string fieldName, bool isNullable, ITypeSymbol type, GeneratorExecutionContext context)
{
ExpressionSyntax getProperty = SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("GetValue"))
.AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.IdentifierName(fieldName)));
if (!SymbolEqualityComparer.Default.Equals(type, context.Compilation.GetSpecialType(SpecialType.System_Object)))
{
getProperty = SyntaxFactory.CastExpression(type.GetTypeSyntax(isNullable), getProperty);
}
return SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithExpressionBody(SyntaxFactory.ArrowExpressionClause(getProperty))
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
}
/// <summary>
/// 生成如下代码
/// <code>
/// <<paramref name="isSetterPublic" />> set => SetValue(<paramref name="fieldName" />, value);
/// </code>
/// </summary>
/// <returns>Setter</returns>
private static AccessorDeclarationSyntax GetSetter(string fieldName, bool isSetterPublic)
{
ExpressionSyntax setProperty = SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("SetValue"))
.AddArgumentListArguments(SyntaxFactory.Argument(SyntaxFactory.IdentifierName(fieldName)), SyntaxFactory.Argument(SyntaxFactory.IdentifierName("value")));
var setter = SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithExpressionBody(SyntaxFactory.ArrowExpressionClause(setProperty))
.WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken));
return !isSetterPublic ? setter.AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)) : setter;
}
/// <summary>
/// 生成如下代码
/// <code>
/// public <paramref name="type" /><<paramref name="isNullable" />> <paramref name="propertyName" /> { <paramref
/// name="getter" />; <paramref name="setter" />; }
/// </code>
/// </summary>
/// <returns>PropertyDeclaration</returns>
private static PropertyDeclarationSyntax GetPropertyDeclaration(string propertyName, bool isNullable, ITypeSymbol type, AccessorDeclarationSyntax getter, AccessorDeclarationSyntax setter)
{
return SyntaxFactory.PropertyDeclaration(type.GetTypeSyntax(isNullable), propertyName)
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(getter, setter);
}
/// <summary>
/// 生成如下代码
/// <code>
/// partial class <paramref name="specificClass" /><br />
/// {<br /> <paramref name="members" /><br />}
/// </code>
/// </summary>
/// <returns>ClassDeclaration</returns>
private static ClassDeclarationSyntax GetClassDeclaration(ISymbol specificClass, IEnumerable<MemberDeclarationSyntax> members)
{
return SyntaxFactory.ClassDeclaration(specificClass.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword))
.AddMembers(members.ToArray());
}
/// <summary>
/// 生成如下代码
/// <code>
/// namespace <paramref name="specificClass" />.ContainingNamespace<br />
/// {<br /> <paramref name="generatedClass" /><br />}
/// </code>
/// </summary>
/// <returns>NamespaceDeclaration</returns>
private static NamespaceDeclarationSyntax GetNamespaceDeclaration(ISymbol specificClass, MemberDeclarationSyntax generatedClass)
{
return SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(specificClass.ContainingNamespace.ToDisplayString()))
.AddMembers(generatedClass)
.WithNamespaceKeyword(SyntaxFactory.Token(SyntaxKind.NamespaceKeyword)
.WithLeadingTrivia(SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.EnableKeyword), true))));
}
/// <summary>
/// 生成如下代码
/// <code>
/// using Microsoft.UI.Xaml;<br />
/// ...<br />
/// <paramref name="generatedNamespace" />
/// </code>
/// </summary>
/// <returns>CompilationUnit</returns>
private static CompilationUnitSyntax GetCompilationUnit(MemberDeclarationSyntax generatedNamespace, IEnumerable<string> namespaces)
{
return SyntaxFactory.CompilationUnit()
.AddMembers(generatedNamespace)
.AddUsings(namespaces.Select(ns => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(ns))).ToArray())
.NormalizeWhitespace();
}
}
此类继承于GetAttributeGenerator,拥有GeneratorAttribute,可以被编译器作为源生成器执行。
其中AttributePathGetter()要写全名,否则会找不到或者重名。
接下来分析的语句来自于形如这里的地方:
[DependencyProperty("Name", typeof(string), nameof(Method), IsNullable = true)]
以下这句可以读取构造函数的前两个参数,其中Type类型用INamedTypeSymbol代替。
if (attribute.ConstructorArguments[0].Value is not string propertyName || attribute.ConstructorArguments[1].Value is not INamedTypeSymbol type)
{
continue;
}
以下这句读取了第三个构造函数的缺省参数,因为可能不存在
if (attribute.ConstructorArguments.Length < 3 || attribute.ConstructorArguments[2].Value is not string propertyChanged)
{
continue;
}
以下这段是对象初始化的内容(不在构造函数里):
var isSetterPublic = true;
var defaultValue = "DependencyProperty.UnsetValue";
var isNullable = false;
foreach (var namedArgument in attribute.NamedArguments)
{
if (namedArgument.Value.Value is { } value)
{
switch (namedArgument.Key)
{
case "IsSetterPublic":
isSetterPublic = (bool) value;
break;
case "DefaultValue":
defaultValue = (string) value;
break;
case "IsNullable":
isNullable = (bool) value;
break;
}
}
}
注:源生成器生成的文件若要使用nullable必须加上如下指令,项目的null性不会影响到生成的文件。
#nullable enable
注:本例使用Roslyn的AST(Abstract Syntax Tree)实现,但其实没必要一定如此,因为直接string操作可以达到同样效果,生成代码耗时也不算在编译时间内。
用string可以如此AddSource:
context.AddSource(fileNameString, sourceString);

引用图片

参考资料
[1] SourceGenerators(https://docs.microsoft.com/zh-cn/dotnet/csharp/roslyn-sdk/source-generators-overview)
QQ:2639914082