目录
介绍
看起来很简单
创建.NET Core 3.1库
将项目添加到解决方案
设置目标框架
构建所有项目而不考虑依赖关系
添加对Microsoft.AspNetCore.Mvc的引用
插件控制器
加载程序集并告诉AspNetCore使用它
但实际上并没有那么简单
在插件中初始化服务
向应用程序公开插件服务
向插件公开应用程序服务
引用其他插件服务的插件
通用插件加载器
仅加载控制器的有趣替代方案
结论
下载应用程序 - 785.9 KB
我发现维护企业级Web API往往会导致大型单体应用程序,尤其是在Microsoft的MVC框架等较旧的技术中。微服务为划分独立服务提供了一个很好的解决方案,但我注意到这会导致大量离散的存储库,增加自动化部署的复杂性,涉及IIS配置,如果全局策略发生更改(安全性、日志记录、数据库连接等),每个微服务都需要被触及。转向ASP.NET Core,我想探索使用运行时插件控制器/服务。这里的想法是核心(没有双关语)应用程序处理所有常见策略,并且对这些策略的更新平等地影响所有控制器/服务。此外,建立服务器/容器或管理微服务的IIS配置没有任何开销,因为额外的控制器/服务只是在运行时添加到核心应用程序中。这种方法可以在许可模型中使用,以仅提供客户付费的那些服务,或者将新功能添加到Web API,而无需部署核心应用程序。不管利弊如何,本文的重点是演示如何为ASP.NET Core Web API应用程序实现适当的插件架构,使其成为架构考虑工具箱中的另一个工具。这种方法可以在许可模型中使用,以仅提供客户付费的那些服务,或者将新功能添加到Web API,而无需部署核心应用程序。不管利弊如何,本文的重点是演示如何为ASP.NET Core Web API应用程序实现适当的插件架构,使其成为架构考虑工具箱中的另一个工具。这种方法可以在许可模型中使用,以仅提供客户付费的那些服务,或者将新功能添加到Web API,而无需部署核心应用程序。不管利弊如何,本文的重点是演示如何为ASP.NET Core Web API应用程序实现适当的插件架构,使其成为架构考虑工具箱中的另一个工具。
基本概念似乎很简单。我们从两个项目开始:
当您下载并运行上面提到的参考项目时,它应该为您的本地IIS配置“Demo”站点名称,您应该看到:
这只不过是控制器响应一些文本。
该库应创建为参考项目中“应用程序”文件夹的同级。我发现我必须从命令行执行此操作。我们将创建两个项目:
打开CLI并输入:
dotnet new classlib -n "Plugin" -lang C#
dotnet new classlib -n "Interfaces" -lang C#
您现在应该看到Application文件夹和我们刚刚创建的两个文件夹及其项目(忽略我的“Article”文件夹)。例如:
将Interfaces和Plugin文件夹中的项目添加到Visual Studio中的解决方案。完成后,您应该拥有:
接下来,打开这两个项目的属性并将目标框架设置为.NET Core 3.1:
在Visual Studio的Tools => Options中,确保取消选中“仅在Run上构建启动项目和依赖项”。
这样做的原因是主项目没有引用该插件,除非您明确构建它们,否则不会构建任何更改——选中此复选框,对未引用的项目进行更改将导致很多“为什么我没有看到我的变化!”
添加对“plugin”项目的Microsoft.AspNetCore.Mvc引用。
我们将从一个只有控制器的简单插件开始。
将默认类“Class1.cs”重命名为“PluginController.cs ”并从一些非常基本的东西开始:
using Microsoft.AspNetCore.Mvc;
namespace Plugin
{
[ApiController]
[Route("[controller]")]
public class PluginController : ControllerBase
{
public PluginController()
{
}
[HttpGet("Version")]
public object Version()
{
return "Plugin Controller v 1.0";
}
}
}
这是有趣的部分。在Startup.cs的ConfigureServices方法中添加以下内容:
Assembly assembly =
Assembly.LoadFrom(@"C:\projects\PluginNetCoreDemo\Plugin\bin\Debug\netcoreapp3.1\Plugin.dll");
var part = new AssemblyPart(assembly);
services.AddControllers().PartManager.ApplicationParts.Add(part);
是的,我已经对路径进行了硬编码——这里的重点是演示插件控制器是如何连接的,而不是讨论如何确定插件列表和路径。这里有趣的是这行:
services.AddControllers().PartManager.ApplicationParts.Add(part);
不幸的是,除了“管理MVC应用程序的部分和特性”之外,几乎没有关于ApplicationPartManager的作用的文档或描述。然而,谷歌搜索“什么是ApplicationPartManager”,这个链接提供了进一步有用的描述。
上面的代码还需要:
using Microsoft.AspNetCore.Mvc.ApplicationParts;
构建项目后,您应该能够导航到localhost/Demo/plugin/version并查看:
这表明控制器端点已经连接好,可以被浏览器访问了!
一旦我们想做一些更有趣的事情,比如使用插件中定义的服务,生活就会变得更加复杂。原因是插件中没有任何东西允许连接服务——没有Startup类也没有ConfigureServices实现。尽管我试图弄清楚如何在主应用程序中使用反射来做到这一点,但我遇到了一些绊脚石,特别是在获取AddSingleton扩展方法的MethodInfo对象时。所以我想出了这里描述的方法,我发现它实际上更灵活。
还记得之前创建的“Interfaces”项目吗?这是我们将开始使用它的地方。首先,在该项目中创建一个简单的接口:
using Microsoft.Extensions.DependencyInjection;
namespace Interaces
{
public interface IPlugin
{
void Initialize(IServiceCollection services);
}
}
请注意,这需要添加包Microsoft.Extensions.DependencyInjection——确保您使用最新的3.1.x版本,因为我们使用的是.NET Core 3.1!
在Plugin项目中,创建一个简单的服务:
namespace Plugin
{
public class PluginService
{
public string Test()
{
return "Tested!";
}
}
}
在Plugin项目中,创建一个实现它的类,以初始化一个服务为例:
using Microsoft.Extensions.DependencyInjection;
using Interfaces;
namespace Plugin
{
public class Plugin : IPlugin
{
public void Initialize(IServiceCollection services)
{
services.AddSingleton();
}
}
}
现在将服务添加到控制器的构造函数中,该构造函数将被注入:
using Microsoft.AspNetCore.Mvc;
namespace Plugin
{
[ApiController]
[Route("[controller]")]
public class PluginController : ControllerBase
{
private PluginService ps;
public PluginController(PluginService ps)
{
this.ps = ps;
}
[HttpGet("Version")]
public object Version()
{
return $"Plugin Controller v 1.0 {ps.Test()}";
}
}
}
请注意,此时,如果我们尝试运行应用程序,我们将看到以下错误:
原因是我们没有在主应用程序中调用该Initialize方法,以便插件可以注册服务。我们将通过ConfigureServices方法中的反射来做到这一点:
var atypes = assembly.GetTypes();
var types = atypes.Where(t => t.GetInterface("IPlugin") != null).ToList();
var aservice = types[0];
var initMethod = aservice.GetMethod("Initialize", BindingFlags.Public | BindingFlags.Instance);
var obj = Activator.CreateInstance(aservice);
initMethod.Invoke(obj, new object[] { services });
现在我们看到控制器正在使用该服务!
上面的代码相当可怕,所以让我们重构它。我们还将让应用程序引用该Interfaces项目,因此我们可以这样做:
var atypes = assembly.GetTypes();
var pluginClass = atypes.SingleOrDefault(t => t.GetInterface(nameof(IPlugin)) != null);
if (pluginClass != null)
{
var initMethod = pluginClass.GetMethod(nameof(IPlugin.Initialize),
BindingFlags.Public | BindingFlags.Instance);
var obj = Activator.CreateInstance(pluginClass);
initMethod.Invoke(obj, new object[] { services });
}
这更干净,使用nameof,并且我们也不关心插件是否没有实现具有此接口的类——也许它没有任何服务。
所以现在,我们有了可以使用自己的服务的插件。需要注意的是,这种方法允许插件根据需要初始化服务:作为单例、作用域或瞬态服务。
但是将服务暴露给应用程序呢?
这是接口变得更有用的地方。让我们将服务重构为:
using Interfaces;
namespace Plugin
{
public class PluginService : IPluginService
{
public string Test()
{
return "Tested!";
}
}
}
并将其IPluginService定义为:
namespace Interfaces
{
public interface IPluginService
{
string Test();
}
}
现在让我们回到我们的Public应用程序控制器并实现IPluginService的依赖注入:
using System;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Interfaces;
namespace Demo.Controllers
{
[ApiController]
[Route("[controller]")]
public class Public : ControllerBase
{
private IPluginService ps;
public Public(IPluginService ps)
{
this.ps = ps;
}
[AllowAnonymous]
[HttpGet("Version")]
public object Version()
{
return new { Version = "1.00", PluginSays = ps.Test() };
}
}
}
同样,这次对于应用程序的public/version路由,我们得到:
原因是插件将其服务初始化为服务类型:
services.AddSingleton();
该行现在必须更改为:
services.AddSingleton();
现在我们看到:
但是我们破坏了插件:
所以我们还必须重构插件控制器来使用依赖注入的接口而不是具体的服务类型:
using Microsoft.AspNetCore.Mvc;
using Interfaces;
namespace Plugin
{
[ApiController]
[Route("[controller]")]
public class PluginController : ControllerBase
{
private IPluginService ps;
public PluginController(IPluginService ps)
{
this.ps = ps;
}
[HttpGet("Version")]
public object Version()
{
return $"Plugin Controller v 1.0 {ps.Test()}";
}
}
}
注意使用IPluginService的变化。现在世界又好了:
最后,我们要测试向插件公开应用程序服务。同样,必须使用Interfaces项目中的接口初始化服务,以便应用程序和插件都可以共享它:
namespace Interfaces
{
public interface IApplicationService
{
string Test();
}
}
还有我们的应用服务:
using Interfaces;
namespace Demo.Services
{
public class ApplicationService : IApplicationService
{
public string Test()
{
return "Application Service Tested!";
}
}
}
它的初始化:
services.AddSingleton();
现在在我们的插件中,将指示应注入此接口:
using Microsoft.AspNetCore.Mvc;
using Interfaces;
namespace Plugin
{
[ApiController]
[Route("[controller]")]
public class PluginController : ControllerBase
{
private IPluginService ps;
private IApplicationService appSvc;
public PluginController(IPluginService ps, IApplicationService appSvc)
{
this.ps = ps;
this.appSvc = appSvc;
}
[HttpGet("Version")]
public object Version()
{
return $"Plugin Controller v 1.0 {ps.Test()} {appSvc.Test()}";
}
}
}
我们看到:
可以对仅提供服务的插件使用相同的方法。例如,让我们添加另一个项目,Plugin2,它只实现一个服务:
using Interfaces;
namespace Plugin2
{
public class Plugin2Service : IPlugin2Service
{
public int Add(int a, int b)
{
return a + b;
}
}
}
和:
using Microsoft.Extensions.DependencyInjection;
using Interfaces;
namespace Plugin2
{
public class Plugin2 : IPlugin
{
public void Initialize(IServiceCollection services)
{
services.AddSingleton();
}
}
}
在应用程序的ConfigureServices方法中,我们将为第二个插件添加硬编码初始化(不要在家里这样做!):
Assembly assembly2 = Assembly.LoadFrom
(@"C:\projects\PluginNetCoreDemo\Plugin2\bin\Debug\netcoreapp3.1\Plugin2.dll");
var part2 = new AssemblyPart(assembly2);
services.AddControllers().PartManager.ApplicationParts.Add(part2);
var atypes2 = assembly2.GetTypes();
var pluginClass2 = atypes2.SingleOrDefault(t => t.GetInterface(nameof(IPlugin)) != null);
if (pluginClass2 != null)
{
var initMethod = pluginClass2.GetMethod(nameof(IPlugin.Initialize),
BindingFlags.Public | BindingFlags.Instance);
var obj = Activator.CreateInstance(pluginClass2);
initMethod.Invoke(obj, new object[] { services });
}
我希望很明显,这仅用于演示目的,您永远不会在ConfigureServices方法中对插件进行硬编码或复制和粘贴初始化代码!
而且,在我们的第一个插件中:
using Microsoft.AspNetCore.Mvc;
using Interfaces;
namespace Plugin
{
[ApiController]
[Route("[controller]")]
public class PluginController : ControllerBase
{
private IPluginService ps;
private IPlugin2Service ps2;
private IApplicationService appSvc;
public PluginController
(IPluginService ps, IPlugin2Service ps2, IApplicationService appSvc)
{
this.ps = ps;
this.ps2 = ps2;
this.appSvc = appSvc;
}
[HttpGet("Version")]
public object Version()
{
return $"Plugin Controller v 1.0 {ps.Test()}
{appSvc.Test()} and 1 + 2 = {ps2.Add(1, 2)}";
}
}
}
我们看到:
证明第一个插件正在使用第二个插件提供的服务,这一切都归功于ASP.NET提供的依赖注入。
一种方法是在appsettings.json文件中指定插件:
"Plugins": [
{ "Path": "C:\\projects\\PluginNetCoreDemo\\Plugin\\bin\\
Debug\\netcoreapp3.1\\Plugin.dll" },
{ "Path": "C:\\projects\\PluginNetCoreDemo\\Plugin2\\bin\\Debug\\netcoreapp3.1\\Plugin2.dll" }
]
我选择提供完整路径而不是使用Assembly.GetExecutingAssembly().Location,因为我认为不假设插件的DLL在应用程序的执行位置会更灵活。
修改该类AppSettings以列出插件:
public class AppSettings
{
public static AppSettings Settings { get; set; }
public AppSettings()
{
Settings = this;
}
public string Key1 { get; set; }
public string Key2 { get; set; }
public List Plugins { get; set; } = new List();
}
我们现在可以实现一个扩展方法来加载插件并调用服务初始化器(如果存在):
public static class ServicePluginExtension
{
public static IServiceCollection LoadPlugins(this IServiceCollection services,
AppSettings appSettings)
{
AppSettings.Settings.Plugins.ForEach(p =>
{
Assembly assembly = Assembly.LoadFrom(p.Path);
var part = new AssemblyPart(assembly);
// services.AddControllers().PartManager.ApplicationParts.Add(part);
// Correction from Colin O'Keefe so that things like customizing the routing or API versioning works,
// which gets ignored using the above commented out AddControllers line.
services.AddControllersWithViews().ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
var atypes = assembly.GetTypes();
var pluginClass = atypes.SingleOrDefault(t => t.GetInterface(nameof(IPlugin)) != null);
if (pluginClass != null)
{
var initMethod = pluginClass.GetMethod(nameof(IPlugin.Initialize),
BindingFlags.Public | BindingFlags.Instance);
var obj = Activator.CreateInstance(pluginClass);
initMethod.Invoke(obj, new object[] { services });
}
});
return services;
}
}
我们在ConfigureServices方法中调用它:
services.LoadPlugins();
当然,还有其他方法。
如果你唯一需要做的就是加载控制器,我偶然发现了这个实现,坦率地说,这对我来说是巫术,因为我对IApplicationProvider的工作原理一无所知。
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Controllers;
...
public class GenericControllerFeatureProvider : IApplicationFeatureProvider
{
public void PopulateFeature(IEnumerable parts, ControllerFeature feature)
{
Assembly assembly = Assembly.LoadFrom(p.Path);
var atypes = assembly.GetTypes();
var types = atypes.Where(t => t.BaseType == typeof(ControllerBase)).ToList();
feature.Controllers.Add(types[0].GetTypeInfo());
}
}
并被调用:
services.AddControllers().PartManager.FeatureProviders.Add
(new GenericControllerFeatureProvider());
这个实现的缺点是它在任何地方都没有我能找到的IServiceCollection实例,因此无法调用插件来注册它的服务。但是如果您的插件中只有控制器(它们仍然可以从您的应用程序中引用服务),那么这是另一种可行的方法。
我发现互联网上非常缺少关于如何实现插件控制器和在应用程序和插件之间共享服务的简明指南。希望这篇文章能填补这一空白。
应该注意的一件事——我还没有实现一个程序集解析器,以防插件直接引用dll而不是应用程序的执行位置。
理想情况下,不会在应用程序和插件之间(或插件和插件之间)共享服务,因为这会通过“接口”库(或更糟的是库)创建耦合,如果您更改实现,那么接口必须改变,然后一切都需要重建。可能的例外是高度稳定的服务,可能是数据库服务。一个有趣的想法是让主web-api应用程序简单地初始化插件和公共服务(日志记录、身份验证、授权等)——这有一定的吸引力,它让我想起了HAL 9000的外观在2001年进行配置:A Space Oddyssey——当模块被拔出时,糟糕的HAL开始退化!但是如前所述,这种方法可能会导致接口依赖,
无论如何,这为典型实现提供了一个有趣的替代方案:
我希望您发现这是创建ASP.NET Core Web API工具箱中的另一个选项。
https://www.codeproject.com/Articles/5321450/ASP-NET-Core-Web-API-Plugin-Controllers-and-Servic