Roslyn 技术解析:如何利用它做代码规范检查与运行时代码生成?

1.什么是 Roslyn

聊起 Roslyn 可能对于有部分小伙伴有些陌生,有些小伙听过但是没接触过,有些小伙伴可能比较擅长,其实在这之前我也是个懵的,听过但是没深入了解,因为我不知道并不影响我做一些增删改查,但是如果你要深入,或者写一些框架底层或者提升效率的工具以及扩展,那这个是必须掌握的技术。

年初时,我在与技术大牛 痴者工良 交流的过程中,算是正式接触到 Roslyn,瞬间被它的强大能力所吸引。他深入浅出的讲解让我意识到,这不仅是编译器黑科技,更是提升代码质量与开发效率的利器。受他启发,我开始系统学习,虽断断续续折腾了一阵,但一直未做总结。最近终于得空,便将所学梳理成文,分享出来,既是记录,也是致敬好朋友严架的帮助。

在正式认识 Roslyn 之前,我们必须先对咱们 C# .NET 的编译流程有个大概了解,当然 VB.NET 也适用,但是接受不来他的语法,有些小伙伴可能知道或者了解,简单的给个图感受一下。

1. C# / .NET 编译流程简述

  1. 源代码阶段:我们手动写出 C# 或者 VB.NET 代码
  2. 编译器阶段:Roslyn 编译器将源代码转换为 IL(Intermediate Language)中间代码
  3. IL 生成:生成 .dll 或 .exe 文件包含 IL 代码和元数据
  4. 运行时编译:CLR 通过 JIT 将 IL 编译为本地机器码执行

这里我们只需要了解大概流程就好了,至于里面是否有再细节一点的流程,甚至 AOT/JIT,就不去深究,后面有机会再分享,属于另外一范畴,可以看到这里就出现了 Roslyn,他的作用就是用于编译原生的 C# 代码为 IL,你可以把他理解为是一个开源编译器平台,而且他本身还是用 C# 写的,相信自己的直觉,没错,用 C# 写的代码编译 C# ,俗称自举,约等于(鸡生蛋、蛋生鸡),形成这种局面开始是在微软诞生了 Roslyn 之后,早期的编译器还是用 C++ 的。

2. 常见问题

Q1:Roslyn 可以编译其他代码吗?如果能编译我自己可以设计一个语言,来用 Roslyn 来编译吗?还是只能编译 C# 和 VB.NET 吗?

  • 其实 Roslyn 只能编译 C# 和 VB.NET,如果咱们使用定义一个 X 语言,也不能用 Roslyn 来编译,除非以 Roslyn 作为参考,自己写解析器。

Q2:他是怎么编译的竟然可以把 C# 代码编译为 IL 代码,Roslyn 编译流程?

  1. 语法分析(Parsing) → 生成 Syntax Tree(语法树)
  2. 语义分析(Semantic Analysis) → 生成 Symbols 和 Bindings
  3. IL 生成(Code Generation) → Emit IL

2. Roslyn 有哪些应用

上面解释了他可以作为编译器来编译 C# 代码,当然他作为一个开源平台 他的作用远不止这些,不过在这里只做一些简单的介绍和示例,后续会单独发布文章做一些分享,下面介绍一下:

功能

  • 语法树(Syntax Tree):解析源代码为一个结构化的表示形式。
  • 语义模型(Semantic Model):提供对代码中符号及其含义的理解。
  • 诊断(Diagnostics):允许开发创建自定义的编译时检查规则。
  • 重构工具:支持开发代码重构工具,如自动修复、代码清理等。
  • 代码生成:可以用来生成新的代码文件或修改现有的代码。

应用场景

  • 开发 Visual Studio 扩展插件。
  • 创建静态分析工具,例如流行的 SonarLint、ReSharper、GitHub Code Scanning。
  • 实现代码质量检查工具,例如检测代码中是否有一些开发团队不允许的代码,循环调用数据库等。
  • 构建代码生成工具,使用源生成器在编译阶段编译通用代码。
  • 动态编译执行代码,在程序运行时,让用户输入一段 C# 代码字符串,然后立即编译并执行。当然有大佬封装了一个库,natasha

