一、partial范式深度探讨
- 前文介绍了partial范式简化SourceGenerator开发和测试
- 查阅SourceGenerator之partial范式及测试
- 本文讲partial范式开发和nuget打包,与前文有部分重叠,侧重点不同
二、本文以自动生成属性为例
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,谢谢!!!