IfItQuacks 1.4.0

dotnet add package IfItQuacks --version 1.4.0
                    
NuGet\Install-Package IfItQuacks -Version 1.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IfItQuacks" Version="1.4.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="IfItQuacks" Version="1.4.0" />
                    
Directory.Packages.props
<PackageReference Include="IfItQuacks">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add IfItQuacks --version 1.4.0
                    
#r "nuget: IfItQuacks, 1.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package IfItQuacks@1.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=IfItQuacks&version=1.4.0
                    
Install as a Cake Addin
#tool nuget:?package=IfItQuacks&version=1.4.0
                    
Install as a Cake Tool

<p align="center"> <img src="https://raw.githubusercontent.com/linkdotnet/IfItQuacks/main/assets/showcase.webp" alt="IfItQuacks: any class and anonymous types satisfy an interface, mismatches fail the build" /> </p>

IfItQuacks

Nuget GitHub tag

Compile-time checked structural (duck) typing for C#: "If it walks like a duck and quacks like a duck, it's a duck."

Getting Started

PM> Install-Package IfItQuacks

Declare an interface, mark a method as duck-typed and pass in anything that fits - no attributes on the interface or the types:

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 implements INamed. The generator verifies at compile time that both have a matching Name and redirects the call through small generated adapters - no reflection, no dynamic. A type that doesn't fit is a build error.

To keep a duck-typed value around, convert it explicitly:

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

Highlights

Anonymous types

Like object literals in TypeScript, anonymous types are ducks too - perfect for tests and quick stubs:

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

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

Compatible, not identical

Members only have to fit, just like an assignment would:

public interface IInventory
{
    IEnumerable<string> Items { get; }
    long Count();
    void Add(string item);
}

public class Warehouse
{
    public List<string> Items { get; } = [];                  // List<string> -> IEnumerable<string>
    public int Count() => Items.Count;                        // int -> long
    public bool Add(object item) { Items.Add($"{item}"); return true; } // string -> object, result discarded
}

Test doubles without a mocking library

A member holding a delegate satisfies an interface method, Duck.Stub throws for everything that is missing, and Duck.Merge replaces a single member of a real object:

var repository = Duck.Stub<IRepository>(new { Find = (Func<int, Order?>)(id => new Order(id, "Rubber duck")) });

Order? saved = null;
var spy = Duck.Merge<IRepository>(new { Save = (Action<Order>)(order => saved = order) }, realRepository);

Copies, not only views

Duck.As returns a view that forwards; Duck.To builds a value of its own, verified the same way:

public record CustomerDto(string Name, string Email);

var dto = Duck.To<CustomerDto>(customer); // new CustomerDto(customer.Name, customer.Email)

Types derived from types

Pick, Omit, Partial, Readonly and intersections, like TypeScript's mapped types. A derived interface would be dead weight in C# - under structural typing every fitting type satisfies it:

[DuckShape<Customer>(Omit = [nameof(Customer.PasswordHash)], Readonly = true)]
public partial interface ICustomerView;   // int Id { get; } string Name { get; } string Email { get; }

Render(new Customer());                                // the entity
Render(new CustomerRow(2, "Donald", "d@example.com")); // a DTO
Render(new { Id = 3, Name = "Daisy", Email = "" });    // an object literal

See Mapped shapes.

Zero-allocation duck typing

Write the duck type as a constrained type parameter and the adapter is passed as a type argument instead of an interface - nothing is boxed, and the runtime specializes the method per shape:

[DuckTyped]
public static int Describe<T>(T person) where T : IPerson => person.Name.Length + person.Age;

Ops.Describe(new Person());   // same call, 0 bytes allocated

Measured per 1000 calls: 3,694 ns and 24,000 B as an interface parameter, 562 ns and nothing as a constraint - the same as putting the concrete type in the signature. With three shapes sharing one method it is 10,459 ns against 3,005 ns, because a constrained call has no virtual dispatch left to guess. See Constrained duck typing.

Fields count as properties

public class LegacyPerson { public string Name = ""; }

new Greeter().Greet(new LegacyPerson { Name = "Steven" }, new Mallard());

Identity is preserved

Adapters forward Equals, GetHashCode and ToString to the original instance, so they work as dictionary keys and in sets. Duck.Unwrap gives you the original back:

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, ...
  • Extension methods: [DuckTyped] static string Greet(this INamed n) makes person.Greet() work
  • Sequences are adapted element by element, so a List<Person> is an IEnumerable<INamed>
  • Interface parameters that don't 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 - and [DuckTyped] methods as delegates
  • static abstract members and operators through 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

Documentation

Runnable examples live in samples.

Support & Contributing

Thanks to all contributors and people that are creating bug-reports and valuable input:

<a href="https://github.com/linkdotnet/IfItQuacks/graphs/contributors"> <img src="https://contrib.rocks/image?repo=linkdotnet/IfItQuacks" alt="Supporters" /> </a>

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.4.0 0 9/20/2026
1.3.0 39 9/18/2026
1.2.0 42 9/17/2026
1.1.0 45 9/17/2026
1.0.0 45 9/17/2026
1.0.0-preview.1 39 9/17/2026