使用 Source Generator 代替 T4 动态生成代码

使用 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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值