.NET源码生成器基于partial范式开发和nuget打包

一、partial范式深度探讨

二、本文以自动生成属性为例

1. 功能简介

  • 场景是通过一个属性获取对象,但不需要这个对象重复创建
  • 单例模式就是其中的场景之一
  • 这样的代码是千篇一律的,非常适合自动生成代码

2. 生成器代码

  • 直接套用ValuesGenerator基类
  • 查找标记了GenerateLazy的属性和方法
  • 预处理为GenerateLazySource对象
  • 执行GenerateLazySource
  • 问题是查找属性和方法不能使用官方的SyntaxValueProvider.ForAttributeWithMetadataName
  • ForAttributeWithMetadataName只能用来找partial的类
  • 这个场景类是partial但需要从方法(或属性)入手
  • 一个类可以有多个方法(或属性)被标记,同个方法(或属性)也可以标记生成多个属性
  • 为此笔者重写了这部分代替ForAttributeWithMetadataName
csharp 复制代码
[Generator(LanguageNames.CSharp)]
public class GenerateLazyGenerator()
    : ValuesGenerator<GenerateLazySource>(
        Attribute,
        new SyntaxFilter(false, SyntaxKind.PropertyDeclaration, SyntaxKind.MethodDeclaration),
        GenerateLazyTransform.Instance,
        new GeneratorExecutor<GenerateLazySource>())
{
    /// <summary>
    /// Attribute标记
    /// </summary>
    public const string Attribute = "Hand.Cache.GenerateLazyAttribute";
}

3. GenerateProvider.CreateByAttribute

  • CreateByAttribute用于代替ForAttributeWithMetadataName
  • 先遍历SyntaxTree
  • 再查找节点并处理为需要的对象
  • 这样可以覆盖ForAttributeWithMetadataName的场景并扩展支持更多的需求
  • 限于篇幅不展开所有代码,大家可以到源码库查看
csharp 复制代码
public static IncrementalValuesProvider<TSource> CreateByAttribute<TSource>(IncrementalGeneratorInitializationContext context, string attributeName, ISyntaxFilter filter, IGeneratorTransform<TSource> transform)
{
    return context.CompilationProvider
        .SelectMany(GetSyntaxTree)
        .SelectMany((syntaxTree, cancellationToken) => GetAttribute(syntaxTree, attributeName, filter, transform, cancellationToken))
        .WithTrackingName("Provider_ByAttribute");
}

4. GenerateLazyTransform处理

  • 由于我们定位的是方法或者属性,类型(TypeDeclarationSyntax)和类型符号(typeSymbol)需要自行获取
  • 另外对类型进行了校验,必须含partial修饰符
  • GetPropertyNameByAttribute获取Attribute配置
  • 如果当前是属性就处理为LazyPropertySource
  • 如果当前是方法就处理为LazyMethodSource
  • GenerateLazySource是抽象类,LazyPropertySource和LazyMethodSource是GenerateLazySource的子类
  • CheckSource判断是否有重名对象,如果有就不生成(强行会生成编译不过的文件)
