搭建项目环境
1. 创建 Net Standard 2.0 项目
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
</ItemGroup>
</Project>
2. 生成器完整结构示例
[Generator]
internal class FlattenPropsGenerator : IIncrementalGenerator
{
public override void Initialize(IncrementalGeneratorInitializationContext context)
{
var propertyPipeline = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => QuickCheckAttribute(node),
transform: (context, _) => TransformClass(context))
.Where(classInfo => classInfo != null);
context.RegisterSourceOutput(propertyPipeline, (spc, source) =>
{
if (source is ClassFlattenInfo classFlattenInfo)
GenerateFlattenedProperties(spc, classFlattenInfo);
});
}
private static readonly string flattenPropsAttrFullName = typeof(GenerateFlattenPropsAttribute).FullName;
private static readonly string flattenAttrFullName = typeof(GenerateFlattenAttribute).FullName;
private static bool QuickCheckAttribute(SyntaxNode node)
{
if (node is not RecordDeclarationSyntax typeDeclaration) return false;
return typeDeclaration.AttributeLists.SelectMany(a => a.Attributes).Any(a => flattenPropsAttrFullName.Contains(a.Name.ToFullString()));
}
private static ClassFlattenInfo? TransformClass(GeneratorSyntaxContext context)
{
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
var semanticModel = context.SemanticModel;
var compilation = semanticModel.Compilation;
if (semanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol symbol)
return null;
var flattenPropsAttributeType = compilation.GetTypeByMetadataName(flattenPropsAttrFullName);
var attribute = symbol.GetAttributes().FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, flattenPropsAttributeType));
if (attribute == null) return null;
var properties = new List<PropertyTransformResult>();
var attributeType = compilation.GetTypeByMetadataName(flattenAttrFullName);
foreach (var property in typeDeclaration.Members.OfType<PropertyDeclarationSyntax>())
{
var propertySymbol = semanticModel.GetDeclaredSymbol(property);
if (propertySymbol == null) continue;
if (propertySymbol.Type is not INamedTypeSymbol propertyType)
continue;
var attr = propertySymbol.GetAttributes().FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType));
if (attr == null) continue;
var attrConfig = AttrConfig.CreateFromAttr(attr, context.SemanticModel);
properties.Add(new PropertyTransformResult
{
PropertySymbol = propertySymbol,
PropertyType = propertyType,
PropertyName = property.Identifier.Text,
PropertySyntax = property,
PropertiesToFlatten = propertyType.GetAllProperties()
});
}
return properties.Count == 0 ? null : new ClassFlattenInfo
{
ContainingType = symbol,
Properties = properties,
IsRecord = symbol.IsRecord
};
}
private static void GenerateFlattenedProperties(SourceProductionContext context, ClassFlattenInfo classFlattenInfo)
{
var classInfo = classFlattenInfo.ContainingType;
try
{
var code = GenerateFlattenPropertiesCode(classFlattenInfo, context);
var fileName = $"{classInfo.Name}_FlattenProps.g.cs";
context.AddSource(fileName, SourceText.From(code, Encoding.UTF8));
}
catch (Exception ex)
{
var descriptor = new DiagnosticDescriptor("FLATTEN001", "Error generating flattened properties", "Error: {0}", "Generation", DiagnosticSeverity.Error, true);
var diagnostic = Diagnostic.Create(descriptor, classInfo.DeclaringSyntaxReferences.First().GetSyntax().GetLocation(), ex.Message);
context.ReportDiagnostic(diagnostic);
}
}
private static string GenerateFlattenPropertiesCode(ClassFlattenInfo classFlattenInfo, SourceProductionContext context)
{
var sb = new StringBuilder();
var clsInfo = classFlattenInfo.ContainingType;
AddGenerateInfo(sb);
AddUsings(sb, classFlattenInfo);
var namespaceName = clsInfo.ContainingNamespace.ToDisplayString();
if (!string.IsNullOrEmpty(namespaceName))
sb.AppendLine($"namespace {namespaceName};");
AddTypeDeclaration(sb, clsInfo);
AddFlattenedProperties(sb, classFlattenInfo, context);
sb.AppendLine("}");
return sb.ToString();
}
private static void AddUsings(StringBuilder sb, ClassFlattenInfo classFlattenInfo)
{
var usings = new HashSet<string>();
var clsNsp = classFlattenInfo.ContainingType.ContainingNamespace;
Stack<ITypeSymbol> stack = new(classFlattenInfo.Properties.Select(p => p.PropertySymbol.Type)
.Concat(classFlattenInfo.Properties.SelectMany(prop => prop.PropertiesToFlatten
.SelectMany(x => x.GetAttributes().Select(a => a.AttributeClass))).OfType<ITypeSymbol>()));
while (stack.Count > 0)
{
var _tsym = stack.Pop();
if (_tsym.ContainingNamespace is { } ns && !SymbolEqualityComparer.Default.Equals(ns, clsNsp))
usings.Add(ns.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
if (_tsym is INamedTypeSymbol namedType && namedType.IsGenericType)
foreach (var typeArg in namedType.TypeArguments)
stack.Push(typeArg);
}
foreach (var s in usings)
sb.AppendLine($"using {s};");
sb.AppendLine();
}
private static void AddTypeDeclaration(StringBuilder sb, INamedTypeSymbol clsInfo)
{
sb.AppendLine($"{clsInfo.DeclaredAccessibility.ToCodeString()} {clsInfo.GetTypeModifiers()} partial {clsInfo.GetTypeKindString()} {clsInfo.Name}");
sb.AppendLine("{");
}
private static void AddFlattenedProperties(StringBuilder sb, ClassFlattenInfo classFlattenInfo, SourceProductionContext context)
{
var flattenedProperties = new Dictionary<string, (IPropertySymbol, PropertyTransformResult)>();
var conflicts = new List<(string msg, PropertyTransformResult prop)>();
var existingProperty = new HashSet<string>(classFlattenInfo.ContainingType.GetMembers().OfType<IPropertySymbol>().Select(x => x.Name));
foreach (var propertyInfo in classFlattenInfo.Properties)
{
foreach (var prop in propertyInfo.PropertiesToFlatten)
{
var propertyName = prop.Name;
var msg = flattenedProperties.ContainsKey(propertyName) ? $"{propertyInfo.PropertyName}.{propertyName}" : "";
if (msg == "" && existingProperty.Contains(propertyName))
msg = $"{propertyInfo.PropertyName}.{propertyName} (已存在于类中)";
if (msg != "")
{
conflicts.Add((msg, propertyInfo));
continue;
}
flattenedProperties[propertyName] = (prop, propertyInfo);
}
}
if (conflicts.Count > 0)
{
const string descId = "FLATTEN002";
const string msgFmt = "检测到属性名称冲突: {0}";
foreach (var (msg, prop) in conflicts)
{
var _msg = string.Format(msgFmt, msg);
sb.AppendLine($"{indent}// {DiagnosticSeverity.Warning} {descId}: {_msg}");
var desc = new DiagnosticDescriptor(descId, "扁平化属性冲突", _msg, "CodeGeneration", DiagnosticSeverity.Error, true);
context.ReportDiagnostic(Diagnostic.Create(desc, prop.PropertySyntax.GetLocation()));
}
sb.AppendLine();
}
foreach (var (prop, propertyInfo) in flattenedProperties.Values)
{
GenerateFlattenedPropertyCode(sb, prop, propertyInfo.PropertyName);
sb.AppendLine();
}
}
private static void GenerateFlattenedPropertyCode(StringBuilder sb, IPropertySymbol sourceProperty, string sourcePropertyName)
{
var propertyName = sourceProperty.Name;
var propertyType = sourceProperty.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
var accessibility = sourceProperty.DeclaredAccessibility.ToCodeString();
sb.AppendLine($"{indent}/// <summary>");
sb.AppendLine($"{indent}/// 从 {sourcePropertyName}.{propertyName} 扁平化的属性");
sb.AppendLine($"{indent}/// </summary>");
var hasGetter = sourceProperty.GetMethod != null;
var hasSetter = sourceProperty.SetMethod != null && !sourceProperty.SetMethod.IsInitOnly;
var isRequired = sourceProperty.IsRequired ? "required " : "";
if (hasGetter && hasSetter)
{
var getterAccess = sourceProperty.GetMethod!.DeclaredAccessibility.ToCodeString();
var setterAccess = sourceProperty.SetMethod!.DeclaredAccessibility.ToCodeString();
var getterMod = getterAccess != accessibility ? $"{getterAccess} " : "";
var setterMod = setterAccess != accessibility ? $"{setterAccess} " : "";
sb.AppendLine($"{indent}{accessibility} {isRequired}{propertyType} {propertyName}");
sb.AppendLine($"{indent}{{");
sb.AppendLine($"{indent}{indent}{getterMod}get => {sourcePropertyName}.{propertyName};");
sb.AppendLine($"{indent}{indent}{setterMod}set => {sourcePropertyName}.{propertyName} = value;");
sb.Append($"{indent}}}");
}
else if (hasGetter)
{
sb.AppendLine($"{indent}{accessibility} {isRequired}{propertyType} {propertyName} => {sourcePropertyName}.{propertyName};");
}
else if (hasSetter)
{
var setterAccess = sourceProperty.SetMethod!.DeclaredAccessibility.ToCodeString();
var setterMod = setterAccess != accessibility ? $"{setterAccess} " : "";
sb.AppendLine($"{indent}{accessibility} {isRequired}{propertyType} {propertyName}");
sb.AppendLine($"{indent}{{");
sb.AppendLine($"{indent}{indent}{setterMod}set => {sourcePropertyName}.{propertyName} = value;");
sb.Append($"{indent}}}");
}
}
struct PropertyTransformResult
{
public IPropertySymbol PropertySymbol { get; set; }
public INamedTypeSymbol PropertyType { get; set; }
public string PropertyName { get; set; }
public PropertyDeclarationSyntax PropertySyntax { get; set; }
public IReadOnlyList<IPropertySymbol> PropertiesToFlatten { get; set; }
}
struct ClassFlattenInfo
{
public INamedTypeSymbol ContainingType { get; set; }
public List<PropertyTransformResult> Properties { get; set; }
public bool IsRecord { get; set; }
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class GenerateFlattenAttribute : Attribute{}
3. 使用示例
public record Person
{
public string? Name { get; set; }
}
[GenerateFlattenProps]
public partial record Student
{
[GenerateFlatten]
Person Person { get; set; } = new Person();
}
如何利用 nuget 方便使用
- 生成器项目无法直接被其他项目作为依赖项目引用,需要借助 nuget
- 修改项目配置文件使生成器项目为 nuget 项目
- 每次修改生成器项目后重新生成,然后在测试项目中重新安装或更新 nuget
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<VersionSuffix>alpha</VersionSuffix>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Preview'">
<VersionSuffix>beta</VersionSuffix>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<VersionSuffix></VersionSuffix>
</PropertyGroup>
<PropertyGroup>
<IncludeBuildOutput>false</IncludeBuildOutput>
<IsAnalyzer>true</IsAnalyzer>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageOutputPath>$(SolutionDir)local-packages</PackageOutputPath>
<PackageId>Arch.CodeGenerator</PackageId>
<Version>1.2.3</Version>
<Version Condition="'$(VersionSuffix)' != ''">$(Version)-$(VersionSuffix)</Version>
<PackageVersion>$(Version)</PackageVersion>
<Authors>会写代码的建筑师</Authors>
<Description>一个源码生成器</Description>
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageType>Dependency</PackageType>
<NoPackageAnalysis>true</NoPackageAnalysis>
<IncludeSourceGeneratorInPackage>true</IncludeSourceGeneratorInPackage>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<IncludeSource>true</IncludeSource>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
如何让生成器项目同时作为 dll 项目
- 其他引用项目可与生成器共享本项目中定义的类
- 使用生成器的项目可直接从 nuget 安装即可使用本项目定义的类型
- 比如 GenerateFlattenAttribute 可直接定义在当前项目,便于统一管理
<Target Name="CopyForPackage" BeforeTargets="GenerateNuspec">
<Copy SourceFiles="$(OutputPath)$(AssemblyName).dll"
DestinationFiles="$(OutputPath)analyzers/$(AssemblyName).dll"
SkipUnchangedFiles="true" />
</Target>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.6.0" />
<None Include="$(OutputPath)$(AssemblyName).dll" Pack="true" PackagePath="lib/netstandard2.0" />
<None Include="$(OutputPath)analyzers/$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
可能遇到的问题:
- 首次引入源生成器项目时报错,需要重启 vs 并重新生成解决方案;