源代码生成器无法匹配类上声明的自定义属性与实际属性定义的问题排查及测试方案咨询
源代码生成器无法匹配类上声明的自定义属性与实际属性定义的问题排查及测试方案咨询
我最近在做一个C#源代码生成器,需求是给带有自定义[AutoDisplay]属性的类,在编译时自动给没有[Display]特性的属性添加上[Display(Name = "some value")]特性。属性和生成器在同一个项目里,但在测试时发现属性匹配可能有问题,想请教排查思路和测试方案。先把我的场景和代码贴出来:
需求场景
给定一个带有特定属性的类,在编译过程中:
- 筛选出带有该属性的类
- 遍历类的所有属性
- 给没有
[Display]装饰器的属性自动添加[Display(Name = "some value")]
自定义属性定义
属性和生成器在同一个项目中,定义如下:
[AttributeUsage(AttributeTargets.Class)] public class AutoDisplayAttribute : Attribute { public AutoDisplayMode Mode { get; } public AutoDisplayAttribute(AutoDisplayMode mode = AutoDisplayMode.All) { Mode = mode; } } // 注:AutoDisplayMode枚举默认包含All、ValidationOnly等值
示例用法
使用[AutoDisplay]属性标记类的示例:
[AutoDisplay] public class FancyClass { public string ProjectName { get; set; } }
源代码生成器实现
我写的DisplayAttributeGenerator实现如下,其中加了调试输出用来排查属性匹配的问题:
[Generator] public class DisplayAttributeGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { var classDeclarations = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (node, _) => node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0, transform: static (ctx, _) => ctx.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)ctx.Node) as INamedTypeSymbol) .Where(static symbol => symbol is not null); var autoDisplayAttrSymbol = context.CompilationProvider .Select((compilation, _) => compilation.GetTypeByMetadataName(typeof(AutoDisplayAttribute).FullName)); var filteredClasses = classDeclarations .Combine(autoDisplayAttrSymbol) .Select((tuple, _) => { var (classSymbol, attrSymbol) = tuple; if (classSymbol is null) return default; // 调试输出:打印类上的所有属性及其程序集信息 foreach (var value in classSymbol.GetAttributes()) { var aSymbol = value.AttributeClass; Debug.WriteLine($"Attr: {aSymbol?.ToDisplayString()} | Assembly: {aSymbol?.ContainingAssembly.Name}"); Debug.WriteLine($"Target: {attrSymbol?.ToDisplayString()} | Assembly: {attrSymbol?.ContainingAssembly.Name}"); Debug.WriteLine($"Equality: {SymbolEqualityComparer.Default.Equals(aSymbol, attrSymbol)}"); } var attr = classSymbol.GetAttributes() .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol)); if (attr is null) return default; var mode = AutoDisplayMode.All; if (attr.ConstructorArguments.Length > 0) { var arg = attr.ConstructorArguments[0]; if (arg.Kind == TypedConstantKind.Enum && arg.Type?.Name == nameof(AutoDisplayMode)) mode = (AutoDisplayMode)arg.Value!; } return (classSymbol, mode); }) .Where(static result => result != default); var classWithProperties = filteredClasses .Select((tuple, _) => { var (classSymbol, mode) = tuple; var props = classSymbol.GetMembers() .OfType<IPropertySymbol>() .Where(p => !HasDisplay(p) && (mode != AutoDisplayMode.ValidationOnly || HasValidation(p))) .ToList(); return (classSymbol, props); }) .Where(pair => pair.props.Count > 0); context.RegisterSourceOutput(classWithProperties, (spc, data) => { var (classSymbol, props) = data; var generatedCode = GeneratePartialClass(classSymbol, props); spc.AddSource($"{classSymbol.Name}.DisplayAttributes.g.cs", generatedCode); }); } private bool HasDisplay(IPropertySymbol prop) => prop.GetAttributes().Any(a => a.AttributeClass?.Name == "DisplayAttribute"); private bool HasValidation(IPropertySymbol prop) => prop.GetAttributes().Any(a => a.AttributeClass?.BaseType?.Name == "ValidationAttribute"); private string GeneratePartialClass(INamedTypeSymbol classSymbol, List<IPropertySymbol> props) { var namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); var lines = props.Select(p => $" [Display(Name = \"some value\")]\n public {p.Type} {p.Name} {{ get; set; }}"); return $$""" // <auto-generated /> using System.ComponentModel.DataAnnotations; namespace {{namespaceName}} { public partial class {{classSymbol.Name}} { {{string.Join("\n\n", lines)}} } } """; } }
测试代码
为了验证生成器是否工作,我写了一个xUnit测试,并且尝试用SourceGenInspector来查看生成的输出文件:
[Fact] public void PrintGeneratedSources() { var source = """ using System; namespace FanceNamespace { [AutoDisplay] public class FancyClass { public string DisplayName { get; set; } } } """; var test = new CSharpSourceGeneratorTest<DisplayAttributeGenerator, EmptyVerifier> { TestState = { Sources = { _usings, source }, AdditionalReferences = { MetadataReference.CreateFromFile(typeof(AutoDisplayAttribute).Assembly.Location) } } }; var results = SourceGenInspector.GenerateCompilationOutput(new DisplayAttributeGenerator(), test.TestState); foreach (var (fileName, content) in results) { _output.WriteLine($"Generated file: {fileName}\n{content}"); } }
遇到的问题与疑问
我在生成器里加了调试输出,用来查看AutoDisplayAttribute的匹配情况,但发现有时候SymbolEqualityComparer.Default.Equals(aSymbol, attrSymbol)返回false,从输出看可能是属性的程序集信息不匹配?我不太确定问题出在哪:
- 为什么同一个项目里的属性,在编译时获取的符号会出现程序集不匹配的情况?
- 有没有更可靠的方式来匹配自定义属性的符号?
- 除了我现在用的这种测试方式,有没有更完善的源代码生成器测试方案,能更方便地验证生成的代码是否符合预期?
内容来源于stack exchange
相关产品推荐
相关产品推荐

