IfItQuacks 1.3.0
dotnet add package IfItQuacks --version 1.3.0
NuGet\Install-Package IfItQuacks -Version 1.3.0
<PackageReference Include="IfItQuacks" Version="1.3.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="IfItQuacks" Version="1.3.0" />
<PackageReference Include="IfItQuacks"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add IfItQuacks --version 1.3.0
#r "nuget: IfItQuacks, 1.3.0"
#:package IfItQuacks@1.3.0
#addin nuget:?package=IfItQuacks&version=1.3.0
#tool nuget:?package=IfItQuacks&version=1.3.0
<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
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)
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)makesperson.Greet()work - Sequences are adapted element by element, so a
List<Person>is anIEnumerable<INamed> - Interface parameters that don't need duck typing (an
ILogger, say) keep accepting implementations,nulland 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 abstractmembers 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
- Getting started
- How does it work? - including what gets allocated
- Diagnostics
- Benchmarks
- Known limitations
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>
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.3.0 | 8 | 9/18/2026 |
| 1.2.0 | 35 | 9/17/2026 |
| 1.1.0 | 38 | 9/17/2026 |
| 1.0.0 | 39 | 9/17/2026 |
| 1.0.0-preview.1 | 33 | 9/17/2026 |