Skip to main content

Duck Typing with Anonymous Types - Part 2

Previously I looked at forwarding property calls of an interface to those of an anonymous type instance. Now I'd like to see if we can forward method calls as well. But how? We can't create an anonymous type instance with methods, and we can't assign assign anonymous methods as properties. This doesn't work:

var duck = new
{
    Color = "white",
    Quack = () => "Quack!"
};

but this does:

[Test]
public void CanInvokeMethod()
{
    var duck = new
    {
        Color = "white",
        Quack = (Func<string>)(() => "Quack!")
    };
}

Let's modify our interceptor once again, this time to retrieve a delegate property named the same as the invoked method and invoke it.

internal class DuckTypingInterceptor : IInterceptor
{
    [... snip ...]

    public void Intercept(IInvocation invocation)
    {
        [... snip ...]

        if (invocation.IsMethodCall())
        {
            var method = GetMethod(invocation.Method.Name);
            invocation.ReturnValue = method.DynamicInvoke(invocation.Arguments);
        }
    }

    [... snip ...]

    private Delegate GetMethod(string name)
    {
        return anonymous.GetPropertyValue(name) as Delegate;
    }
}
[Test]
public void CanInvokeMethod()
{
    var duck = new
    {
        Color = "white",
        Quack = (Func<string>)(() => "Quack!")
    };

    var typedDuck = duck.As<IDuck>();

    Assert.That("Quack!" == typedDuck.Quack());
}

It works! Now... what about overloads? This is a bit trickier. We can start by deciding that overloads will be stored as an array of System.Delegates.

[Test]
public void CanInvokeOverloadedMethods()
{
    var launchPad = new
    {
        Fly = new Delegate[]
        {
            (Func<Direction>)(() => Direction.West),
            (Func<Direction, Direction>)(d => d)
        }
    };

    var launchPadMcQuack = launchPad.As<IDuck>();

    Assert.That(Direction.West == launchPadMcQuack.Fly());
    Assert.That(Direction.South == launchPadMcQuack.Fly(Direction.South));
}

A modified GetMethod method in our interceptor allows the test to pass.

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.GetParameters().Count() == invocation.Arguments.Count()).First();
}

This is obviously a naive implementation - overload resolution is extremely difficult. This will fail for overloads with the same number of parameters differentiated by type, methods with the params keyword, and methods with out/ref parameters. For now, though, we have something that will work well for very simple interfaces. In my final post, I'll try to make the overload resolution a little smarter and handle one other scenario I haven't talked about - indexed properties.

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...