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