找回密码
 立即注册
首页 业界区 业界 asp.net core如何实现Controller热更新

asp.net core如何实现Controller热更新

昝琳怡 3 小时前
可能是以往的习惯,我希望生产环境的服务可以热更新。有人会说Docker,可我希望能更简单一些。所以一直关注asp.net core如何热更新
早前读过这文章,工作关系没有继续学习。今天遇到一个关键问题,还是这文章启发了我。
https://www.cnblogs.com/artech/p/dynamic-controllers.html
第一步,dll需要在使用后,依然可以被修改和替换。我们需要一个继承自AssemblyLoadContext的类
3.gif
2.gif
  1. /// <summary>
  2. /// 支持真正卸载的插件加载上下文
  3. /// </summary>
  4. public class CollectiblePluginLoadContext : AssemblyLoadContext
  5. {
  6.     private readonly string _pluginPath;
  7.     private readonly string? _pluginDirectory;
  8.     public CollectiblePluginLoadContext(string pluginPath) : base(isCollectible: true)
  9.     {
  10.         _pluginPath = pluginPath;
  11.         _pluginDirectory = Path.GetDirectoryName(pluginPath);
  12.     }
  13.     protected override Assembly? Load(AssemblyName assemblyName)
  14.     {
  15.         // 尝试从插件目录加载依赖项
  16.         if (!string.IsNullOrEmpty(_pluginDirectory))
  17.         {
  18.             var assemblyPath = Path.Combine(_pluginDirectory, assemblyName.Name + ".dll");
  19.             
  20.             if (File.Exists(assemblyPath))
  21.             {
  22.                 // 使用流加载避免锁定依赖DLL文件
  23.                 using var fileStream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read);
  24.                 return LoadFromStream(fileStream);
  25.             }
  26.         }
  27.         
  28.         // 如果在插件目录中找不到,则返回null,让默认上下文处理
  29.         return null;
  30.     }
  31.     protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
  32.     {
  33.         // 尝试从插件目录加载非托管DLL
  34.         if (!string.IsNullOrEmpty(_pluginDirectory))
  35.         {
  36.             var unmanagedDllPath = Path.Combine(_pluginDirectory, unmanagedDllName + ".dll");
  37.             
  38.             if (File.Exists(unmanagedDllPath))
  39.             {
  40.                 // 对于非托管DLL,仍然需要使用路径加载
  41.                 // 但可以在加载后立即关闭句柄以减少锁定
  42.                 return LoadUnmanagedDllFromPath(unmanagedDllPath);
  43.             }
  44.         }
  45.         
  46.         // 如果在插件目录中找不到,则返回零,让默认上下文处理
  47.         return IntPtr.Zero;
  48.     }
  49.     public Assembly LoadPluginAssembly()
  50.     {
  51.         // 使用流加载避免锁定DLL文件
  52.         using var fileStream = new FileStream(_pluginPath, FileMode.Open, FileAccess.Read, FileShare.Read);
  53.         return LoadFromStream(fileStream);
  54.     }
  55. }
复制代码
CollectiblePluginLoadContext 第二步,ApplicationPartManager添加动态加载的Assembly。
一开始我以为加入前移除就能完整热加载
  1. // 从应用部件管理器中移除程序集
  2. var partToRemove = _partManager.ApplicationParts
  3.     .OfType()
  4.     .FirstOrDefault(p => p.Assembly == assembly);
  5. if (partToRemove != null)
  6. {
  7.     _partManager.ApplicationParts.Remove(partToRemove);
  8. }<br>
复制代码
  1. // 将程序集添加到应用部件管理器
  2. var assemblyPart = new AssemblyPart(assembly);
  3. _partManager.ApplicationParts.Add(assemblyPart);
复制代码
实际上不能,如一开头的文章说到的
  1. ...但是MVC默认情况下对提供的ActionDescriptor对象进行了缓存。<br>如果框架能够使用新的ActionDescriptor对象,需要告诉它当前应用提供的ActionDescriptor列表发生了改变,而这可以利用自定义的IActionDescriptorChangeProvider来实现。<br>为此我们定义了如下这个DynamicChangeTokenProvider类型,该类型实现了IActionDescriptorChangeProvider接口,并利用GetChangeToken方法返回IChangeToken对象通知<br>MVC框架当前的ActionDescriptor已经发生改变。从实现实现代码可以看出,当我们调用NotifyChanges方法的时候,状态改变通知会被发出去。
复制代码
  1. public class DynamicChangeTokenProvider : IActionDescriptorChangeProvider
  2. {
  3.     private CancellationTokenSource _source;
  4.     private CancellationChangeToken _token;
  5.     public DynamicChangeTokenProvider()
  6.     {
  7.         _source = new CancellationTokenSource();
  8.         _token = new CancellationChangeToken(_source.Token);
  9.     }
  10.     public IChangeToken GetChangeToken() => _token;
  11.     public void NotifyChanges()
  12.     {
  13.         var old = Interlocked.Exchange(ref _source, new CancellationTokenSource());
  14.         _token = new CancellationChangeToken(_source.Token);
  15.         old.Cancel();
  16.     }
  17. }
复制代码
有了蒋金楠(大内老A)的上面的代码,事情就好办了。以下是我的Program.cs的主要代码
  1. // 添加MVC服务以支持动态控制器
  2. builder.Services.AddControllers();
  3. builder.Services.AddSingleton<DynamicChangeTokenProvider>();
  4. builder.Services.AddSingleton<IActionDescriptorChangeProvider>(provider => provider.GetRequiredService<DynamicChangeTokenProvider>());
  5. var app = builder.Build();// 初始化改进的插件管理器(支持真正卸载)
  6. var partManager = app.Services.GetRequiredService();
  7. var tokenProvider = app.Services.GetRequiredService<DynamicChangeTokenProvider>();
  8. var improvedPluginManager = new ImprovedPluginManager(partManager, tokenProvider);
  9. // 预加载已存在的插件
  10. await improvedPluginManager.LoadAllPluginsAsync();
  11. // 映射控制器路由
  12. app.MapControllers();
  13. // 默认根路径
  14. app.MapGet("/", () => "Dynamic Controller Demo Running!");
  15. // 重新加载所有插件端点
  16. app.MapPost("/reload-plugins", async () =>
  17. {
  18.     await improvedPluginManager.LoadAllPluginsAsync();
  19.     return "Plugins reloaded with true unloading";
  20. });
  21. // 获取已加载插件列表
  22. app.MapGet("/loaded-plugins", () =>
  23. {
  24.     return improvedPluginManager.GetLoadedPlugins();
  25. });
复制代码
好了。程序跑起来。主程序没有Controller的实现。程序提供的WebApi,由plugin目录中的dll所包含Controller决定。至此期待的url正常响应了。将新的dll拷贝到plugin目录,替换旧的,post一下
  1. /reload-plugins
