一只好奇的猫之我的热更新研究

1>静态插件在启动的时候加入。
1.0>原始的方法

#region 加载视图
//加载视图的这句我没有用到。因为我现在关注的是api.mvc模式的api.
var assemblyView = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1.Views.dll");

var viewAssemblyPart = new CompiledRazorAssemblyPart(assemblyView);
//加载视图以上
#endregion
//以上的我没有用到。
var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1.dll");


var controllerAssemblyPart = new AssemblyPart(assembly);
var mvcBuilders = services.AddMvc();

mvcBuilders.ConfigureApplicationPartManager(apm =>
{
apm.ApplicationParts.Add(controllerAssemblyPart);
apm.ApplicationParts.Add(viewAssemblyPart);//没有加入视图。
});

2>在运行中启用它
在Action中激活组件
直接增加了
不起作用。
据作者看了很多资料,最后说:
,在ASP.NET Core 2.2中有一个类是ActionDescriptorCollectionProvider,它的子类DefaultActionDescriptorCollectionProvider是用来配置Controller和Action的。
但是这个类很明显,不能在外面用。
这里ActionDescriptors属性中记录了当ASP.NET Core程序启动后,匹配到的所有Controller/Action集合。
UpdateCollection方法使用来更新ActionDescriptors集合的。
在构造函数中设计了一个触发器,ChangeToken.OnChange(GetCompositeChangeToken,UpdateCollection)。这里程序会监听一个Token对象,当这个Token对象发生变化时,就自动触发UpdateCollection方法。
这里Token是由一组IActionDescriptorChangeProvider接口对象组合而成的。
所以这里我们就可以通过自定义一个IActionDescriptorChangeProvider接口对象,并在组件激活方法Enable中修改这个接口Token的方式,使DefaultActionDescriptorCollectionProvider中的CompositeChangeToken发生变化,从而实现控制器的重新装载。
使用IActionDescriptorChangeProvider在运行时激活控制器
所以最后要自己写一个
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();

public CancellationTokenSource TokenSource { get; private set; }

public bool HasChanged { get; set; }

public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
注册成单例。
public void ConfigureServices(IServiceCollection services)
{
...

services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
services.AddSingleton(MyActionDescriptorChangeProvider.Instance);

...
}

public class PluginsController : Controller
{
public IActionResult Enable()
{
var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll");
var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll");
var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);

var controllerAssemblyPart = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(controllerAssemblyPart);
_partManager.ApplicationParts.Add(viewAssemblyPart);

MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

return Content("Enabled");
}
}

api在以上代码就好了。
但是如果是mvc就不行了。
找不到对就应的views。
services.Configure<RazorViewEngineOptions>(o =>
{
o.AreaViewLocationFormats.Add("/Modules/{2}/{1}/Views/{0}" + RazorViewEngine.ViewExtension);
});
这里{2}代表Area名称, {1}代表Controller名称, {0}代表Action名称。

这里Modules是作者重新创建的一个目录,后续所有的插件都会放置在这个目录中。

同样的,我们还需要在Configure方法中为Area注册路由。

app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");

routes.MapRoute(
name: "default",
template: "Modules/{area}/{controller=Home}/{action=Index}/{id?}");
});
因为我们已经不需要使用Razor的预编译视图,所以Enable方法我们的最终代码如下

public IActionResult Enable()
{
var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "Modules\\DemoPlugin1\\DemoPlugin1.dll");

var controllerAssemblyPart = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(controllerAssemblyPart);

MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

return Content("Enabled");
}

3>安装略过。
4>升级与删除
using ZipTool = System.IO.Compression.ZipArchive;
public void Initialize(Stream stream)
{
var tempFolderName = $"{ AppDomain.CurrentDomain.BaseDirectory }{ Guid.NewGuid().ToString()}";
ZipTool archive = new ZipTool(stream, ZipArchiveMode.Read);

archive.ExtractToDirectory(tempFolderName);//解压缩。

var folder = new DirectoryInfo(tempFolderName);

var files = folder.GetFiles();

var configFiles = files.Where(p => p.Name == "plugin.json");

if (!configFiles.Any())
{
throw new Exception("The plugin is missing the configuration file.");
}
else
{
using (var s = configFiles.First().OpenRead())
{
LoadConfiguration(s);
}
}

folder.Delete(true);

_folderName = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{_pluginConfiguration.Name}";

if (Directory.Exists(_folderName))
{
throw new Exception("The plugin has been existed.");
}

stream.Position = 0;
archive.ExtractToDirectory(_folderName);
}
查看了某电商的方案不合适
nopcommerce 它的操作是
‎手动插件安装‎
‎将插件上传到 nopCommerce 目录中的 /plugins 文件夹。‎
‎重新启动应用程序(或单击“重新加载插件列表”按钮)。‎
‎向下滚动插件列表以查找新安装的插件。‎
‎单击“安装”链接以选择要安装的插件。‎
‎单击顶部面板上的“重新启动应用程序以应用更改”按钮以完成安装过程。‎

按文档,已完成这部分的工作。

public class MyAssemblyLoadcontext: AssemblyLoadContext
{
public MyAssemblyLoadcontext()
: base(isCollectible: true)
{
}

protected override Assembly Load(AssemblyName name)
{
return null;
}
}

这段意思是说ASP.NET Core暂时不支持动态加载程序集,如果要在当前版本实现功能,需要自己实现一个AssemblyPart类, 在获取程序集路径的时候,返回空集合而不是空字符串。

PS: 官方已经将这个问题放到了.NET 5 Preview 1中,相信.NET 5中会得到真正的解决。

根据官方的方案,Startup.cs文件的最终版本

