Skip to main content

Duck Typing with Anonymous Types - Part 3

Continuing my short series, it's time to add some polish to what we have so far.

Overload Resolution

At first I thought overload resolution was going to be a huge pain, but really the compiler does everything for us. As long as we have assigned an implementation with exactly matching parameters for the method that the compiler calls, we're good to go. The necessary modifications to the interceptor fit into a small reflection utility method.

private Delegate GetMethod(IInvocation invocation)
{
    var methodProperty = anonymous.GetPropertyValue(invocation.Method.Name);
    if (methodProperty is Delegate)
    {
        return methodProperty as Delegate;
    }

    var overloads = methodProperty as Delegate[];
    return overloads.Where(m => m.Method.SignatureMatches(invocation.Method)).First();
}
internal static bool SignatureMatches(this MethodInfo method, MethodInfo other)
{
    var theseParameters = method.GetParameters();
    var thoseParameters = other.GetParameters();

    if (theseParameters.Count() != thoseParameters.Count())
    {
        return false;
    }

    foreach (var parameter in theseParameters)
    {
        var parameterIndex = Array.IndexOf(theseParameters, parameter);
        if (!parameter.ParameterType.Equals(thoseParameters[parameterIndex].ParameterType))
        {
            return false;
        }
    }

    return true;
}

Here is the modified IDuck interface and corresponding test (note that by explicitly declaring brains as an object, the compiler will emit code that calls the overload with object parameter(s)):

public interface IDuck
{
    string Color { get; set; }
    Direction Fly();
    Direction Fly(Direction direction);
    string Quack();
    string Quack(int times);
    string Quack(string sound);
    string Quack(object sound);
    string Quack(params object[] sounds);
    object this[string attribute] { get; set; }
}
[Test]
public void CanInvokeOverloadedMethodsWithSomewhatAmbiguousSignatures()
{
    var zombie = new
    {
        Quack = new Delegate[]
        {
            (Func<int, string>)(i => String.Format("{0} BRAAAINS!", i)),
            (Func<string, string>)(s => String.Format("{0} BRAAAINS!", s)),
            (Func<object, string>)(o => String.Format("{0}", o)),
            (Func<object[], string>)(o => String.Format("{0} BRAAAIN OBJECTS!", o.Count()))
        }
    }.As<IDuck>();

    object brains = "BRAAAINS!";

    Assert.That("10 BRAAAINS!" == zombie.Quack(10));
    Assert.That("EAT BRAAAINS!" == zombie.Quack("EAT"));
    Assert.That("BRAAAINS!" == zombie.Quack(brains));
    Assert.That("3 BRAAAIN OBJECTS!" == zombie.Quack(brains, brains, brains));
}

Indexed Properties

I'm not going to go through all the changes I had to make to support an indexed property, but it wasn't very complicated. Basically, if we give our anonymous type a property named Item, it will correspond to an index on an interface (because an indexer is just syntactic sugar around an Item property). This property should be a Delegate[] with a method for the getter and/or setter (depending on the interface contract). The setter won't automatically store values unless we implement it that way. Here are some examples:

[Test]
public void CanInvokeIndexGetter()
{
    var buckshot = new object();

    var duck = new
    {
        Item = new Delegate[]
        {
            (Func<string, object>)(s => buckshot)
        }                     
    }.As<IDuck>();

    Assert.AreSame(buckshot, duck["weakness"]);
}

[Test]
public void CanInvokeIndexSetter()
{
    var isInfected = false;
    var duck = new
    {
        Item = new Delegate[]
        {
            (Action<string, object>)((key,value) => isInfected = true)               
        }   
    }.As<IDuck>();

    duck["disease"] = "bird flu";
    Assert.IsTrue(isInfected);
}

[Test]
public void CanStoreAndRetrieveWithIndexedProperty()
{
    var store = new Dictionary<string, object>();
    var duck = new
    {
        Item = new Delegate[]
        {
            (Func<string, object>)(s => store[s]),
            (Action<string, object>)((s,o) => store[s] = o)
        }
    }.As<IDuck>();

    var feathers = new object();
    duck["feathers"] = feathers;
            
    Assert.AreSame(feathers, duck["feathers"]);
}

