Как искать метод в C # по атрибуту? - PullRequest
3 голосов
/ 10 марта 2019

Я пишу утилиту командной строки в C # для .NET Core. Я хочу позволить пользователю указать «действие» для запуска на основе параметра командной строки. Я имитирую параметры командной строки в стиле PowerShell, поэтому один из моих вариантов - /Action. Так, например, пользователь может вызвать приложение с /Action:Update или /Action:Reset.

В C # у меня есть метод для каждого действия, которое следует за определенной сигнатурой метода. Итак, для метода Update, описанного выше, у меня есть такой метод: public static int Update(Dictionary<string,string> cmdLineArgs, SomeObject obj). Каждый метод, связанный с действительным параметром в /Action, имеет одинаковую сигнатуру (одинаковые типы и имена переменных).

Сейчас у меня просто есть блок switch для вызова действий, но это кажется невероятно неэффективным:

int returnValue;
switch (parsedArgs["action"]) {
    case "update":
        returnValue = Update(parsedArgs, o);
        break;
    case "reset":
        returnValue = Reset(parsedArgs, o);
        break;
    ...
    default:
        returnValue=255;
        Console.WriteLine($"No such action {parsedArgs["action"]}.");
        break;
}

Я использовал атрибуты в контексте веб-API, и они кажутся естественной отправной точкой, чтобы сделать это более общим. В идеале это привело бы к ситуации, когда добавление нового действия так же просто, как написание его метода и добавление правильного атрибута с именем, по которому пользователь может вызвать его с помощью переключателя /Action. Моя идея состоит в том, чтобы создать пользовательский атрибут (скажем, AppActionName), а затем поместить этот атрибут в любой метод, который можно вызвать как действие из командной строки:

[AppActionName("update")]
public static int Update(Dictionary<string,string> cmdLineArgs, SomeObject obj)
...

[AppActionName("reset")]
public static int Reset(Dictionary<string,string> cmdLineArgs, SomeObject obj)
...

Альтернативой, о которой я подумал, которая могла бы использовать преимущества безопасности типов, было бы использование интерфейса, определяющего метод действия:

public interface IAppAction 
{
    int Run(Dictionary<string,string> cmdLineArgs, SomeObject obj);
}

[AppActionName("update")]
public class UpdateAction : IAppAction
{
    public int Run(Dictionary<string,string> cmdLineArgs, SomeObject obj)
    ...

[AppActionName("reset")]
public class ResetAction : IAppAction
{
    public int Run(Dictionary<string,string> cmdLineArgs, SomeObject obj)
    ...

В любом случае, в чем я не уверен, так это в том, как на самом деле искать, создавать и запускать метод.

В первом варианте (помещение AppActionName непосредственно в метод) я вижу две проблемы: 1) необходимость выяснить, как искать во всех методах в сборке те, у которых есть заданный атрибут, фильтрация, а затем как на самом деле вызовите метод и 2) если я не знаю, как это сделать, я не думаю, что с помощью этого метода смогу принудительно установить правильную сигнатуру метода.

int returnValue;
// in other languages you can get a variable and then call it, but this isn't other languages
// but you might do something like: myMethod = findMethodWithAttribute("update"); returnValue=myMethod(parsedArgs, o);

Второй вариант (интерфейс на интерфейсе, реализующем класс) кажется более безопасным с точки зрения типов и должен быть проще в реализации (объявить переменную интерфейса, а затем назначить ее экземпляру правильного класса), но я все еще не уверен, как на самом деле найти атрибут с правильным именем.

int returnValue;
// how would you do this correctly?
IAppAction appActionClass = new FindTheClassWithTheAttributeWithParameter("update")();
returnValue = appActionClass.Run(parsedArgs, o);

Так что я думаю, что суть моего вопроса такова: «Как мне найти, какой метод / класс имеет атрибут, который я определил с указанным параметром, и как мне на самом деле создать / вызвать результат?»

1 Ответ

1 голос
/ 10 марта 2019

Второй подход, как правило, должен быть легче иметь дело.

Я сделал пример (который вы можете запустить здесь ) для получения классов, которые реализуют интерфейс и их атрибут.

Учитывая что-то вроде этого:

public class AppActionNameAttribute : Attribute 
{ 
    public string Action { get; set; } 

    public AppActionNameAttribute(string action) { Action = action; } 
}

public interface IAppAction 
{
    int Run(Dictionary<string,string> cmdLineArgs, SomeObject obj);
}

[AppActionName("update")]
public class UpdateAction : IAppAction
{
    public int Run(Dictionary<string,string> cmdLineArgs, SomeObject obj) 
    { 
        Console.WriteLine("Handled :)"); 
        return 1;
    }
}

public class SomeObject { }

Вы можете сделать это:

var handlers = typeof(Program).Assembly
     // Get all types in the assembly
    .GetExportedTypes()
     // that are classes and implement IAppAction
    .Where(x => x.IsClass && x.GetInterface("IAppAction") != null)
    .Select(x => new 
    {
        // assuming they are always decorated with [AppActionName]
        Action = x.GetCustomAttribute<AppActionNameAttribute>().Action,
        // get a new instance, assuming parameterless constructor
        Handler = (IAppAction)Activator.CreateInstance(x)
    })
    // and convert it to a Dictionary that you can easily use
    .ToDictionary(x => x.Action, x => x.Handler);

, который вы можете сохранить (желательно в некотором статическом поле, поскольку вы не хотите запускать это часто) и просто использовать так:

private static Dictionary<string, IAppAction> _handlers = ...;

public static void Main()
{
    string action = Console.ReadLine();

    // you should check the action actually exists in the Dictionary
    var handler = _handlers[action];

    // and then run it:
    Console.WriteLine(handler.Run(someData, otherData));
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...