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