Created
December 16, 2012 16:32
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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