是不是看了之后很惊讶,甚至有可能之前觉得不可能,甚至不知道怎么实现的技术,似乎找到了一些眉目,其实他的强大在于他能拿到你源代码的语法树,进行语法分析,语义分析,如果您搞不清语法和语义分析是什么意思,看下面的例子,我尽可能的讲清楚。


3. 语法分析

下面定义一个 C# 代码,其实在编译时它们是被读取为字符串的,因为编译时 Roslyn 肯定是将代码都是作为文件然后读取字符串的,不然怎么解析呢?

字符串中包含 5 个 using 引用,一个类型声明,2 个方法,1 个带参数,空返回值,一个不带参数,空返回值。

复制代码
using System;
using System.Collections;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }

        static void Main1()
        {
            Console.WriteLine("HelloMain1!");
        }
    }
}

然后我们使用 VS 打开这个代码,用可视化语法树工具查看,你就能理解为什么叫语法树分析,左边是源代码,右边就是工具分析出的这个源代码的语法树结构,第一层根节点叫【CompilationUnit】又叫编译单元,相当于一个文件就是一个单独的编译单元,而且呈现树形生长,你把鼠标移到对应的源代码元素上,都会在右侧可视化工具中找到对应的树节点。

这里面有不同的颜色标记的都叫做一个语法:

  1. 语法节点(SyntaxNode) 被标记为蓝色,例如方法、类、表达式等,
  2. 语法标记(SyntaxToken) 被标记为绿色,例如关键字 static、void ,VoidKeyword 就代表空返回值。
  3. 语法杂项(SyntaxTrivia) 被标记为红色,例如一些空格注释

Syntax 语法 API

Syntax类型用于表示源代码的语法结构,是构建和操作 C# 代码抽象语法树(AST)的基础。

一般语法树从大到小:

  1. using 指令 - UsingDirectiveSyntax
  2. 成员定义的语法 - MemberDeclarationSyntax每一个 node 都包含有 MemberDeclarationSyntax
  3. 命名空间语法 - NamespaceDeclarationSyntax
  4. 类定义语法 - ClassDeclarationSyntax
  5. 方法定义语法 - MethodDeclarationSyntax
  6. 参数定义语法 - ParameterSyntax

可以访问这个网站:https://roslynquoter.azurewebsites.net/

然后把代码粘贴进去点击生成就会出现以下内容:

我们思考下,我们都拿到了代码的逻辑语法结构,是不是找什么就容易了,因为源代码的每一个字符,每一个代码都对应一个语法标记,现在知道为什么我们使用 VS 开发代码时,有时候没写括号或者少了标点,就会提示错误了吧?其实就是实时在检测您写的代码的语法树,是不是符合规则,如果不符合就产生对应的错误。

看着头疼,如果不能理解可以指出。读不懂没有关系,因为这一步是主要说明什么是语法树和语法结构,可以判断你的结构对不对,那如何判断内容和意义对不对呢?接着往下看!


4. 语义分析

例如我一个方法返回值是 Int ,我返回一个 string

复制代码
static int Main2()
{
    return "1";
}

再分别看可视化的语法树也正常长出来了,在线的分析工具也能分析出来。

但是我们作为开发人员肯定知道,我要 INT 你返回 string,能用才怪呢,逻辑就对不上,在 VS 中飘红是因为他有检测,如果你用记事本写,是不是没啥问题,符合 C# 语法,但它没有足够的信息来标识所引用的内容是什么意思。因为名称可能表示一种类型,方法,局部变量,语义不一样,这个时候就要说另外一个东西了,就是 语义分析,就是我解析生成了语法结构,我还得知道每个节点代表什么意思他的意义是什么。只有知道了语义之后才能真正"活"起来。


5. 利用 Roslyn API 进行语法以及语义分析

先定义代码字符串,因为在编译时 Roslyn 就是将源代码文件作为字符串读取,形成上面描述那样的语法树逻辑结构。

