How to create an instance of generic type whose constructor requires a delegate function parameter?
Asked Answered
F

3

6

I need to use the following generic class and method ParseFrom() in it:

public sealed class MessageParser<T> : MessageParser where T : IMessage<T>
{
    public MessageParser(Func<T> factory); //constructor
    public T ParseFrom(byte[] data);
}

Now, I do not know the type of the parameter for this class at compile time, so I use type reflection and MakeGenericType() method to do that:

//Assuming itemInstance is given as input parameter 
Type typeArgument = itemInstance.GetType();
Type genericClass = typeof(MessageParser<>);
var genericType = genericClass.MakeGenericType(typeArgument);
var instance = Activator.CreateInstance(genericType);

It gives me a runtime error: MessageParser<> does not have a parameterless constructor. But when I try to pass Func<T> factory as a parameter for CreateInstance():

var instance = Activator.CreateInstance(genericType, () => Activator.CreateInstance(typeArgument));

it gives me a compile error: Cannot convert lambda expression to type 'string' because it is not a delegate type. Am I using the wrong syntax for a delegate function here?

Fasten answered 19/8, 2021 at 14:46 Comment(7)
Expression<Func<object>> create = () => Activator.CreateInstance(typeArgument); var instance = Activator.CreateInstance(genericType, Expression.Lambda(Expression.Convert(create, typeArgument)).Compile());Multilateral
@Multilateral That won't necessarily provide a delegate of the right type,. It can be any delegate of the right signature.Tonne
yes it will ... it just the same as Func<T> func = () => (T)Activator.CreateInstance(typeof(T)); where T is defined by typeArgument .... it's working fineMultilateral
@Multilateral No, in the second snippet you provided you're explicitly declaring the delegate type. When you explicitly declare the delegate type it will of course use that delegate type. Expression.Lambda however is documented as returning any delegate type it wants. There are overloads that do provide a delegate type, but you are not using them.Tonne
Yes, delegate is Func<Message> ... there is second expresion there Expression.Convert ... which makes right delegateMultilateral
@Multilateral The delegate is whatever the implementation wants to provide. It can be anything. It might work, it might not. You should not write code relying on private implementation details subject to change. Providing the delegate type explicitly is necessary to know that the code will reliably work.Tonne
@Multilateral The Convert only ensures that the return type of the delegate is of the right type (by performing the cast in the delegate body, which isn't really preferable when you can construct the original expression to be of the right type, but that's not a dealbreaker, just a missed optimization), not that the delegate itself is of the right type, which is a dealbreaker.Tonne
T
7

Constructing a delegate of an unknown type dynamically isn't as easy as using reflection to call a method, so the easiest option is to just write a statically typed method to construct the delegate, and then just call it using reflection.

public class DelegateCreator
{
    public static Func<T> MakeConstructorStatically<T>()
    {
        return Activator.CreateInstance<T>;
    }

    public static object MakeConstructorDynamically(Type type)
    {
        return typeof(DelegateCreator)
            .GetMethod(nameof(MakeConstructorStatically))
            .MakeGenericMethod(type)
            .Invoke(null, Array.Empty<object>());
    }
}
Tonne answered 19/8, 2021 at 15:7 Comment(0)
B
0

To create the Func<T> through reflection, CreateDelegate is the way to go. Therefore a method, that has the expected signature - including the type contraints (T is IMessage<T>)- is needed.

Here's how you can get it work. A downside is, that you will still need to use reflection to invoke the parser's methods, at least those that work with the type parameter:

public class CreateParserLateBound {

    //The method with the matching signature
    public static T MessageParserFactory<T>()
        where T : IMessage<T>
    {
        //your factory code, you pass to MessageParser(Func<T> factory) goes here...
        return default(T); 
    }

        ...
    
        // itemInstance == item that is IMesage<T>, with T unknown at compiletime;
        var itemType          = itemInstance.GetType();

        var boundParserType   = typeof(MessageParser<>).MakeGenericType(itemType);
        
        var boundFuncType     = typeof(Func<>).MakeGenericType(itemType);
        
        var factoryMethodInstance   = typeof(CreateParserLateBound )
                    .GetMethod("MessageParserFactory") 
                    .MakeGenericMethod(itemType)
                    .CreateDelegate(boundFuncType);
        
        var parserInstance    = Activator.CreateInstance(boundParserType, 
                     new object[]{ factoryMethodInstance } );

        //Invoke ParseFrom (also through reflection)   
        byte[] data = {1,2,3,4};                                 
        boundParserType.InvokeMember("ParseFrom", 
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.InvokeMethod, null, 
                parserInstance, new object[] {data});      


    

Full runnable code @ https://dotnetfiddle.net/RIOEXA

Boreal answered 19/8, 2021 at 18:8 Comment(7)
Thanks for the example. However, it was not useful to me, because MessageParser<> is a built-in class, along with its method ParseFrom(), which is placed in Google.Protobuf assembly. I am not able to move its parsing code into another place, like you did in your example, I just need to use it somehow.Fasten
Hi thx for the feedback. I just had put these stubs into my project to let it compile, you won't need them. It should none the less work with the true Protobuf API. What I don't understand is how you can parse a message (that byte array) and create a new instance of that unkown type T in a real-life-environment. Even if you can create the instance technicaly, how are you supposed to fill it with reasonable data. And then work with it in Protobuf?Boreal
I'm not sure. The original task was to make this code: byte[] content; Epic payload; payload = Epic.Parser.ParseFrom(content); a generic one, that is to be able to work with any type of object that implements Google.Protobuf.IMessage interface, not only the Epic object. The code worked just fine for one particular type of object. It became pain in the ass when I started to convert it to be generic.Fasten
I tried your proposed approach, it seems to be working overall, except for one little error inside InvokeMember() method: ArgumentNullException: Value cannot be null. (Parameter 'message') Any ideas what could be causing this? Should I provide anything instead of the third null parameter? i tried to evaluate the value of every other parameter during runtime and they were not null except for this one.Fasten
The method I've used to bind the delegate (that becomes the "factory" parameter of in the MessageParser ctor) returns default(T) which equals null. You must return sth reasonable there, which is not null. It's used by the ParseFrom method, I'd bet.Boreal
I tried to return simply the object of a given type inside that function, as the parsing implementation is not available to me, all I have is the given object and byte data that needs to be populated inside that object. Like so: return (Google.Protobuf.IMessage)Activator.CreateInstance(itemInstance.GetType()); But it says that a static local function cannot contain a reference to itemInstance. I think I'll have to try another approach eventually, as this one is still not working for me after multiple attempts to use it.Fasten
I already asked 5 dasy ago "Even if you can create the instance technically, how are you supposed to fill it with reasonable data? And then work with it in Protobuf?" It doesn't make sense to create an instance of an unknown type- unless you just need an empty mock.Boreal
J
0

The easy answer is to write your own generic method, then call that via reflection.

public static class Foo
{
    public static MessageParser<T> CreateParser<T>() where T : IMessage<T>, new()
        => new MessageParser<T>(() => new T());

    private static MethodInfo _createMethod = typeof(Foo)
        .GetMethods()
        .Where(m => m.Name == nameof(CreateParser) && m.IsGenericMethod)
        .Single();

    public static MessageParser CreateParser(Type type)
        => (MessageParser)_createMethod.MakeGenericMethod(type)
               .Invoke(null, new object[] { });
}
Joy answered 20/8, 2021 at 1:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.