人工智能机械臂 发表于 2025-2-6 23:14:58

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{    "api//{DynamicParam}")]        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      "GetRequest/{functionName}")]      public IActionResult GetRequest(string functionName, 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      "PostRequestBody/{functionName}")]      public IActionResult PostRequestBody(string functionName, Dictionary<string, string> queryParameters, 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      "PostRequestForm/{functionName}")]      public IActionResult PostRequestForm(string functionName, 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{    "api/")]        public class PluginManageController : ControllerBase    {      private static string _pluginFolder;      public PluginManageController()      {            _pluginFolder = Path.Combine(Directory.GetCurrentDirectory(), "Plugins");      }      "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>      "LoadPlugin")]      public IActionResult LoadPlugin( 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>      128 * 1024 * 1024)] // 128 MB      "MergeLoadPlugin")]      public IActionResult MergeLoadPlugin( IFormFile mainDll, List<IFormFile> otherDllList)      {            PluginDllHelper.MergeLoadPlugin(mainDll, otherDllList);            return Ok("LoadPlugin success");      }      "UnloadPlugin")]      public IActionResult UnloadPlugin( 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{    "BLOG", TableDescription = "博客")]    public class Blog    {      = "ID", IsIdentity = true, IsPrimaryKey = true)]      public int Id { get; set; }      = "Title")]      public string Title { get; set; }      = "Context")]      public string Context { get; set; }      = "UserId")]      public int UserId { get; set; }      = "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
项目结构截图:

 

 
页: [1]
查看完整版本: C# WebAPI 插件热插拔