博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从零开始实现 ASP.NET Core MVC 的插件式开发(九) - 如何启用预编译视图
阅读量:4033 次
发布时间:2019-05-24

本文共 18016 字,大约阅读时间需要 60 分钟。

标题:从零开始实现 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

前景回顾

简介

在这个项目创建的时候,项目的初衷是使用预编译视图来呈现界面,但是由于多次尝试失败,最后改用了运行时编译视图,这种方式在第一次加载的时候非常的慢,所有的插件视图都要在运行时编译。而且从便携性上来说,预编译视图更好。近日,在几位同道的共同努力下,终于实现了这种加载方式。近日,在几位同道的共同努力下,终于实现了这种加载方式。


此篇要鸣谢网友 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 个内部类DefaultViewCompilerProviderDefaultViewCompiler。但是由于这 2 个类是内部类,所以没有办法继承并重写,更谈不上调试了。

j4587698的思路和我不同,他的做法是,在当前主项目中,直接复制DefaultViewCompilerProviderDefaultViewCompiler2 个类的代码,并将其定义为公开类,在程序启动时,替换默认依赖注入容器中的类实现,使用公开的DefaultViewCompilerProviderDefaultViewCompiler类,替换 ASP.NET Core 默认指定的内部类。

根据他的思路,我新增了一个基于IServiceCollection的扩展类,追加了Replace方法来替换注入容器中的实现。

    public static class ServiceCollectionExtensions    {        public static IServiceCollection Replace
(this IServiceCollection services)            where TImplementation : TService        {            return services.Replace
(typeof(TImplementation));        }        public static IServiceCollection Replace
(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
 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
();        services.AddSingleton
();        services.AddScoped
();        services.AddScoped
();        services.AddScoped
();        services.AddSingleton
();        services.AddSingleton
(MystiqueActionDescriptorChangeProvider.Instance);        services.AddSingleton
();        services.AddSingleton
();        services.AddSingleton(MystiqueActionDescriptorChangeProvider.Instance);        ...        services.Replace
();    }

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
());        }        public IViewCompiler GetCompiler() => _compiler;    }

PS: 此处只是直接复制了 ASP.NET Core 源码中DefaultViewCompilerProviderDefaultViewCompiler2 个类的代码,稍作修改,保证编译通过。

    public class MyViewCompiler : IViewCompiler    {        private readonly Dictionary
> _compiledViews;        private readonly ConcurrentDictionary
 _normalizedPathCache;        private readonly ILogger _logger;        public MyViewCompiler(            IList
 compiledViews,            ILogger logger)        {            ...        }        /// 
        public Task
 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
(),            });        }        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    {        ///         /// Initializes a new instance of 
.        /// 
        /// 
The 
        public CompiledRazorAssemblyPart(Assembly assembly)        {            Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));        }        /// 
        /// Gets the 
.        /// 
        public Assembly Assembly { get; }        /// 
        public override string Name => Assembly.GetName().Name;        IEnumerable
 IRazorCompiledItemProvider.CompiledItems        {            get            {                var loader = new RazorCompiledItemLoader();                return loader.LoadItems(Assembly);            }        }    }

这个类非常的简单,它通过RazorCompiledItemLoader类对象从程序集中加载的视图, 并将最终的编译视图都存放在一个RazorCompiledItem类的集合里。

    public class RazorCompiledItemLoader    {        public virtual IReadOnlyList
 LoadItems(Assembly assembly)        {            if (assembly == null)            {                throw new ArgumentNullException(nameof(assembly));            }            var items = new List
();            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
 LoadAttributes(Assembly assembly)        {            if (assembly == null)            {                throw new ArgumentNullException(nameof(assembly));            }            return assembly.GetCustomAttributes
();        }    }

这里我们可以参考前面的调试方式,创建出一套自己的视图加载类,代码和当前的实现一模一样

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
 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 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
 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 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
());        }        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,进入插件主页面,页面正常显示了。

至此运行时加载与编译视图的场景也顺利解决了。

你可能感兴趣的文章
/etc/resolv.conf
查看>>
container_of()传入结构体中的成员,返回该结构体的首地址
查看>>
linux sfdisk partition
查看>>
ipconfig,ifconfig,iwconfig
查看>>
opensuse12.2 PL2303 minicom
查看>>
电平触发方式和边沿触发的区别
查看>>
网络视频服务器移植
查看>>
Encoding Schemes
查看>>
移植QT
查看>>
如此调用
查看>>
计算机的发展史
查看>>
带WiringPi库的交叉编译如何处理一
查看>>
带WiringPi库的交叉笔译如何处理二之软链接概念
查看>>
Spring事务的七种传播行为
查看>>
ES写入找不到主节点问题排查
查看>>
Java8 HashMap集合解析
查看>>
ArrayList集合解析
查看>>
欢迎使用CSDN-markdown编辑器
查看>>
Android计算器实现源码分析
查看>>
Android系统构架
查看>>