弘十一法师 發表於 2026-3-27 11:26:00

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

<h2 id="一partial范式深度探讨">一、partial范式深度探讨</h2>
<blockquote>
<ul>
<li>前文介绍了partial范式简化SourceGenerator开发和测试</li>
<li>查阅SourceGenerator之partial范式及测试</li>
<li>本文讲partial范式开发和nuget打包,与前文有部分重叠,侧重点不同</li>
</ul>
</blockquote>
<h2 id="二本文以自动生成属性为例">二、本文以自动生成属性为例</h2>
<h3 id="1-功能简介">1. 功能简介</h3>
<blockquote>
<ul>
<li>场景是通过一个属性获取对象,但不需要这个对象重复创建</li>
<li>单例模式就是其中的场景之一</li>
<li>这样的代码是千篇一律的,非常适合自动生成代码</li>
</ul>
</blockquote>
<h3 id="2-生成器代码">2. 生成器代码</h3>
<blockquote>
<ul>
<li>直接套用ValuesGenerator基类</li>
<li>查找标记了GenerateLazy的属性和方法</li>
<li>预处理为GenerateLazySource对象</li>
<li>执行GenerateLazySource</li>
<li>问题是查找属性和方法不能使用官方的SyntaxValueProvider.ForAttributeWithMetadataName</li>
<li>ForAttributeWithMetadataName只能用来找partial的类</li>
<li>这个场景类是partial但需要从方法(或属性)入手</li>
<li>一个类可以有多个方法(或属性)被标记,同个方法(或属性)也可以标记生成多个属性</li>
<li>为此笔者重写了这部分代替ForAttributeWithMetadataName</li>
</ul>
</blockquote>
<pre><code class="language-csharp">
public class GenerateLazyGenerator()
    : ValuesGenerator&lt;GenerateLazySource&gt;(
      Attribute,
      new SyntaxFilter(false, SyntaxKind.PropertyDeclaration, SyntaxKind.MethodDeclaration),
      GenerateLazyTransform.Instance,
      new GeneratorExecutor&lt;GenerateLazySource&gt;())
{
    /// &lt;summary&gt;
    /// Attribute标记
    /// &lt;/summary&gt;
    public const string Attribute = "Hand.Cache.GenerateLazyAttribute";
}
</code></pre>
<h3 id="3-generateprovidercreatebyattribute">3. GenerateProvider.CreateByAttribute</h3>
<blockquote>
<ul>
<li>CreateByAttribute用于代替ForAttributeWithMetadataName</li>
<li>先遍历SyntaxTree</li>
<li>再查找节点并处理为需要的对象</li>
<li>这样可以覆盖ForAttributeWithMetadataName的场景并扩展支持更多的需求</li>
<li>限于篇幅不展开所有代码,大家可以到源码库查看</li>
</ul>
</blockquote>
<pre><code class="language-csharp">public static IncrementalValuesProvider&lt;TSource&gt; CreateByAttribute&lt;TSource&gt;(IncrementalGeneratorInitializationContext context, string attributeName, ISyntaxFilter filter, IGeneratorTransform&lt;TSource&gt; transform)
{
    return context.CompilationProvider
      .SelectMany(GetSyntaxTree)
      .SelectMany((syntaxTree, cancellationToken) =&gt; GetAttribute(syntaxTree, attributeName, filter, transform, cancellationToken))
      .WithTrackingName("Provider_ByAttribute");
}
</code></pre>
<h3 id="4-generatelazytransform处理">4. GenerateLazyTransform处理</h3>
<blockquote>
<ul>
<li>由于我们定位的是方法或者属性,类型(TypeDeclarationSyntax)和类型符号(typeSymbol)需要自行获取</li>
<li>另外对类型进行了校验,必须含partial修饰符</li>
<li>GetPropertyNameByAttribute获取Attribute配置</li>
<li>如果当前是属性就处理为LazyPropertySource</li>
<li>如果当前是方法就处理为LazyMethodSource</li>
<li>GenerateLazySource是抽象类,LazyPropertySource和LazyMethodSource是GenerateLazySource的子类</li>
<li>CheckSource判断是否有重名对象,如果有就不生成(强行会生成编译不过的文件)</li>
</ul>
</blockquote>
<pre><code class="language-csharp">public class GenerateLazyTransform : IGeneratorTransform&lt;GenerateLazySource&gt;
{
    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 &amp;&amp; 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 &amp;&amp; methodSymbol.ReturnType is INamedTypeSymbol symbol)
                source = new LazyMethodSource(method, type, typeSymbol, propertyName, symbol, method.Modifiers.IsStatic());
      }
      // 判断是否已经存在同名属性
      // 不存在才返回
      if (source is not null &amp;&amp; CheckSource(source, compilation))
            return source;
      return null;
    }
    /// &lt;summary&gt;
    /// 判断延迟缓存源对象是否合法
    /// &lt;/summary&gt;
    /// &lt;param name="source"&gt;&lt;/param&gt;
    /// &lt;param name="compilation"&gt;&lt;/param&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    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;
    }
    // ...
}
</code></pre>
<h3 id="5-generatelazysource">5. GenerateLazySource</h3>
<blockquote>
<ul>
<li>首先复制原类型,不用管是类,是结构体还是record,是否有命名空间,这些与原类型保持一致即可</li>
<li>Clone会清理一些成员(方法、字段、属性等),避免编译出错</li>
<li>定义了3个字段1个属性</li>
<li>属性的get处理器是用开源项目EasySyntax定义的,非常简洁</li>
<li>使用锁和双重判断实现的线程安全懒汉单例模式</li>
<li>如果原方法(或属性)是静态的,生成对象也增加静态修饰符</li>
<li>GetValueExpression是从原代码中提取代码,这在LazyPropertySource和LazyMethodSources实现稍有不同</li>
</ul>
</blockquote>
<pre><code class="language-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;
    }
    /// &lt;summary&gt;
    /// 构造属性处理器
    /// &lt;/summary&gt;
    /// &lt;returns&gt;&lt;/returns&gt;
    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();
    // ...
}
</code></pre>
<h3 id="6-按方法生成属性的case">6. 按方法生成属性的Case</h3>
<h4 id="61-原始代码">6.1 原始代码</h4>
<pre><code class="language-csharp">using Hand;
using Hand.Cache;
namespace GenerateCachedPropertyTests;