复制代码
public  const string ProgramText =
   @"using System;
       using System.Collections;
       using System.Linq;
       using System.Text;
       using Microsoft.CodeAnalysis;
       namespace HelloWorld
       {
           class Program
           {
               static void Main(string[] args)
               {
                   Console.WriteLine(""Hello, World!"");
               }

               static void Main1()
               {       
                      var list= new List<string>() { ""21""};
                       list.Add(""c"");
                   Console.WriteLine(""Hello, Main1!"");
               }
           }
       }";

1. 语法分析

直接从语法节点获取返回类型 && 使用语法树分析遍历每个节点

复制代码
static void Main(string[] args)
{
    SyntaxTree tree = CSharpSyntaxTree.ParseText(ProgramText);
    CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
    
    WriteLine($"语法树有 {root.Members.Count} 个元素在里面.");
    WriteLine($"这个语法树有 {root.Usings.Count} using 语句,分别是:");
    foreach (UsingDirectiveSyntax element in root.Usings)
        WriteLine($"\t{element.Name}");

    MemberDeclarationSyntax firstMember = root.Members[0];
    WriteLine($"第一个成员是: {firstMember.Kind()}.");
    var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;

    WriteLine($"命名空间{helloWorldDeclaration.Name}下声明了 {helloWorldDeclaration.Members.Count} 个成员.");
    WriteLine($"第一个成员的类型是: {helloWorldDeclaration.Members[0].Kind()}.");
 
    var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
    WriteLine($"有 {programDeclaration.Members.Count} 个成员定义在 {programDeclaration.Identifier} 类中.");

    //直接从语法节点获取返回类型
    for (int i = 0; i < programDeclaration.Members.Count; i++)
    {
        WriteLine($"第{i+1}个成员是一个 {programDeclaration.Members[i].Kind()}类型.");
        var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[i];

        WriteLine($" {mainDeclaration.Identifier} :方法的返回类型是: {mainDeclaration.ReturnType}.");
        WriteLine($"方法有: {mainDeclaration.ParameterList.Parameters.Count} 个参数.");
        foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
            WriteLine($"{item.Identifier} 参数的类型是: {item.Type}.");
        WriteLine($"{mainDeclaration.Identifier} 方法体内容如下:");
        WriteLine(mainDeclaration.Body.ToFullString());

        if (mainDeclaration.ParameterList.Parameters.Any())
        {
            var argsParameter = mainDeclaration.ParameterList.Parameters[0];
            var firstParameters = from methodDeclaration in root.DescendantNodes()
                                                    .OfType<MethodDeclarationSyntax>()
                                  where methodDeclaration.Identifier.ValueText == "Main"
                                  select methodDeclaration.ParameterList.Parameters.First();

            var argsParameter2 = firstParameters.Single();

            WriteLine(argsParameter == argsParameter2);
        }

    }
}

输出

复制代码
语法树有 1 个元素在里面.
这个语法树有 5 using 语句,分别是:
        System
        System.Collections
        System.Linq
        System.Text
        Microsoft.CodeAnalysis
        
第一个成员是: NamespaceDeclaration.
命名空间HelloWorld下声明了 1 个成员.
第一个成员的类型是: ClassDeclaration.

有 2 个成员定义在 Program 类中.
第1个成员是一个 MethodDeclaration类型.
     Main :方法的返回类型是: void.
        方法有: 1 个参数.
            args 参数的类型是: string[].
                Main 方法体内容如下:
                    {
                        Console.WriteLine("Hello, World!");
                    }

第2个成员是一个 MethodDeclaration类型.
     Main1 :方法的返回类型是: void.
        方法有: 0 个参数.
            Main1 方法体内容如下:
                {
                    Console.WriteLine("Hello, Main1!");
                }

是不是感觉理解了一些,接着看语法分析可以拿到你的代码块中你想关注的更多有用的信息。

2. 语义分析

接下来我们开始进行语义分析,说白了就是:在语法结构正确的基础上,搞清楚这段代码到底要干什么。它会顺着语法树,一层层看懂每个部分的真正含义,比如变量是谁、函数怎么用、类型对不对,最后把程序的'真实意图'给挖出来。

