Ok so let's assume I have this kind of class hierarchy:
/// In 3rd party library
public class WidgetBase
{
protected void Register<THandler>(Action<THandler> handler) { /* do something */ }
}
public record Message1();
public record Message2();
public sealed class MyWidget : Base
{
public MyClass()
{
RegisterHandlers(this);
}
[Handler]
private void Handle(Message1 msg) {}
[Handler]
private void Handle(Message2 msg) {}
}
public static class Ext
{
// Would prefer extension or normal static method
// and not impose inheritance by putting this
// in an intermediatery base class.
public static void RegisterHandlers<T>(this T t)
{
// Discovers methods with 'Handler' attribute and calls t.Register()
}
}
So the objective is to implement RegisterHandlers
which would introspect over the object's methods and then produce an executable Expression
which calls the base classes register method. Think on the lines of Asp.Net Core Controller handlers.
I just can't figure out how to do this. The point of the expression would be to be more performant though even a pure reflection based solution would be ok.
I can discover the methods and even produce an expression like t => this.Handle(t)
but can't understand how the call to the generic base class method is done without the type.
There are a lot of similar questions in SO but couldn't find exact solution.
[Edit] Made the example more clear.
You can do something like:
public static void RegisterHandlers<T>(T t) where T : Base
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(Expression.Constant(t), method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(Expression.Constant(t), typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action>(Expression.Block(blockItems)).Compile();
compiled();
}
Note that there's no particular advantage to doing this, over just using reflection. You're not caching the generated compiled
or blockItems
, which is where the savings in using compiled expressions for this sort of thing come from.
You could extend it slightly however, and add in such a cache:
private static class Cache<T> where T : Base
{
public static readonly Action<T> Instance = CreateInstance();
private static Action<T> CreateInstance()
{
var methods = typeof(T).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).Where(x => x.GetCustomAttribute<HandlerAttribute>() != null);
var registerMethod = typeof(Base).GetMethod("Register", BindingFlags.NonPublic | BindingFlags.Instance);
var instanceParameter = Expression.Parameter(typeof(T));
var blockItems = new List<Expression>();
foreach (var method in methods)
{
if (method.IsGenericMethod || method.GetParameters().Length != 1 || method.ReturnType != typeof(void))
throw new Exception($"Invalid method signature for method {method}");
// The type of the Handle method's parameter (e.g. SomeType1 or SomeType2)
var parameterType = method.GetParameters()[0].ParameterType;
// MethodInfo for e.g. the Register<SomeType1> method
var typedRegisterMethod = registerMethod.MakeGenericMethod(parameterType);
// The type of delegate we'll pass to Register, e.g. Action<SomeType1>
var delegateType = typeof(Action<>).MakeGenericType(parameterType);
// Construct the x => Handle(x) delegate
var delegateParameter = Expression.Parameter(parameterType);
var delegateConstruction = Expression.Lambda(delegateType, Expression.Call(instanceParameter, method, delegateParameter), delegateParameter);
// Construct the Register(delegate) call
var methodCall = Expression.Call(instanceParameter, typedRegisterMethod, new[] { delegateConstruction });
// Add this to the list of expressions we'll put in our block
blockItems.Add(methodCall);
}
var compiled = Expression.Lambda<Action<T>>(Expression.Block(blockItems), instanceParameter).Compile();
return compiled;
}
}
public static void RegisterHandlers<T>(T t) where T : Base
{
Cache<T>.Instance(t);
}
Note how we're now taking the T
instance as a parameter, and this lets us cache the generated Action<T>
.
OK, your solution is more fun than mine. System.Linq.Expressions is an interesting namespace to play around with.
I probably should've been more clear about the performance considerations. It's ok if the registering is a bit slowish. It would be called in the class constructor. How ever the delegate/action that is registered should have same performance as a normal action as it will be executed a lot. The context is Akka.Net where you register the message handlers. I was looking into a way to remove some boilerplate and to use similar approach as Asp.Net Core does.
About the caching. I'm not sure that code is correct. You would need to cache per instance right since the generated delegates are bound to a specific object, which in this case defeats the point. I guess you could implement a partial cache for the discovery part and then do the final binding and compile when needed.
@MarkkuL No need to use expressions then: a delegate created using reflection is going to be just as fast as one created by a compiled expression. Re the caching, there's one compiled expression per type, and it takes the instance as a parameter: it's effectively just the method
void E(MyWidget w) { w.Register(Message1 msg => w.Handle(msg); w.Register(Message2 msg => w.Handle(msg)); }
. Notice how the methodE
is specialised to the typeMyWidget
, but takes the exact widgetw
as a parameter?I was able to get this version to work with slight modifications. The actual "register" method has a deglaration
protected void Receive<T>(Action<T> handler, Predicate<T> shouldHandle = null)
so I had to provide the predicate argument also:c# var nullPred = Expression.Convert( Expression.Constant(null), typeof(Predicate<>).MakeGenericType(messageType));