byteram
byteram

Reputation: 147

Passing an object that implement an Interface without explicitly using a specific class

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

Answers (1)

Matt Thomas
Matt Thomas

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.

Use dynamic type

public 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!

Downsides

  • Slow
  • IDE won't help you refactor property/field/method names
  • Programming errors show up later (during runtime instead of compile time)

Dynamically make a proxy object at runtime with 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;
    }
}

Run it here

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!

Dynamically emit IL into a dynamically compiled assembly

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

Related Questions