概述:
插件框架是一种软件设计模式,它允许开发者在不修改核心系统代码的情况下,通过添加或替换插件模块来扩展或改变系统的功能。这种设计模式在软件开发中非常常见,特别是在需要高度可定制化和可扩展性的系统中。今天给大家推荐一个 .net 中非常好用的插件框架:Prise。 由于项目需要,本人刚刚接触过这款插件,期间也遇到了一些问题,踩了一些坑,因此特意将使用过程记录下来,让需要的朋友少走弯路。
项目说明
插件系统一般有这么几个角色:
- 主机(Host)项目,也叫宿主项目,负责挂载插件,提供核心功能
- 契约(Contract)项目,负责约定插件要实现的接口或抽象类,一般用接口
- 插件(Plugin)项目,真正实现定制化的服务的项目,一般通过发布到指定目录,以供主机项目加载
下面我将通过简单的代码示例来介绍具体用法,本实例采用 .net8.0 实现,也可以采用 net6/7都是可以的。下图为本次示例的项目目录。
契约项目
创建一个类库项目,代码如下:
/// 契约服务
public interface IDataService
{
Task<dynamic> GetDataAsync(dynamic input);
}
/// 插件桥,以供插件调用主机中的服务
/// 在主机实现真正的业务逻辑,而在插件中采用代理类进行调用,后面介绍
public interface IPluginBridge
{
string GetConfig(string key);
}
主机项目
创建一个 webapi 项目,首先安装Nuget包:
<PackageReference Include="Prise" Version="6.0.0" />
<PackageReference Include="Prise.Plugin" Version="6.0.0" />
<PackageReference Include="Prise.Proxy" Version="6.1.0" />
在根目录下创建一个文件夹(_plugins),用于存放插件项目的发布文件。
接着创建一个帮助类,用于查找插件
public static class PluginHelper
{
/// <summary>
/// 在指定目录,寻找插件,这里的目录为主机跟目录下的 _plugins
/// </summary>
public static async Task<AssemblyScanResult> GetPlugin<T>(this IPluginLoader pluginLoader, string pluginName)
{
var path = Path.Combine(Path.GetFullPath("./"), "_plugins");
var pluginResults = await pluginLoader.FindPlugins<T>(path);
var pluginResult = pluginResults.FirstOrDefault(x => x.PluginType.Name == pluginName);
if (pluginResult == null)
throw new Exception($"plugin loader can not find {pluginName}");
return pluginResult;
}
}
实现插件桥
public class PluginBridgeProvider(IConfiguration configuration) : IPluginBridge
{
public string GetConfig(string key)
{
return configuration[key] ?? "";
}
}
然后添加一个控制器,作为本次测试的入口
[ApiController]
[Route("/api/[controller]/[action]")]
public class ValuesController(IPluginLoader pluginLoader, IPluginBridge pluginBridge) : ControllerBase
{
[HttpGet]
public async Task<dynamic> Get(string pluginName)
{
// 查找插件
var pluginInfo = await pluginLoader.GetPlugin<IDataService>(pluginName);
// 加载找到的插件
var pluginService = await pluginLoader.LoadPlugin<IDataService>(pluginInfo,
configure: (loadContext) =>
{
// 注册插件桥
// 如果在插件中要使用主机服务,则这里需要将主机中的服务注入进来
loadContext.AddHostService(pluginBridge);
});
// 调用插件提供的服务
return await pluginService.GetDataAsync(new { });
}
}
项目启动时,配置 Prise,并注入插件桥服务
builder.Services.AddSingleton<IPluginBridge, PluginBridgeProvider>();
builder.Services.AddPrise();
插件项目
创建一个类库项目,安装Nuget包,引用契约项目
<PackageReference Include="Prise.Plugin" Version="6.0.0" />
<PackageReference Include="Prise.ReverseProxy" Version="6.1.0" />
添加插件服务类
[Plugin(PluginType = typeof(IDataService))]
public class OneDataService : IDataService
{
//这个地方需要注意,如果在插件中需要使用主机服务,那必须写一个单独的插件内部服务类,这里为 IInternalService,下面会介绍
//在那里调用注入到插件中的主机服务,而不能直接在这个类中直接调用
//然后在这里引用内部服务,像下面这样:
[PluginService(ServiceType = typeof(IInternalService))]
private readonly IInternalService _internalService;
public async Task<dynamic> GetDataAsync(dynamic input)
{
return await Task.FromResult(_internalService.GetContent(input.ToString()));
}
}
主机服务代理类,继承自 ReverseProxy,实现插件桥
public class PluginBridgeProxy(object hostService) : ReverseProxy(hostService),
IPluginBridge
{
public string GetConfig(string key)
{
return InvokeOnHostService<string>(key);
}
}
插件内部服务类
public interface IInternalService
{
dynamic GetContent(string key);
}
// 这个 IPluginBridge 只能在插件内部服务中使用
public class InternalService(IPluginBridge pluginBridge) : IInternalService
{
public dynamic GetContent(string key)
{
return new
{
Version = "v2",
Content = pluginBridge.GetConfig(key)
};
}
}
添加插件配置类
// 这个 OneDataService 就是上面的插件服务实现类,主机测试的时候需要把这个插件名称传过来
[PluginBootstrapper(PluginType = typeof(OneDataService))]
public class Bootstrapper : IPluginBootstrapper
{
[BootstrapperService(
ServiceType = typeof(IPluginBridge), // The X.Contract.IPluginBridge interface
ProxyType = typeof(PluginBridgeProxy))] // The ReverseProxy type that lives inside of this project
private readonly IPluginBridge _pluginBridge;
public IServiceCollection Bootstrap(IServiceCollection services)
{
services.AddScoped(sp => _pluginBridge);
//内部服务注册
services.AddScoped<IInternalService, InternalService>();
return services;
}
}
然后配置插件项目的发布目录,指定到主机项目的 _plugins 下,这里需要注意,在设置的时候,需要在指定插件目录后面多指定一级(OnePlugin),否则会提示找不到插件,这里相对路径为:/MyPrise.Host/_plugins/OnePlugin, 如下图所示。配置好之后,发布一下插件项目
至此,所有示例代码写完了,接下来进行测试
测试
运行主机项目,输入插件名称进行测试
可以看到得到了结果, 插件框架最打的好处就是,修改插件项目,不需要重启主机服务,真正实现了热插拔效果,大家可以根据上面步骤试一下,看看能否实现热插拔,我在这里就不在演示。