目录
第1部分——使用命令行参数和MEF识别任务处理程序的简单控制台EXE
实例化Lazy实例并调用ITaskHandler的方法OnExecute
将Element CopyLocalLockFileAssemblies添加到Task1和Task2
添加Post Build步骤将Task1和Task2的输出复制到插件文件夹
MefConsoleApplicationWithPluginsFolder.sln
问题描述
您正在开发命令行实用程序。这可以是一组自定义批处理作业,以支持企业应用程序的管理。您的命令行实用程序应执行多个后端任务,并且每个任务都通过特定于任务的参数进行参数化。如果我们的需求有很好的约束和限制,这看起来根本不是一个挑战。我的经验告诉我,任何软件在开始时都很简单,但随着时间的推移呈指数增长。您的最终用户将要求更多功能,很快,您将面临管理非常复杂的软件开发和交付的艰巨任务。
如果您正在构建企业应用程序托管工具,那么您的用户将是支持组织中有IT操作的人员,或者如果您是像Github或Aws / 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模块轻松扩展。Azure和AWS都提供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对象将分别填充键name、description及其值。请记住——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类中的属性Value和IsValueCreated是有用的。
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实现中,我们有两个方法——DisplayAllTasks和DisplayTaskSpecificHelp。要发现有关其他任务的信息,我们需要访问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处理程序类与主可执行文件分离的问题。这将允许我们以模块化方式扩展系统,而无需重新编译完整的可执行文件。
为契约创建类库
添加接口ITaskHandler和IParent。接口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。将对象AssemblyCatalog和DirectoryCatalog对象组合成一个单独的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
与前几节的实现类似。
创建Task1和Task2插件类库
///
///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添加到Task1和Task2
Task1和Task2的csproj文件需要修改一行。我们应该将元素CopyLocalLockFileAssemblies设置为true。我们为什么这样做?我们希望类库发现所有引用的程序集。如果这是.NET Framework,您可以通过设置“复制本地”属性来实现相同目的。对于.NET Standard和Core项目,不会立即发现依赖程序集。
<PropertyGroup>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
添加Post Build步骤将Task1和Task2的输出复制到插件文件夹
我们应该记住,我们正在摆脱“硬”的引用。EXE没有编译时知道存在Task1和Task2。在这种情况下,Task1和Task2的输出应该通过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
演示从外部文件夹加载插件程序集。