使用管理扩展性框架构建模块化控制台应用程序

目录

问题描述

示例——自定义后端管理工具

示例——Github

示例——Azure CLI

背景

可能的方法

方法1——自定义PowerShell cmdlet

方法2——基于每个任务的多个命令行应用程序

方法3——单个命令行应用程序,其中每个任务都是插件

简要介绍管理扩展性框架(MEF)

第1步——设计契约

第2步——实现实现契约的各种插件类

第3步——设计您的主机应用程序以接受发现的实现

第4步——使用MEF的目录类发现插件

在MEF中延迟加载类

第1部分——使用命令行参数和MEF识别任务处理程序的简单控制台EXE

概述

同意命令行参数的标准系统

.NET Core EXE

契约接口

Nuget包

创建任务处理程序类

创建容器类以导入任务处理程序的所有实例

使用AssemblyCatalog类发现插件

实例化Lazy实例并调用ITaskHandler的方法OnExecute

全部放在一起——从主方法执行插件

测试EXE

第2部分——通过实现简单的帮助系统扩展控制台EXE

概述

用于显示帮助的命令行协议

使用MEF元数据使每个任务都发出自己的文档

创建一个新的ITaskHanlder实现来显示帮助

MEF导入如何工作?

方法——DisplayAllTasks

方法——DisplayTaskSpecificHelp

全部放在一起——解析主要方法中的命令行参数

测试——显示所有任务列表

测试——显示任务特定的帮助

第3部分——在单独的程序集中实现任务

概述

为契约创建类库

创建.NET Core EXE

创建Task1和Task2插件类库

将Element CopyLocalLockFileAssemblies添加到Task1和Task2

添加Post Build步骤将Task1和Task2的输出复制到插件文件夹

使用代码

Github

MefConsoleApplication.sln

MefConsoleApplicationWithPluginsFolder.sln

参考


 

问题描述

您正在开发命令行实用程序。这可以是一组自定义批处理作业,以支持企业应用程序的管理。您的命令行实用程序应执行多个后端任务,并且每个任务都通过特定于任务的参数进行参数化。如果我们的需求有很好的约束和限制,这看起来根本不是一个挑战。我的经验告诉我,任何软件在开始时都很简单,但随着时间的推移呈指数增长。您的最终用户将要求更多功能,很快,您将面临管理非常复杂的软件开发和交付的艰巨任务。

如果您正在构建企业应用程序托管工具,那么您的用户将是支持组织中有IT操作的人员,或者如果您是像GithubAws / Azure这样的公司,那么有数百万开发人员。在本文中,我将通过利用Microsoft的管理扩展框架(Managed Extensibility Framework)来解决上述问题。

示例——自定义后端管理工具

Util.exe --task Backups --from 01/01/2019  --to 30/06/3019 --destination c:\Back\
Util.exe --task IndexDocuments
Util.exe --task ExtractImages -destination c:\dump\

示例——Github

git config –global user.name "[name]"
git commit -m "[ Type in the commit message]"
git diff –staged
git rm [file]
git checkout -b [branch name]

示例——Azure CLI

az group create --name myResourceGroup --location westeurope
az vm create --resource-group myResourceGroup --name myVM --image UbuntuLTS --generate-ssh-keys
az group delete --name myResourceGroup

背景

关于.NET的知识,C#是必不可少的。关于管理扩展框架(Managed Extensibility Framework)的一些想法很有用。

可能的方法

方法1——自定义PowerShell cmdlet

PowerShell是一个漂亮的框架,可以通过编写自定义.NET模块轻松扩展。AzureAWS都提供PowerShell接口,以便与云上各自的基础架构进行交互。PowerShell cmdlet是继承自CmdletBase的简单C#类。PowerShell cmdlet为您提供两全其美的功能——一个以Visual Studio.NET形式的强类型开发环境和一个出色的脚本平台,它将成为您的cmdlet(类库)的客户端。

 

方法2——基于每个任务的多个命令行应用程序

这是一种非常简单的方法。如果您的要求很小并且很紧迫,那么它很有效。

方法3——单个命令行应用程序,其中每个任务都是插件

在本文中,我将重点介绍第三种方法。一个命令行应用程序可以执行各种任务,每个任务都封装在自己的类中,然后在后续阶段,任务实现被物理隔离到组件中。

