惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
C
Check Point Blog
博客园_首页
B
Blog
D
Docker
U
Unit 42
量子位
I
InfoQ
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
GbyAI
GbyAI
L
LangChain Blog
云风的 BLOG
云风的 BLOG
博客园 - Franky
美团技术团队
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
月光博客
月光博客
Vercel News
Vercel News
Recent Announcements
Recent Announcements
雷峰网
雷峰网
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Google DeepMind News
Google DeepMind News

博客园 - xiangji

Roslyn生成DTO及影子编程的尝试 C#.NET源生成器如何处理XML注释文档 C# .NET源生成器如何处理Attribute .net源生成器必须知道的4套类型 开源完美模块组件化可扩展的Xml解析器Hand.ParseXml Roslyn语法的模式匹配之EasySyntax增加模式匹配支持 SourceGenerator之扑风捉影 开源项目PocoEmit.Mapper重构之扑风捉影 SourceGenerator之partial范式及测试 .NET源码生成器之SyntaxTree踩坑 .NET源码生成器使用SyntaxTree生成代码及简化语法 DBShadow.net之依赖注入 DBShadow.net之化繁为简 DBShadow.net之性能优化的坎坷路 DBShadow横空出世,Dapper.net的天花板盖不住了 Aspire+.NET10+手搓线程池打造抓不死的云应用 鸡肋的TaskFactory是时候抛弃了 重构《手搓》TaskFactory带你更安全的起飞 异步"伪线程"重构《手搓》线程池,支持任务清退 《手搓》线程池优化的追求 《手搓》TaskFactory带你安全的起飞 《手搓》线程池 致敬1024,《手搓》轻量级EventBus PocoEmit遥遥领先于AutoMapper之打通充血模型的任督二脉 PocoEmit遥遥领先于AutoMapper之循环引用 微软.net表达式编译居然有bug? 如何使用PocoEmit.Mapper替代AutoMapper
.NET源码生成器基于partial范式开发和nuget打包
xiangji · 2026-03-27 · via 博客园 - xiangji

一、partial范式深度探讨

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

1. 功能简介

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

2. 生成器代码

  • 直接套用ValuesGenerator基类
  • 查找标记了GenerateLazy的属性和方法
  • 预处理为GenerateLazySource对象
  • 执行GenerateLazySource
  • 问题是查找属性和方法不能使用官方的SyntaxValueProvider.ForAttributeWithMetadataName
  • ForAttributeWithMetadataName只能用来找partial的类
  • 这个场景类是partial但需要从方法(或属性)入手
  • 一个类可以有多个方法(或属性)被标记,同个方法(或属性)也可以标记生成多个属性
  • 为此笔者重写了这部分代替ForAttributeWithMetadataName
[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的场景并扩展支持更多的需求
  • 限于篇幅不展开所有代码,大家可以到源码库查看
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判断是否有重名对象,如果有就不生成(强行会生成编译不过的文件)
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实现稍有不同
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 原始代码

using Hand;
using Hand.Cache;
namespace GenerateCachedPropertyTests;

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

6.2 生成代码

// <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 原始代码

using Hand;
using Hand.Cache;
namespace GenerateCachedPropertyTests;

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

7.2 生成代码

  • 原属性Now是静态的,生成的LazyTime也是静态的
// <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,这是避免生成器本身输出到lib目录
  • 引用的IncludeAssets设置为compile和analyzers,compile是为了生成器本身编译,analyzers是为了能用于执行生成器时调用
  • PrivateAssets为compile是为了排除生成器的依赖项目参与调用生成器项目的编译,因为它是本项目私有不继续传递,会被排除
  • 最后None配置到analyzers/dotnet/cs就是生成器目录专用
  • 另外NoWarn配置为NU5128,是排除警告信息,由于生成器只需要analyzers文件夹,没有lib文件夹导致警告是误报
<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里面的生成器专用
<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
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,谢谢!!!