可能是以往的习惯,我希望生产环境的服务可以热更新。有人会说Docker,可我希望能更简单一些。所以一直关注asp.net core如何热更新
早前读过这文章,工作关系没有继续学习。今天遇到一个关键问题,还是这文章启发了我。
https://www.cnblogs.com/artech/p/dynamic-controllers.html
第一步,dll需要在使用后,依然可以被修改和替换。我们需要一个继承自AssemblyLoadContext的类
- /// <summary>
- /// 支持真正卸载的插件加载上下文
- /// </summary>
- public class CollectiblePluginLoadContext : AssemblyLoadContext
- {
- private readonly string _pluginPath;
- private readonly string? _pluginDirectory;
- public CollectiblePluginLoadContext(string pluginPath) : base(isCollectible: true)
- {
- _pluginPath = pluginPath;
- _pluginDirectory = Path.GetDirectoryName(pluginPath);
- }
- protected override Assembly? Load(AssemblyName assemblyName)
- {
- // 尝试从插件目录加载依赖项
- if (!string.IsNullOrEmpty(_pluginDirectory))
- {
- var assemblyPath = Path.Combine(_pluginDirectory, assemblyName.Name + ".dll");
-
- if (File.Exists(assemblyPath))
- {
- // 使用流加载避免锁定依赖DLL文件
- using var fileStream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read);
- return LoadFromStream(fileStream);
- }
- }
-
- // 如果在插件目录中找不到,则返回null,让默认上下文处理
- return null;
- }
- protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
- {
- // 尝试从插件目录加载非托管DLL
- if (!string.IsNullOrEmpty(_pluginDirectory))
- {
- var unmanagedDllPath = Path.Combine(_pluginDirectory, unmanagedDllName + ".dll");
-
- if (File.Exists(unmanagedDllPath))
- {
- // 对于非托管DLL,仍然需要使用路径加载
- // 但可以在加载后立即关闭句柄以减少锁定
- return LoadUnmanagedDllFromPath(unmanagedDllPath);
- }
- }
-
- // 如果在插件目录中找不到,则返回零,让默认上下文处理
- return IntPtr.Zero;
- }
- public Assembly LoadPluginAssembly()
- {
- // 使用流加载避免锁定DLL文件
- using var fileStream = new FileStream(_pluginPath, FileMode.Open, FileAccess.Read, FileShare.Read);
- return LoadFromStream(fileStream);
- }
- }
复制代码 CollectiblePluginLoadContext 第二步,ApplicationPartManager添加动态加载的Assembly。
一开始我以为加入前移除就能完整热加载- // 从应用部件管理器中移除程序集
- var partToRemove = _partManager.ApplicationParts
- .OfType()
- .FirstOrDefault(p => p.Assembly == assembly);
- if (partToRemove != null)
- {
- _partManager.ApplicationParts.Remove(partToRemove);
- }<br>
复制代码- // 将程序集添加到应用部件管理器
- var assemblyPart = new AssemblyPart(assembly);
- _partManager.ApplicationParts.Add(assemblyPart);
复制代码 实际上不能,如一开头的文章说到的- ...但是MVC默认情况下对提供的ActionDescriptor对象进行了缓存。<br>如果框架能够使用新的ActionDescriptor对象,需要告诉它当前应用提供的ActionDescriptor列表发生了改变,而这可以利用自定义的IActionDescriptorChangeProvider来实现。<br>为此我们定义了如下这个DynamicChangeTokenProvider类型,该类型实现了IActionDescriptorChangeProvider接口,并利用GetChangeToken方法返回IChangeToken对象通知<br>MVC框架当前的ActionDescriptor已经发生改变。从实现实现代码可以看出,当我们调用NotifyChanges方法的时候,状态改变通知会被发出去。
复制代码- public class DynamicChangeTokenProvider : IActionDescriptorChangeProvider
- {
- private CancellationTokenSource _source;
- private CancellationChangeToken _token;
- public DynamicChangeTokenProvider()
- {
- _source = new CancellationTokenSource();
- _token = new CancellationChangeToken(_source.Token);
- }
- public IChangeToken GetChangeToken() => _token;
- public void NotifyChanges()
- {
- var old = Interlocked.Exchange(ref _source, new CancellationTokenSource());
- _token = new CancellationChangeToken(_source.Token);
- old.Cancel();
- }
- }
复制代码 有了蒋金楠(大内老A)的上面的代码,事情就好办了。以下是我的Program.cs的主要代码- // 添加MVC服务以支持动态控制器
- builder.Services.AddControllers();
- builder.Services.AddSingleton<DynamicChangeTokenProvider>();
- builder.Services.AddSingleton<IActionDescriptorChangeProvider>(provider => provider.GetRequiredService<DynamicChangeTokenProvider>());
- var app = builder.Build();// 初始化改进的插件管理器(支持真正卸载)
- var partManager = app.Services.GetRequiredService();
- var tokenProvider = app.Services.GetRequiredService<DynamicChangeTokenProvider>();
- var improvedPluginManager = new ImprovedPluginManager(partManager, tokenProvider);
- // 预加载已存在的插件
- await improvedPluginManager.LoadAllPluginsAsync();
- // 映射控制器路由
- app.MapControllers();
- // 默认根路径
- app.MapGet("/", () => "Dynamic Controller Demo Running!");
- // 重新加载所有插件端点
- app.MapPost("/reload-plugins", async () =>
- {
- await improvedPluginManager.LoadAllPluginsAsync();
- return "Plugins reloaded with true unloading";
- });
- // 获取已加载插件列表
- app.MapGet("/loaded-plugins", () =>
- {
- return improvedPluginManager.GetLoadedPlugins();
- });
复制代码 好了。程序跑起来。主程序没有Controller的实现。程序提供的WebApi,由plugin目录中的dll所包含Controller决定。至此期待的url正常响应了。将新的dll拷贝到plugin目录,替换旧的,post一下WebApi也被新的程序响应了。当然我们也可以监视一下plugin目录,有文件修改时自动加载。
附上ImprovedPluginManager.cs的源代码