public class MyAssemblyPart : AssemblyPart, ICompilationReferencesProvider
{
public MyAssemblyPart(Assembly assembly) : base(assembly) { }

public IEnumerable<string> GetReferencePaths() => Array.Empty<string>();
}

var context = new MyAssemblyLoadcontext();
var filePath = @"E:\jmqjproject2021\JmqJStudio\AppBox.WebHost\bin\Debug\netcoreapp3.1\fuckabc\PluginsDemo.dll";
var modulename = "PluginsDemo";
using (var fs = new FileStream(filePath, FileMode.Open))
{
var assembly = context.LoadFromStream(fs);

var controllerAssemblyPart = new MyAssemblyPart(assembly);//因为3.0不支持。据说5.0会加。我用的是3.1所以还是要自己实现一个。

//mvcBuilders.PartManager.ApplicationParts.Add(controllerAssemblyPart);
_partManager.ApplicationParts.Add(controllerAssemblyPart);

//PluginsLoadContexts.AddPluginContext(plugin.Name, context);
}

//var assembly = context.LoadFromAssemblyPath();
//var assembly = Assembly.LoadFile(@"E:\jmqjproject2021\JmqJStudio\AppBox.WebHost\bin\Debug\netcoreapp3.1\fuckabc\PluginsDemo.dll");
//var controllerAssemblyPart = new AssemblyPart(assembly);

MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
以上为新的加载。

var moduleName = "PluginsDemo";
//var moduleName = "

var matchedItem = _partManager.ApplicationParts.FirstOrDefault(p =>
p.Name == moduleName);

if (matchedItem != null)
{
_partManager.ApplicationParts.Remove(matchedItem);
matchedItem = null;
}

MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
以上为删除。
存在的问题。多次加载。再次请求。
The request matched multiple endpoints.

5>加载引用的dll
DefaultReferenceLoader _referenceLoader

public class DefaultReferenceContainer: IReferenceContainer
{
private static Dictionary<CachedReferenceItemKey, Stream> _cachedReferences = new Dictionary<CachedReferenceItemKey, Stream>();

public List<CachedReferenceItemKey> GetAll()
{
return _cachedReferences.Keys.ToList();
}

public bool Exist(string name, string version)
{
return _cachedReferences.Keys.Any(p => p.ReferenceName == name
&& p.Version == version);
}

public void SaveStream(string name, string version, Stream stream)
{
if (Exist(name, version))
{
return;
}

_cachedReferences.Add(new CachedReferenceItemKey { ReferenceName = name, Version = version }, stream);
}

public Stream GetStream(string name, string version)
{
var key = _cachedReferences.Keys.FirstOrDefault(p => p.ReferenceName == name
&& p.Version == version);

if (key != null)
{
_cachedReferences[key].Position = 0;
return _cachedReferences[key];
}

return null;
}

public class DefaultReferenceLoader : IReferenceLoader
{
private readonly IReferenceContainer _referenceContainer = null;
private readonly ILogger<DefaultReferenceLoader> _logger = null;

public DefaultReferenceLoader(IReferenceContainer referenceContainer, ILogger<DefaultReferenceLoader> logger)
{
_referenceContainer = referenceContainer;
_logger = logger;
}

public void LoadStreamsIntoContext(MyAssemblyLoadcontext context, string moduleFolder, Assembly assembly)
{
var references = assembly.GetReferencedAssemblies();

foreach (var item in references)
{
var name = item.Name;

var version = item.Version.ToString();

var stream = _referenceContainer.GetStream(name, version);

if (stream != null)
{
_logger.LogDebug($"Found the cached reference '{name}' v.{version}");
context.LoadFromStream(stream);
}
else
{

if (IsSharedFreamwork(name))
{
continue;
}

var dllName = $"{name}.dll";
var filePath = $"{moduleFolder}\\{dllName}";

if (!File.Exists(filePath))
{
_logger.LogWarning($"The package '{dllName}' is missing.");
continue;
}

using (var fs = new FileStream(filePath, FileMode.Open))
{
var referenceAssembly = context.LoadFromStream(fs);

var memoryStream = new MemoryStream();

fs.Position = 0;
fs.CopyTo(memoryStream);
fs.Position = 0;
memoryStream.Position = 0;
_referenceContainer.SaveStream(name, version, memoryStream);

LoadStreamsIntoContext(context, moduleFolder, referenceAssembly);
}
}
}
}

private bool IsSharedFreamwork(string name)
{
return SharedFrameworkConst.SharedFrameworkDLLs.Contains($"{name}.dll");//共享的不加了。
}

实现
DeletePlugs();

if (string.IsNullOrWhiteSpace(pathname))
{
pathname = "fuckabc";
}
var context = new MyAssemblyLoadcontext();
var filePath = @$"E:\jmqjproject2021\JmqJStudio\AppBox.WebHost\bin\Debug\netcoreapp3.1\{pathname}\PluginsDemo.dll";
var modulename = "PluginsDemo";
using (var fs = new FileStream(filePath, FileMode.Open))
{
var assembly = context.LoadFromStream(fs);

var controllerAssemblyPart = new MyAssemblyPart(assembly);
_referenceLoader.LoadStreamsIntoContext(context, @$"E:\jmqjproject2021\JmqJStudio\AppBox.WebHost\bin\Debug\netcoreapp3.1\{pathname}", assembly);
_partManager.ApplicationParts.Add(controllerAssemblyPart);
}


MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();

抄完了。
我的目标,
1>我不是有视图的这种。是前后端分离,而且,前端用的是vue这些框架。后端热升级。
2>我的插件系统目前只支持一种业务,一种dll.不允许多个业务业务dll.

防止路由冲突

代码抄自 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图 - LamondLu - 博客园 (cnblogs.com)

posted @ 2022-07-31 23:15  forhells  阅读(145)  评论(0编辑  收藏  举报