using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
public string Mail { get; set; } = string.Empty;
IMapping duck = Quack.From(anon).LikeA<IMapping>();
Console.WriteLine(duck.Mail);
const int numRounds = 100_000_000;
var stopwatch = Stopwatch.StartNew();
for (var i = 0; i < numRounds; ++i)
var proxyTime = stopwatch.Elapsed;
Console.WriteLine($"Proxy: {numRounds / proxyTime.TotalSeconds:N} Hz");
for (var i = 0; i < numRounds; ++i)
var dynamicTime = stopwatch.Elapsed;
Console.WriteLine($"Dynamic: {numRounds / dynamicTime.TotalSeconds:N} Hz");
for (var i = 0; i < numRounds; ++i)
var realTime = stopwatch.Elapsed;
Console.WriteLine($"Real: {numRounds / realTime.TotalSeconds:N} Hz");
sealed class Quack<TConcrete>
readonly TConcrete _concrete;
public Quack(TConcrete concrete)
public TInterface LikeA<TInterface>()
object proxy = DispatchProxy.Create<TInterface, MethodMappingProxy<TInterface, TConcrete>>()!;
((MethodMappingProxy<TInterface, TConcrete>)proxy).Load(_concrete);
return (TInterface)proxy;
public static Quack<TConcrete> From<TConcrete>(TConcrete concrete) => new(concrete);
class MethodMappingProxy<TInterface, TConcrete> : DispatchProxy
static readonly Lazy<IReadOnlyDictionary<MethodInfo, Func<TConcrete, object?[]?, object?>>> MethodMapping = new(InitializeMethodMapping);
TConcrete _instance = default!;
static IReadOnlyDictionary<MethodInfo, Func<TConcrete, object?[]?, object?>> InitializeMethodMapping()
var dictionary = new Dictionary<MethodInfo, Func<TConcrete, object?[]?, object?>>();
var concreteMethods = typeof(TConcrete)
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(methodInfo => methodInfo.Name);
foreach (var methodInfo in typeof(TInterface).GetMethods(BindingFlags.Public | BindingFlags.Instance))
var name = methodInfo.Name;
if (!concreteMethods.TryGetValue(name, out var concreteMethodInfo))
throw new Exception($"Missing method {name}");
if (methodInfo.ReturnType != concreteMethodInfo.ReturnType)
throw new Exception($"{name} method has wrong return type");
var interfaceMethodParameters = methodInfo.GetParameters();
var concreteMethodParameters = concreteMethodInfo.GetParameters();
if (interfaceMethodParameters.Length != concreteMethodParameters.Length)
throw new Exception($"{name} method has wrong number of parameters");
for (var i = 0; i < interfaceMethodParameters.Length; ++i)
if (interfaceMethodParameters[i].ParameterType != concreteMethodParameters[i].ParameterType)
throw new Exception($"{name} method parameter #{i + 1} is wrong type");
var instanceParameter = Expression.Parameter(typeof(TConcrete));
var argsParameter = Expression.Parameter(typeof(object?[]));
if (interfaceMethodParameters.Length == 0)
body = Expression.Call(instanceParameter, concreteMethodInfo);
var castArgs = concreteMethodParameters
.Select((parameterInfo, index) => (Expression)Expression.Convert(
Expression.ArrayAccess(argsParameter, Expression.Constant(index, typeof(int))),
parameterInfo.ParameterType
var func = Expression.Lambda<Func<TConcrete, object?[]?, object?>>(body, instanceParameter, argsParameter).Compile();
dictionary[methodInfo] = func;
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
if (targetMethod is null)
var methodMapping = MethodMapping.Value;
if (!methodMapping.TryGetValue(targetMethod, out var func))
throw new InvalidOperationException();
return func(_instance, args);
public void Load(TConcrete instance)
GC.KeepAlive(MethodMapping.Value);