1

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?

  • 3
    You don't really need the `Dictionary`. You could add your own custom attribute to those methods and then use Reflection to search for methods with that attribute. The attribute could contain the unique name with which to invoke that method. If you wanted to, you could do an initial search for methods with that attribute at startup and those you find to a `Dictionary` then, so as to make future invocations by name quicker. – jmcilhinney Jan 13 '23 at 10:48
  • OP, what you're considering with [CustomAttribute] seems reasonable and is not unheard-of. As long as you don't need the functions called in a specific determined order. – hugo Jan 13 '23 at 10:54
  • 1
    As stated by @jmcilhinney, reflection is the key here. You don't even need attributes if you want to add _all_ methods. You could just look them up using reflection. Note, this is just the answer to your question. However, this kind of design in C# is usually a sign of code smell. You come from another language, an are trying to port the constructs you are used to to C#. However, C# has it's own solution to these kinds of problems. I think this is an XY problem. What is the underlying issue you are trying to solve? – JHBonarius Jan 13 '23 at 10:56
  • Try use reflection to get method name and method from class https://stackoverflow.com/questions/2933221/can-you-get-a-funct-or-similar-from-a-methodinfo-object – MichaelMao Jan 13 '23 at 10:58
  • @JHBonarius I am building a text parser which can call functions with a single argument (which can be the result of another function, or something else that is parsed). I don't want to expose every function, just a predetermined set. I know how to achieve everything I need, it just doesn't feel clean. – emilyisstewpid Jan 13 '23 at 11:02

2 Answers2

0

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!");
Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
0

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());
noel
  • 531
  • 2
  • 7