csharp 复制代码
public class GenerateLazyTransform : IGeneratorTransform<GenerateLazySource>
{
    public GenerateLazySource? Transform(AttributeContext context, CancellationToken cancellation)
    {
        if (cancellation.IsCancellationRequested)
            return null;
        var targetNode = context.TargetNode;
        if (targetNode.Parent is not TypeDeclarationSyntax type || !type.Modifiers.IsPartial())
            return null;
        var semanticModel = context.SemanticModel;
        var typeSymbol = semanticModel.GetDeclaredSymbol(type, cancellation);
        if (typeSymbol is null) 
            return null;
        var compilation = semanticModel.Compilation;
        var attributeType = compilation.GetTypeByMetadataName(GenerateLazyGenerator.Attribute);
        if (attributeType is null) 
            return null;
        var propertyName = GetPropertyNameByAttribute(context.Attributes, attributeType);
        GenerateLazySource? source = null;
        if (targetNode is PropertyDeclarationSyntax property)
        {
            var propertySymbol = semanticModel.GetDeclaredSymbol(property, cancellation);
            if (propertySymbol is not null && propertySymbol.Type is INamedTypeSymbol symbol)
                source = new LazyPropertySource(property, type, typeSymbol, propertyName, symbol, property.Modifiers.IsStatic());
        }
        else if (targetNode is MethodDeclarationSyntax method)
        {
            var methodSymbol = semanticModel.GetDeclaredSymbol(method, cancellation);
            if (methodSymbol is not null && methodSymbol.ReturnType is INamedTypeSymbol symbol)
                source = new LazyMethodSource(method, type, typeSymbol, propertyName, symbol, method.Modifiers.IsStatic());
        }
        // 判断是否已经存在同名属性
        // 不存在才返回
        if (source is not null && CheckSource(source, compilation))
            return source;
        return null;
    }
    /// <summary>
    /// 判断延迟缓存源对象是否合法
    /// </summary>
    /// <param name="source"></param>
    /// <param name="compilation"></param>
    /// <returns></returns>
    public static bool CheckSource(GenerateLazySource source, Compilation compilation)
    {
        var descriptor = new SymbolTypeBuilder()
            .WithProperty()
            .WithField()
            .Build(compilation, source.Symbol);
        // 存在同名属性不生成
        if(descriptor.GetProperty(source.PropertyName) is not null)
            return false;
        // 存在同名字段不生成
        if (descriptor.GetField(source.ValueName) is not null)
            return false;
        if (descriptor.GetField(source.StateName) is not null)
            return false;
        if (descriptor.GetField(source.LockName) is not null)
            return false;
        return true;
    }
    // ...
}

5. GenerateLazySource

  • 首先复制原类型,不用管是类,是结构体还是record,是否有命名空间,这些与原类型保持一致即可
  • Clone会清理一些成员(方法、字段、属性等),避免编译出错
  • 定义了3个字段1个属性
  • 属性的get处理器是用开源项目EasySyntax定义的,非常简洁
  • 使用锁和双重判断实现的线程安全懒汉单例模式
  • 如果原方法(或属性)是静态的,生成对象也增加静态修饰符
  • GetValueExpression是从原代码中提取代码,这在LazyPropertySource和LazyMethodSources实现稍有不同
csharp 复制代码
public abstract class GenerateLazySource(TypeDeclarationSyntax type, INamedTypeSymbol typeSymbol, string propertyName, INamedTypeSymbol propertySymbol, bool isStatic, string valueName, string stateName, string lockName)
    : IGeneratorSource
{
    public SyntaxGenerator Generate()
    {
        var builder = SyntaxGenerator.Clone(_type);
        var _valueField = _propertyType.Field(_value.Identifier, SyntaxGenerator.DefaultLiteral)
            .Private();
        var _stateField = SyntaxGenerator.BoolType.Field(_state.Identifier, SyntaxGenerator.FalseLiteral)
            .Private();
        var _lockField = SyntaxGenerator.LockType.Field(_lock.Identifier, SyntaxFactory.ImplicitObjectCreationExpression())
            .Private();
        var property = _propertyType.Property(_propertyName, CreateAccessor())
            .Public();
        if (_isStatic)
        {
            builder.AddMember(_valueField.Static());
            builder.AddMember(_stateField.Static());
            builder.AddMember(_lockField.Static());
            builder.AddMember(property.Static());
        }
        else
        {
            builder.AddMember(_valueField);
            builder.AddMember(_stateField);
            builder.AddMember(_lockField);
            builder.AddMember(property);
        }
        
        return builder;
    }
    /// <summary>
    /// 构造属性处理器
    /// </summary>
    /// <returns></returns>
    public AccessorDeclarationSyntax CreateAccessor()
    {
        return SyntaxGenerator.PropertyGetDeclaration()
            .ToBuilder()
            // if(_state)
            .If(_state)
                // return _value
                .Return(_value)
            // lock(_lock){
            .Lock(_lock)
                //if(_state)
                .If(_state)
                    // return _value
                    .Return(_value)
                // _value = GetValue()
                .Add(_value.Assign(GetValueExpression()))
                // _state = true
                .Add(_state.Assign(SyntaxGenerator.TrueLiteral))
                // }
                .End()
            // reurn _value
            .Return(_value);
    }
    protected abstract ExpressionSyntax GetValueExpression();
    // ...
  }

