Search code examples
c#lambdaexpression-trees.net-6.0minimal-apis

Dynamic delegate to minimal api


Hello fellow programmers. Basically, I want to pass a dynamically built delegate to minimal api MapGet or MapPost method. This is the method that constructs the delegate:

private static Delegate GetDelegate(Type type, MethodInfo method, ParameterInfo[] parameters)
{
    /* Method dynamically build this lambda expression:
    * (Type1 arg1, Type2 arg2, ..., TypeN argN) =>
    {
        var instance = GetTypeInstance(type);
        return instance.SomeMethod(arg1, arg2, ..., argN);
    }
    * Where N = number of arguments
    */

    var paramExpresions = new List<ParameterExpression>();
    foreach (var parameter in parameters)
          paramExpresions.Add(Expression.Parameter(parameter.ParameterType, parameter.Name));

    // Instance variable
    var instance = Expression.Variable(type, "instance");

    // Get instance of type
    MethodInfo getTypeInstance = typeof(DynamicControllerCompiler).GetMethod("GetTypeInstance");
    var callExpression = Expression.Call(getTypeInstance, Expression.Constant(type));
    var expressionConversion = Expression.Convert(callExpression, type);
    var assignSentence = Expression.Assign(instance, expressionConversion);

    var returnTarget = Expression.Label(method.ReturnType);
    var returnExpression = Expression.Return(returnTarget, Expression.Call(instance, method, paramExpresions), method.ReturnType);
    var returnLabel = Expression.Label(returnTarget, Expression.Default(method.ReturnType));

    var fullBlock = Expression.Block(
        new[] { instance },
        assignSentence,
        returnExpression,
        returnLabel
    );

    var lambda = Expression.Lambda(fullBlock, "testLambda", paramExpresions);
    return lambda.Compile();
}

The referenced method "GetTypeInstance" just returns service from container, but for simplicity let it just be:

public static object GetTypeInstance(Type type)
{
    return new EchoService();
}

The service is very simple:

public class EchoService
{
    public string Echo(string message)
    {
        return message;
    }

    public string EchoDouble(string message)
    {
        return message + "_" + message;
    }
}

So I want to map a get method to minimal api using it like this:

var type = typeof(EchoService);
foreach (var method in type.GetMethods())
{
    ParameterInfo[] parameters = method.GetParameters();
    var methodDelegate = GetDelegate(type, method, parameters);

    //test
    var result = methodDelegate.DynamicInvoke("test");

    app.MapGet($"api/{method.Name}", methodDelegate);
}

To test if the dynamic delegate works, I call it with "DynamicInvoke" and everything seems fine. Yet if I pass the delegate to MapGet the error is thrown:

System.InvalidOperationException: 'A parameter does not have a name! Was it generated? All parameters must be named.'

I cannot seem to understand what is going on. The delegate works fine if called by DynamicInvoke, and inside all the parameters has names.


Solution

  • The source of issue is how the expression trees compilation works. I don't have enough knowledge to explain why but by default it does not emit parameter names:

    Expression<Func<int, string>> expr = i => i.ToString();
    var compiledMethod = expr.Compile().Method;
    Console.WriteLine(compiledMethod.GetParameters().Count(p => p.Name != null)); // prints "0"
    

    You can try to overcome this by using Compile overload accepting bool preferInterpretation parameter (at least it works for my setup) but then you will face another issue - compiled method has actually two parameters (one for handling closures, I assume):

    compiledMethod = expr.Compile(true).Method;
    Console.WriteLine(compiledMethod.GetParameters().Count(p => p.Name != null)); // prints "2"
    

    And I don't see an easy workaround from here.

    Other approaches you can try - dynamic code generation using System.Reflection.Emit namespace or Roslyn but I would say that better one would be to generate this code during build time using source generators.

    Also there is another approach worth considering in some cases - create a generic method which has the needed setup for Map... and call it with reflection like was done in this answer.