Skip to main content

If It Quacks - Part 2

9/18/2026
3 minute read

Some days ago I wrote about duck-typing in C# with interceptors. Back then it was a proof-of-concept with a lot of rough edges. Now it is a real library: IfItQuacks.

The idea in 30 seconds#

Declare an interface, mark your method with [DuckTyped] and pass in anything that fits - no base class, no implements, no attributes on the types themselves:

csharp
using IfItQuacks;

public interface INamed
{
    string Name { get; }
}

public class Person { public string Name => "Steven"; }
public class Mallard { public string Name => "Donald"; }

public partial class Greeter
{
    [DuckTyped]
    public string Greet(INamed first, INamed second, string greeting = "Hello") =>
        $"{greeting}, {first.Name} and {second.Name}!";
}

new Greeter().Greet(new Person(), new Mallard()); // Hello, Steven and Donald!

Neither Person nor Mallard knows about INamed. The source generator checks at compile time that both have a matching Name and routes the call through a small generated adapter. No reflection, no dynamic. If it doesn't fit, you get an analyzer error.

If you want to hold on to the duck, convert it explicitly:

csharp
List<INamed> names = [Duck.As<INamed>(new Person()), Duck.As<INamed>(new Mallard())];

Highlights#

Here is the short tour. The documentation has the long one.

Anonymous types are ducks#

This is my favorite one. Anonymous types are object literals, basically the same thing TypeScript hands you and they quack just fine:

csharp
new Greeter().Greet(new { Name = "Steven" }, new { Name = "Donald" });

Basically, you can write your tests with that. "Instant Stubs"!

csharp
public interface IPerson
{
    string Name { get; }
    int Age { get; }
}

public partial class AgeChecker
{
    [DuckTyped]
    public bool IsAdult(IPerson person) => person.Age >= 18;
}

[Test]
public void Should_detect_adults()
{
    var checker = new AgeChecker();

    Assert.That(checker.IsAdult(new { Name = "Steven", Age = 90 }), Is.True);
    Assert.That(checker.IsAdult(new { Name = "Baby Duck", Age = 1 }), Is.False);
}

Or:

csharp
IPerson stub = Duck.As<IPerson>(new { Name = "Donald", Age = 90 });

List<IPerson> family =
[
    Duck.As<IPerson>(new { Name = "Huey", Age = 10 }),
    Duck.As<IPerson>(new { Name = "Dewey", Age = 10 }),
    Duck.As<IPerson>(new { Name = "Louie", Age = 10 }),
];

Identity is preserved#

Adapters forward Equals, GetHashCode and ToString to the wrapped instance, so they behave in dictionaries and sets. And you can get the original back:

csharp
var person = new Person();
Duck.As<INamed>(person).Equals(Duck.As<INamed>(person)); // true
Duck.Unwrap(Duck.As<INamed>(person)) is Person;          // true

And more#

  • Any interface works, including framework ones: Duck.As<IDisposable>(x), IEnumerable<T> parameters, ...
  • Interface parameters that do not need duck typing (an ILogger, say) keep accepting implementations, null and default values
  • Generic interfaces (IContainer<T>) and generic [DuckTyped] methods with inferred type arguments
  • Events, indexers and default interface members
  • Delegates, lambdas and method groups for single-method interfaces
  • static abstract members and operators via duck-typed constraints (where T : IAddable<T>) - generic math included
  • Instance and static methods, ref/out, params, default values and named arguments
  • Zero setup: install the package, no project file changes

Resources#

An error has occurred. This application may no longer respond until reloaded.Reload x