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.
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.