C#源生成器

提到.NET的Source Generator,就不得不提及Fody,两者都是强大的.NET代码生成技术。这些代码生成技术是硬核技能,给开发者提供了制作语法糖的利器,在框架设计中被广泛使用,以下是两者的主要区别:

在使用以上两种方式生成代码时,踩了许多坑,特此写两篇技术文档做一个记录!本文结合实例先对Source Generator的用法进行介绍。

需求描述

在WPF项目中,如果为TextBox的Text属性绑定一个非string类型的后台数据,那么当用户在TextBox中输入非法文本(不能转换成后台数据类型)时,后台数据依然会保存上一次的合法数据,该合法数据会进入业务流,但界面的非法数据无法得到检验,从而造成用户体验的下降。

针对该问题,有一个解决方法:①将需要进行校验的后台数据定义为string类型,那么任何非法输入都可以得到校验;②为该string属性定义额外的真实类型数据,避免用户手动进行类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[ObservableProperty]
[Range(0,100,ErrorMessage ="年龄必须在0~100之间!")]
[NotifyDataErrorInfo]
private string _age;
public int? AgeValue {
get {
if (int.TryParse(Age, out var resValue))
return resValue;
return null;
}
set {
Age = value.ToString();
}
}

这时,对于任何非法输入,都可以通过以下校验进行拦截:

1
2
3
4
5
6
7
8
9
10
[RelayCommand]
private void ShowData() {
ValidateAllProperties();
if (HasErrors) {
string res = string.Join(Environment.NewLine, GetErrors());
MessageBox.Show(res);
return;
}
// 合法数据才能进入业务流...
}

用户通过额外添加的AgeValue属性就可以获取并设置真实的string属性。

问题是,为很多属性添加这种额外属性势必是一项繁重的重复性劳动,怎么能够从代码编写的角度简化这项工作呢?能不能为这个属性添加一个特定注解就能在后台自动生成,类似CommunityToolkit.MVVM等框架一样?当然可以,而且我们可以做得更加精致,如果为string属性添加[GenIntValue][GenDoubleValue]注解,直接为该属性生成PropertyNameValue形式的数据属性;如果为string字段添加这两个注解,我们同步检测该字段是否也被[ObservableProperty]注解,如果注解了,就生成对应的数据属性,支持field_fieldm_field三种字段命名,与CommunityToolkit.MVVM协调。

下面就一步步实现这个语法糖!

创建源生成器项目

  • 创建一个netstandard2.0类库项目,添加必要的NuGet包:

  • 创建一个实现了IIncrementalGenerator接口的源生成器,实现核心逻辑函数Initialize()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LHSourceGenerator1 {
[Generator(LanguageNames.CSharp)]
public class GenValuePropertySourceGenerator : IIncrementalGenerator {
public void Initialize(IncrementalGeneratorInitializationContext context) {
var nodes = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => (node is PropertyDeclarationSyntax p && p.AttributeLists.Count > 0)
|| (node is FieldDeclarationSyntax f && f.AttributeLists.Count > 0),
transform: static (ctx, _) => GetPropertyDeclarationWithAttribute(ctx)
).Where(static m => m is not null);

var compilationAndProperties = context.CompilationProvider.Combine(nodes.Collect());

context.RegisterSourceOutput(compilationAndProperties,
static (spc, source) => Execute(source.Item1, source.Item2, spc));
}

/// <summary>
/// 返回有相应注解标记的string类型的字段和属性,如果是字段,不许同时有CommunityToolkit的ObservableProperty注解
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
private static SyntaxNode GetPropertyDeclarationWithAttribute(GeneratorSyntaxContext context) {
if (context.Node is PropertyDeclarationSyntax propertyDeclaration) {
var typeName = propertyDeclaration.Type.ToString();
if (!typeName.Equals("string", System.StringComparison.OrdinalIgnoreCase))
return null;

foreach (var attributeList in propertyDeclaration.AttributeLists) {
foreach (var attribute in attributeList.Attributes) {
if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol)
continue;
var attributeSymbolName = attributeSymbol.ContainingType.ToDisplayString();
if (attributeSymbolName.Equals("SourceGeneratorAttribute.GenDoubleValueAttribute", StringComparison.Ordinal) ||
attributeSymbolName.Equals("SourceGeneratorAttribute.GenIntValueAttribute", StringComparison.Ordinal)) {
return propertyDeclaration;
}
}
}
} else if (context.Node is FieldDeclarationSyntax fieldDeclaration) {
var typeName = fieldDeclaration.Declaration.Type.ToString();
if (!typeName.Equals("string", System.StringComparison.OrdinalIgnoreCase))
return null;

bool flag1 = false;
bool flag2 = false;
foreach (var attributeList in fieldDeclaration.AttributeLists) {
foreach (var attribute in attributeList.Attributes) {
if (context.SemanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol)
continue;
var attributeSymbolName = attributeSymbol.ContainingType.ToDisplayString();
if (attributeSymbolName.Equals("SourceGeneratorAttribute.GenDoubleValueAttribute", StringComparison.Ordinal) ||
attributeSymbolName.Equals("SourceGeneratorAttribute.GenIntValueAttribute", StringComparison.Ordinal)) {
flag1 = true;
} else if (attributeSymbolName.Equals("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute", StringComparison.Ordinal)) {
flag2 = true;
}
if (flag1 & flag2)
return fieldDeclaration;
}
}
}
return null;
}

