Skip to content

Instantly share code, notes, and snippets.

@jmhdez
Created December 16, 2012 16:32
Show Gist options
  • Save jmhdez/4309152 to your computer and use it in GitHub Desktop.
Save jmhdez/4309152 to your computer and use it in GitHub Desktop.
Duck typing con C# preparado para realizar implementaciones de interfaces con objetos anónimos
// Required libs
// - NUnit
// - Castle.DynamicProxy
using System;
using System.Linq;
using System.Reflection;
using Castle.DynamicProxy;
using NUnit.Framework;
namespace DynamicMocks
{
public static class DuckType
{
public static T As<T>(object target) where T : class
{
return new ProxyGenerator().CreateInterfaceProxyWithoutTarget<T>(new DuckTypingInterceptor(target));
}
private class DuckTypingInterceptor : IInterceptor
{
private readonly object target;
public DuckTypingInterceptor(object target)
{
this.target = target;
}
public void Intercept(IInvocation invocation)
{
var methods = target.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public);
// This should (probably) be cached
var method = methods.FirstOrDefault(x => IsCompatible(x, invocation.Method));
if (invocation.GenericArguments != null && invocation.GenericArguments.Length > 0)
{
if (!method.IsGenericMethod)
throw MissingMethodException(invocation);
method = method.MakeGenericMethod(invocation.GenericArguments);
}
if (method == null)
{
var property = TryMatchProperty(invocation);
if (property == null)
throw MissingMethodException(invocation);
var action = (Delegate)property.GetValue(target, null);
invocation.ReturnValue = action.DynamicInvoke(invocation.Arguments);
return;
}
if (method == null)
throw MissingMethodException(invocation);
invocation.ReturnValue = method.Invoke(target, invocation.Arguments);
}
private PropertyInfo TryMatchProperty(IInvocation invocation)
{
var properties = target.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
var targetMethod = invocation.GetConcreteMethod();
var hasReturnValue = targetMethod.ReturnType != typeof(void);
var actionTypes = new[]
{
typeof(Action), typeof (Action<>), typeof (Action<,>), typeof (Action<,,>),
typeof (Action<,,,>), typeof (Action<,,,,>), typeof (Action<,,,,,>),
};
var funcTypes = new[]
{
null, typeof (Func<>), typeof (Func<,>), typeof (Func<,,>), typeof (Func<,,,>),
typeof (Func<,,,,>), typeof (Func<,,,,,>), typeof (Func<,,,,,,>),
};
var types = targetMethod.GetParameters().Select(x => x.ParameterType).ToList();
if (hasReturnValue)
types.Add(targetMethod.ReturnType);
var delegateTypes = hasReturnValue ? funcTypes : actionTypes;
if (types.Count > delegateTypes.Length)
throw new InvalidOperationException("Too many arguments in method. Consider refactor to parameter object");
var expectedPropertyType = delegateTypes[types.Count].MakeGenericType(types.ToArray());
return properties.FirstOrDefault(x => x.PropertyType == expectedPropertyType && x.Name == targetMethod.Name);
}
private MissingMethodException MissingMethodException(IInvocation invocation)
{
return new MissingMethodException(string.Format("Cannot found compatible method {0} on type {1}", invocation.Method.Name, target.GetType().Name));
}
private bool IsCompatible(MethodInfo m1, MethodInfo m2)
{
// FIXME: handle ref/out paramenters and generic arguments restrictions
if (m1.Name != m2.Name || m1.ReturnType != m2.ReturnType)
return false;
if (!m1.IsGenericMethod)
{
var parameterTypes1 = m1.GetParameters().Select(p => p.ParameterType);
var parameterTypes2 = m2.GetParameters().Select(p => p.ParameterType);
return parameterTypes1.SequenceEqual(parameterTypes2);
}
return true;
}
}
}
[TestFixture]
public class DuckInterceptorTest
{
public interface ICalculator
{
int AddNumbers(int a, int b);
string Name { get; set; }
string Generic<T>(T t);
}
public class NotACalculator
{
private string name = "I'm not a calculator";
public int AddNumbers(int a, int b)
{
return a + b;
}
public string Name
{
get { return name; }
set { name = value; }
}
public string Generic<T>(T t)
{
return t.ToString();
}
}
public interface INamed
{
string Name { get; }
}
public interface ISample
{
int Sum(int a, int b);
void Do(string s);
}
[Test]
public void DuckTyping_of_methods()
{
ICalculator someAsCalculator = DuckType.As<ICalculator>(new NotACalculator());
Assert.That(someAsCalculator.AddNumbers(2, 3), Is.EqualTo(5));
}
[Test]
public void DuckTyping_of_property_getters_and_setters()
{
ICalculator someAsCalculator = DuckType.As<ICalculator>(new NotACalculator());
Assert.That(someAsCalculator.Name, Is.EqualTo("I'm not a calculator"));
someAsCalculator.Name = "Yes, you are";
Assert.That(someAsCalculator.Name, Is.EqualTo("Yes, you are"));
}
[Test]
public void DuckTyping_of_generic_methods()
{
ICalculator someAsCalculator = DuckType.As<ICalculator>(new NotACalculator());
Assert.That(someAsCalculator.Generic(2), Is.EqualTo("2"));
}
[Test]
public void DuckType_of_anonymous_object_property()
{
var anon = new {Name = "Lucas"};
INamed named = DuckType.As<INamed>(anon);
Assert.That(named.Name, Is.EqualTo("Lucas"));
}
[Test]
public void DuckType_of_anonymous_object_func()
{
var anon = new
{
Sum = new Func<int, int, int>((a, b) => a + b),
};
var command = DuckType.As<ISample>(anon);
Assert.That(command.Sum(2, 3), Is.EqualTo(5));
}
[Test]
public void DuckType_of_anonymous_object_action()
{
var done = string.Empty;
var anon = new
{
Do = new Action<string>(x => done = x)
};
var command = DuckType.As<ISample>(anon);
command.Do("did it!");
Assert.That(done, Is.EqualTo("did it!"));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment