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

项目结构截图:

 

 

posted @   黄金程序员  阅读(2293)  评论(16编辑  收藏  举报
相关博文:
阅读排行:
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· 2分钟学会 DeepSeek API,竟然比官方更好用!
· .NET 使用 DeepSeek R1 开发智能 AI 客户端
· DeepSeek本地性能调优
· autohue.js:让你的图片和背景融为一体,绝了!
点击右上角即可分享
微信分享提示