/// <summary>
/// 遍历满足要求的字段和属性,生成相应的代码
/// </summary>
/// <param name="compilation"></param>
/// <param name="nodes"></param>
/// <param name="context"></param>
private static void Execute(Compilation compilation, IEnumerable<SyntaxNode> nodes, SourceProductionContext context) {
if (!nodes.Any())
return;

// 按照所在的类对进行分组
var groupedNodes = nodes.GroupBy(GetContainingClass);

// 对每个类都要生成一个partial类
foreach (var group in groupedNodes) {
var classDeclaration = group.Key;
if (classDeclaration == null)
continue;

var semanticModel = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol;
if (classSymbol == null)
continue;

// 命名空间名和类名
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;

// 源代码
var sourceBuilder = new StringBuilder();

sourceBuilder.AppendLine($"// <auto-generated/>");
sourceBuilder.AppendLine($"using System;");
sourceBuilder.AppendLine();
sourceBuilder.AppendLine($"namespace {namespaceName}");
sourceBuilder.AppendLine("{");
sourceBuilder.AppendLine($" partial class {className}");
sourceBuilder.AppendLine(" {");

// 对筛选后的字段和属性进行代码生成
foreach (var node in group) {
string symbolName = GetSymbolName(node);
if (symbolName == null) continue;

// 属性节点
if (node is PropertyDeclarationSyntax propertyDeclaration) {
var computedPropertyName = $"{symbolName}Value";
// 以第一个注解为准
foreach (var attributeList in propertyDeclaration.AttributeLists) {
foreach (var attribute in attributeList.Attributes) {
if (semanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol)
continue;
var attributeSymbolName = attributeSymbol.ContainingType.ToDisplayString();
if (attributeSymbolName.Equals("SourceGeneratorAttribute.GenDoubleValueAttribute", StringComparison.Ordinal)) {
sourceBuilder.AppendLine($@"
public double? {computedPropertyName}{{
get{{
if(double.TryParse({symbolName},out var resValue))
return resValue;
return null;
}}
set{{
{symbolName}=value.ToString();
}}
}}");
break;
}else if (attributeSymbolName.Equals("SourceGeneratorAttribute.GenIntValueAttribute", StringComparison.Ordinal)) {
sourceBuilder.AppendLine($@"
public int? {computedPropertyName}{{
get{{
if(int.TryParse({symbolName},out var resValue))
return resValue;
return null;
}}
set{{
{symbolName}=value.ToString();
}}
}}");
break;
}
}
}
}
// 字段节点
else if (node is FieldDeclarationSyntax fieldDeclaration) {
// 获取名称
var propertyName = GetPropertyName(symbolName);
var computedPropertyName = $"{propertyName}Value";
// 以第一个注解为准
foreach (var attributeList in fieldDeclaration.AttributeLists) {
foreach (var attribute in attributeList.Attributes) {
if (semanticModel.GetSymbolInfo(attribute).Symbol is not IMethodSymbol attributeSymbol)
continue;
var attributeSymbolName = attributeSymbol.ContainingType.ToDisplayString();
if (attributeSymbolName.Equals("SourceGeneratorAttribute.GenDoubleValueAttribute", StringComparison.Ordinal)) {
sourceBuilder.AppendLine($@"
public double? {computedPropertyName}{{
get{{
if(double.TryParse({propertyName},out var resValue))
return resValue;
return null;
}}
set{{
{propertyName}=value.ToString();
}}
}}");
break;
}else if (attributeSymbolName.Equals("SourceGeneratorAttribute.GenIntValueAttribute", StringComparison.Ordinal)) {
sourceBuilder.AppendLine($@"
public int? {computedPropertyName}{{
get{{
if(int.TryParse({propertyName},out var resValue))
return resValue;
return null;
}}
set{{
{propertyName}=value.ToString();
}}
}}");
break;
}
}
}
}
}

sourceBuilder.AppendLine(" }");
sourceBuilder.AppendLine("}");

context.AddSource($"{className}_GenValue.g.cs",
SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
}

private static ClassDeclarationSyntax GetContainingClass(SyntaxNode node) {
return node.Parent as ClassDeclarationSyntax;
}

/// <summary>
/// 根据字段名获得属性名(_fieldName,m_fieldName,fieldName)
/// </summary>
/// <param name="fieldName"></param>
/// <returns></returns>
private static string GetPropertyName(string fieldName) {
if(string.IsNullOrWhiteSpace(fieldName))
return null;

string propertyName = fieldName;
if (fieldName.StartsWith("_")) {
if (fieldName.Length == 1)
return null;
propertyName = fieldName.Substring(1);
} else if (fieldName.StartsWith("m_")) {
if (fieldName.Length == 2)
return null;
propertyName = fieldName.Substring(2);
}
return char.ToUpper(propertyName[0]) + propertyName.Substring(1);
}

/// <summary>
/// 获得属性和字段的符号名
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public static string GetSymbolName(SyntaxNode node) {
if (node is FieldDeclarationSyntax fieldDeclaration) {
foreach (var variable in fieldDeclaration.Declaration.Variables) {
return variable.Identifier.ValueText;
}
} else if (node is PropertyDeclarationSyntax propertyDeclaration) {
return propertyDeclaration.Identifier.ValueText;
}
throw new ArgumentException("Unsupported syntax node type", nameof(node));
}
}
}

创建注解定义库

创建一个netstandard2.0类库项目,定义我们需要的两个注解类:

1
2
3
4
5
using System;
namespace SourceGeneratorAttribute {
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class GenDoubleValueAttribute : Attribute { }
}
1
2
3
4
5
using System;
namespace SourceGeneratorAttribute {
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class GenIntValueAttribute : Attribute { }
}

创建测试项目

  • 如果要使用.NET Framework框架,必须使用SDK风格的项目
  • 在项目配置文件中引入对以上两个项目的引用,对于源生成器的项目引用需要额外添加两个特殊的属性,表明这是一个分析器项目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net48</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\LHSourceGenerator1\LHSourceGenerator1.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\SourceGeneratorAttribute\SourceGeneratorAttribute.csproj" />
</ItemGroup>
</Project>
  • 添加测试主程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using CommunityToolkit.Mvvm.ComponentModel;
using SourceGeneratorAttribute;

namespace LHSourceGeneratorTest {
public static partial class Program {
static void Main(string[] args) {
HelloFrom("李浩");

Person person = new Person() { Age = "200" };
Console.WriteLine(person.AgeValue ?? 0);

Console.ReadKey();
}
static partial void HelloFrom(string name);
}

public partial class Person : ObservableObject {
[GenIntValue]
public string Age { get; set; }

[ObservableProperty]
[GenDoubleValue]
private string _grade1;
}
}

对生成的exe进行反编译,可以看到已经生成了我们定义的代码:

(转载本站文章请注明作者和出处lihaohello.top,请勿用于任何商业用途)

评论