Reputation: 147
I have a method with the argument of type IMapper, and I want to be able to pass Anonymous types on it that implement the IMapper without creating a class and explicitly tell that the class must implement the interface using semicolon;
public interface IMapper {
string Mail { get; set; }
}
public static void Mapper(IMapper mappable)
{
Console.WriteLine(mappable.Mail);
}
public static void Main(string[] args)
{
var test = new { Mail = "[email protected]"};
Mapper(test);
}
Upvotes: 2
Views: 1979
Reputation: 5744
I want to be able to pass Anonymous types on it that implement the IMapper
As @Flydog57 said, C# doesn't have structural typing (A.K.A. "duck" typing).
Also, anonymous types cannot implement interfaces.
And worse than that, anonymous types are immutable! So even if they could implement interfaces, they wouldn't be able to implement your IMapper
interface because it has a property setter.
So the technically correct answer is: it's not possible.
At the end of the day, I think this is just another reminder that C#'s type system is not as flexible or powerful as other languages.
But! A couple of alternatives in C# land come to mind.
dynamic
typepublic static void Mapper(dynamic mappable) // Notice "dynamic" type
{
Console.WriteLine(mappable.Mail);
}
public static void Main(string[] args)
{
var test = new { Mail = "[email protected]"};
Mapper(test);
}
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/using-type-dynamic
The dynamic
type bypasses static type checking. That's just a fancy way to say your code will compile no matter what properties you access or methods you call on mappable
. That doesn't mean it'll work if you call mappable.ThisMethodDoesNotExist()
, though!
DispatchProxy
This has all the downsides of dynamic
while being even slower, but it's very powerful.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
class Program
{
interface IMapping
{
string Mail { get; }
}
class Mapping : IMapping
{
public string Mail { get; set; } = string.Empty;
}
static void Main()
{
var anon = new
{
Mail = "Hello, world!"
};
IMapping duck = Quack.From(anon).LikeA<IMapping>();
Console.WriteLine(duck.Mail); // See? It works!
// Compare performance of various options
// First, using our proxy generated above
const int numRounds = 100_000_000;
var stopwatch = Stopwatch.StartNew();
for (var i = 0; i < numRounds; ++i)
{
GC.KeepAlive(duck.Mail);
}
var proxyTime = stopwatch.Elapsed;
Console.WriteLine($"Proxy: {numRounds / proxyTime.TotalSeconds:N} Hz"); // 16,943,811.22 Hz
// Second, using the `dynamic` type
var dyn = (dynamic)anon;
stopwatch.Restart();
for (var i = 0; i < numRounds; ++i)
{
GC.KeepAlive(dyn.Mail);
}
var dynamicTime = stopwatch.Elapsed;
Console.WriteLine($"Dynamic: {numRounds / dynamicTime.TotalSeconds:N} Hz"); // 30,946,883.98 Hz
// Third, using the idiomatic solution, which is to use a named type instead of an anonymous type
var real = new Mapping
{
Mail = "Hello, world!"
};
stopwatch.Restart();
for (var i = 0; i < numRounds; ++i)
{
GC.KeepAlive(real.Mail);
}
var realTime = stopwatch.Elapsed;
Console.WriteLine($"Real: {numRounds / realTime.TotalSeconds:N} Hz"); // 279,305,111.23 Hz -- this is what you're missing out on by trying to be so extravagant
}
}
sealed class Quack<TConcrete>
{
readonly TConcrete _concrete;
public Quack(TConcrete concrete)
{
_concrete = concrete;
}
public TInterface LikeA<TInterface>()
{
object proxy = DispatchProxy.Create<TInterface, MethodMappingProxy<TInterface, TConcrete>>()!;
((MethodMappingProxy<TInterface, TConcrete>)proxy).Load(_concrete);
return (TInterface)proxy;
}
}
static class Quack
{
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))
{
// Remember, properties are just syntax sugar for underlying get/set methods
// Validate that this interface method exists in the concrete type, and that its signature matches
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");
}
// Set up the expression for calling this method on the concrete type. We'll wrap the concrete method
// invocation in a Func<TConcrete, object?[]?, object?> delegate
var instanceParameter = Expression.Parameter(typeof(TConcrete));
var argsParameter = Expression.Parameter(typeof(object?[]));
Expression body;
if (interfaceMethodParameters.Length == 0)
{
body = Expression.Call(instanceParameter, concreteMethodInfo);
}
else
{
var castArgs = concreteMethodParameters
.Select((parameterInfo, index) => (Expression)Expression.Convert(
Expression.ArrayAccess(argsParameter, Expression.Constant(index, typeof(int))),
parameterInfo.ParameterType
))
.ToArray();
body = Expression.Call(
instanceParameter,
concreteMethodInfo,
castArgs
);
}
var func = Expression.Lambda<Func<TConcrete, object?[]?, object?>>(body, instanceParameter, argsParameter).Compile();
// Squirrel away this function
dictionary[methodInfo] = func;
}
return dictionary;
}
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
if (targetMethod is null)
return 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); // Ensure the mapping has been created, and throw any exceptions now instead of later
_instance = instance;
}
}
The interesting thing about using a DispatchProxy
object is you can hook into method calls and make them do whatever you want, and at runtime.
I'll leave it as an exercise for the reader to figure out how to make the above work for static properties/methods. I only focused on non-static properties and methods because anonymous types don't have static members!
And the previous option reminds me of the final nuclear option. Did you know it's possible to dynamically generate .Net assemblies? And did you know that you can load such an assembly at runtime?
The keywords you need to search for are "C# IL generation".
Once you learn how to do that, the world is your oyster. You could write C# code that writes whatever C# code it takes to automatically declare and instantiate a thin wrapper type around your anonymous type.
I'm leaving this as an exercise for the reader.
Upvotes: 2