
In C#, dynamic can be handy at boundaries. The problem is that it can silently leak into places where...
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, so I built a small Roslyn analyzer to catch them early. I am the author of DynamicLeakAnalyzer (NuGet: DimonSmart.DynamicLeakAnalyzer), and this post explains the problem it targets and how to fix leaks at the boundary.
dynamic really meansIn 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 difference is that the compiler emits runtime binding (DLR call sites). Those call sites have caching, so repeated calls can get faster after the first bind.
Two patterns make leaks hard to spot during review:
int and still perform dynamic conversions inside.var can silently become dynamic: when the right side is dynamic, the inferred type becomes dynamic.Here is a small example that shows both problems. The method looks fully static and returns int, yet dynamic enters through the parameter.
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, for example prm.Payload.Id.
Dapper makes it easy to introduce dynamic without noticing it. Calling QueryFirst without <T> 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 <T> 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.
Prefer typed APIs at the boundary:
int id = cn.QuerySingle<int>("select Id from Users where IsActive = 1");
Or map to a small DTO:
public sealed record UserId(int Id);
int id = cn.QuerySingle<UserId>("select Id from Users where IsActive = 1").Id;
If you really must use a dynamic row, kill it immediately:
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".
DynamicLeakAnalyzer is a Roslyn analyzer that makes these leaks loud before they spread.
It reports two rules:
dynamic expression is used where a static type is expected (return, assignment, argument, etc.). The code compiles, but the conversion happens at runtime.var inferred as dynamic): var captures a dynamic result and becomes dynamic.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
dynamic is fine, and where it is notGood boundary examples:
Avoid dynamic in:
QueryFirst( and non-generic Query( calls that return dynamic rows.
gemmaI ported the whole Gemma-4 family — E2B, E4B, 12B, 31B, and the 26B-A4B MoE — to run on...
communityHey DEV, I'm Tobore. Let's actually connect. I've been on here for a while now, mostly writing and...
ai(yep, kinda clickbait, just for the funsies 😊) At the beginning of the year, I relaunched my...
aiMy laptop was sitting idle with the fan at full tilt. Nothing was running that I knew of. The culprit...
githubactionsI Built a Thing! TL;DR — Google Gemini-based Pull Request reviews and Issue Triaging for...
aiI've been hearing the word "harness" thrown around a lot lately. I assumed it just meant "the IDE" or...
Workflows from the Neura Market marketplace related to this DeepSeek resource