---
title: "Quack Quack: Duck-Typing in C# with Interceptors"
description: "If it walks like a duck and it quacks like a duck - it is a duck. So duck it and make that work in C# thanks to interceptors!"
updated: 2026-09-12
tags: ["C#", ".NET"]
canonical: "https://steven-giesel.com/blogPost/5170e165-29e8-437a-b5ce-446a84943809/quack-quack-ducktyping-in-c-with-interceptors"
---

# Quack Quack: Duck-Typing in C# with Interceptors

If it walks like a duck and it quacks like a duck - it is a duck. So duck it and make that work in C# thanks to interceptors!

> [!NOTE]
> This is an educational blog post - feel free to us it in production ;) 

## TypeScript and where we wanna go

So in TypeScript you can do something like:
```typescript
class A {
  Do(): void { console.log("A.Do"); }
}

class B {
  Do(): void { console.log("B.Do"); }
}

function foo(a: { Do(): void }) {
  a.Do();
}
```

And I want to have something similar in C#:
```csharp
public class A { public void Do() => Console.WriteLine("A.Do"); }
public class B { public void Do() => Console.WriteLine("B.Do"); }

public void Foo(??? a) => a.Do();
```

`A` and `B` have nothing in common. No shared base class, no shared interface. `???` is the shape we need to invent - something that lets `Foo(new A())` and `Foo(new B())` both compile and both call the right `Do()`, without touching `A` or `B` at all.

Also none of the following are allowed:
 * `dynamic` - even though that is the coolest ever
 * accepting `object` and use `is` or `as` cast stuff
 * not touching `A` or `B` (like adding a shared base - be it interface or abstract base class)

## Interceptors
Since .NET 8, C# has an experimental compiler feature called [interceptors](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md). A source generator can emit a method, slap `[InterceptsLocation]` on it pointing at a specific call site in your code, and the compiler will silently swap the call to go there instead - at zero runtime cost, because it's resolved entirely at compile time.

The way that normally works is a combination of a custom attribute (`DuckType` and `DuckShape`) that is emitted by the source code generator and the interceptor logic - so basically getting all callsides and "do the right thing".

So the usage wise we still something like an interface to "explain the shape" (or property):

```csharp
[DuckShape]
public interface IDoable { void Do(); }

public static partial class Ops
{
    [DuckTyped]
    public static void Foo(IDoable a) => a.Do();
}
```

You write `Foo(IDoable a) => a.Do()` exactly like you'd want to. `[DuckShape]` marks `IDoable` as a structural contract rather than something you're expected to implement. `[DuckTyped]` tells the generator "this is the real logic, now go make it callable with anything that structurally matches."

I will link the whole generator/interceptor at the end of the article and will mainly show the interesting bids.

Anyway. Each call site resolves to this generic construct:
```csharp
[InterceptsLocation(1, "…")]
public static void Interceptor_1(A value)
{
    Ops.Foo((IDoable)(new ShapeAdapter_IDoable_A(value)));
}
```

`ShapeAdapter_IDoable_A` is a tiny generated `readonly struct` that implements `IDoable` by forwarding straight to `A`:

```csharp
internal readonly struct ShapeAdapter_IDoable_A : IDoable
{
    private readonly A _value;
    public ShapeAdapter_IDoable_A(A value) => _value = value;
    public void Do() => _value.Do();
}
```

That is the whole trick. It comes with a small dispatch cost (maybe inlining would help here a bit more).

So this perfectly resolves:

```csharp
Ops.Foo(new A());   // "A.Do"
Ops.Foo(new B());   // "B.Do"
```

It works for properties too:
```csharp
[DuckShape]
public interface INameable { string Name { get; set; } }

public class Person { public string Name { get; set; } = ""; }

[DuckTyped]
public static string Greet(INameable n) => $"Hello, {n.Name}!";

Greet(new Person { Name = "Steven" }); // "Hello, Steven!"
```

## NuGet Package?
Currently this is somewhere between a PoC and an actual library. I can make a NuGet package out of it, if someone feels a real benefit from it. There are edge cases that are currently not covered and I know of.

## Conclusion
So is it useful? Probably not - but that wasn't really the point here. So while the language doesn't allow it directly, one can "emulate" it to a certain extend.

## Update
I did create a full-blown library that is much easier to handle and support more use-cases: https://github.com/linkdotnet/IfItQuacks

## Resources
 * Code for this blog post: https://github.com/linkdotnet/BlogExamples/tree/main/DuckType
 * All my blog post examples can be found here: https://github.com/linkdotnet/BlogExamples