使用 Source Generator 代替 T4 动态生成代码
使用 Source Generator 代替 T4 動態(tài)生成代碼
Intro
在 Source Generator 出現(xiàn)之前有一些重復性的代碼,我會使用 T4 去生成,這樣就可以一定程度上避免復制粘貼和可維護性也會更好一些。
在了解了一些 Source Generator 之后,就想嘗試把現(xiàn)在項目里的一些 T4 換成 Source Generator 來實現(xiàn),大部分場景應該都是沒有問題的,可以直接用 Source Generator 替換,而且 Source Generator 可以根據(jù)編譯信息動態(tài)的去生成,更加的智能和自動化。
接著來看一下我是如何使用 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){}} <#???}? #> }模板比較簡單,動態(tài)生成的代碼如下:
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){}} }我是在開發(fā)時動態(tài)生成的,聽大師說也可以改成在編譯的時候進行生成,不過我沒去嘗試過,有興趣的可以了解一下 https://docs.microsoft.com/en-us/visualstudio/modeling/run-time-text-generation-with-t4-text-templates
After
使用 Source Generator 分成了兩步,第一步還是比較手動的,保留了上面的 types 數(shù)組,第二步則是自動的根據(jù)編譯的信息動態(tài)的獲取 types 數(shù)組
首先我們要確定哪個項目是要動態(tài)生成代碼的項目,哪個項目是要寫 Source Generator 的項目
原來我們用 T4 生成代碼的項目(Business)就是我們要動態(tài)生成代碼的項目,也就是這個項目應該是引用 Source Generator 的項目,
那我們 Source Generator 應該要放在哪個項目里呢,理論上來說要生成代碼的項目哪一個都是可以的,新建一個項目也是可以的,Business 直接依賴于 Database 項目,所以我選擇了 Database 項目來實現(xiàn) Source Generator
Update1
首先我們需要配置 Source Generator 環(huán)境,首先為我們要寫 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>然后要動態(tài)生成代碼的項目也需要配置一下,只需要修改項目文件,原來的 T4 模板可以刪掉了,可以參考下面的配置
<PropertyGroup><EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> </PropertyGroup> <ItemGroup><ProjectReference?Include="..\OpenReservation.Database\OpenReservation.Database.csproj"OutputItemType="Analyzer"?/> </ItemGroup>在 ProjectReference 中聲明 OutputItemType="Analyzer" 以使用 Generator 的功能,通過配置 EmitCompilerGeneratedFiles 以生成動態(tài)代碼幫助我們調(diào)試
之后就開始寫我們的 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 已經(jīng)可以工作了,生成的代碼和上面的完全一樣,而且生成的代碼可以不需要保存在代碼庫里了,編譯的時候會動態(tài)生成,已經(jīng)完全可以取代 T4 了
詳細修改可以參考這個 Commit:https://github.com/OpenReservation/ReservationServer/commit/8a723ba652a10fb393e90bf70923631f58294da8
Update2
接著上面的修改,雖然已經(jīng)代替了 T4,但是似乎并不能夠體現(xiàn)出 Source Generator 的優(yōu)勢啊,于是就想再改一版,利用編譯信息自動的獲取上面的 types 數(shù)組,因為 types 不是隨便寫的是 model 的名字,所以從編譯信息中獲取理論上來說是可以做到的,于是有了第二版的實現(xiàn),實現(xiàn)代碼如下:
[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();//?獲取屬性泛型類型參數(shù),并獲取泛型類型參數(shù)的名稱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();//?添加要動態(tài)生成的代碼context.AddSource(nameof(ServiceGenerator),?codeText);} }除了上面 Generator 的修改之外,還需要增加 EFCore 依賴項,這也是目前使用 SourceGenerator 的一個痛點,我的 EF 擴展 WeihanLi.EntityFramework 已經(jīng)依賴了 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 動態(tài)的自動生成 service 代碼了,以后新加表只需要在 ReservationDbContext 中加入新的表就可以了,編譯器也會自動生成新加表的服務類,不需要再手動配置 types 數(shù)組了,舒服~~
More
通過上面的示例,再次戳到了痛點,希望后面的版本更新中能夠有所優(yōu)化,也希望 VS 能夠提供更有力的支持。
以上就是所有內(nèi)容了,希望能夠?qū)δ阌兴鶐椭?#xff0c;上面的示例代碼可以從 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
總結
以上是生活随笔為你收集整理的使用 Source Generator 代替 T4 动态生成代码的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .Net之多语言配置
- 下一篇: 我的注释那去了?