复制代码
WebApi也被新的程序响应了。当然我们也可以监视一下plugin目录,有文件修改时自动加载。
附上ImprovedPluginManager.cs的源代码
4.gif
  1.   1 public class ImprovedPluginManager
  2.   2 {
  3.   3     private readonly ApplicationPartManager _partManager;
  4.   4     private readonly Dictionary<string, (CollectiblePluginLoadContext context, Assembly assembly)> _loadedPlugins;
  5.   5     private readonly List<PluginInfo> _pluginInfos;
  6.   6     private readonly string _pluginsDirectory;
  7.   7     private readonly DynamicChangeTokenProvider _tokenProvider;
  8.   8
  9.   9     public ImprovedPluginManager(ApplicationPartManager partManager, DynamicChangeTokenProvider tokenProvider)
  10. 10     {
  11. 11         _tokenProvider = tokenProvider;
  12. 12         _partManager = partManager;
  13. 13         _loadedPlugins = new Dictionary<string, (CollectiblePluginLoadContext, Assembly)>();
  14. 14         _pluginInfos = new List<PluginInfo>();
  15. 15         
  16. 16         // 设置插件目录
  17. 17         _pluginsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
  18. 18         if (!Directory.Exists(_pluginsDirectory))
  19. 19         {
  20. 20             Directory.CreateDirectory(_pluginsDirectory);
  21. 21         }
  22. 22     }
  23. 23
  24. 24     public async Task<bool> LoadPluginAsync(string pluginPath)
  25. 25     {
  26. 26         try
  27. 27         {
  28. 28             if (!File.Exists(pluginPath))
  29. 29             {
  30. 30                 throw new FileNotFoundException($"Plugin file not found: {pluginPath}");
  31. 31             }
  32. 32
  33. 33             // 检查文件扩展名
  34. 34             if (!pluginPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
  35. 35             {
  36. 36                 throw new ArgumentException("Plugin must be a .dll file");
  37. 37             }
  38. 38
  39. 39             // 创建可收集的加载上下文
  40. 40             var loadContext = new CollectiblePluginLoadContext(pluginPath);
  41. 41            
  42. 42             // 加载程序集
  43. 43             var assembly = loadContext.LoadPluginAssembly();
  44. 44            
  45. 45             // 获取所有控制器类型
  46. 46             var controllerTypes = assembly.GetTypes()
  47. 47                 .Where(t => t.IsSubclassOf(typeof(ControllerBase)) && !t.IsAbstract)
  48. 48                 .ToList();
  49. 49
  50. 50             if (!controllerTypes.Any())
  51. 51             {
  52. 52                 throw new InvalidOperationException($"No controllers found in plugin: {pluginPath}");
  53. 53             }
  54. 54
  55. 55             // 检查是否已经加载了相同的程序集
  56. 56             if (_loadedPlugins.ContainsKey(assembly.FullName))
  57. 57             {
  58. 58                 await UnloadPluginAsync(assembly.FullName);
  59. 59             }
  60. 60
  61. 61             // 将程序集添加到应用部件管理器
  62. 62             var assemblyPart = new AssemblyPart(assembly);
  63. 63             _partManager.ApplicationParts.Add(assemblyPart);
  64. 64            
  65. 65             // 记录已加载的插件和上下文
  66. 66             _loadedPlugins[assembly.FullName] = (loadContext, assembly);
  67. 67
  68. 68             // 创建插件信息
  69. 69             var pluginInfo = new PluginInfo
  70. 70             {
  71. 71                 Name = Path.GetFileNameWithoutExtension(pluginPath),
  72. 72                 Version = assembly.GetName().Version?.ToString() ?? "Unknown",
  73. 73                 Description = "Dynamic controller plugin",
  74. 74                 FilePath = pluginPath,
  75. 75                 LoadedAt = DateTime.Now,
  76. 76                 ControllerTypes = controllerTypes.Select(t => t.Name).ToList()
  77. 77             };
  78. 78
  79. 79             _pluginInfos.Add(pluginInfo);
  80. 80
  81. 81             Console.WriteLine($"Successfully loaded plugin: {pluginPath}");
  82. 82             foreach (var controller in controllerTypes)
  83. 83             {
  84. 84                 Console.WriteLine($"  - Controller: {controller.Name}");
  85. 85             }
  86. 86
  87. 87             return true;
  88. 88         }
  89. 89         catch (Exception ex)
  90. 90         {
  91. 91             Console.WriteLine($"Failed to load plugin: {ex.Message}");
  92. 92             return false;
  93. 93         }
  94. 94     }
  95. 95
  96. 96     public async Task<bool> UnloadPluginAsync(string assemblyFullName)
  97. 97     {
  98. 98         try
  99. 99         {
  100. 100             if (!_loadedPlugins.ContainsKey(assemblyFullName))
  101. 101             {
  102. 102                 return false;
  103. 103             }
  104. 104
  105. 105             var (loadContext, assembly) = _loadedPlugins[assemblyFullName];
  106. 106
  107. 107            
  108. 108             // 从应用部件管理器中移除程序集
  109. 109             var partToRemove = _partManager.ApplicationParts
  110. 110                 .OfType()
  111. 111                 .FirstOrDefault(p => p.Assembly == assembly);
  112. 112
  113. 113             if (partToRemove != null)
  114. 114             {
  115. 115                 _partManager.ApplicationParts.Remove(partToRemove);
  116. 116             }
  117. 117
  118. 118             // 从已加载插件列表中移除
  119. 119             _loadedPlugins.Remove(assemblyFullName);
  120. 120
  121. 121             // 从插件信息列表中移除
  122. 122             var pluginInfo = _pluginInfos.FirstOrDefault(p => p.FilePath == assembly.Location);
  123. 123             if (pluginInfo != null)
  124. 124             {
  125. 125                 _pluginInfos.Remove(pluginInfo);
  126. 126             }
  127. 127
  128. 128             // 卸载加载上下文
  129. 129             loadContext.Unload();
  130. 130            
  131. 131             // 强制垃圾回收以释放程序集
  132. 132             GC.Collect();
  133. 133             GC.WaitForPendingFinalizers();
  134. 134
  135. 135             Console.WriteLine($"=== UNLOAD DIAGNOSTICS ===");
  136. 136             Console.WriteLine($"Successfully unloaded plugin: {assemblyFullName}");
  137. 137             Console.WriteLine($"Assembly location: {assembly.Location}");
  138. 138             Console.WriteLine($"Loaded plugins count after unload: {_loadedPlugins.Count}");
  139. 139             Console.WriteLine($"Plugin infos count after unload: {_pluginInfos.Count}");
  140. 140             Console.WriteLine($"Application parts count after unload: {_partManager.ApplicationParts.Count}");
  141. 141             Console.WriteLine("========================");
  142. 142             return true;
  143. 143         }
  144. 144         catch (Exception ex)
  145. 145         {
  146. 146             Console.WriteLine($"Failed to unload plugin: {ex.Message}");
  147. 147             return false;
  148. 148         }
  149. 149     }
  150. 150
  151. 151     public List<PluginInfo> GetLoadedPlugins()
  152. 152     {
  153. 153         return _pluginInfos.ToList();
  154. 154     }
  155. 155
  156. 156     public async Task<List<FileInfo>> ScanPluginFilesAsync()
  157. 157     {
  158. 158         var pluginDirInfo = new DirectoryInfo(_pluginsDirectory);
  159. 159         if (!pluginDirInfo.Exists)
  160. 160         {
  161. 161             return new List<FileInfo>();
  162. 162         }
  163. 163
  164. 164         var dllFiles = pluginDirInfo.GetFiles("*.dll", SearchOption.AllDirectories);
  165. 165         return dllFiles.ToList();
  166. 166     }
  167. 167
  168. 168     public async Task<bool> LoadAllPluginsAsync()
  169. 169     {
  170. 170         // 先卸载所有已加载的插件
  171. 171         var assembliesToUnload = _loadedPlugins.Keys.ToList();
  172. 172         foreach (var assemblyName in assembliesToUnload)
  173. 173         {
  174. 174             await UnloadPluginAsync(assemblyName);
  175. 175         }
  176. 176         
  177. 177         var pluginFiles = await ScanPluginFilesAsync();
  178. 178         var successCount = 0;
  179. 179
  180. 180         foreach (var pluginFile in pluginFiles)
  181. 181         {
  182. 182             if (await LoadPluginAsync(pluginFile.FullName))
  183. 183             {
  184. 184                 successCount++;
  185. 185             }
  186. 186         }
  187. 187         _tokenProvider.NotifyChanges();
  188. 188         Console.WriteLine($"Loaded {successCount} out of {pluginFiles.Count} plugins");
  189. 189         return successCount > 0;
  190. 190     }
  191. 191 }
复制代码
ImprovedPluginManager如有错误请指正。
 

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册