Util.exe --task Backups --from 01/01/2019  --to 30/06/3019 --destination c:\Back\
Util.exe --task IndexDocuments
Util.exe --task ExtractImages -destination c:\dump\

简要介绍管理扩展性框架(MEF

MEF是一个基于Microsoft .NET Framework / Core构建的库,简化了基于插件的应用程序的开发。MEF可以被认为是依赖注入框架,具有发现跨程序集分区的依赖关系的能力。MEF开辟了将主应用程序与实现分离的可能性。可以在此处找到Microsoft关于MEF的文档。MEF解决了软件开发生命周期中经常出现的一些非常相关的问题:

  • 您的应用程序在发布后是否可以扩展而无需重新编译整个代码库?
  • 您的应用程序是否可以以这样的方式设计,以便应用程序可以在运行时找到其模块而不是编译时间绑定?
  • 通过添加新模块/插件可以轻松扩展您的应用程序吗?

1步——设计契约

public interface IMyPlugin
{
    void DoSomeWork()
}

2步——实现实现契约的各种插件类

///
///Class1 in Assembly1
///
[Export(typeof(IMyPlugin))]
public class Plugin1 : IMyPlugin
{
}

///
///Class2 in Assembly2
///
[Export(typeof(IMyPlugin))]
public class Plugin2 : IMyPlugin
{
}

3步——设计您的主机应用程序以接受发现的实现

///
///Host application
///
public class MyHost 
{
    [ImportMany(typeof(IMyPlugin))]
    IMyPlugin>[] Plugins {get;set;}
}

4步——使用MEF的目录类发现插件

///
///TO BE DONE  - Show snippets of catalog here
///

MEF中延迟加载类

您可以使MEF延迟插件类的实例化。MEF使用类Lazy来发现实现并保持对插件元数据的引用。实例化仅在需要时完成。类Lazy允许插件导出元数据。例如,插件的唯一名称。

///
///Plugin class with metadata
///
[Export(typeof(IMyPlugin))]
[ExportMetadata("name","someplugin2")]
[ExportMetadata("description","Description of someplugin2")]
public class Plugin2 : IMyPlugin
{
}

属性ExportMetadata在这里起着至关重要的作用。当类MyHost已经使用MEF组成,每个可调用插件类的Lazy实例中的Dictionary对象将分别填充键namedescription及其值。请记住——plugin类尚未实例化。

///
///Host application - with lazy loading of plugins
///
public class MyHost 
{
    [ImportMany(typeof(IMyPlugin))]
    Lazy<IMyPlugin,Dictionary<string, object>>[] Plugins {get;set;}
}

1部分——使用命令行参数和MEF识别任务处理程序的简单控制台EXE

概述

在本小节中,我们将开发一个简单的EXE,它被模块化为Task处理程序类,并且这些类驻留在可执行文件本身中。

同意命令行参数的标准系统

出于本文的目的,我们将命令行应用程序命名为MefSkeletal.exe,第一个参数将是任务的简称。随后的所有参数都只是特定于任务的参数。

Myutil.exe [nameoftask] [任务参数1] [任务参数2]

MySkeletal.exe task1 arg0 arg1 arg3
MySkeletal.exe task2 arg5 arg6
MySkeletal.exe task3

.NET Core EXE

创建.NET Core EXE项目MefSkeletal。现在,我们将遵循一个简单的方法,其中所有任务处理程序类都包含在EXE项目中。在后面的阶段,我们将重构解决方案,以便每个任务都包含在一个单独的类库项目中。

契约接口

创建一个Contracts子文件夹并创建一个类文件ITaskHandler.cs

///
///Every Task handler must implement this interface
///
public interface ITaskHandler
{
    void OnExecute(string[] args)
}

Nuget

添加对以下包的引用:

  • Install-Package System.ComponentModel.Composition——Version 4.5.0

创建任务处理程序类

我们将添加Task特定的处理程序类。创建一个Tasks子文件夹,并在此子文件夹中添加以下类。每个类都实现了接口ITaskHandler。添加MEF元数据name以使其可被发现。

///
///Task 1 - 
///
[Export(typeof(ITaskHandler))]
[ExportMetadata("name","task1")]
public class Task1 : ITaskHandler
{
    public void OnExecute(string[] args)
    {
        Console.WriteLine("This is Task 1");
    }
}
///
///Task 2 - TO BE DONE - Add MEF metadata
///
[Export(typeof(ITaskHandler))]
[ExportMetadata("name","task2")]
public class Task2 : ITaskHandler
{
    public void OnExecute(string[] args)
    {
        Console.WriteLine("This is Task 2");
    }
}
///
///Task 3 - TO BE DONE - Add MEF metadata
///
[Export(typeof(ITaskHandler))]
[ExportMetadata("name","task3")]
public class Task3 : ITaskHandler
{
    public void OnExecute(string[] args)
    {
        Console.WriteLine("This is Task 3");
    }
}

创建容器类以导入任务处理程序的所有实例

///
///Container class 
///
public class Container
{	    
[ImportMany(typeof(ITaskHandler))] 
    public Lazy<itaskhandler, dictionary="">>[] Tasks { get; set; }        
}

使用AssemblyCatalog类发现插件

MEF提供了解决依赖关系的不同方法。对于我们的示例,我们将使用AssemblyCatalog来发现各种任务类:

///
///Container - Discover plugins
///
public class Container
{
    public Container() 
    { 
        var assem = System.Reflection.Assembly.GetExecutingAssembly(); 
        var cat = new AssemblyCatalog(assem); 
        var compose = new CompositionContainer(cat); 
        compose.ComposeParts(this); 
    } 
}

实例化Lazy实例并调用ITaskHandler的方法OnExecute

MEF元数据是将实现与其实际类分离的有用方法。我们为每个插件ITaskHandler实现类提供了一个简短的名称。我们将使用名称元数据属性来查找和实例化一个具体的ITaskHandler实例。在Lazy类中的属性ValueIsValueCreated是有用的。

internal void ExecTask(string taskname,string[] args)
{
    var lazy = this.Tasks.FirstOrDefault(t => (string)t.Metadata["name"] == taskname);
    if (lazy == null)
    {
        throw new ArgumentException($"No task with name={taskname} was found" );
    }
    ITaskHandler task = lazy.Value;
    task.OnExecute(args);
}

全部放在一起——从主方法执行插件

我们差不多完成了。main方法将绑定我们刚刚完成的所有操作。

static void Main(string[] args)
{
    try
    {
        Container container = new Container();
        string taskname = args[0];
        container.ExecTask(taskname, args);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

测试EXE

导航到输出文件夹并触发以下命令:

 

2部分——通过实现简单的帮助系统扩展控制台EXE

概述

如果我们简单的.NET Console EXE可以公开一些使用文档,那就太好了。与PowerShell帮助系统类似,我们希望在命令行上显示文档。理想情况下,我们希望每个Task处理程序都负责发布自己的文档。为什么不单独实现一个ITaskHandler专门用于显示帮助(HelpTask.cs)的实现?HelpTask类应该充分利用MEF元数据属性的名称和说明。

用于显示帮助的命令行协议

///
///When the user types any of the following commands 
///     1)Preliminary information should be displayed
///     2)The list of available Tasks should be displayed
///
MyUtil help
MyUtil /?
MyUtil
///
///When the user types the following command, 
/// 1)Display help information specific to the task. 
///
MyUtil help task1

使用MEF元数据使每个任务都发出自己的文档

我们将为每个ITaskHandler实现添加帮助元数据。该属性将存储有意义的使用信息: 

[Export(typeof(ITaskHandler))] 
[ExportMetadata("name", "task1")] 
[ExportMetadata("help", "This is Task1. Usage: --arg0 value0 --arg1 value1 --arg2 value2")] 
public class Task1 : ITaskHandler 
{ 
    public void OnExecute(string[] args) 
    { 
       Console.WriteLine("This is Task 1"); 
    } 
}

创建一个新的ITaskHanlder实现来显示帮助

HelpTask实现中,我们有两个方法——DisplayAllTasksDisplayTaskSpecificHelp。要发现有关其他任务的信息,我们需要访问Container类的实例。MEF属性Import帮助我们在Lazy实例化对象时注入依赖项:

[Export(typeof(ITaskHandler))]
[ExportMetadata("name", "help")]
public class HelpTask : ITaskHandler 
{ 
    public void OnExecute(string[] args) 
    { 
        if (args.Length  == 0)
        {
            DisplayAllTasks();
        }
        else
        {
            string taskname = args[0];
            DisplayTaskSpecificHelp(taskname);
        }
    }
    
    ///
    ///MEF will resolve this dependency at the time of instantiation
    ///
    [Import("parent")]
    public Container Parent { get; set; }        
}

MEF导入如何工作?

要解析由Import属性标记的依赖关系,MEF将查找使用Export属性注释的匹配属性:

public class Container
{
///
/// Used for dependency injection. E.g. HelpTask.cs 
/// would need this to discover all other Task objects
///
[Export("parent")]
public Container Parent { get; set; }
}

方法——DisplayAllTasks

/// 
/// Display a short list of all Task names. 
///         
private void DisplayAllTasks()
{
    Console.WriteLine("List of all Tasks");
    foreach(var lazy in this.Parent.Tasks)
    {
        string task = ((string)lazy.Metadata["name"]).ToLower();
        if (task == "help") continue;
        Console.WriteLine("-----------------------");
        string help = null;
        if (lazy.Metadata.ContainsKey("help"))
        {
            help = lazy.Metadata["help"] as string;
        }
        else
        {
            help = "";
        }
        Console.WriteLine($"{task}      {help}");
    }
}

方法——DisplayTaskSpecificHelp

/// 
/// Display the help description for the specified Task 
/// 
private void DisplayTaskSpecificHelp(string taskname)
{
    Console.WriteLine($"Displaying help on Task:{taskname}");
    var lazy = Parent.Tasks.FirstOrDefault
          (t => (string)t.Metadata["name"] == taskname.ToLower());
    if (lazy == null)
    {
        throw new ArgumentException($"No task with name={taskname} was found");
    }
    string help = (lazy.Metadata.ContainsKey("help") == false) ? 
    "No help documentation found" : (string)lazy.Metadata["help"];
    Console.WriteLine($"Task:{taskname}");
    Console.WriteLine($"{help}");
}

全部放在一起——解析主要方法中的命令行参数

Start
|
|
|
Analyze command line arguments
|
|
|
If zero arguments OR args[0] is 'help' then execute task 'help'
static void Main(string[] args)
{
    try
    {
        Container container = new Container();
        string taskname = null;
        if ((args.Length == 0) || (args[0].ToLower() == "help" ) || 
                                  (args[0].ToLower() =="/?"))
        {
            taskname = "help";//This is our custom ITaskHandler
                              //implementation responsible for displaying Help
        }
        else
        {
            taskname = args[0];
        }
        container.ExecTask(taskname, args.Skip(1).ToArray());
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

测试——显示所有任务列表

 

测试——显示任务特定的帮助

 

3部分——在单独的程序集中实现任务

概述

我们现在知道将复杂的可执行文件重构为多个类,其中每个类执行特定的任务。我们知道如何通过MEF延迟加载发现这些类,并最终通过契约接口调用这些方法。还有最后一步。我们需要解决将各种Task处理程序类与主可执行文件分离的问题。这将允许我们以模块化方式扩展系统,而无需重新编译完整的可执行文件。

为契约创建类库

添加接口ITaskHandlerIParent。接口IParent将为以下每个ITaskHandler实现提供上下文信息:

/// 
/// Should be implemented by every custom Task implementation
/// 
public interface ITaskHandler
{
    void OnExecute(string[] args);
}
/// Allows a Task implementation to interact with the host
/// E.g. Task1 can get to know about other Task implementations 
/// that have been discovered through MEF
public interface IParent
{
	Lazy<ITaskHandler,Dictionary<string,Object>>[] Tasks {get;}
}

创建.NET Core EXE

添加以下类。添加对Contracts类库项目的引用。为简洁起见,我只展示了部分源代码。

MefHost.cs

发现插件子文件夹中的所有子文件夹,并为每个子文件夹创建一个DirectoryCatalog。将对象AssemblyCatalogDirectoryCatalog对象组合成一个单独的AggregateCatalog实例。

public class MefHost : MefDemoWithPluginsFolder.Contracts.IParent
{
    ///
    ///Responsible for discovering plugins by using a combination 
    ///of AggregateCatalog, AssemblyCatalog and DirectoryCatalog
    ///
    public MefHost(string folderPlugins)
    {
        List<DirectoryCatalog> lstPluginsDirCatalogs = new List<DirectoryCatalog>();
        ///
        ///Create a collection of DirectoryCatalog objects
        ///
        string[] subFolders = System.IO.Directory.GetDirectories(folderPlugins);
        foreach(var subFolder in subFolders)
        {
            var dirCat = new DirectoryCatalog(subFolder, "*plugin*.dll");
            lstPluginsDirCatalogs.Add(dirCat);
        }
        var assem = System.Reflection.Assembly.GetExecutingAssembly();
        var catThisAssembly = new AssemblyCatalog(assem);
        ///
        ///Combine all the DirectoryCatalog and 
        ///AssemblyCatalog using AggregrateCatalog
        ///
        var catAgg = new AggregateCatalog(lstPluginsDirCatalogs);
        catAgg.Catalogs.Add(catThisAssembly);
        var compose = new CompositionContainer(catAgg);
        this.Parent = this;
        compose.ComposeParts(this);
    }
}

Program.cs

我们在EXE下使用了所有插件程序集的Plugins子文件夹:

class Program
{
    static void Main()
    {
        string exeFile = System.Reflection.Assembly.GetExecutingAssembly().Location;
        string exeFolder = System.IO.Path.GetDirectoryName(exeFile);
        string folderPlugins = System.IO.Path.Combine(exeFolder, "Plugins");
        MefHost host = new MefHost(folderPlugins);
        string taskname = null;
        if ((args.Length == 0) || (args[0].ToLower() == "help") || 
                                  (args[0].ToLower() == "/?"))
        {
            taskname = "help";
        }
        else
        {
            taskname = args[0];
        }
        host.ExecTask(taskname, args.Skip(1).ToArray());
    }
}

HelpTask.cs

与前几节的实现类似。

创建Task1Task2插件类库

///
///Task 1
///
[Export(typeof(MefDemoWithPluginsFolder.Contracts.ITaskHandler))]
[ExportMetadata("name", "task1")]
[ExportMetadata("help", "This is Task1. 
        Usage: --arg0 value0 --arg1 value1 --arg2 value2")]
public class Class1 : Contracts.ITaskHandler
{
    public void OnExecute(string[] args)
    {
        string sArgs = string.Join("|",args);
        Console.WriteLine($"This is Task 1. Arguments:{sArgs}");
    }
}

Element CopyLocalLockFileAssemblies添加到Task1Task2

Task1Task2csproj文件需要修改一行。我们应该将元素CopyLocalLockFileAssemblies设置为true我们为什么这样做?我们希望类库发现所有引用的程序集。如果这是.NET Framework,您可以通过设置复制本地属性来实现相同目的。对于.NET StandardCore项目,不会立即发现依赖程序集。

<PropertyGroup>
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>

添加Post Build步骤将Task1Task2的输出复制到插件文件夹

我们应该记住,我们正在摆脱的引用。EXE没有编译时知道存在Task1Task2。在这种情况下,Task1Task2的输出应该通过Plugins文件夹复制。对于这个项目,我们在EXE下直接选择了子文件夹'Plugins'。为避免重复,我们将编写BAT文件脚本来执行XCOPY操作。该BAT文件驻留在解决方案的根目录。解决方案的物理布局如下:

EXE----
        |
        |
       Bin--
            |
            |
            Release
                |
                |
                netcoreapp2.1
                    |
                    |
                   Plugins
                        |
                        |
                        Task 1
                        |
                        |   (all assemblies,PDB and other files from the Bin of Task1)
                        |
                        |
                        Task 2

                            (all assemblies,PDB and other files from the Bin of Task2)

使用代码

Visual Studio 2017将是必要的。

Github

MefConsoleApplication.sln

演示一个简单的.NET Core控制台EXE并使用MEF在同一个可执行程序集中发现ITaskHandler实现。

MefConsoleApplicationWithPluginsFolder.sln

演示从外部文件夹加载插件程序集。

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值