C# dynamic 是个陷阱:在泄漏扩散之前阻止它们(Dapper 用户必读)

发布: (2026年1月14日 GMT+8 18:56)
5 min read
原文: Dev.to

Source: Dev.to

Cover image for C# dynamic is a trap door: stop the leaks before they spread (Must read for Dapper users)

在 C# 中,dynamic 在边界处非常方便。问题在于它可能悄悄泄漏到你期望使用静态类型的地方,而你的方法签名仍然看起来完全安全。

我在真实项目中遇到了这些泄漏,并编写了一个小型 Roslyn 分析器来提前捕获它们。我是 DynamicLeakAnalyzer(NuGet:DimonSmart.DynamicLeakAnalyzer)的作者。本文阐述了它针对的问题以及如何在边界处修复泄漏。

What dynamic really means

在 C# 中,dynamic 是一种 静态类型,但它 绕过编译时类型检查 对该表达式。成员访问、重载解析、运算符和转换都在运行时绑定。

在运行时,值仍然以 object 的形式流动。编译器会生成运行时绑定(DLR 调用点)并进行缓存,因此在第一次绑定之后,重复调用可以更快。

“动态传染”是如何发生的

两种模式会让泄漏在审查时难以发现:

  • 不可见的运行时工作: 成员访问和转换在运行时发生。
  • 欺骗性的签名: 方法可以返回 int,但内部仍然执行动态转换。
  • var 可能悄然变为 dynamic 当右侧是动态的时,推断出的类型会变为 dynamic

示例

class Program
{
    static int GetX(int i) => i;

    static void Main()
    {
        dynamic prm = 123;

        int a = GetX(prm); // DSM001: 隐式的运行时动态转换
        var b = GetX(prm); // DSM002: 因为调用是动态的,b 变为 dynamic
    }
}

在实际代码中,prm 往往不是局部变量。它可能是传入方法的对象,且动态类型可能隐藏在字段或属性中,例如 prm.Payload.Id

Dapper 陷阱

Dapper 让人在不知不觉中引入 dynamic 变得很容易。调用 QueryFirst 而不指定返回类型会返回一个动态行对象(通常是 DapperRow),因此属性访问会变为动态的。

using Dapper;
using System.Data;

public static class Repo
{
    public static int GetActiveUserId(IDbConnection cn)
    {
        // QueryFirst() without returns a dynamic row (DapperRow).
        var row = cn.QueryFirst("select Id from Users where IsActive = 1");

        // row.Id is dynamic, the conversion to int happens at runtime.
        return row.Id; // DSM001
    }
}

这种代码经常能够通过审查,因为签名显示返回 int。动态绑定隐藏在中间。

Source:

Dapper 中更安全的替代方案

强类型 API

int id = cn.QuerySingle("select Id from Users where IsActive = 1");

映射到小型 DTO

public sealed record UserId(int Id);

int id = cn.QuerySingle("select Id from Users where IsActive = 1").Id;

如必须使用动态行时立即强制转换

var row = cn.QueryFirst("select Id from Users where IsActive = 1");
int id = (int)row.Id;

目标并不是“永远不要使用 dynamic”。目标是“在边界处阻止泄漏”。

解决方案:DynamicLeakAnalyzer

DynamicLeakAnalyzer 是一个 Roslyn 分析器,在泄漏扩散之前将其放大。它报告两个规则:

  • DSM001(隐式 dynamic 转换): 在需要静态类型的地方(返回、赋值、参数等)使用了 dynamic 表达式。代码能够编译,但转换在运行时发生。
  • DSM002(var 推断为 dynamic): var 捕获了 dynamic 结果,从而成为 dynamic。

安装并强制执行

添加分析器:

dotnet add package DimonSmart.DynamicLeakAnalyzer

使用 .editorconfig 让警告变为错误:

root = true

[*.cs]
dotnet_diagnostic.DSM001.severity = error
dotnet_diagnostic.DSM002.severity = error

在何处 dynamic 可以使用,何处不宜

良好的边界示例

  • COM 互操作
  • JSON 适配器和胶水代码
  • 数据库适配器(包括 dynamic Dapper 行)

应避免使用 dynamic 的场景

  • 核心领域逻辑
  • 热循环
  • 面向其他开发者的库

下一步

  • 在真实代码库上运行分析器,查看动态泄漏已经出现的位置。
  • 如果使用 Dapper,搜索返回动态行的 QueryFirst( 和非泛型 Query( 调用。
  • 如果出现误报或漏报,请提交一个包含最小复现案例的 issue。
Back to Blog

相关文章

阅读更多 »