C#-在命令工具中使用属性和反射[重复]

问题描述 投票:2回答:1

我正在使用C#开发命令工具,尽管不是用于终端命令行。我已经阅读了有关反射和属性的文档,但是我不确定确切的解决方法是什么。

问题不是很复杂,但是需要轻松扩展。我只需要在检查其触发字符串的位置拾取并加载命令,如果它们匹配,就调用方法。正如概念验证一样,我是如何做到的:

    [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;
        }
    }

现在,我装饰了该类,它将通过接口实现方法。最终将有许多命令,而我的想法是使编程经验最少的人可以轻松进入并创建命令。

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()

    }
}

这被注入到控制台应用程序中,它将在其中加载命令并获取传递的消息

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

    }

然后,将加载具有匹配属性的所有内容,并且在接收到消息时,它将根据所有CommandAttribute装饰类的触发字符串检查其内容,如果匹配,请对该命令类调用ExecuteAction方法。

我所见/尝试过的:我了解如何使用反射来获取自定义属性数据,但是我对于获取方法和调用它们以及如何将所有这些配置为在反射时具有相当的性能感到困惑正在使用。我看到了使用类似方法的CLI工具和聊天机器人,我只是无法窥视它们的处理程序以了解如何加载这些处理程序,也找不到任何资源来解释如何访问这些处理程序的methods类。在这里,属性可能不是正确的答案,但我不确定该怎么做。

真的,我的主要问题是:

  • 我如何设置CommandHandler来加载所有装饰有属性的类调用它们的方法,以及如何在其中实例化它们。我知道第二部分可能会比​​较主观,但是更新它们是否会不合适?是否应该以某种方式将它们添加到DI中?

我的解决方案最终仅使用激活器和列表。我仍然需要对此进行调整以提高性能并运行更广泛的压力测试,但这是我的快速代码:

// 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);
    }
}

可能有一种更灵活的方式来处理将对象添加到列表中的操作,而列表以外的其他数据结构可能更合适,我只是不确定HashSet是否正确,因为这不是直接的Equals调用。最终,我将泛化此类的接口并将所有这些逻辑隐藏在基类中。还有很多工作要做。

[目前,仅在调用LoadCommands之前放秒表会显示整个加载需要4毫秒。这具有3个类和一个非常贫乏的属性,但是我不太担心比例,因为我希望在启动时而不是在命令处理过程中产生任何开销。

c# .net-core custom-attributes
1个回答
0
投票

使用我为this answer编写的一些代码,您可以找到实现interface的所有类型,例如IBotCommand,然后检索自定义属性:

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);
        }
    }
}

注意:如果在运行时创建类型,则需要运行RefreshLoadableTypes以确保返回它们。

如果您担心不存在IBotCommandCommandAttribute实现者,则可以过滤ImplementingTypes,否则:

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);

使用命令Type的扩展方法:

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

您可以像使用Dictionary

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

总体上,我可能建议在静态类中为命令提供方法属性并具有静态方法,因此可以将多个(相关的?)命令捆绑在一个类中。

PS My命令解释器具有与每个命令关联的帮助,以及将命令分组的类别,因此,也许还有更多属性参数和/或另一个IBotCommand方法可返回帮助字符串。

© www.soinside.com 2019 - 2024. All rights reserved.