C# WebAPI 插件热插拔

背景

WebAPI 插件热插拔是指在不重启应用程序的情况下,能够动态地加载、更新或卸载功能模块(即插件)的能力。这种设计模式在软件开发中非常有用,尤其是在需要频繁更新或扩展功能的大型系统中。通过实现插件架构,可以将系统的不同部分解耦,使得它们可以独立开发、测试和部署。

对于WebAPI来说,这意味着服务端可以在运行时根据业务需求灵活调整其提供的API接口和服务逻辑,而无需担心每次修改都要重新启动整个应用,从而减少停机时间,提高系统的稳定性和灵活性。

程序演示

我们启动程序通过调用动态接口使用插件的增删改查等功能;,其中带{DynamicParam}的是你要使用的插件的名称;{funName}是你要使用插件的接口名称。

程序运行界面

C# WebAPI 插件热插拔

 

 

查询筛选接口

使用postman 进行查询筛选博客操作

 

C# WebAPI 插件热插拔

 

 

 

插件新增接口

插件的新增博客接口,然后看数据库变化

 

 

C# WebAPI 插件热插拔

 

C# WebAPI 插件热插拔

 

 

 

插件更新接口

插件的更新博客接口

C# WebAPI 插件热插拔

 

C# WebAPI 插件热插拔

 

 

插件删除接口

C# WebAPI 插件热插拔

 

C# WebAPI 插件热插拔

 

插件上传文件接口

C# WebAPI 插件热插拔

 

代码实现

前提准备

需要安装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 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));                    };                 });         }     } }     using System.Runtime.Loader;  namespace DynamicPluginApiDemo.Utils {     public class PluginDllHelper     {         public static List<IPluginDllApi> _labPlugins = new List<IPluginDllApi>();         private static string _pluginFolder = Path.Combine(Directory.GetCurrentDirectory(), "Plugins");           /// <summary>         /// 重新加载         /// </summary>         public static void ReLoadDll()         {             if (!Directory.Exists(_pluginFolder))                 Directory.CreateDirectory(_pluginFolder);              _labPlugins = new List<IPluginDllApi>();             foreach (var dllPath in Directory.GetFiles(_pluginFolder, "*.dll"))             {                 try                 {                     var assemblyLoadContext = new AssemblyLoadContext(Path.GetFileNameWithoutExtension(dllPath), true);                     var assembly = assemblyLoadContext.LoadFromAssemblyPath(dllPath);                      var pluginTypes = assembly.GetTypes()                         .Where(t => typeof(IPluginDllApi).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);                      foreach (var pluginType in pluginTypes)                     {                         if (Activator.CreateInstance(pluginType) is IPluginDllApi pluginInstance)                         {                             if (pluginInstance != null)                                 _labPlugins.Add((pluginInstance));                         }                     }                 }                 catch (Exception ex)                 {                     Console.WriteLine($"Failed to load assembly {dllPath}: {ex.Message}");                 }             }         }     } }

3,然后我们在程序启动的时候进行调用

C# WebAPI 插件热插拔

 

 4,接下来我们创建一个PluginController.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/ApifulPlugin/apidll/GetRequest/Query?SearchName=AAA         [HttpGet("GetRequest/{functionName}")]         public IActionResult GetRequest(string functionName, [FromQuery] Dictionary<string, string> queryParameters)         {             var instance = PluginDllHelper._labPlugins.FirstOrDefault(x => x.Name == _dynamicParam);             if (instance == null)                 return NotFound();             return instance.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._labPlugins.FirstOrDefault(x => x.Name == _dynamicParam);             if (instance == null)                 return NotFound();             return instance.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._labPlugins.FirstOrDefault(x => x.Name == _dynamicParam);             if (instance == null)                 return NotFound();             return instance.PostRequestForm(functionName, queryParameters, formData);         }        } }

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表的实体,服务,模型等。当然这些可以放在主项目中,通过项目引用使用主项目的代码。

C# WebAPI 插件热插拔

 

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文件夹下就可以了。

 注意:

系统必须被设计为能够识别和管理不同的插件版本,并且能够在运行时安全地切换这些版本。

结语

Web API插件的热插拔是一个复杂但非常有价值的功能,它不仅提高了系统的灵活性和可用性,还增强了用户体验。通过精心规划和技术实践,可以使这一特性成为现代Web应用和服务的一个亮点。

发表评论

评论已关闭。

相关文章