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

Published: (January 14, 2026 at 05:56 AM EST)
3 min read
Source: 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)

In C#, dynamic can be handy at boundaries. The problem is that it can silently leak into places where you expect static typing, and your method signatures can still look perfectly safe.

I ran into these leaks in real projects and built a small Roslyn analyzer to catch them early. I am the author of DynamicLeakAnalyzer (NuGet: DimonSmart.DynamicLeakAnalyzer). This post explains the problem it targets and how to fix leaks at the boundary.

What dynamic really means

In C#, dynamic is a static type, but it bypasses compile‑time type checking for the expression. Member access, overload resolution, operators, and conversions are bound at runtime.

At runtime, values still flow as object. The compiler emits runtime binding (DLR call sites) with caching, so repeated calls can get faster after the first bind.

How “dynamic contagion” happens

Two patterns make leaks hard to spot during review:

  • Invisible runtime work: member access and conversions happen at runtime.
  • Deceptive signatures: a method can return int and still perform dynamic conversions inside.
  • var can silently become dynamic: when the right side is dynamic, the inferred type becomes dynamic.

Example

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

    static void Main()
    {
        dynamic prm = 123;

        int a = GetX(prm); // DSM001: implicit dynamic conversion at runtime
        var b = GetX(prm); // DSM002: b becomes dynamic because the invocation is dynamic
    }
}

In real code, prm is often not a local variable. It can be an object passed into the method, and dynamic can hide in a field or property, e.g., prm.Payload.Id.

The Dapper trap

Dapper makes it easy to introduce dynamic without noticing it. Calling QueryFirst without returns a dynamic row object (usually DapperRow), so property access becomes dynamic.

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
    }
}

This kind of code often passes reviews because the signature says int. The dynamic binding is hidden in the middle.

Safer alternatives in Dapper

Typed API

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

Map to a small DTO

public sealed record UserId(int Id);

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

Cast immediately if you must use a dynamic row

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

The goal is not “never use dynamic”. The goal is “stop the leak at the boundary”.

The solution: DynamicLeakAnalyzer

DynamicLeakAnalyzer is a Roslyn analyzer that makes these leaks loud before they spread. It reports two rules:

  • DSM001 (Implicit dynamic conversion): a dynamic expression is used where a static type is expected (return, assignment, argument, etc.). The code compiles, but the conversion happens at runtime.
  • DSM002 (var inferred as dynamic): var captures a dynamic result and becomes dynamic.

Install and enforce

Add the analyzer:

dotnet add package DimonSmart.DynamicLeakAnalyzer

Make warnings hurt using .editorconfig:

root = true

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

Where dynamic is fine, and where it is not

Good boundary examples

  • COM interop
  • JSON adapters and glue code
  • Database adapters (including dynamic Dapper rows)

Avoid dynamic in

  • Core domain logic
  • Hot loops
  • Libraries meant for other developers

Next steps

  • Run the analyzer on a real codebase and see where dynamic leaks already exist.
  • If you use Dapper, search for QueryFirst( and non‑generic Query( calls that return dynamic rows.
  • If you have false positives or missed cases, open an issue with a minimal repro.
Back to Blog

Related posts

Read more »

C#.NET - day 08

Day 08 : Unit Testing — The Real Reason Interfaces Exist Proving service behavior without repositories or databases Introduction Many tutorials introduce inter...