Monday, May 25, 2009

Duck typing with Castle DynamicProxy

I was trying to come up with some duck typing solution using C# 4.0, based on this, and this, when I realized that DynamicProxy could do this without any new language features:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Castle.Core.Interceptor;
using Castle.DynamicProxy;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DuckTypingTests {
    public class Duck {
        public void Quack() {
            Console.WriteLine("Quack Quack!");
        }

        public void Swim() {
            Console.WriteLine("Swimming...");
        }
    }

    public interface IQuack {
        void Quack();
    }

    public interface ISwimmer {
        void Swim();
    }

    [TestClass]
    public class DuckTypingTests {
        [TestMethod]
        public void DuckTyping() {
            var duck = new Duck();
            duck.As<IQuack>().Quack();
            duck.As<ISwimmer>().Swim();
        }
    }

    public static class DuckTypingExtensions {
        private static readonly ProxyGenerator generator = new ProxyGenerator();

        public static T As<T>(this object o) {
            return generator.CreateInterfaceProxyWithoutTarget<T>(new DuckTypingInterceptor(o));
        }
    }

    public class DuckTypingInterceptor : IInterceptor {
        private readonly object target;

        public DuckTypingInterceptor(object target) {
            this.target = target;
        }

        public void Intercept(IInvocation invocation) {
            var methods = target.GetType().GetMethods()
                .Where(m => m.Name == invocation.Method.Name)
                .Where(m => m.GetParameters().Length == invocation.Arguments.Length)
                .ToList();
            if (methods.Count > 1)
                throw new ApplicationException(string.Format("Ambiguous method match for '{0}'", invocation.Method.Name));
            if (methods.Count == 0)
                throw new ApplicationException(string.Format("No method '{0}' found", invocation.Method.Name));
            var method = methods[0];
            if (invocation.GenericArguments != null && invocation.GenericArguments.Length > 0)
                method = method.MakeGenericMethod(invocation.GenericArguments);
            invocation.ReturnValue = method.Invoke(target, invocation.Arguments);
        }
    }
}

Again, this is just a spike, it only matches methods, not properties, and even the method matching in the interceptor will break with method overloading. And let's not mention performance.

It would be interesting to somehow merge David Meyer's Duck Typing Project into Castle DynamicProxy, as it is the most complete duck-typing solution for .net that I know...

1 comment:

Daniel Wolf said...

Great blog post! I was looking for a way to write an interceptor relaying any access to a simple object. Finally I can convert an object to an interface which it doesn't explicitly implement.

By the way: Property access is represented as calling a method get_X/set_X, so your code works just fine for properties, too!