2

I'm working on a command tool in C#, although not for a terminal command-line. I have read the documentation on reflection and attributes but I'm not sure exactly what the "right" way to go about this is.

The problem isn't very complicated, but it needs to be easily extended. I need to just have Commands that are picked up and loaded in where their triggering strings are checked and if they match, methods are called. How I went about it just as a proof-of-concept was:

    [System.AttributeUsage(System.AttributeTargets.Class)]
    public class CommandAttribute : Attribute
    {
        public string Name { get; private set; } //e.g Help
        public string TriggerString { get; private set; } //e.g. help, but generally think ls, pwd, etc

        public CommandAttribute(string name, string triggerStrings)
        {
            this.Name = name;
            this.TriggerString = triggerString;
        }
    }

Now, I decorated the class and it will implement methods from an interface. Eventually there will be many commands and my idea is to make it easy for someone with minimal programming experience to jump in and make a command.

using Foo.Commands.Attributes;
using Foo.Infrastructure;

namespace Foo.Commands
{
    [Command("Help", "help")]
    public class Help : IBotCommand
    {
        // as an example, if the message's contents match up with this command's triggerstring
        public async Task ExecuteAction()

    }
}

This gets injected into the console app where it will load the commands and get passed messages

    public interface ICommandHandler
    {
        Task LoadCommands();
        Task CheckMessageForCommands();

    }

Then, everything with a matching attribute will get loaded in and when a message is received, it will check its contents against all CommandAttribute decorated classes' triggering strings, and if it matches, call the method ExecuteAction on that command class.

What I've seen/tried: I understand how to use reflection to get custom attribute data, however I'm confused as to getting the methods and calling them, and how all of this should be configured to be fairly performant with reflection being used. I see CLI tools and chat bots that use a similar method, I just cannot peek into their handlers to see how these get loaded in and I can't find a resource that explains how to go about accessing the methods of these classes. Attributes may not be the right answer here but I'm not sure how else to go about it.

Really, my main question is:

  • How do I setup The CommandHandler to load all of the attribute-decorated classes and call their methods, and how they should be instantiated within it. I know the second piece may be a bit more subjective but would newing them up be improper? Should they somehow be added to DI?

My solution ended up just using the Activator and lists. I still need to tweak this for performance and run more extensive stress tests, but here is my quick code for it:

// for reference: DiscordCommandAttribute is in Foo.Commands library where all the commands are, so for now it's the target as I removed the base class
// IDiscordCommand has every method needed, so casting it as that means down the line I can call my methods off of it. The base class was just for some reflection logic I was testing and was removed, so it's gone

public void LoadCommands() // called in ctor
{
    var commands =
        from t in typeof(DiscordCommandAttribute).Assembly.GetTypes()
        let attribute = t.GetCustomAttribute(typeof(DiscordCommandAttribute), true)
        where attribute != null
        select new { Type = t, Attribute = attribute };

    foreach (var obj in commands)
    {
        _commandInstances.Add((IDiscordCommand)Activator.CreateInstance(obj.Type));
        _commandAttributes.Add(obj.Attribute as DiscordCommandAttribute);
    }
}

There is probably a more sugary way to handle adding the objects to the lists, and some other data structure besides Lists might be more suitable, I'm just not sure if HashSet is right because it's not a direct Equals call. Eventually I will genericize the interface for this class and hide all of this logic in a base class. Still a lot of work to do.

Currently, just putting a stopwatch start before calling LoadCommands shows that the entire load takes 4ms. This is with 3 classes and a pretty anemic attribute, but I'm not too worried about the scale as I want any overhead on launch and not during command handling.

perustaja
  • 181
  • 2
  • 10
  • 1
    Also check https://stackoverflow.com/questions/752/how-to-create-a-new-object-instance-from-a-type – Progman Dec 31 '19 at 22:05
  • I had read the first post, however I wasn't sure how to move forward with accessing the methods. That second post may be the ticket, I'll test it right now. – perustaja Dec 31 '19 at 22:12
  • There was a comment here earlier about making these classes static. The problem is I want to have the flexibility of applying interfaces to them and some of these commands will have a repository injected for data access. Apologies for any confusion surrounded by not being explicit about that. – perustaja Dec 31 '19 at 22:59

1 Answers1

0

Using some code I wrote for this answer, you can find all types that implement an interface, e.g. IBotCommand, and then retrieve the custom attribute:

public static class TypeExt {
    public static bool IsBuiltin(this Type aType) => new[] { "/dotnet/shared/microsoft", "/windows/microsoft.net" }.Any(p => aType.Assembly.CodeBase.ToLowerInvariant().Contains(p));

    static Dictionary<Type, HashSet<Type>> FoundTypes = null;
    static List<Type> LoadableTypes = null;

    public static void RefreshLoadableTypes() {
        LoadableTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetLoadableTypes()).ToList();
        FoundTypes = new Dictionary<Type, HashSet<Type>>();
    }

    public static IEnumerable<Type> ImplementingTypes(this Type interfaceType, bool includeAbstractClasses = false, bool includeStructs = false, bool includeSystemTypes = false, bool includeInterfaces = false) {
        if (FoundTypes != null && FoundTypes.TryGetValue(interfaceType, out var ft))
            return ft;
        else {
            if (LoadableTypes == null)
                RefreshLoadableTypes();

            var ans = LoadableTypes
                       .Where(aType => (includeAbstractClasses || !aType.IsAbstract) &&
                                       (includeInterfaces ? aType != interfaceType : !aType.IsInterface) &&
                                       (includeStructs || !aType.IsValueType) &&
                                       (includeSystemTypes || !aType.IsBuiltin()) &&
                                       interfaceType.IsAssignableFrom(aType) &&
                                       aType.GetInterfaces().Contains(interfaceType))
                       .ToHashSet();

            FoundTypes[interfaceType] = ans;

            return ans;
        }
    }
}

public static class AssemblyExt {
    //https://stackoverflow.com/a/29379834/2557128
    public static IEnumerable<Type> GetLoadableTypes(this Assembly assembly) {
        if (assembly == null)
            throw new ArgumentNullException("assembly");
        try {
            return assembly.GetTypes();
        }
        catch (ReflectionTypeLoadException e) {
            return e.Types.Where(t => t != null);
        }
    }
}

Note: If you create types at runtime, you will need to run RefreshLoadableTypes to ensure they get returned.

If you are concerned about IBotCommand implementors existing without the CommandAttribute, you can filter the ImplementingTypes, otherwise:

var botCommands = typeof(IBotCommand)
                    .ImplementingTypes()
                    .Select(t => new { Type = t, attrib = t.GetTypeInfo().GetCustomAttribute<CommandAttribute>(false) })
                    .Select(ta => new {
                        ta.Type,
                        TriggerString = ta.attrib.TriggerString
                    })
                    .ToDictionary(tct => tct.TriggerString, tct => tct.Type);

With an extension method for your command Types:

public static class CmdTypeExt {
    public static Task ExecuteAction(this Type commandType) {
        var cmdObj = (IBotCommand)Activator.CreateInstance(commandType);
        return cmdObj.ExecuteAction();
    }
}

You can use the Dictionary like:

var cmdString = Console.ReadLine();
if (botCommands.TryGetValue(cmdString, out var cmdType))
    await cmdType.ExecuteAction();

Overall, I might suggest having a method attribute and having static methods in static classes for commands, so multiple (related?) commands can be bundled in a single class.

PS My command interpreters have help associates with each command, and categories to group commands, so perhaps some more attribute parameters and/or another IBotCommand method to return a help string.

NetMage
  • 26,163
  • 3
  • 34
  • 55