Home > Software design >  How to register a function (eg. to a class)?
How to register a function (eg. to a class)?

Time:01-13

Context

I am writing an app which can call functions at runtime with the given function name.
The following setup could be an example:

internal class Caller
{
    public static Dictionary<string, Func<object, object>> reg = new Dictionary<string, Func<object, object>>();
    static Caller()
    {
        reg.Add("example1", Funcs.example1);
        reg.Add("example2", Funcs.example2);
    }
    
    public object Call(string name, object obj)
    {
        return reg[name](obj);
    }
}

internal static class Funcs
{
    public static object example1(object obj)
    {
        // do sth
        return obj;
    }

    public static object example2(object obj)
    {
        // do sth else
        return obj;
    }
}

Problem

The Problem with this is that my code isn't expandable. It would be difficult to register more functions at runtime/in another assembly, and it is a lot of typing for each function.
Coming from Python, I am used to using @decorators. They could be applied on every function I want to register, and then it would register it automatically.
In c# we have [Attributes], so I am thinking about giving each function a [CustomAttribute] and in my static Caller() I loop through every assembly, every static class in the assembly, and then register all functions with the [CustomAttribute].

I don't know whether this is ok to do from a design and performance standpoint. I know of [HttpGet] from ASP.NET, what approach do they use? (Couldn't find it in the source). Is there perhaps a design pattern I am missing?

CodePudding user response:

You can try obtaining all the methods via Reflection, e.g.

Let's obtain all public static methods from Funcs such that return object and take exactly one argument of type object (you can add your custom Where to apply more filters and narrow down the methods which should be registered):

using System.Linq;
using System.Reflection;

...

internal class Caller {
  public static readonly IReadOnlyDictionary<string, Func<object, object>> reg = typeof(Funcs)
    .GetMethods(BindingFlags.Static | BindingFlags.Public)
    .Where(method => method.ReturnType == typeof(object))
    .Where(method => method.GetParameters().Length == 1 && method
       .GetParameters()
       .All(prm => prm.ParameterType == typeof(object)))
    .ToDictionary(method => method.Name,
                  method => new Func<object, object>(arg => method.Invoke(null, new[] { arg })));

  public object Call(string name, object obj) {
    return reg[name](obj);
  }
}

Usage:

Caller caller = new Caller();

...

var result = caller.Call("example1", "Hello World!");

CodePudding user response:

If you want to use an Attribute to mark invokable methods @Dmitry Bychenkos answer will already get you most of the way there.

Just define an attribute

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
class RuntimeInvokable : Attribute { }

And some syntax sugar to help with the reflection (I've intentionally left the parameter count and return type checks out to keep it shorter, see Dmitrys answer for that)

static class ExtensionMethods
{
    public static Func<object, object?> ToFunc(this MethodInfo method)
    {
        return new Func<object, object?>(o => method.Invoke(null, new[] { o }));
    }
    public static IEnumerable<MethodInfo> GetRuntimeInvokableMethods(this Type type)
    {
        return type
            .GetMethods(BindingFlags.Public | BindingFlags.Static)
            .Where(m => m.GetCustomAttribute<RuntimeInvokable>() is not null);
    }
}

I think using a Dictionary is fine, so you won't have to use reflection on every call. I'd rather use a singleton instead of a static caller but that´s optional

class Caller
{
    Dictionary<string, Func<object, object?>> reg = new();

    public Caller()
    {
        reg = typeof(*someType*)
            .GetRuntimeInvokableMethods()
            .ToDictionary(x => x.Name, x => x.ToFunc());
    }

    public object? Call(string name, object obj)
    {
        return reg[name](obj);
    }
}

If you want to not be limited to methods of a single class, you can allow the attribute to be used for classes as well and check all types of an assembly

public static IEnumerable<Type> GetRuntimeInvokableTypes(this Assembly assembly)
{
    return assembly
        .GetTypes()
        .Where(x => x.GetCustomAttribute<RuntimeInvokable>() is not null);
}

And change the Caller constructor to

reg = typeof(Caller)
    .Assembly
    .GetRuntimeInvokableTypes()
    .SelectMany(x => x.GetRuntimeInvokableMethods())
    .ToDictionary(x => x.Name, x => x.ToFunc());
  •  Tags:  
  • c#
  • Related