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](https://github.com/DimonSmart/DynamicLeakAnalyzer)** (NuGet: `DimonSmart.DynamicLeakAnalyzer`), and 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 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.
## 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.
Here is a small example that shows both problems. The method looks fully static and returns `int`, yet dynamic enters through the parameter.
```csharp
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`.
## The Dapper trap
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.
```csharp
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.
### Safer alternatives in Dapper
Prefer typed APIs at the boundary:
```csharp
int id = cn.QuerySingle<int>("select Id from Users where IsActive = 1");
```
Or map to a small DTO:
```csharp
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:
```csharp
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:
```bash
dotnet add package DimonSmart.DynamicLeakAnalyzer
```
Make warnings hurt using `.editorconfig`:
```ini
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.