How to pass an argument by reference using Reflection

Viewed 638

How do I call a method that accepts a parameter by reference (using the ref keyword) using reflection? JsonConverter<T> defines the following method:

public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);

And I hold a derived type but have the generic parameter T as a Type object only. This doesn't compile:

converter.GetType().GetMethod("Read").Invoke(ref reader, type, options);

Clarification

Utf8JsonReader is a struct. My question wasn't about getting the method to be called, but rather how to do it without passing the parameter by value (and causing the struct to be copied).

2 Answers

If you had T, everything would be very simple:

// declare a delegate
private delegate T ReadDelegate(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);

// create and invoke a delegate
var readDelegate = Delegate.CreateDelegate(typeof(ReadDelegate), converter, "Read") as ReadDelegate;
var result = readDelegate.Invoke(ref reader, type, options);

Source: second search result from Google

But since you haven't, things go much more fun. Here is my solution (not as clean as I would like, but it works). First you need a supplementary classes:

internal abstract class ReadHelper
{
    public abstract object Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
}

internal class ReadHelper<T> : ReadHelper
{
    private readonly ReadDelegate _readDelegate;

    private delegate T ReadDelegate(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);

    public Reader(object converter)
    {
        _readDelegate = Delegate.CreateDelegate(typeof(ReadDelegate), converter, "Read") as ReadDelegate;
    }

    public override object Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
        => _readDelegate.Invoke(ref reader, type, options);
}

And now you can use them as this:

// I assume you know how to get this:
// var converter = ...
// var typeOfT = ...
// var reader = ...
// var type = ...
// var options = ...

var readHelperType = typeof(ReadHelper<>).MakeGenericType(typeOfT);
var readHelper = Activator.CreateInstance(readerType, converter) as Reader;

// and finally:
var result = readHelper.Read(ref reader, type, options);

And just in case, you don't know how to get typeOfT:

private Type FindTypeOfT(object converter)
{
    var type = converter.GetType();
    while (type != null)
        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(JsonConverter<>))
            return type.GetGenericArguments()[0];
        else
            type = type.BaseType;

    return null;
}

If all else fails, you can do this with compiled expressions:

class Program
{
    private delegate object ReadDelegate(JsonConverter converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);

    public static void Main()
    {
        var converter = new FooConverter();
        var converterType = converter.GetType();
        var typeOfT = typeof(int);

        var converterParameter = Expression.Parameter(typeof(JsonConverter));
        var readerParameter = Expression.Parameter(typeof(Utf8JsonReader).MakeByRefType());
        var typeToConvertParameter = Expression.Parameter(typeof(Type));
        var optionsParameter = Expression.Parameter(typeof(JsonSerializerOptions));
        var readMethodInfo = converterType.GetMethod("Read");
        var castConverter = Expression.Convert(converterParameter, converterType);
        var call = Expression.Call(
            castConverter,
            readMethodInfo,
            readerParameter,
            typeToConvertParameter,
            optionsParameter);
        var castResult = Expression.Convert(call, typeof(object));
        var lambda = Expression.Lambda<ReadDelegate>(
            castResult,
            converterParameter,
            readerParameter,
            typeToConvertParameter,
            optionsParameter).Compile();

        var reader = new Utf8JsonReader();
        var result = lambda(converter, ref reader, typeof(int), new JsonSerializerOptions());
    }
}

public class FooConverter : JsonConverter<int>
{
    public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => 3;

    public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) => throw new NotImplementedException();
}

Make sure you cache lambda -- it's relatively expensive to create, but very cheap to invoke.

This essentially builds a method at runtime, which looks a bit like this:

public object Lambda(
    JsonConverter converterParameter,
    ref Utf8JsonReader readerParameter,
    Type typeToConvertParameter,
    JsonSerializerOptions optionsParameter)
{
    var castConverter = (FooConverter)converterParameter;
    var call = castConverter.Read(ref readerParameter, typeToConvertParameter, optionsParameter);
    return (object)call;
}

That said, you would probably be better off writing a generic method, and then invoking it with reflection, rather than using reflection to invoke the Read method directly:

class Program
{
    public static void Main()
    {
        var converter = new FooConverter();
        var typeOfT = typeof(int);

        var methodInfo = typeof(Program).GetMethod("Foo").MakeGenericMethod(typeOfT);
        var result = methodInfo.Invoke(null, new[] { converter });
    }

    public static T Foo<T>(JsonConverter<T> converter)
    {
        var reader = new Utf8JsonReader();
        return converter.Read(ref reader, typeof(int), new JsonSerializerOptions());
    }
}

Admittedly this does just move the problem around, but it might work for you.

Related