从零开始实现 ASP.NET Core MVC 的插件式开发(九) - 如何启用预编译视图
標(biāo)題:從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(九) - 升級.NET 5及啟用預(yù)編譯視圖
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/13992077.html
源代碼:https://github.com/lamondlu/Mystique
適用版本:.NET Core 3.1, .NET 5
前景回顧
從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(一) - 使用 Application Part?動態(tài)加載控制器和視圖
從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(二) - 如何創(chuàng)建項目模板
從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(三) - 如何在運行時啟用組件
從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(四) - 插件安裝
從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(五) - 使用 AssemblyLoadContext 實現(xiàn)插件的升級和刪除
從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(六) - 如何加載插件引用
從零開始實現(xiàn) ASP.NET Core MVC 的插件式開發(fā)(七) - 問題匯總及部分問題解決方案
從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(八) - Razor視圖相關(guān)問題及解決方案
簡介
在這個項目創(chuàng)建的時候,項目的初衷是使用預(yù)編譯視圖來呈現(xiàn)界面,但是由于多次嘗試失敗,最后改用了運行時編譯視圖,這種方式在第一次加載的時候非常的慢,所有的插件視圖都要在運行時編譯。而且從便攜性上來說,預(yù)編譯視圖更好。近日,在幾位同道的共同努力下,終于實現(xiàn)了這種加載方式。近日,在幾位同道的共同努力下,終于實現(xiàn)了這種加載方式。
此篇要鳴謝網(wǎng)友 j4587698 和 yang-er 對針對當(dāng)前項目的支持,你們的思路幫我解決了當(dāng)前項目針對不能啟用預(yù)編譯視圖的 2 個主要的問題
在當(dāng)前項目目錄結(jié)構(gòu)下,啟動時加載組件,組件預(yù)編譯視圖不能正常使用
運行時加載組件之后,組件中的預(yù)編譯視圖不能正常使用
升級.NET 5
隨著.NET 5 的發(fā)布,當(dāng)前項目也升級到了.NET 5 版本。
整個升級的過程比我預(yù)想的簡單的多,只是修改了一下項目使用的Target fremework。重新編譯打包了一下插件程序,項目就可以正常運行了,整個過程中沒有產(chǎn)生任何因為版本升級導(dǎo)致的編譯問題。
預(yù)編譯視圖不能使用的問題
在升級了.NET 5 之后,我重新嘗試在啟動時關(guān)閉了運行時編譯,加載預(yù)編譯視圖 View, 借此測試.NET 5 對預(yù)編譯視圖的支持情況。
????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();...}運行項目之后,你會發(fā)現(xiàn)項目竟然會得到一個無法找到視圖的錯誤。
這里的結(jié)果很奇怪,因為參考第一章的場景,ASP.NET Core 默認(rèn)是支持啟動時加載預(yù)編譯視圖的。在第一章的時候,我們創(chuàng)建了 1 個組件,在啟動時,直接加載到主AssemblyLoadContext中,啟動之后,我們是可以正常訪問到視圖的。
在仔細(xì)思考之后,我想到的兩種可能性。
一種可能是因為我們的組件加載在獨立的AssemblyLoadContext中,而非主AssemblyLoadContext中,所以可能導(dǎo)致加載失敗
插件的目錄結(jié)構(gòu)與第一章不符合,導(dǎo)致加載失敗
但是苦于不能調(diào)試 ASP.NET Core 的源碼,所以這一部分就暫時擱置了。直到前幾天,網(wǎng)友j4587698 在項目 Issue 中針對運行時編譯提出的方案給我的調(diào)試思路。
在 ASP.NET Core 中,默認(rèn)的視圖的編譯和加載使用了 2 個內(nèi)部類DefaultViewCompilerProvider和DefaultViewCompiler。但是由于這 2 個類是內(nèi)部類,所以沒有辦法繼承并重寫,更談不上調(diào)試了。
j4587698的思路和我不同,他的做法是,在當(dāng)前主項目中,直接復(fù)制DefaultViewCompilerProvider和DefaultViewCompiler2 個類的代碼,并將其定義為公開類,在程序啟動時,替換默認(rèn)依賴注入容器中的類實現(xiàn),使用公開的DefaultViewCompilerProvider和DefaultViewCompiler類,替換 ASP.NET Core 默認(rèn)指定的內(nèi)部類。
根據(jù)他的思路,我新增了一個基于IServiceCollection的擴展類,追加了Replace方法來替換注入容器中的實現(xiàn)。
????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: 此處只是直接復(fù)制了 ASP.NET Core 源碼中DefaultViewCompilerProvider和DefaultViewCompiler2 個類的代碼,稍作修改,保證編譯通過。
????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方法,它會根據(jù)傳入的相對路徑,在加載的編譯視圖集合中加載視圖。下面我們在此處打上斷點,并模擬進(jìn)入DemoPlugin1的主頁。
看完這個調(diào)試過程,你是不是發(fā)現(xiàn)了點什么,當(dāng)我們訪問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
而當(dāng)我們查看現(xiàn)在已有的編譯視圖映射時,你會發(fā)現(xiàn)注冊的對應(yīng)視圖路徑卻是/Views/Plugin1/HelloWorld.cshtml。下面我們再回過頭來看看DemoPlugin1的目錄結(jié)構(gòu)
由此我們推斷出,預(yù)編譯視圖在生成的時候,會記錄當(dāng)前視圖的相對路徑,而在主程序加載的插件的過程中,由于我們使用了Area來區(qū)分模塊,多出的一級目錄,所以導(dǎo)致目錄映射失敗了。因此如果我們將DemoPlugin1的插件視圖目錄結(jié)構(gòu)改為以上提示的 6 個地址之一,問題應(yīng)該就解決了。
那么這里有沒有辦法,在不改變路徑的情況下,讓視圖正常加載呢,答案是有的。參照之前的代碼,在加載視圖組件的時候,我們使用了內(nèi)置類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>();}}這里我們可以參考前面的調(diào)試方式,創(chuàng)建出一套自己的視圖加載類,代碼和當(dāng)前的實現(xiàn)一模一樣
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類的構(gòu)造函數(shù)部分打上斷點。
重新啟動項目之后,你會發(fā)現(xiàn)當(dāng)加載 DemoPlugin1 的視圖時,這里的Identifier屬性其實就是當(dāng)前編譯試圖項的映射目錄。這樣我們很容易就想到在此處動態(tài)修改映射目錄,為此我們需要將模塊名稱通過構(gòu)造函數(shù)傳入,以上 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屬性的值被動態(tài)修改,此處的值也要修改,否則調(diào)試會不成功。
修改完成之后,我們重啟項目,來測試一下。
編譯視圖的映射路徑動態(tài)修改成功,頁面成功被打開了,至此啟動時的預(yù)編譯視圖加載完成。
運行時加載編譯視圖
最后我們來到了運行加載編譯視圖的問題,有了之前的調(diào)試方案,現(xiàn)在調(diào)試起來就輕車熟路。
為了測試,我們再運行時加載編譯視圖,我們首先禁用掉DemoPlugin1, 然后重啟項目,并啟用DemoPlugin1
通過調(diào)試,很明顯問題出在預(yù)編譯視圖的加載上,在啟用組件之后,編譯視圖映射集合沒有更新,所以導(dǎo)致加載失敗。這也證明了我們之前第三章時候的推斷。當(dāng)使用IActionDescriptorChangeProvider重置Controller/Action映射的時候,ASP.NET Core 不會更新視圖映射集合,從而導(dǎo)致視圖加載失敗。
????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來重新創(chuàng)建了一個新的IViewCompiler, 并填充了最新的視圖映射。
PS: 這里的實現(xiàn)方式參考了DefaultViewCompilerProvider的實現(xiàn),該類是在構(gòu)造中填充的視圖映射。
根據(jù)以上修改,在使用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,進(jìn)入插件主頁面,頁面正常顯示了。
至此運行時加載與編譯視圖的場景也順利解決了。
總結(jié)
以上是生活随笔為你收集整理的从零开始实现 ASP.NET Core MVC 的插件式开发(九) - 如何启用预编译视图的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微服务很香--麻辣味,但要慢慢消化
- 下一篇: 如何踢掉 sql 语句中的尾巴,我用 C