C# WebAPI 插件热插拔
背景
WebAPI 插件热插拔是指在不重启应用程序的情况下,能够动态地加载、更新或卸载功能模块(即插件)的能力。这种设计模式在软件开发中非常有用,尤其是在需要频繁更新或扩展功能的大型系统中。通过实现插件架构,可以将系统的不同部分解耦,使得它们可以独立开发、测试和部署。
对于WebAPI来说,这意味着服务端可以在运行时根据业务需求灵活调整其提供的API接口和服务逻辑,而无需担心每次修改都要重新启动整个应用,从而减少停机时间,提高系统的稳定性和灵活性。
程序演示
我们启动程序通过调用动态接口使用插件的增删改查等功能;,其中带{DynamicParam}的是你要使用的插件的名称;{funName}是你要使用插件的接口名称。
程序运行界面
查询筛选接口
使用postman 进行查询筛选博客操作
插件新增接口
插件的新增博客接口,然后看数据库变化
插件更新接口
插件的更新博客接口
插件删除接口
插件上传文件接口
热插拔功能演示
1,我们先看一下目前加载的所有插件,发现只有一个BlogPluginApi。
2,然后我们加载一个PersonnelExamResultApi(人员管理)的插件。再次查询后,发现就有两个插件了。
3,接下来,我们执行以下人员管理插件的查询方法。可以看到正常请求了。
4,那么我们删除一下人员管理插件后,再次执行下查询方法。发现提示找不到对象了。
5,我们再查看一下所有插件,发现人员管理的插件被成功清除掉了。
代码实现
前提准备
需要安装nuget程序包:Newtonsoft.Json,SqlSugarCore。方便我们做类型转换和数据存储的相关功能;
1,首先我们创建一个webapi 的项目,然后定义一个插件IPluginDllApi.cs的接口(后续新增的类库需要继承用)
using Microsoft.AspNetCore.Mvc; namespace DynamicPluginApiDemo.Utils { public interface IPluginDllApi { string Name { get; } IActionResult GetRequest(string funName, Dictionary<string, string> queryParameters); IActionResult PostRequestForm(string funName, Dictionary<string, string> queryParameters, IFormCollection formData); IActionResult PostRequestBody(string funName, Dictionary<string, string> queryParameters, object jsonData); } }
2,创建一个插件帮助类和数据库帮助类。
using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Logging; using System.IO; using System.Reflection; using System.Runtime.Loader; namespace DynamicPluginApiDemo.Utils { public class PluginDllHelper { private static string _pluginFolder = Path.Combine(Directory.GetCurrentDirectory(), "Plugins"); private static List<PluginContextDto> PluginContextList = new List<PluginContextDto>(); /// <summary> /// 初始化加载指定的目录的dll文件。 /// </summary> public static void InitLoadDll() { if (!Directory.Exists(_pluginFolder)) Directory.CreateDirectory(_pluginFolder); foreach (var dllPath in Directory.GetFiles(_pluginFolder, "*.dll")) { var dllName = Path.GetFileNameWithoutExtension(dllPath); var stream = File.OpenRead(dllPath); var contextData = new PluginContextDto(new AssemblyLoadContext(dllName, true)); var assembly = contextData.AssemblyLoadContext.LoadFromStream(stream); var obj = assembly.GetTypes().Where(x => typeof(IPluginDllApi).IsAssignableFrom(x)).FirstOrDefault(); if (obj != null && !string.IsNullOrEmpty(obj.FullName)) { if (assembly.CreateInstance(obj.FullName) is IPluginDllApi instance) { contextData.Service = instance; PluginContextList.Add(contextData); } } } } /// <summary> /// 加载插件 /// </summary> /// <param name="dll"></param> /// <param name="domainName"></param> public static void LoadPlugin(string domainName, Stream stream) { //删除旧插件 UnloadPlugin(domainName); //新增或更新新插件 var contextData = new PluginContextDto(new AssemblyLoadContext(domainName, true)); var assembly = contextData.AssemblyLoadContext.LoadFromStream(stream); var obj = assembly.GetTypes().Where(x => typeof(IPluginDllApi).IsAssignableFrom(x)).FirstOrDefault(); if (obj != null && !string.IsNullOrEmpty(obj.FullName)) { if (assembly.CreateInstance(obj.FullName) is IPluginDllApi instance) { contextData.Service = instance; PluginContextList.Add(contextData); } } } /// <summary> /// 加载dll连同同文件夹下依赖的都加载到程序集中; /// </summary> /// <param name="path"></param> /// <returns></returns> public static void MergeLoadPlugin(IFormFile mainDll, List<IFormFile> otherDllList) { var fileName = Path.GetFileNameWithoutExtension(mainDll.FileName); var fileStream = mainDll.OpenReadStream(); var contextData = new PluginContextDto(new AssemblyLoadContext(fileName, true)); var assembly = contextData.AssemblyLoadContext.LoadFromStream(fileStream); //加载其他依赖dll foreach (var otherDll in otherDllList) { var otherDllName = Path.GetFileNameWithoutExtension(otherDll.FileName); var otherDllStream = otherDll.OpenReadStream(); contextData.AssemblyLoadContext.LoadFromStream(otherDllStream); } var obj = assembly.GetTypes().Where(x => typeof(IPluginDllApi).IsAssignableFrom(x)).FirstOrDefault(); if (obj != null && !string.IsNullOrEmpty(obj.FullName)) { if (assembly.CreateInstance(obj.FullName) is IPluginDllApi instance) { contextData.Service = instance; PluginContextList.Add(contextData); } } } /// <summary> /// 卸载插件 /// </summary> /// <param name="domainName"></param> public static void UnloadPlugin(string domainName) { var contextDataList = PluginContextList.Where(x => x.Service.Name == domainName); if (contextDataList != null) { var listItem = contextDataList.ToList(); foreach (var contextData in listItem) { contextData.Service = null;// 有一点坑,就是说如果被别处引用或者使用中,就无法释放; contextData.AssemblyLoadContext.Unload(); PluginContextList.Remove(contextData); for (int i = 0; contextData.weakReference.IsAlive & (i < 10); i++) { GC.Collect(); GC.WaitForPendingFinalizers(); } } } //var dlls = AppDomain.CurrentDomain.GetAssemblies().Where(x => x.GetName().Name?.Contains("System") == false); } /// <summary> /// 获取所有插件 /// </summary> /// <returns></returns> public static List<PluginContextDto> GetAllDll() { return PluginContextList; } } }
using System.Runtime.Loader; namespace DynamicPluginApiDemo.Utils { public class PluginContextDto { public PluginContextDto(AssemblyLoadContext assemblyLoadContext) { AssemblyLoadContext = assemblyLoadContext; weakReference = new WeakReference(AssemblyLoadContext, true); } public AssemblyLoadContext AssemblyLoadContext { get; set; } public WeakReference weakReference { get; set; } public IPluginDllApi Service { get; set; } } }
using SqlSugar; namespace DynamicPluginApiDemo.Utils { public static class DbHelper { public static SqlSugarClient Db; static DbHelper() { if (Db == null) //创建数据库对象 (用法和EF Dappper一样通过new保证线程安全) Db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = "server=192.168.1.61;Database=testdb;Uid=root;Pwd=MyNewPass@123;SslMode=None;AllowPublicKeyRetrieval=true;", DbType = SqlSugar.DbType.MySql, IsAutoCloseConnection = true }, db => { db.Aop.OnLogExecuting = (sql, pars) => { //获取原生SQL推荐 5.1.4.63 性能OK Console.WriteLine(UtilMethods.GetNativeSql(sql, pars)); }; }); } } }
3,然后我们在程序启动的时候进行调用
4,接下来我们创建一个PluginController.cs和PluginManageController.cs控制器。代码如下:
using DynamicPluginApiDemo.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; namespace DynamicPluginApiDemo.Controllers { [Route("api/[controller]/{DynamicParam}")] [ApiController] public class PluginController : ControllerBase { /// <summary> /// 动态插件的名称 /// </summary> private readonly string _dynamicParam = string.Empty; public PluginController(IHttpContextAccessor httpContextAccessor) { var dynamicParamKey = httpContextAccessor?.HttpContext?.GetRouteValue("DynamicParam"); if (dynamicParamKey != null) _dynamicParam = dynamicParamKey?.ToString() ?? string.Empty; } // GET: api/Plugin/apidll/GetRequest/Query?SearchName=AAA [HttpGet("GetRequest/{functionName}")] public IActionResult GetRequest(string functionName, [FromQuery] Dictionary<string, string> queryParameters) { var instance = PluginDllHelper.GetAllDll().FirstOrDefault(x => x.Service.Name == _dynamicParam); if (instance == null) return NotFound(); return instance.Service.GetRequest(functionName, queryParameters); } // POST: api/Dynamic/json // Receives query parameters and JSON body [HttpPost("PostRequestBody/{functionName}")] public IActionResult PostRequestBody(string functionName, [FromQuery] Dictionary<string, string> queryParameters, [FromBody] object jsonData) { var instance = PluginDllHelper.GetAllDll().FirstOrDefault(x => x.Service.Name == _dynamicParam); if (instance == null) return NotFound(); return instance.Service.PostRequestBody(functionName, queryParameters, jsonData); } // POST: api/Dynamic/form [HttpPost("PostRequestForm/{functionName}")] public IActionResult PostRequestForm(string functionName, [FromQuery] Dictionary<string, string> queryParameters, IFormCollection formData) { var instance = PluginDllHelper.GetAllDll().FirstOrDefault(x => x.Service.Name == _dynamicParam); if (instance == null) return NotFound(); return instance.Service.PostRequestForm(functionName, queryParameters, formData); } } }
using DynamicPluginApiDemo.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.IdentityModel.Logging; using NetTaste; using System.IO; using System.IO.Pipes; using System.Reflection; using System.Runtime.Loader; using static System.Net.Mime.MediaTypeNames; namespace DynamicPluginApiDemo.Controllers { [Route("api/[controller]")] [ApiController] public class PluginManageController : ControllerBase { private static string _pluginFolder; public PluginManageController() { _pluginFolder = Path.Combine(Directory.GetCurrentDirectory(), "Plugins"); } [HttpGet("GetAllPlugin")] public IActionResult GetAllPlugin() { var allP = PluginDllHelper.GetAllDll().Select(s => s.AssemblyLoadContext.Name).ToList(); return Ok(string.Join(",", allP)); //var pluginAssemblies = new List<Assembly>(); //foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) //{ // // 获取程序集中的所有类型 // var types = assembly.GetTypes(); // foreach (var type in types) // { // // 检查类型是否实现了IPluginDllApi且不是接口或抽象类 // if (typeof(IPluginDllApi).IsAssignableFrom(type) && !type.IsInterface && !type.IsAbstract) // { // // 如果找到了一个实现了IPluginDllApi的类型,添加此程序集到列表中并跳出循环 // pluginAssemblies.Add(assembly); // break; // } // } //} //return Ok(string.Join(" | ", pluginAssemblies.Select(s => s.FullName))); } /// <summary> /// 加载扩展插件到系统 /// </summary> /// <param name="file"></param> /// <returns></returns> [HttpPost("LoadPlugin")] public IActionResult LoadPlugin([FromForm] IFormFile file) { if (file != null) { var fileStream = file.OpenReadStream(); var fileName = Path.GetFileNameWithoutExtension(file.FileName); PluginDllHelper.LoadPlugin(file.FileName, fileStream); } return Ok("LoadPlugin success"); } /// <summary> /// 加载扩展插件到系统,含插件的依赖dll文件 /// </summary> /// <param name="mainDll">主要的dll</param> /// <param name="otherDllList">其他依赖的dll</param> /// <returns></returns> [RequestSizeLimit(128 * 1024 * 1024)] // 128 MB [HttpPost("MergeLoadPlugin")] public IActionResult MergeLoadPlugin([FromForm] IFormFile mainDll, [FromForm] List<IFormFile> otherDllList) { PluginDllHelper.MergeLoadPlugin(mainDll, otherDllList); return Ok("LoadPlugin success"); } [HttpGet("UnloadPlugin")] public IActionResult UnloadPlugin([FromQuery] string dllName) { PluginDllHelper.UnloadPlugin(dllName); return Ok("UnloadPlugin success"); } } }
5,webapi项目的内容差不多了。接下来我们创建一个类库项目,名字叫做BlogPluginApi。然后项目引用一下主项目,并且创建一个BlogPluginApi.cs文件继承主项目的IPluginDllApi。
using BlogPluginApi.Service; using DynamicPluginApiDemo.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.SqlServer.Server; namespace BlogPluginApi { public class BlogPluginApi : IPluginDllApi { public string Name => "BlogPluginApi"; public IActionResult GetRequest(string funName, Dictionary<string, string> queryParameters) { var result = string.Empty; switch (funName) { case "Query": result = BlogService.Query(queryParameters); break; } return new ContentResult { StatusCode = 200, ContentType = "text/plain", Content = result }; } public IActionResult PostRequestBody(string funName, Dictionary<string, string> queryParameters, object jsonData) { var result = string.Empty; switch (funName) { case "Save": result = BlogService.Save(jsonData); break; case "Update": result = BlogService.Update(jsonData); break; case "Delete": result = BlogService.Delete(jsonData); break; } return new ContentResult { StatusCode = 200, ContentType = "text/plain", Content = result }; } public IActionResult PostRequestForm(string funName, Dictionary<string, string> queryParameters, IFormCollection formData) { var result = string.Empty; switch (funName) { case "UploadFile": result = BlogService.UploadFile(queryParameters, formData); break; } return new ContentResult { StatusCode = 200, ContentType = "text/plain", Content = result }; } } }
6,然后我们增加blog表的实体,服务,模型等。当然这些可以放在主项目中,通过项目引用使用主项目的代码。
using SqlSugar; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlogPluginApi.Entitys.Blogs { [SugarTable("BLOG", TableDescription = "博客")] public class Blog { [SugarColumn(ColumnName = "ID", IsIdentity = true, IsPrimaryKey = true)] public int Id { get; set; } [SugarColumn(ColumnName = "Title")] public string Title { get; set; } [SugarColumn(ColumnName = "Context")] public string Context { get; set; } [SugarColumn(ColumnName = "UserId")] public int UserId { get; set; } [SugarColumn(ColumnName = "CreateTime")] public DateTime CreateTime { get; set; } } } using SqlSugar; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlogPluginApi.Models.Blogs { public class BlogInputDto { public int? Id { get; set; } public string? Title { get; set; } public string? Context { get; set; } public int? UserId { get; set; } public DateTime? CreateTime { get; set; } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlogPluginApi.Models.Blogs { public class SearchDto { public string SearchName { get; set; } } } using AutoMapper; using BlogPluginApi.Entitys.Blogs; using BlogPluginApi.Models.Blogs; using DynamicPluginApiDemo.Utils; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BlogPluginApi.Service { public static class BlogService { public static string Query(Dictionary<string, string> queryParameters) { SearchDto searchBlogDto = new SearchDto(); if (queryParameters.TryGetValue("SearchName", out var searchName)) { searchBlogDto.SearchName = searchName; } var result = string.Join(",", DbHelper.Db.Queryable<Blog>().Select(s => s.Title).ToList()); return result; } public static string Delete(object param) { var strParam = param.ToString(); if (string.IsNullOrEmpty(strParam)) return string.Empty; var deleteIds = JsonConvert.DeserializeObject<List<int>>(strParam); if (deleteIds != null) { var count = DbHelper.Db.Deleteable<Blog>().Where(x => deleteIds.Contains(x.Id)).ExecuteCommand(); return count.ToString(); } return string.Empty; } public static string Save(object param) { var strParam = param.ToString(); if (string.IsNullOrEmpty(strParam)) return string.Empty; var inputDto = JsonConvert.DeserializeObject<Blog>(strParam); if (inputDto != null) { var count = DbHelper.Db.Insertable(inputDto).ExecuteCommand(); } return "success"; } public static string Update(object param) { var strParam = param.ToString(); if (string.IsNullOrEmpty(strParam)) return string.Empty; var inputDto = JsonConvert.DeserializeObject<Blog>(strParam); if (inputDto != null) { var count = DbHelper.Db.Updateable(inputDto).ExecuteCommand(); } return "success"; } public static string UploadFile(Dictionary<string, string> queryParameters, IFormCollection formData) { string UploadId = string.Empty; if (queryParameters.TryGetValue("uploadId", out var uploadId)) { UploadId = uploadId; } var file = formData.Files.FirstOrDefault(); if (file != null) return file.FileName; return string.Empty; } } }
7,然后我们生成一下BlogPluginApi项目,把生成的dll文件放在放在主项目的Plugins文件夹下就可以了。
8,人员管理插件代码:
using DynamicPluginApiDemo.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace PersonnelExamResultApi { public class PersonnelExamResultApi : IPluginDllApi { public string Name => "PersonnelExamResultApi"; public IActionResult GetRequest(string funName, Dictionary<string, string> queryParameters) { var result = "success"; return new ContentResult { StatusCode = 200, ContentType = "text/plain", Content = result }; } public IActionResult PostRequestBody(string funName, Dictionary<string, string> queryParameters, object jsonData) { var result = "success"; return new ContentResult { StatusCode = 200, ContentType = "text/plain", Content = result }; } public IActionResult PostRequestForm(string funName, Dictionary<string, string> queryParameters, IFormCollection formData) { var result = "success"; return new ContentResult { StatusCode = 200, ContentType = "text/plain", Content = result }; } public void SayHello() { Console.WriteLine("Hello from MyLibrary!"); } } }
注意:
系统必须被设计为能够识别和管理不同的插件版本,并且能够在运行时安全地切换这些版本。
结语
Web API插件的热插拔是一个复杂但非常有价值的功能,它不仅提高了系统的灵活性和可用性,还增强了用户体验。通过精心规划和技术实践,可以使这一特性成为现代Web应用和服务的一个亮点。
文献参考:.NetCore新版本的AssemblyLoadContext热插拔实现_哔哩哔哩_bilibili
项目结构截图:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· autohue.js:让你的图片和背景融为一体,绝了!