标题:从零开始实现ASP.NET Core MVC的插件式开发(九) - 升级.NET 5及启用预编译视图
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/13992077.html
源代码:https://github.com/lamondlu/Mystique
适用版本:.NET Core 3.1
,.NET 5
前景回顾
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图
- 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板
- 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
- 从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
- 从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除
- 从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用
- 从零开始实现ASP.NET Core MVC的插件式开发(七) - 近期问题汇总及部分解决方案
- 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案
简介
在这个项目创建的时候,项目的初衷是使用预编译视图来呈现界面,但是由于多次尝试失败,最后改用了运行时编译视图,这种方式在第一次加载的时候非常的慢,所有的插件视图都要在运行时编译,而且从便携性上来说,预编译视图更好。近日,在几位同道的共同努力下,终于实现了这种加载方式。
此篇要鸣谢网友 j4587698 和 yang-er 对针对当前项目的支持,你们的思路帮我解决了当前项目针对不能启用预编译视图的2个主要的问题
- 在当前项目目录结构下,启动时加载组件,组件预编译视图不能正常使用
- 运行时加载组件之后,组件中的预编译视图不能正常使用
升级.NET 5
随着.NET 5的发布,当前项目也升级到了.NET 5版本。
整个升级的过程比我预想的简单的多,只是修改了一下项目使用的Target fremework
。重新编译打包了一下插件程序,项目就可以正常运行了,整个过程中没有产生任何因为版本升级导致的编译问题。
预编译视图不能使用的问题
在升级了.NET 5之后,我重新尝试在启动时关闭了运行时编译,加载预编译视图View, 借此测试.NET 5对预编译视图的支持情况。
public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration) { ... IMvcBuilder mvcBuilder = services.AddMvc(); ServiceProvider provider = services.BuildServiceProvider(); using (IServiceScope scope = provider.CreateScope()) { ... foreach (ViewModels.PluginListItemViewModel plugin in allEnabledPlugins) { CollectibleAssemblyLoadContext context = new CollectibleAssemblyLoadContext(plugin.Name); string moduleName = plugin.Name; string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName, $"{moduleName}.dll"); string viewFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName, $"{moduleName}.Views.dll"); string referenceFolderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules", moduleName); _presets.Add(filePath); using (FileStream fs = new FileStream(filePath, FileMode.Open)) { Assembly assembly = context.LoadFromStream(fs); context.SetEntryPoint(assembly); loader.LoadStreamsIntoContext(context, referenceFolderPath, assembly); MystiqueAssemblyPart controllerAssemblyPart = new MystiqueAssemblyPart(assembly); mvcBuilder.PartManager.ApplicationParts.Add(controllerAssemblyPart); PluginsLoadContexts.Add(plugin.Name, context); BuildNotificationProvider(assembly, scope); } using (FileStream fsView = new FileStream(viewFilePath, FileMode.Open)) { Assembly viewAssembly = context.LoadFromStream(fsView); loader.LoadStreamsIntoContext(context, referenceFolderPath, viewAssembly); CompiledRazorAssemblyPart moduleView = new CompiledRazorAssemblyPart(viewAssembly); mvcBuilder.PartManager.ApplicationParts.Add(moduleView); } context.Enable(); } } } AssemblyLoadContextResoving(); ... }
运行项目之后,你会发现项目竟然会得到一个无法找到视图的错误。
这里的结果很奇怪,因为参考第一章的场景,ASP.NET Core默认是支持启动时加载预编译视图的。在第一章的时候,我们创建了1个组件,在启动时,直接加载到主AssemblyLoadContext
中,启动之后,我们是可以正常访问到视图的。
在仔细思考之后,我想到的两种可能性。
- 一种可能是因为我们的组件加载在独立的
AssemblyLoadContext
中,而非主AssemblyLoadContext
中,所以可能导致加载失败 - 插件的目录结构与第一章不符合,导致加载失败
但是苦于不能调试ASP.NET Core的源码,所以这一部分就暂时搁置了。直到前几天,网友j4587698 在项目Issue中针对运行时编译提出的方案给我的调试思路。
在ASP.NET Core中,默认的视图的编译和加载使用了2个内部类DefaultViewCompilerProvider
和DefaultViewCompiler
。但是由于这2个类是内部类,所以没有办法继承并重写,更谈不上调试了。
j4587698
的思路和我不同,他的做法是,在当前主项目中,直接复制DefaultViewCompilerProvider
和DefaultViewCompiler
2个类的代码,并将其定义为公开类,在程序启动时,替换默认依赖注入容器中的类实现,使用公开的DefaultViewCompilerProvider
和DefaultViewCompiler
类,替换ASP.NET Core默认指定的内部类。
根据他的思路,我新增了一个基于IServiceCollection
的扩展类,追加了Replace
方法来替换注入容器中的实现。
public static class ServiceCollectionExtensions { public static IServiceCollection Replace<TService, TImplementation>(this IServiceCollection services) where TImplementation : TService { return services.Replace<TService>(typeof(TImplementation)); } public static IServiceCollection Replace<TService>(this IServiceCollection services, Type implementationType) { return services.Replace(typeof(TService), implementationType); } public static IServiceCollection Replace(this IServiceCollection services, Type serviceType, Type implementationType) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (serviceType == null) { throw new ArgumentNullException(nameof(serviceType)); } if (implementationType == null) { throw new ArgumentNullException(nameof(implementationType)); } if (!services.TryGetDescriptors(serviceType, out var descriptors)) { throw new ArgumentException($"No services found for {serviceType.FullName}.", nameof(serviceType)); } foreach (var descriptor in descriptors) { var index = services.IndexOf(descriptor); services.Insert(index, descriptor.WithImplementationType(implementationType)); services.Remove(descriptor); } return services; } private static bool TryGetDescriptors(this IServiceCollection services, Type serviceType, out ICollection<ServiceDescriptor> descriptors) { return (descriptors = services.Where(service => service.ServiceType == serviceType).ToArray()).Any(); } private static ServiceDescriptor WithImplementationType(this ServiceDescriptor descriptor, Type implementationType) { return new ServiceDescriptor(descriptor.ServiceType, implementationType, descriptor.Lifetime); } }
并在程序启动时,使用公开的MyViewCompilerProvider
类,替换了原始注入类DefaultViewCompilerProvider
public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration) { _serviceCollection = services; services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton<IMvcModuleSetup, MvcModuleSetup>(); services.AddScoped<IPluginManager, PluginManager>(); services.AddScoped<ISystemManager, SystemManager>(); services.AddScoped<IUnitOfWork, Repository.MySql.UnitOfWork>(); services.AddSingleton<INotificationRegister, NotificationRegister>(); services.AddSingleton<IActionDescriptorChangeProvider>(MystiqueActionDescriptorChangeProvider.Instance); services.AddSingleton<IReferenceContainer, DefaultReferenceContainer>(); services.AddSingleton<IReferenceLoader, DefaultReferenceLoader>(); services.AddSingleton(MystiqueActionDescriptorChangeProvider.Instance); ... services.Replace<IViewCompilerProvider, MyViewCompilerProvider>(); }
在MyViewCompilerProvider
中, 直接返回了新定义的MyViewCompiler
public class MyViewCompilerProvider : IViewCompilerProvider { private readonly MyViewCompiler _compiler; public MyViewCompilerProvider( ApplicationPartManager applicationPartManager, ILoggerFactory loggerFactory) { var feature = new ViewsFeature(); applicationPartManager.PopulateFeature(feature); _compiler = new MyViewCompiler(feature.ViewDescriptors, loggerFactory.CreateLogger<MyViewCompiler>()); } public IViewCompiler GetCompiler() => _compiler; }
PS: 此处只是直接复制了ASP.NET Core源码中
DefaultViewCompilerProvider
和DefaultViewCompiler
2个类的代码,稍作修改,保证编译通过。
public class MyViewCompiler : IViewCompiler { private readonly Dictionary<string, Task<CompiledViewDescriptor>> _compiledViews; private readonly ConcurrentDictionary<string, string> _normalizedPathCache; private readonly ILogger _logger; public MyViewCompiler( IList<CompiledViewDescriptor> compiledViews, ILogger logger) { ... } /// <inheritdoc /> public Task<CompiledViewDescriptor> CompileAsync(string relativePath) { if (relativePath == null) { throw new ArgumentNullException(nameof(relativePath)); } // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already // normalized and a cache entry exists. if (_compiledViews.TryGetValue(relativePath, out var cachedResult)) { return cachedResult; } var normalizedPath = GetNormalizedPath(relativePath); if (_compiledViews.TryGetValue(normalizedPath, out cachedResult)) { return cachedResult; } // Entry does not exist. Attempt to create one. return Task.FromResult(new CompiledViewDescriptor { RelativePath = normalizedPath, ExpirationTokens = Array.Empty<IChangeToken>(), }); } private string GetNormalizedPath(string relativePath) { ... } }
针对DefaultViewCompiler
,这里的重点是CompileAsync
方法,它会根据传入的相对路径,在加载的编译视图集合中加载视图。下面我们在此处打上断点,并模拟进入DemoPlugin1
的主页。
看完这个调试过程,你是不是发现了点什么,当我们访问DemoPlugin1
的主页路由/Modules/DemoPlugin/Plugin1/HelloWorld
的时候,ASP.NET Core尝试查找的视图相对路径是·
/Areas/DemoPlugin1/Views/Plugin1/HelloWorld.cshtml
/Areas/DemoPlugin1/Views/Shared/HelloWorld.cshtml
/Views/Shared/HelloWorld.cshtml
/Pages/Shared/HelloWorld.cshtml
/Modules/DemoPlugin1/Views/Plugin1/HelloWorld.cshtml
/Views/Shared/HelloWorld.cshtml
而当我们查看现在已有的编译视图映射是,你会发现注册的对应视图路径确是/Views/Plugin1/HelloWorld.cshtml
。
下面我们再回过头来看看DemoPlugin1
的目录结构
由此我们推断出,预编译视图在生成的时候,会记录当前视图的相对路径,而在主程序加载的插件的过程中,由于我们使用了Area
来区分模块,多出的一级目录,所以导致目录映射失败了。因此如果我们将DemoPlugin1
的插件视图目录结构改为以上提示的6个地址之一,问题应该就解决了。
那么这里有没有办法,在不改变路径的情况下,让视图正常加载呢,答案是有的。参照之前的代码,在加载视图组件的时候,我们使用了内置类CompiledRazorAssemblyPart
, 那么让我们来看看它的源码。
public class CompiledRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider { /// <summary> /// Initializes a new instance of <see cref="CompiledRazorAssemblyPart"/>. /// </summary> /// <param name="assembly">The <see cref="System.Reflection.Assembly"/></param> public CompiledRazorAssemblyPart(Assembly assembly) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); } /// <summary> /// Gets the <see cref="System.Reflection.Assembly"/>. /// </summary> public Assembly Assembly { get; } /// <inheritdoc /> public override string Name => Assembly.GetName().Name; IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems { get { var loader = new RazorCompiledItemLoader(); return loader.LoadItems(Assembly); } } }
这个类非常的简单,它通过RazorCompiledItemLoader
类对象从程序集中加载的视图, 并将最终的编译视图都存放在一个RazorCompiledItem
类的集合里。
public class RazorCompiledItemLoader { public virtual IReadOnlyList<RazorCompiledItem> LoadItems(Assembly assembly) { if (assembly == null) { throw new ArgumentNullException(nameof(assembly)); } var items = new List<RazorCompiledItem>(); foreach (var attribute in LoadAttributes(assembly)) { items.Add(CreateItem(attribute)); } return items; } protected virtual RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute) { if (attribute == null) { throw new ArgumentNullException(nameof(attribute)); } return new DefaultRazorCompiledItem(attribute.Type, attribute.Kind, attribute.Identifier); } protected IEnumerable<RazorCompiledItemAttribute> LoadAttributes(Assembly assembly) { if (assembly == null) { throw new ArgumentNullException(nameof(assembly)); } return assembly.GetCustomAttributes<RazorCompiledItemAttribute>(); } }
这里我们可以参考前面的调试方式,创建出一套自己的视图加载类,代码和当前的实现一模一样
MystiqueModuleViewCompiledItemLoader
public class MystiqueModuleViewCompiledItemLoader : RazorCompiledItemLoader { public MystiqueModuleViewCompiledItemLoader() { } protected override RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute) { if (attribute == null) { throw new ArgumentNullException(nameof(attribute)); } return new MystiqueModuleViewCompiledItem(attribute); } }
MystiqueRazorAssemblyPart
public class MystiqueRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider { public MystiqueRazorAssemblyPart(Assembly assembly) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); AreaName = areaName; } public Assembly Assembly { get; } public override string Name => Assembly.GetName().Name; IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems { get { var loader = new MystiqueModuleViewCompiledItemLoader(); return loader.LoadItems(Assembly); } } }
MystiqueModuleViewCompiledItem
public class MystiqueModuleViewCompiledItem : RazorCompiledItem { public override string Identifier { get; } public override string Kind { get; } public override IReadOnlyList<object> Metadata { get; } public override Type Type { get; } public MystiqueModuleViewCompiledItem(RazorCompiledItemAttribute attr, string moduleName) { Type = attr.Type; Kind = attr.Kind; Identifier = attr.Identifier; Metadata = Type.GetCustomAttributes(inherit: true).ToList(); } }
这里我们在MystiqueModuleViewCompiledItem
类的构造函数部分打上断点。
重新启动项目之后,你会发现当加载DemoPlugin1的视图时,这里的Identifier
属性其实就是当前编译试图项的映射目录。这样我们很容易就想到在此处动态修改映射目录,为此我们需要将模块名称通过构造函数传入,以上3个类的更新代码如下:
MystiqueModuleViewCompiledItemLoader
public class MystiqueModuleViewCompiledItemLoader : RazorCompiledItemLoader { public string ModuleName { get; } public MystiqueModuleViewCompiledItemLoader(string moduleName) { ModuleName = moduleName; } protected override RazorCompiledItem CreateItem(RazorCompiledItemAttribute attribute) { if (attribute == null) { throw new ArgumentNullException(nameof(attribute)); } return new MystiqueModuleViewCompiledItem(attribute, ModuleName); } }
MystiqueRazorAssemblyPart
public class MystiqueRazorAssemblyPart : ApplicationPart, IRazorCompiledItemProvider { public MystiqueRazorAssemblyPart(Assembly assembly, string moduleName) { Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); ModuleName = moduleName; } public string ModuleName { get; } public Assembly Assembly { get; } public override string Name => Assembly.GetName().Name; IEnumerable<RazorCompiledItem> IRazorCompiledItemProvider.CompiledItems { get { var loader = new MystiqueModuleViewCompiledItemLoader(ModuleName); return loader.LoadItems(Assembly); } } }
MystiqueModuleViewCompiledItem
public class MystiqueModuleViewCompiledItem : RazorCompiledItem { public override string Identifier { get; } public override string Kind { get; } public override IReadOnlyList<object> Metadata { get; } public override Type Type { get; } public MystiqueModuleViewCompiledItem(RazorCompiledItemAttribute attr, string moduleName) { Type = attr.Type; Kind = attr.Kind; Identifier = "/Modules/" + moduleName + attr.Identifier; Metadata = Type.GetCustomAttributes(inherit: true).Select(o => o is RazorSourceChecksumAttribute rsca ? new RazorSourceChecksumAttribute(rsca.ChecksumAlgorithm, rsca.Checksum, "/Modules/" + moduleName + rsca.Identifier) : o).ToList(); } }
PS: 这里有个容易疏漏的点,就是
MystiqueModuleViewCompiledItem
中的MetaData
, 它使用了Identifier
属性的值,所以一旦Identifier
属性的值被动态修改,此处的值也要修改,否则调试会不成功。
修改完成之后,我们重启项目,来测试一下。
编译视图的映射路径动态修改成功,页面成功被打开了,至此启动时的预编译视图加载完成。
运行时加载编译视图
最后我们来到了运行加载编译视图的问题,有了之前的调试方案,现在调试起来就轻车熟路。
为了测试,我们再运行时加载编译视图,我们首先禁用掉DemoPlugin1
, 然后重启项目,并启用DemoPlugin1
通过调试,很明显问题出在预编译视图的加载上,在启用组件之后,编译视图映射集合没有更新,所以导致加载失败。这也证明了我们之前第三章时候的推断。当使用IActionDescriptorChangeProvider
重置Controller/Action
映射的时候,ASP.NET Core不会更新视图映射集合,从而导致视图加载失败。
MystiqueActionDescriptorChangeProvider.Instance.HasChanged = true; MystiqueActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
那么解决问题的方式也就很清楚了,我们需要在使用IActionDescriptorChangeProvider
重置Controller/Action
映射之后,刷新视图映射集合。为此,我们需要修改之前定义的MyViewCompilerProvider
, 添加Refresh
方法来刷新映射。
public class MyViewCompilerProvider : IViewCompilerProvider { private MyViewCompiler _compiler; private ApplicationPartManager _applicationPartManager; private ILoggerFactory _loggerFactory; public MyViewCompilerProvider( ApplicationPartManager applicationPartManager, ILoggerFactory loggerFactory) { _applicationPartManager = applicationPartManager; _loggerFactory = loggerFactory; Refresh(); } public void Refresh() { var feature = new ViewsFeature(); _applicationPartManager.PopulateFeature(feature); _compiler = new MyViewCompiler(feature.ViewDescriptors, _loggerFactory.CreateLogger<MyViewCompiler>()); } public IViewCompiler GetCompiler() => _compiler; }
Refresh
方法是借助ViewsFeature
来重新创建了一个新的IViewCompiler
, 并填充了最新的视图映射。
PS: 这里的实现方式参考了
DefaultViewCompilerProvider
的实现,该类是在构造中填充的视图映射。
根据以上修改,在使用IActionDescriptorChangeProvider
重置Controller/Action映射之后, 我们使用Refresh
方法来刷新映射。
private void ResetControllActions() { MystiqueActionDescriptorChangeProvider.Instance.HasChanged = true; MystiqueActionDescriptorChangeProvider.Instance.TokenSource.Cancel(); var provider = _context.HttpContext .RequestServices .GetService(typeof(IViewCompilerProvider)) as MyViewCompilerProvider; provider.Refresh(); }
最后,我们重新启动项目,再次在运行时启用DemoPlugin1
,进入插件主页面,页面正常显示了。
至此运行时加载与编译视图的场景也顺利解决了。
原文转载:http://www.shaoqun.com/a/490352.html
zen-cart:https://www.ikjzd.com/w/1282
黑石集团:https://www.ikjzd.com/w/1339.html
上海跨境通:https://www.ikjzd.com/w/1329
在这个项目创建的时候,项目的初衷是使用预编译视图来呈现界面,但是由于多次尝试失败,最后改用了运行时编译视图,这种方式在第一次加载的时候非常的慢,所有的插件视图都要在运行时编译。这个问题困扰我很久。近日,在几位同道的共同努力下,终于实现了这种加载方式。标题:从零开始实现ASP.NETCoreMVC的插件式开发(九)-升级.NET5及启用预编译视图作者:LamondLu地址:https://www.c
mein:mein
菜鸟网:菜鸟网
中国湖北武当山特色菜:襄樊糊辣汤 - :中国湖北武当山特色菜:襄樊糊辣汤 -
大理宾川鸡足山住宿怎么样?有哪些住宿比较好的?:大理宾川鸡足山住宿怎么样?有哪些住宿比较好的?
港中旅的全称是什么:港中旅的全称是什么
No comments:
Post a Comment