6. 按方法生成属性的Case

6.1 原始代码

csharp 复制代码
using Hand;
using Hand.Cache;
namespace GenerateCachedPropertyTests;

public partial class MethodTests
{
    [GenerateLazy(""LazyTime"")]
    public DateTime CreateTime()
    {
        return DateTime.Now;
    }
}

6.2 生成代码

csharp 复制代码
// <auto-generated/>
namespace GenerateCachedPropertyTests;
partial class MethodTests
{
    private System.DateTime _valueLazyTime = default;
    private bool _stateLazyTime = false;
    private object _lockLazyTime = new();
    public System.DateTime LazyTime
    {
        get
        {
            if (_stateLazyTime)
                return _valueLazyTime;
            lock (_lockLazyTime)
            {
                if (_stateLazyTime)
                    return _valueLazyTime;
                _valueLazyTime = DateTime.Now;
                _stateLazyTime = true;
            }

            return _valueLazyTime;
        }
    }
}

7. 按方法生成属性的Case

7.1 原始代码

csharp 复制代码
using Hand;
using Hand.Cache;
namespace GenerateCachedPropertyTests;

public partial class PropertyTests
{
    [GenerateLazy(""LazyTime"")]
    public static DateTime Now { get; } = DateTime.Now;
}

7.2 生成代码

  • 原属性Now是静态的,生成的LazyTime也是静态的
csharp 复制代码
// <auto-generated/>
namespace GenerateCachedPropertyTests;
partial class PropertyTests
{
    private static System.DateTime _valueLazyTime = default;
    private static bool _stateLazyTime = false;
    private static object _lockLazyTime = new();
    public static System.DateTime LazyTime
    {
        get
        {
            if (_stateLazyTime)
                return _valueLazyTime;
            lock (_lockLazyTime)
            {
                if (_stateLazyTime)
                    return _valueLazyTime;
                _valueLazyTime = Now;
                _stateLazyTime = true;
            }

            return _valueLazyTime;
        }
    }
}

三、生成器nuget打包技巧

1. 开发容易打包难

  • 特别是包含依赖项的生成器打包更难
  • 首先分享一篇博客园扑克子博主的经验
  • 非常感谢这个博主,再此基础上笔者摸索出更好的方法
  • partial范式依赖EasySyntax和GenerateCore还有Microsoft.CodeAnalysis.CSharp的5.0版本
  • 如果不打包这些依赖会导致生成器无法正常工作
  • 打包方式不对又会导致生成器及依赖项目的dll会出现被调用项目的生成目录
  • 正常情况生成器用于编译时代码生成,自己本身不输出到生成目录

2. 还是自动生成属性项目为例

  • TargetFramework最好设置为netstandard2.0
  • EnforceExtendedAnalyzerRules最好设置为true
  • IncludeBuildOutput设置为fase,这是避免生成器本身输出到生成目录
  • 引用的IncludeAssets设置为compile和analyzers,compile是为了生成器本身编译,analyzers是为了能用于执行生成器时调用
  • PrivateAssets为compile是为了排除生成器的依赖项目参与调用生成器项目的编译,因为它是本项目私有不继续传递,会被排除
  • 最后None配置到analyzers/dotnet/cs就是生成器目录专用
  • 另外NoWarn配置为NU5128,是避免排除警告信息,由于生成器值需要analyzers文件夹,没有lib文件夹导致警告是误报
