Duck-Typing with PeanutButter #1
Davyd McColl
Posted on December 14, 2019
Some time ago I started a thread on duck-typing, and then went on to elucidate further. I've also given a presentation on duck-typing at my work.
Honestly, I've been putting off this blog because it's a little daunting to go through the full process for how PeanutButter.DuckTyping actually works. There's type emission, IL generation and quite a lot of reflection. I'm quite sure most people would lose interest quite quickly. If you're not one of those people -- great! You're more than welcome to check out the source on GitHub
As Scott Hanselman says, we have a limited number of keystrokes, so I'd rather spend them on something that delivers a little more value: a run-down of why you might want duck-typing in .NET (or, rather, why I wanted it, and why PeanutButter.DuckTyping
was born), and an introduction to how to use the .DuckAs<T>()
, .FuzzyDuckAs<T>()
and .ForceFuzzyDuckAs<T>()
extension methods provided by PeanutButter.DuckTyping
. So here we go!
What is Duck-Typing?
That's a good question!
I feel I've covered this in prior posts (1 2) and the presentation linked above). Feel free to check those out and come back here (:
Why would you want to duck-type?
Ok, so there are many reasons why a person might want to duck-type -- heck, Microsoft even created the dynamic
keyword and ExpandoObject
because there are use-cases for leaving type determination to as late as possible.
Side-note: please don't use dynamic
:
- it's slow... really... slow.
- it negates your compiler's type-checking abilities
- it's not supported any more and has some serious bugs
My use-case was that I was building a web front-end to a generic workflow system. The front-end should be able to query information (what workflows are available / in progress) and invoke actions on the workflows. However, I wanted to leave the decisions about what could be done on a workflow as late as possible, because I wanted workflows to define the actions that were available at any time.
So I ended up wanting a single, tolerant end-point I could hit with JSON data determined from the currently-loaded workflow and the user's actions.
I thought that some kind of duck-typing would be ideal for what I wanted to do. There were already some available options:
- I could have a bunch of custom reflection code in my web api code
- I could use ImpromptuInterface
The former seemed like I'd end up with a rather "dirty" project. The latter I only discovered once I'd done the bare minimum of PeanutButter.DuckTyping, and by then I had requirements that ImpromptuInteface couldn't handle, like fuzzy-ducking (more on this later). A colleague of mine didn't see eye-to-eye on this: he reckoned I was wasting my time. Perhaps I was! But I learned a lot, and in the end, I have a library which works incredibly well for all the workloads I throw at it. It's consistently the one project of mine that I'm surprised by when I use it for the first time in a long time because it just does what I expect it to.
Like any code, it could be better and prettier. Like any code, it's not perfect. But unlike most of my code, I still like it (:
Enough jibber-jabber! I want to duck-type now!
Cool beans!
For my presentation, I wrote a cli demo which takes you through some of the capabilities of PeanutButter.DuckTyping and scenarios you might want to use it for. I'm pretty-much going to run through those here:
1. Reading from unrelated classes
Let's imagine that we have a class from an assembly that we don't control, and it doesn't implement any interfaces -- but we'd really like it to, perhaps for testing, perhaps for other purposes (which we'll explore later). We just want to be able to read from it, for now at least (modified from demo source):
public interface IReadOnlyEntity
{
int Id { get; }
string Name { get; }
}
// imagine this came from another assembly
public class LooksLikeAnEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
public override void Run()
{
var unrelatedObject = new LooksLikeAnEntity()
{
Id = 42,
Name = "Douglas Adams"
};
var ducked = unrelatedObject.DuckAs<IReadOnlyEntity>();
Log("Original:\n", unrelatedObject, "\nDucked:\n", ducked);
DoStuff(ducked); // will compile!
}
private void DoStuff(IReadOnlyEntity entity)
{
// gnarly logic goes here
}
Let's unpack this:
- we had our original object
- we had an interface we'd like it to conform to
- this interface matches the types and names of the original object perfectly
- in addition, the interface only requires read-access to the properties
But why bother? Why not just make DoStuff
take an instance of LooksLikeAnEntity
? Some reasons include:
- not having a third-party class exposed in our api
- not requiring full (ie write-) access to the object in
DoStuff
and wanting to prevent write-back - the entity we're duck-typing may be a lot more complex, perhaps with tens of properties and/or methods we don't care about, and we'd like to keep the consuming code simpler.
That last reason brings me to an idea I call interface shielding, which is where I might want to shield the full data of an object from a consumer, for example:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public DateTime DateOfBirth { get; set; }
}
The class above could be exposing too much data for the code that will consume it. Imagine if you'd like to pass off this data to a consumer without letting that consumer have access to the Address
or DateOfBirth
. Of course, you could write another class and map values -- or you could shield with a simpler interface:
public interface IIdAndName
{
int Id { get; }
string Name { get; }
}
// elsewhere
public void Producer()
{
foreach (var person in FetchPersons())
{
Consumer(person.DuckAs<IIdAndName>());
}
}
private void Consumer(IIdAndName details)
{
// code here can't get to the Address or DateOfBirth
// -> due to the way DuckAs<T> works, it would even be
// very tricky via reflection
}
Not only would Consumer
not have access to sensitive data, but it would also not have write-access to insensitive data. This can also help to make the intent of Consumer
clearer.
I use this method quite a lot in tests, for example when I want to prove that an item retrieved from the database matches an expected item and some of the properties on both are auto-generated (like Created DateTime values) -- here I might create a lesser interface, duck-type the two objects onto it and Expect(actual).To.Deep.Equal(expected);
(see my posts on NExpect to learn more about how to use this handy deep equality assertion!)
There's a lot more PeanutButter.DuckTyping can do, including:
- duck-typing app configuration (ie NameValueCollection) onto an interface to pass to code consuming configuration
- I use this quite a lot: it's really convenient!
- it's even possible to use key prefixes and duck-type only the settings with that prefix
- duck-typing dictionaries to known interfaces, with write-back
- duck-typing methods from one object onto a known interface
- fuzzy duck-typing where:
- member names don't have to match case
- extraneous characters in member names are ignored (eg underscores)
- property types don't have to explicitly match: auto-casting is done for you
- forceful fuzzy duck-typing which allows duck-typing an empty dictionary onto an interface and collecting the data which is written back
- merged duck-typing in which a collection of objects or a collection of dictionaries can be duck-typed onto a single interface, with priority given to the first matching member found amongst any item in the collection
If any of this sounds interesting, tune in for the next part in this series!
Posted on December 14, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.