复制代码
// 为 programText 常量中的代码文本生成语法树
SyntaxTree tree = CSharpSyntaxTree.ParseText(ProgramText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
 
var compilation = CSharpCompilation.Create("HelloWorld")
         .AddReferences(MetadataReference.CreateFromFile(
             typeof(string).Assembly.Location))
         .AddSyntaxTrees(tree);
       
var methods2 = (from methodDeclaration in root.DescendantNodes()
                                                     .OfType<MethodDeclarationSyntax>()
                select methodDeclaration).ToList();
      
//遍历语法节点 ,找到所有的方法定义
foreach (var item in methods2)
{
    //获取整个语义模型
    var model = compilation.GetSemanticModel(tree);

    //根据当前语法节点,利用语义模型找到当前方法的符号
    var semanticMethod = model.GetDeclaredSymbol(item);
     
    //获取当前方法的返回值
    Console.WriteLine(semanticMethod.ReturnType);
}

其实语义分析的重点就是 compilation.GetSemanticModel(tree),他的作用就是得到一个语义模型,然后通过它可以查询出,当前分析的这段代码的意思,他在这个范围内的名称是什么,他可以访问哪些成员,定义了哪些变量。

如果您还没体会到好处,可能不太深刻,可以按照例子自己试试。


6. 扩展

看了上面的语法和语义分析,您可能还是有点懵,说了一大堆,拿到了有啥用,看了跟没看一样,他能做什么,不过不要紧,在这里我尽可能的让您知道他的好处。

  1. 相信您只要做了开发,一定对在代码中提交事务不会陌生吧,在团队开发中,曾几何时是否有忘记过写 Commit() 然后发现一顿操作无效
  1. 在团队开发中,有些人的代码总是不合要求,让入参小写,非要大写,方法名让大写,他小写,等到一段时间之后,代码看着痛苦不堪,又或者上线后因为异步方法使用 void 来作为方法返回值,产生了莫名其妙的异常,查了半天,还没搞定,最后回滚代码。

但是作为技术 leader 或者高级开发的你精力有限,不可能每个人我都去盯着吧,就这样不知所措....

此时你想要是能在一开始就不让写这样的代码不就好了,就像我在 VS 写的时候直接飘红,那我怎么弄呢,恭喜您已经入门了,这时候就可以自己定义一个代码分析器来检查这些问题,上面已经提到 Roslyn 的一个重要应用场景就是代码分析,Roslyn 的特点和作用,我们在语法和语义分析部分已经大概了解,联想一下,是不是若有所思,思路如下:

  • 通过 roslyn 解析我的代码,然后解析出语法结构和语义模型
  • 根据语法树,我找到所有的方法节点,然后通过语义解析出,找到所有的 Task 方法,将符合并且返回值是 void 直接调出来是不是就可以了

但是也不要被此局限,因为他提供的远不止我简单描述的做这些。


7. 用 Roslyn 打造代码规约和动态编译

1. 用 Roslyn 构建代码规范检查器,禁止 async void 方法

① 创建自定义分析器

我们定义一个类 UserDiagnosticAnalyzer,继承自 DiagnosticAnalyzer,并标注 [DiagnosticAnalyzer(LanguageNames.CSharp)],表明这是一个针对 C# 语言的语法分析器。

复制代码
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class UserDiagnosticAnalyzer : DiagnosticAnalyzer
{
    // ...
}

② 定义诊断规则

通过 DiagnosticDescriptor定义一条诊断规则,当检测到违规代码时,输出一条错误信息,例如"异步方法不能返回 void"。

复制代码
private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(
    id: "Code001",
    title: "示例规则标题",
    messageFormat: "代码规范检查:{0}",
    category: "Usage",
    defaultSeverity: DiagnosticSeverity.Error,
    isEnabledByDefault: true
);

③ 注册分析逻辑

Initialize方法中,我们注册一个语法节点分析动作,监听所有 方法声明(MethodDeclaration) 节点:

复制代码
public override void Initialize(AnalysisContext context)
{
    context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
    context.EnableConcurrentExecution();
    context.RegisterSyntaxNodeAction(AnalyzeSymbolAnalysisContext, SyntaxKind.MethodDeclaration);
}
  • EnableConcurrentExecution()提升分析性能;
  • RegisterSyntaxNodeAction指定当解析到方法声明时,调用 AnalyzeSymbolAnalysisContext进行分析。

④ 实现分析逻辑

在回调方法中,我们判断方法是否同时满足两个条件:

  • 包含 async关键字;

  • 返回类型为 void

    private static void AnalyzeSymbolAnalysisContext(SyntaxNodeAnalysisContext context)
    {
    if (context.Node is MethodDeclarationSyntax method)
    {
    if (method.Modifiers.Any(x => x.IsKind(SyntaxKind.AsyncKeyword))
    && method.ReturnType.ToString() == "void")
    {
    var diagnostic = Diagnostic.Create(
    Rule,
    context.Node.GetLocation(),
    "异步方法不能返回void"
    );
    context.ReportDiagnostic(diagnostic);
    }
    }
    }

然后在业务代码中引用,当你写这种不符合规范的代码,IDE 会立即在代码下方显示红色波浪线,就会提示错误。

2. 使用 Roslyn 来动态编译代码

它的另外一个作用就是 动态编译,Roslyn 不仅仅是一个编译器平台,它还提供了强大的动态编译与代码执行能力,这一特性在构建可扩展的中后台系统时很实用。

举个典型的场景:我们有一个底层通用功能平台(比如审批流程、数据校验、报表生成等),多个业务系统都基于这个平台进行开发。虽然核心逻辑是通用的,但每个业务方可能需要在标准流程中插入自定义逻辑,比如在某个方法执行前后修改数据、记录日志、调用特定服务等。

传统做法是通过接口 + 插件模式或依赖注入来实现扩展,但这要求编译期就确定实现类,不够灵活。而借助 Roslyn 的动态编译能力,我们可以让业务开发人员以脚本形式编写扩展逻辑,在运行时动态编译并执行,真正做到热插拔式的定制。

我们定义一个通用的数据处理流程,在关键节点允许业务方传入一段 C# 脚本:

复制代码
var script = @"parameters.Value += 1;";
var action = RoslynScriptRunner.CreateScript(script);
AA aA1 = new AA();
aA1.Value = 99;
action.Invoke(aA1);
Console.WriteLine(aA1.Value); // 输出 100

在这个例子中:

  • AA是我们约定的数据上下文对象。
  • script是由业务方提供的 C# 表达式脚本,表示对 Value加 1。
  • RoslynScriptRunner是封装了 Roslyn 编译和执行逻辑的工具类。
  • 在运行时,平台动态编译这段脚本,并将业务对象 aA1作为参数传入执行。

这样一来,不同业务系统可以在不修改主流程代码的前提下,灵活注入自己的逻辑,实现真正的运行时扩展。

这种模式特别适用于:

  • 需要频繁变更的业务规则;
  • 多租户系统中的个性化定制;
  • 平台化产品中开放二次开发能力;

通过 Roslyn,我们把"代码"当作"配置"来管理,提升系统的灵活性,后续会专门再开文章分享,关于Roslyn的另外应用场景和进阶使用,包括但不限于如何实现运行时编译执行和源生成器。

相关推荐
IDOlaoluo9 小时前
如何安装 NDP48-x86-x64-AllOS-ENU.exe | .NET Framework 4.8 离线安装教程
.net
追逐时光者10 小时前
使用 Visual Studio 快速创建 NuGet 程序包并发布到 NuGet 官网
后端·.net·visual studio
焚 城14 小时前
.net8.0_webapi 生成二维码
.net
焚 城14 小时前
.NET 程序自动更新的回忆总结
.net
云草桑14 小时前
.net AI MCP 入门 适用于模型上下文协议的 C# SDK 简介(MCP)
ai·c#·.net·mcp
CsharpDev-奶豆哥1 天前
ASP.NET中for和foreach使用指南
windows·microsoft·c#·asp.net·.net
weixin_456904272 天前
C# 串口通信完整教程 (.NET Framework 4.0)
网络·c#·.net
追逐时光者2 天前
C#/.NET/.NET Core技术前沿周刊 | 第 57 期(2025年10.1-10.12)
后端·.net
INCerry2 天前
.NET周刊【9月第4期 2025-09-28】
.net