xml 复制代码
<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
		<IncludeBuildOutput>false</IncludeBuildOutput>
		<NoWarn>$(NoWarn);NU5128</NoWarn>
	</PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
			<IncludeAssets>compile;analyzers</IncludeAssets>
			<PrivateAssets>compile</PrivateAssets>
		</PackageReference>
		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0">
			<IncludeAssets>compile;analyzers</IncludeAssets>
			<PrivateAssets>compile</PrivateAssets>
		</PackageReference>
	</ItemGroup>
	<ItemGroup>
		<ProjectReference Include="..\Hand.GenerateCore\Hand.GenerateCore.csproj">
			<IncludeAssets>compile;analyzers</IncludeAssets>
			<PrivateAssets>compile</PrivateAssets>
		</ProjectReference>
		<ProjectReference Include="..\Hand.Generators.EasySyntax\Hand.Generators.EasySyntax.csproj">
			<IncludeAssets>compile;analyzers</IncludeAssets>
			<PrivateAssets>compile</PrivateAssets>
		</ProjectReference>
	</ItemGroup>
	<ItemGroup>
		<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
	</ItemGroup>
</Project>

3. 生成器依赖项目需要特殊配置

  • EasySyntax和GenerateCore就是生成器依赖项目
  • TargetFramework最好设置为netstandard2.0
  • EnforceExtendedAnalyzerRules最好设置为true
  • None也配置到analyzers/dotnet/cs是为了生成器调用
  • 这样nuget里面有两份dll,lib的可以直接引用,analyzers里面的生成器
xml 复制代码
<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
		<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
	</PropertyGroup>
	<ItemGroup>
		<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
	</ItemGroup>
	<ItemGroup>
		<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
	</ItemGroup>
</Project>

4. 脚本的方法

  • 安装时执行install.ps1
  • 卸载时执行uninstall.ps1
  • 参考开源项目OneOf.SourceGenerator
shell 复制代码
param($installPath, $toolsPath, $package, $project)

$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve

foreach($analyzersPath in $analyzersPaths)
{
    # Install the language agnostic analyzers.
    if (Test-Path $analyzersPath)
    {
        foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll)
        {
            if($project.Object.AnalyzerReferences)
            {
                $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
            }
        }
    }
}

# $project.Type gives the language name like (C# or VB.NET)
$languageFolder = ""
if($project.Type -eq "C#")
{
    $languageFolder = "cs"
}
if($project.Type -eq "VB.NET")
{
    $languageFolder = "vb"
}
if($languageFolder -eq "")
{
    return
}

foreach($analyzersPath in $analyzersPaths)
{
    # Install language specific analyzers.
    $languageAnalyzersPath = join-path $analyzersPath $languageFolder
    if (Test-Path $languageAnalyzersPath)
    {
        foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll)
        {
            if($project.Object.AnalyzerReferences)
            {
                $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
            }
        }
    }
}

四、总结

1. 打包方法1

  • 需要的文件文件都通过PackagePath输出到analyzers
  • 问题是nuget包会增大,丢失了项目的依赖关系

2. 打包方法2

  • 通过IncludeAssets和PrivateAssets准确配置包作用域
  • PackagePath只打包当前文件
  • 问题是依赖自己不能控制的包(没有analyzers文件夹)不好处理

3. 打包方法3

  • 通过脚本处理nuget安装和卸载,无需analyzers输出
  • 缺点是要写额外脚本

4.笔者推荐方法2

  • 大家喜欢哪种方法呢
  • 有的时候可能需要不同方法配置使用

源码托管地址: https://github.com/donetsoftwork/Hand.Generators ,欢迎大家直接查看源码。

gitee同步更新:https://gitee.com/donetsoftwork/hand.-generators

如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

相关推荐
xiangji8 天前
SourceGenerator之partial范式及测试
sourcegenerator·easysyntax·generatecore
xiangji17 天前
.NET源码生成器之SyntaxTree踩坑
sourcegenerator·syntaxtree·easysyntax
xiangji18 天前
.net源码生成器使用SyntaxTree生成代码及简化语法
sourcegenerator·syntaxtree·easysyntax
mysolisoft5 个月前
Avalonia+ReactiveUI+Sourcegenerators实现异步命令
avalonia·reactiveui·sourcegenerator
mysolisoft6 个月前
Avalonia+ReactiveUI实现记录自动更新
c#·avalonia·reactiveui·sourcegenerator