一、partial范式深度探讨
- 前文介绍了partial范式简化SourceGenerator开发和测试
- 查阅SourceGenerator之partial范式及测试
- 本文讲partial范式开发和nuget打包,与前文有部分重叠,侧重点不同
二、本文以自动生成属性为例
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 生成代码
- //
- 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也是静态的
- //
- 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文件夹导致警告是误报
- <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,谢谢!!!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |