原始方式虽然可以帮助我们成功加载插件引用程序集,但是它并不效率,如果插件1和插件2引用了相同的程序集,当插件1的AssemblyLoadContext加载所有的引用程序集之后,插件2会将插件1所干的事情重复一遍。这并不是我们想要的,我们希望如果多个插件同时使用了相同的程序集,就不需要重复读取dll文件了。
如何避免重复读取dll文件呢?这里我们可以使用一个静态字典来缓存文件流信息,从而避免重复读取dll文件。
如果大家觉着在ASP.NET Core MVC中使用静态字典来缓存文件流信息不安全,可以改用其他缓存方式,这里只是为了简单演示。
这里我们首先创建一个引用程序集缓存容器接口IReferenceContainer, 其代码如下:
public interface IReferenceContainer { List<CachedReferenceItemKey> GetAll(); bool Exist(string name, string version); void SaveStream(string name, string version, Stream stream); Stream GetStream(string name, string version); }代码解释
GetAll方法会在后续使用,用来获取系统中加载的所有引用程序集
Exist方法判断了指定版本程序集的文件流是否存在
SaveStream是将指定版本的程序集文件流保存到静态字典中
GetStream是从静态字典中拉取指定版本程序集的文件流
然后我们可以创建一个引用程序集缓存容器的默认实现DefaultReferenceContainer类,其代码如下:
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; } }这个类比较简单,我就不做太多解释了。
完成了引用缓存容器之后,我修改了之前创建的IReferenceLoader接口,及其默认实现DefaultReferenceLoader。
public interface IReferenceLoader { public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext context, string moduleFolder, Assembly assembly); } public class DefaultReferenceLoader : IReferenceLoader { private IReferenceContainer _referenceContainer = null; private readonly ILogger<DefaultReferenceLoader> _logger = null; public DefaultReferenceLoader(IReferenceContainer referenceContainer, ILogger<DefaultReferenceLoader> logger) { _referenceContainer = referenceContainer; _logger = logger; } public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext 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"); } }代码解释:
这里LoadStreamsIntoContext方法的assembly参数,即当前插件程序集。
这里我通过GetReferencedAssemblies方法,获取了插件程序集引用的所有程序集。
如果引用程序集在引用容器中不存在,我们就是用文件流加载它,并将其保存到引用容器中, 如果引用程序集已存在于引用容器,就直接加载到当前插件的AssemblyLoadContext中。这里为了检验效果,如果程序集来自缓存,我使用日志组件输出了一条日志。
由于插件引用的程序集,有可能是来自Shared Framework, 这种程序集是不需要加载的,所以这里我选择跳过这类程序集的加载。(这里我还没有考虑Self-Contained发布的情况,后续这里可能会更改)
最后我们还是需要修改MystiqueStartup.cs和MvcModuleSetup.cs中启用插件的代码。