Duck Typing Plain Objects

A few fairly simple modifications, and we can support duck typing objects that aren't anonymous types.

public class ScroogeMcDuck
{
    public string Quack()
    {
        return "Bah! Humbug!";
    }
}

[Test]
public void CanInvokeMethod()
{
    var scrooge = new ScroogeMcDuck().As<IDuck>();

    Assert.That("Bah! Humbug!" == scrooge.Quack());
}

Syntax

Casting the function/action type of the method can look kind of ugly, so I made a utility class that we can pass anonymous methods through to return Delegates. We still have to specify parameter types, so it's still pretty crusty, but it's there if we want to use it.

var duck = new
{
    Quack = Anon.Methods
    (
        Anon.Method(() => "Quack!"),
        Anon.Method<int, string>(i => String.Format("{0} quacks!", i))
    )
};

Conclusion

This is a somewhat serviceable solution for ducktyping adaptation of legacy code or quick mocking, especially of data. We haven't made any special considerations for generics, so it may fail in those scenarios. Named parameters will almost certainly cause problems, and we haven't done any performance tuning. I should also note that I used this post by Mauricio Scheffer as a resource when researching how to begin. The code can be found here on github.

Comments

Popular posts from this blog

Migrating Hg Repos with hg-fast-export and Windows Subsystem for Linux

Introduction I prefer Mercurial (hg) to git . I don’t really have any reason for this preference - they both do the same thing, and the user experience for 90% of the use cases is the same. It probably comes from the conditions of the DVCS landscape when I started using these systems. Some of this may have been perception only, but it looked like this: GitHub didn’t have free private repos BitBucket did have free private repos BitBucket was very hg-friendly Joel Spolsky had an amazing tutorial that served as both a how-to for hg as well as a general intro to DVCS hg was much more Windows-friendly than git Since hg was written in python, I felt like extending it would be easier than doing so for git if I ever needed to (admittedly, this is a pretty ridiculous reason) hg felt like a more unified, “coherent” system than the very linux-y feeling git and its extensions (also pretty ridiculous) Where they differed, I liked the verbs hg used better than git’s counterparts ...

Enabling Globalization Invariant Mode for .NET Core App on Raspberry Pi Running LibreElec

I had an app I wanted to run on my Raspberry Pi 3 running LibreElec . In LibreElec you can install the dotnet core 2.2 runtime as an addon, and in Visual Studio you can compile for ARM processors with ‘Target Runtime’ set to ‘linux-arm’ in the publish profile. So, I published to a folder from VS using that profile, and I copied the output over to my RPi which had the dotnet runtime installed. I did a simple dotnet Whatever.dll to run the app (actually in this case, it was /storage/.kodi/addons/tools.dotnet-runtime/bin/dotnet Whatever.dll because of the way the addon is installed) and was met with this error: FailFast: Couldn't find a valid ICU package installed on the system. Set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support. at System.Environment.FailFast(System.String) at System.Globalization.GlobalizationMode.GetGlobalizationInvariantMode() at System.Globalization.GlobalizationMode..cctor() at Syste...

Stubbing Static Methods with PostSharp

TypeMock uses the Profiler API to allow mocking, stubbing, etc. of classes used by code under test. It has the ability to handle sealed classes, static classes, non-virtual methods, and other troublesome-yet-oft-encountered scenarios in the world of unit testing. Other frameworks rely on proxies to intercept method calls, limiting them to be able to only fake virtual, abstract, and interface members. They also rely on dependecy injection to place the proxies as the concrete implementation of calls to the abstracted interface members. Anyone working with a legacy codebase is bound to run into static method calls (especially in the data access layer), dependencies on concrete types with non-virtual methods, and sealed class dependencies (HttpContext anyone?). The only way to unit test this without refactoring is with TypeMock. I've never used TypeMock, and I'm sure it's a great product, but it's not free. I decided to spike some code to see if I could solve the prob...