public partial class MethodTests
{
   
    public DateTime CreateTime()
    {
      return DateTime.Now;
    }
}
</code></pre>
<h4 id="62-生成代码">6.2 生成代码</h4>
<pre><code class="language-csharp">// &lt;auto-generated/&gt;
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;
      }
    }
}
</code></pre>
<h3 id="7-按属性生成属性的case">7. 按属性生成属性的Case</h3>
<h4 id="71-原始代码">7.1 原始代码</h4>
<pre><code class="language-csharp">using Hand;
using Hand.Cache;
namespace GenerateCachedPropertyTests;

public partial class PropertyTests
{
   
    public static DateTime Now { get; } = DateTime.Now;
}
</code></pre>
<h4 id="72-生成代码">7.2 生成代码</h4>
<blockquote>
<ul>
<li>原属性Now是静态的,生成的LazyTime也是静态的</li>
</ul>
</blockquote>
<pre><code class="language-csharp">// &lt;auto-generated/&gt;
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;
      }
    }
}
</code></pre>
<h2 id="三生成器nuget打包技巧">三、生成器nuget打包技巧</h2>
<h3 id="1-开发容易打包难">1. 开发容易打包难</h3>
<blockquote>
<ul>
<li>特别是包含依赖项的生成器打包更难</li>
<li>首先分享一篇博客园扑克子博主的经验</li>
<li>非常感谢这个博主,在此基础上笔者摸索出更好的方法</li>
<li>partial范式依赖EasySyntax和GenerateCore还有Microsoft.CodeAnalysis.CSharp的5.0版本</li>
<li>如果不打包这些依赖会导致生成器无法正常工作</li>
<li>打包方式不对又会导致生成器及依赖项目的dll会出现被调用项目的生成目录</li>
<li>正常情况生成器用于编译时代码生成,自己本身不输出到生成目录</li>
</ul>
</blockquote>
<h3 id="2-还是自动生成属性项目为例">2. 还是自动生成属性项目为例</h3>
<blockquote>
<ul>
<li>TargetFramework最好设置为netstandard2.0</li>
<li>EnforceExtendedAnalyzerRules最好设置为true</li>
<li>IncludeBuildOutput设置为fase,这是避免生成器本身输出到lib目录</li>
<li>引用的IncludeAssets设置为compile和analyzers,compile是为了生成器本身编译,analyzers是为了能用于执行生成器时调用</li>
<li>PrivateAssets为compile是为了排除生成器的依赖项目参与调用生成器项目的编译,因为它是本项目私有不继续传递,会被排除</li>
<li>最后None配置到analyzers/dotnet/cs就是生成器目录专用</li>
<li>另外NoWarn配置为NU5128,是排除警告信息,由于生成器只需要analyzers文件夹,没有lib文件夹导致警告是误报</li>
</ul>
</blockquote>
<pre><code class="language-xml">&lt;Project Sdk="Microsoft.NET.Sdk"&gt;
        &lt;PropertyGroup&gt;
                &lt;TargetFramework&gt;netstandard2.0&lt;/TargetFramework&gt;
                &lt;EnforceExtendedAnalyzerRules&gt;true&lt;/EnforceExtendedAnalyzerRules&gt;
                &lt;IncludeBuildOutput&gt;false&lt;/IncludeBuildOutput&gt;
                &lt;NoWarn&gt;$(NoWarn);NU5128&lt;/NoWarn&gt;
        &lt;/PropertyGroup&gt;
        &lt;ItemGroup&gt;
                &lt;PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0"&gt;
                        &lt;IncludeAssets&gt;compile;analyzers&lt;/IncludeAssets&gt;
                        &lt;PrivateAssets&gt;compile&lt;/PrivateAssets&gt;
                &lt;/PackageReference&gt;
                &lt;PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0"&gt;
                        &lt;IncludeAssets&gt;compile;analyzers&lt;/IncludeAssets&gt;
                        &lt;PrivateAssets&gt;compile&lt;/PrivateAssets&gt;
                &lt;/PackageReference&gt;
        &lt;/ItemGroup&gt;
        &lt;ItemGroup&gt;
                &lt;ProjectReference Include="..\Hand.GenerateCore\Hand.GenerateCore.csproj"&gt;
                        &lt;IncludeAssets&gt;compile;analyzers&lt;/IncludeAssets&gt;
                        &lt;PrivateAssets&gt;compile&lt;/PrivateAssets&gt;
                &lt;/ProjectReference&gt;
                &lt;ProjectReference Include="..\Hand.Generators.EasySyntax\Hand.Generators.EasySyntax.csproj"&gt;
                        &lt;IncludeAssets&gt;compile;analyzers&lt;/IncludeAssets&gt;
                        &lt;PrivateAssets&gt;compile&lt;/PrivateAssets&gt;
                &lt;/ProjectReference&gt;
        &lt;/ItemGroup&gt;
        &lt;ItemGroup&gt;
                &lt;None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /&gt;
        &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<h3 id="3-生成器依赖项目需要特殊配置">3. 生成器依赖项目需要特殊配置</h3>
