提到.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
、_field
和m_field
三种字段命名,与CommunityToolkit.MVVM协调。
下面就一步步实现这个语法糖!
创建源生成器项目
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)); } 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 ; } private static void Execute (Compilation compilation, IEnumerable<SyntaxNode> nodes, SourceProductionContext context ) { if (!nodes.Any()) return ; var groupedNodes = nodes.GroupBy(GetContainingClass); 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; } 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 ); } 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进行反编译,可以看到已经生成了我们定义的代码: