使用 Source Generator 代替 T4 动态生成代码
Intro
在 Source Generator 出现之前有一些重复性的代码,我会使用 T4 去生成,这样就可以一定程度上避免复制粘贴和可维护性也会更好一些。
在了解了一些 Source Generator 之后,就想尝试把现在项目里的一些 T4 换成 Source Generator 来实现,大部分场景应该都是没有问题的,可以直接用 Source Generator 替换,而且 Source Generator 可以根据编译信息动态的去生成,更加的智能和自动化。
接着来看一下我是如何使用 Source Generator 来代替 T4 生成代码的吧
Before
首先来看一下修改之前的项目情况,项目结构是这样的
原来在 Business
项目里有一个 T4 模板,定义如下:
<#@ template debug="false" hostSpecific="true" language="C#" #>
<#@ output extension=".generated.cs" encoding="utf-8" #>
<#@ Assembly Name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Collections" #>
<#string[] types = {"BlockType","BlockEntity","OperationLog","Reservation","ReservationPlace","ReservationPeriod","SystemSettings","Notice","DisabledPeriod"};
#>
using OpenReservation.Database;
using OpenReservation.Models;
using WeihanLi.EntityFramework;namespace OpenReservation.Business
{
<# foreach (var item in types){
#>public partial interface IBLL<#= item #>: IEFRepository<ReservationDbContext, <#= item #>>{}public partial class BLL<#= item #> : EFRepository<ReservationDbContext, <#= item #>>, IBLL<#= item #>{public BLL<#= item #>(ReservationDbContext dbContext) : base(dbContext){}}
<# }
#>
}
模板比较简单,动态生成的代码如下:
using OpenReservation.Database;
using OpenReservation.Models;
using WeihanLi.EntityFramework;namespace OpenReservation.Business
{public partial interface IBLLBlockType: IEFRepository<ReservationDbContext, BlockType>{}public partial class BLLBlockType : EFRepository<ReservationDbContext, BlockType>, IBLLBlockType{public BLLBlockType(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLBlockEntity: IEFRepository<ReservationDbContext, BlockEntity>{}public partial class BLLBlockEntity : EFRepository<ReservationDbContext, BlockEntity>, IBLLBlockEntity{public BLLBlockEntity(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLOperationLog: IEFRepository<ReservationDbContext, OperationLog>{}public partial class BLLOperationLog : EFRepository<ReservationDbContext, OperationLog>, IBLLOperationLog{public BLLOperationLog(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLReservation: IEFRepository<ReservationDbContext, Reservation>{}public partial class BLLReservation : EFRepository<ReservationDbContext, Reservation>, IBLLReservation{public BLLReservation(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLReservationPlace: IEFRepository<ReservationDbContext, ReservationPlace>{}public partial class BLLReservationPlace : EFRepository<ReservationDbContext, ReservationPlace>, IBLLReservationPlace{public BLLReservationPlace(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLReservationPeriod: IEFRepository<ReservationDbContext, ReservationPeriod>{}public partial class BLLReservationPeriod : EFRepository<ReservationDbContext, ReservationPeriod>, IBLLReservationPeriod{public BLLReservationPeriod(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLSystemSettings: IEFRepository<ReservationDbContext, SystemSettings>{}public partial class BLLSystemSettings : EFRepository<ReservationDbContext, SystemSettings>, IBLLSystemSettings{public BLLSystemSettings(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLNotice: IEFRepository<ReservationDbContext, Notice>{}public partial class BLLNotice : EFRepository<ReservationDbContext, Notice>, IBLLNotice{public BLLNotice(ReservationDbContext dbContext) : base(dbContext){}}public partial interface IBLLDisabledPeriod: IEFRepository<ReservationDbContext, DisabledPeriod>{}public partial class BLLDisabledPeriod : EFRepository<ReservationDbContext, DisabledPeriod>, IBLLDisabledPeriod{public BLLDisabledPeriod(ReservationDbContext dbContext) : base(dbContext){}}
}
我是在开发时动态生成的,听大师说也可以改成在编译的时候进行生成,不过我没去尝试过,有兴趣的可以了解一下 https://docs.microsoft.com/en-us/visualstudio/modeling/run-time-text-generation-with-t4-text-templates
After
使用 Source Generator 分成了两步,第一步还是比较手动的,保留了上面的 types
数组,第二步则是自动的根据编译的信息动态的获取 types
数组
首先我们要确定哪个项目是要动态生成代码的项目,哪个项目是要写 Source Generator 的项目
原来我们用 T4 生成代码的项目(Business
)就是我们要动态生成代码的项目,也就是这个项目应该是引用 Source Generator 的项目,
那我们 Source Generator 应该要放在哪个项目里呢,理论上来说要生成代码的项目哪一个都是可以的,新建一个项目也是可以的,Business
直接依赖于 Database
项目,所以我选择了 Database
项目来实现 Source Generator
Update1
首先我们需要配置 Source Generator 环境,首先为我们要写 Generator 的项目增加对 Microsoft.CodeAnalysis.CSharp
的引用
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
因为 Source Generator 有外部依赖,所以需要声明依赖项,和上一篇文章类似,在项目文件中增加下面的配置:
<PropertyGroup><GetTargetPathDependsOn>;GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<ItemGroup><PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
</ItemGroup>
<ItemGroup><PackageReference Include="WeihanLi.EntityFramework" Version="2.0.0-preview-*" GeneratePathProperty="true" />
</ItemGroup>
<Target Name="GetDependencyTargetPaths"><ItemGroup><TargetPathWithTargetPlatformMoniker Include="$(PKGWeihanLi_EntityFramework)\lib\netstandard2.1\WeihanLi.EntityFramework.dll" IncludeRuntimeDependency="false" /></ItemGroup>
</Target>
然后要动态生成代码的项目也需要配置一下,只需要修改项目文件,原来的 T4 模板可以删掉了,可以参考下面的配置
<PropertyGroup><EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup><ProjectReference Include="..\OpenReservation.Database\OpenReservation.Database.csproj"OutputItemType="Analyzer" />
</ItemGroup>
在 ProjectReference
中声明 OutputItemType="Analyzer"
以使用 Generator 的功能,通过配置 EmitCompilerGeneratedFiles
以生成动态代码帮助我们调试
之后就开始写我们的 Generator
了,最终代码如下:
[Generator]
public class ServiceGenerator : ISourceGenerator
{public void Initialize(GeneratorInitializationContext context){}public void Execute(GeneratorExecutionContext context){var types = new[]{"BlockType","BlockEntity","OperationLog","Reservation","ReservationPlace","ReservationPeriod","SystemSettings","Notice","DisabledPeriod"};var codeBuilder = new StringBuilder();codeBuilder.AppendLine(@"
using OpenReservation.Database;
using OpenReservation.Models;
using WeihanLi.EntityFramework;namespace OpenReservation.Business
{");foreach (var item in types){codeBuilder.AppendLine($@"public partial interface IBLL{item}: IEFRepository<ReservationDbContext, {item}>{{}}public partial class BLL{item} : EFRepository<ReservationDbContext, {item}>, IBLL{item}{{public BLL{item}(ReservationDbContext dbContext) : base(dbContext){{}}}}");}codeBuilder.AppendLine("}");var codeText = codeBuilder.ToString();context.AddSource(nameof(ServiceGenerator), codeText);}
}
此时,我们的 Generator 已经可以工作了,生成的代码和上面的完全一样,而且生成的代码可以不需要保存在代码库里了,编译的时候会动态生成,已经完全可以取代 T4 了
详细修改可以参考这个 Commit:https://github.com/OpenReservation/ReservationServer/commit/8a723ba652a10fb393e90bf70923631f58294da8
Update2
接着上面的修改,虽然已经代替了 T4,但是似乎并不能够体现出 Source Generator 的优势啊,于是就想再改一版,利用编译信息自动的获取上面的 types
数组,因为 types
不是随便写的是 model 的名字,所以从编译信息中获取理论上来说是可以做到的,于是有了第二版的实现,实现代码如下:
[Generator]
public class ServiceGenerator : ISourceGenerator
{public void Initialize(GeneratorInitializationContext context){// Debugger.Launch();}public void Execute(GeneratorExecutionContext context){// 从编译信息中获取 DbSet<> 类型var dbContextType = context.Compilation.GetTypeByMetadataName(typeof(DbSet<>).FullName);// 从编译信息中获取 ReservationDbContext 类型var reservationDbContextType = context.Compilation.GetTypeByMetadataName(typeof(ReservationDbContext).FullName);// 获取 ReservationDbContext 中的 DbSet<> 属性var propertySymbols = reservationDbContextType.GetMembers().OfType<IMethodSymbol>().Where(x => x.IsVirtual&& x.MethodKind == MethodKind.PropertyGet&& x.ReturnType is INamedTypeSymbol{IsGenericType: true,IsUnboundGenericType: false,} typeSymbol&& ReferenceEquals(typeSymbol.ConstructedFrom.ContainingAssembly, dbContextType.ContainingAssembly)).ToArray();// 获取属性的返回值var propertyReturnType = propertySymbols.Select(r => ((INamedTypeSymbol)r.ReturnType)).ToArray();// 获取属性泛型类型参数,并获取泛型类型参数的名称var modelTypeNames = propertyReturnType.Select(t => t.TypeArguments).SelectMany(x => x).Select(x => x.Name).ToArray();var codeBuilder = new StringBuilder();codeBuilder.AppendLine(@"
using OpenReservation.Database;
using OpenReservation.Models;
using WeihanLi.EntityFramework;namespace OpenReservation.Business
{");foreach (var item in modelTypeNames){codeBuilder.AppendLine($@"
public partial interface IBLL{item}: IEFRepository<ReservationDbContext, {item}>{{}}public partial class BLL{item} : EFRepository<ReservationDbContext, {item}>, IBLL{item}
{{public BLL{item}(ReservationDbContext dbContext) : base(dbContext){{}}
}}");}codeBuilder.AppendLine("}");var codeText = codeBuilder.ToString();// 添加要动态生成的代码context.AddSource(nameof(ServiceGenerator), codeText);}
}
除了上面 Generator 的修改之外,还需要增加 EFCore 依赖项,这也是目前使用 SourceGenerator 的一个痛点,我的 EF 扩展 WeihanLi.EntityFramework
已经依赖了 EFCore ,但还是需要再声明一下,声明方式和前面类似
<ItemGroup><PackageReference Include="WeihanLi.EntityFramework" Version="2.0.0-preview-*" GeneratePathProperty="true" />
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.5" GeneratePathProperty="true" /></ItemGroup><Target Name="GetDependencyTargetPaths"><ItemGroup><TargetPathWithTargetPlatformMoniker Include="$(PKGWeihanLi_EntityFramework)\lib\netstandard2.1\WeihanLi.EntityFramework.dll" IncludeRuntimeDependency="false" />
+ <TargetPathWithTargetPlatformMoniker Include="$(PKGMicrosoft_EntityFrameworkCore)\lib\netstandard2.1\Microsoft.EntityFrameworkCore.dll" IncludeRuntimeDependency="false" /></ItemGroup></Target>
这样我们就可以通过 Source Generator 动态的自动生成 service 代码了,以后新加表只需要在 ReservationDbContext
中加入新的表就可以了,编译器也会自动生成新加表的服务类,不需要再手动配置 types
数组了,舒服~~
More
通过上面的示例,再次戳到了痛点,希望后面的版本更新中能够有所优化,也希望 VS 能够提供更有力的支持。
以上就是所有内容了,希望能够对你有所帮助,上面的示例代码可以从 https://github.com/OpenReservation/ReservationServer 进行获取
References
C# 强大的新特性 Source Generator
https://docs.microsoft.com/en-us/visualstudio/modeling/design-time-code-generation-by-using-t4-text-templates?view=vs-2019
https://docs.microsoft.com/en-us/visualstudio/modeling/run-time-text-generation-with-t4-text-templates?view=vs-2019
https://github.com/OpenReservation/ReservationServer/tree/9d2e0987d12143d297d4233bc37c06785bfa0cff/OpenReservation.Business
https://github.com/OpenReservation/ReservationServer/commit/8a723ba652a10fb393e90bf70923631f58294da8
https://github.com/OpenReservation/ReservationServer/blob/dev/OpenReservation.Database/ServiceGenerator.cs
https://github.com/OpenReservation/ReservationServer