<blockquote>
<ul>
<li>EasySyntax和GenerateCore就是生成器依赖项目</li>
<li>TargetFramework最好设置为netstandard2.0</li>
<li>EnforceExtendedAnalyzerRules最好设置为true</li>
<li>None也配置到analyzers/dotnet/cs是为了生成器调用</li>
<li>这样nuget里面有两份dll,lib的可以直接引用,analyzers里面的生成器专用</li>
</ul>
</blockquote>
<pre><code class="language-xml">&lt;Project Sdk="Microsoft.NET.Sdk"&gt;
        &lt;PropertyGroup&gt;
                &lt;TargetFramework&gt;netstandard2.0&lt;/TargetFramework&gt;
                &lt;EnforceExtendedAnalyzerRules&gt;true&lt;/EnforceExtendedAnalyzerRules&gt;
        &lt;/PropertyGroup&gt;
        &lt;ItemGroup&gt;
                &lt;PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" /&gt;
                &lt;PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" /&gt;
        &lt;/ItemGroup&gt;
        &lt;ItemGroup&gt;
                &lt;None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" /&gt;
        &lt;/ItemGroup&gt;
&lt;/Project&gt;
</code></pre>
<h3 id="4-脚本的方法">4. 脚本的方法</h3>
<blockquote>
<ul>
<li>安装时执行install.ps1</li>
<li>卸载时执行uninstall.ps1</li>
<li>参考开源项目OneOf.SourceGenerator</li>
</ul>
</blockquote>
<pre><code class="language-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)
            }
      }
    }
}
</code></pre>
<h3 id="四总结">四、总结</h3>
<h4 id="1-打包方法1">1. 打包方法1</h4>
<blockquote>
<ul>
<li>需要的文件都通过PackagePath输出到analyzers</li>
<li>问题是nuget包会增大,丢失了项目的依赖关系</li>
</ul>
</blockquote>
<h4 id="2-打包方法2">2. 打包方法2</h4>
<blockquote>
<ul>
<li>通过IncludeAssets和PrivateAssets准确配置包作用域</li>
<li>PackagePath只打包当前文件</li>
<li>问题是依赖自己不能控制的包(没有analyzers文件夹)不好处理</li>
</ul>
</blockquote>
<h4 id="3-打包方法3">3. 打包方法3</h4>
<blockquote>
<ul>
<li>通过脚本处理nuget安装和卸载,无需analyzers输出</li>
<li>缺点是要写额外脚本</li>
</ul>
</blockquote>
<h4 id="4笔者推荐方法2">4.笔者推荐方法2</h4>
<blockquote>
<ul>
<li>大家喜欢哪种方法呢</li>
<li>有的时候可能需要不同方法配合使用</li>
</ul>
</blockquote>
<p>源码托管地址: https://github.com/donetsoftwork/Hand.Generators ,欢迎大家直接查看源码。<br>
gitee同步更新:https://gitee.com/donetsoftwork/hand.-generators</p>
<p>如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!</p><br><br>
来源:https://www.cnblogs.com/xiangji/p/19781120
頁: [1]
查看完整版本: .NET源码生成器基于partial范式开发和nuget打包