- 1 public class ImprovedPluginManager
- 2 {
- 3 private readonly ApplicationPartManager _partManager;
- 4 private readonly Dictionary<string, (CollectiblePluginLoadContext context, Assembly assembly)> _loadedPlugins;
- 5 private readonly List<PluginInfo> _pluginInfos;
- 6 private readonly string _pluginsDirectory;
- 7 private readonly DynamicChangeTokenProvider _tokenProvider;
- 8
- 9 public ImprovedPluginManager(ApplicationPartManager partManager, DynamicChangeTokenProvider tokenProvider)
- 10 {
- 11 _tokenProvider = tokenProvider;
- 12 _partManager = partManager;
- 13 _loadedPlugins = new Dictionary<string, (CollectiblePluginLoadContext, Assembly)>();
- 14 _pluginInfos = new List<PluginInfo>();
- 15
- 16 // 设置插件目录
- 17 _pluginsDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
- 18 if (!Directory.Exists(_pluginsDirectory))
- 19 {
- 20 Directory.CreateDirectory(_pluginsDirectory);
- 21 }
- 22 }
- 23
- 24 public async Task<bool> LoadPluginAsync(string pluginPath)
- 25 {
- 26 try
- 27 {
- 28 if (!File.Exists(pluginPath))
- 29 {
- 30 throw new FileNotFoundException($"Plugin file not found: {pluginPath}");
- 31 }
- 32
- 33 // 检查文件扩展名
- 34 if (!pluginPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
- 35 {
- 36 throw new ArgumentException("Plugin must be a .dll file");
- 37 }
- 38
- 39 // 创建可收集的加载上下文
- 40 var loadContext = new CollectiblePluginLoadContext(pluginPath);
- 41
- 42 // 加载程序集
- 43 var assembly = loadContext.LoadPluginAssembly();
- 44
- 45 // 获取所有控制器类型
- 46 var controllerTypes = assembly.GetTypes()
- 47 .Where(t => t.IsSubclassOf(typeof(ControllerBase)) && !t.IsAbstract)
- 48 .ToList();
- 49
- 50 if (!controllerTypes.Any())
- 51 {
- 52 throw new InvalidOperationException($"No controllers found in plugin: {pluginPath}");
- 53 }
- 54
- 55 // 检查是否已经加载了相同的程序集
- 56 if (_loadedPlugins.ContainsKey(assembly.FullName))
- 57 {
- 58 await UnloadPluginAsync(assembly.FullName);
- 59 }
- 60
- 61 // 将程序集添加到应用部件管理器
- 62 var assemblyPart = new AssemblyPart(assembly);
- 63 _partManager.ApplicationParts.Add(assemblyPart);
- 64
- 65 // 记录已加载的插件和上下文
- 66 _loadedPlugins[assembly.FullName] = (loadContext, assembly);
- 67
- 68 // 创建插件信息
- 69 var pluginInfo = new PluginInfo
- 70 {
- 71 Name = Path.GetFileNameWithoutExtension(pluginPath),
- 72 Version = assembly.GetName().Version?.ToString() ?? "Unknown",
- 73 Description = "Dynamic controller plugin",
- 74 FilePath = pluginPath,
- 75 LoadedAt = DateTime.Now,
- 76 ControllerTypes = controllerTypes.Select(t => t.Name).ToList()
- 77 };
- 78
- 79 _pluginInfos.Add(pluginInfo);
- 80
- 81 Console.WriteLine($"Successfully loaded plugin: {pluginPath}");
- 82 foreach (var controller in controllerTypes)
- 83 {
- 84 Console.WriteLine($" - Controller: {controller.Name}");
- 85 }
- 86
- 87 return true;
- 88 }
- 89 catch (Exception ex)
- 90 {
- 91 Console.WriteLine($"Failed to load plugin: {ex.Message}");
- 92 return false;
- 93 }
- 94 }
- 95
- 96 public async Task<bool> UnloadPluginAsync(string assemblyFullName)
- 97 {
- 98 try
- 99 {
- 100 if (!_loadedPlugins.ContainsKey(assemblyFullName))
- 101 {
- 102 return false;
- 103 }
- 104
- 105 var (loadContext, assembly) = _loadedPlugins[assemblyFullName];
- 106
- 107
- 108 // 从应用部件管理器中移除程序集
- 109 var partToRemove = _partManager.ApplicationParts
- 110 .OfType()
- 111 .FirstOrDefault(p => p.Assembly == assembly);
- 112
- 113 if (partToRemove != null)
- 114 {
- 115 _partManager.ApplicationParts.Remove(partToRemove);
- 116 }
- 117
- 118 // 从已加载插件列表中移除
- 119 _loadedPlugins.Remove(assemblyFullName);
- 120
- 121 // 从插件信息列表中移除
- 122 var pluginInfo = _pluginInfos.FirstOrDefault(p => p.FilePath == assembly.Location);
- 123 if (pluginInfo != null)
- 124 {
- 125 _pluginInfos.Remove(pluginInfo);
- 126 }
- 127
- 128 // 卸载加载上下文
- 129 loadContext.Unload();
- 130
- 131 // 强制垃圾回收以释放程序集
- 132 GC.Collect();
- 133 GC.WaitForPendingFinalizers();
- 134
- 135 Console.WriteLine($"=== UNLOAD DIAGNOSTICS ===");
- 136 Console.WriteLine($"Successfully unloaded plugin: {assemblyFullName}");
- 137 Console.WriteLine($"Assembly location: {assembly.Location}");
- 138 Console.WriteLine($"Loaded plugins count after unload: {_loadedPlugins.Count}");
- 139 Console.WriteLine($"Plugin infos count after unload: {_pluginInfos.Count}");
- 140 Console.WriteLine($"Application parts count after unload: {_partManager.ApplicationParts.Count}");
- 141 Console.WriteLine("========================");
- 142 return true;
- 143 }
- 144 catch (Exception ex)
- 145 {
- 146 Console.WriteLine($"Failed to unload plugin: {ex.Message}");
- 147 return false;
- 148 }
- 149 }
- 150
- 151 public List<PluginInfo> GetLoadedPlugins()
- 152 {
- 153 return _pluginInfos.ToList();
- 154 }
- 155
- 156 public async Task<List<FileInfo>> ScanPluginFilesAsync()
- 157 {
- 158 var pluginDirInfo = new DirectoryInfo(_pluginsDirectory);
- 159 if (!pluginDirInfo.Exists)
- 160 {
- 161 return new List<FileInfo>();
- 162 }
- 163
- 164 var dllFiles = pluginDirInfo.GetFiles("*.dll", SearchOption.AllDirectories);
- 165 return dllFiles.ToList();
- 166 }
- 167
- 168 public async Task<bool> LoadAllPluginsAsync()
- 169 {
- 170 // 先卸载所有已加载的插件
- 171 var assembliesToUnload = _loadedPlugins.Keys.ToList();
- 172 foreach (var assemblyName in assembliesToUnload)
- 173 {
- 174 await UnloadPluginAsync(assemblyName);
- 175 }
- 176
- 177 var pluginFiles = await ScanPluginFilesAsync();
- 178 var successCount = 0;
- 179
- 180 foreach (var pluginFile in pluginFiles)
- 181 {
- 182 if (await LoadPluginAsync(pluginFile.FullName))
- 183 {
- 184 successCount++;
- 185 }
- 186 }
- 187 _tokenProvider.NotifyChanges();
- 188 Console.WriteLine($"Loaded {successCount} out of {pluginFiles.Count} plugins");
- 189 return successCount > 0;
- 190 }
- 191 }
复制代码 ImprovedPluginManager如有错误请指正。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |