标題:從零開始實作ASP.NET Core MVC的插件式開發(八) - Razor視圖相關問題及解決方案
作者:Lamond Lu
位址:https://www.cnblogs.com/lwqlun/p/13197683.html
源代碼:https://github.com/lamondlu/Mystique
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicGcq5CNyYTN5QDN0QTL4gDM3IDMxcTM5IjNwAjMwITLxMDO1YzLcZDMwIDMy8CXxMDO1YzLcd2bsJ2Lc12bj5ycn9Gbi52YuAjMwIzZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
系列文章
- 從零開始實作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視圖相關問題及解決方案
簡介
在上一篇中,我給大家分享了程式調試問題的解決方案以及如何實作插件中的消息傳遞,完稿之後,又收到了不少問題回報,其中最嚴重的問題應該就是運作時編譯Razor視圖失敗的問題。
本篇我就給大家分享一下我針對此問題的解決方案,最後還會補上上一篇中鴿掉的動态加載菜單(T.T)。
Razor視圖中引用出錯問題
為了模拟一下目前的問題,我們首先之前的插件1中添加一個新類
TestClass
, 并在
HelloWorld
方法中建立一個
TestClass
對象作為視圖模型傳遞給Razor視圖,并在Razor視圖中展示出
TestClass
的
Message
屬性。
- TestClass.cs
public class TestClass
{
public string Message { get; set; }
}
- HelloWorld.cshtml
@using DemoPlugin1.Models;
@model TestClass
@{
}
<h1>@ViewBag.Content</h1>
<h2>@Model.Message</h2>
- Plugin1Controller.cs
[Area("DemoPlugin1")]
public class Plugin1Controller : Controller
{
private INotificationRegister _notificationRegister;
public Plugin1Controller(INotificationRegister notificationRegister)
{
_notificationRegister = notificationRegister;
}
[HttpGet]
public IActionResult HelloWorld()
{
string content = new Demo().SayHello();
ViewBag.Content = content + "; Plugin2 triggered";
TestClass testClass = new TestClass();
testClass.Message = "Hello World";
_notificationRegister.Publish("LoadHelloWorldEvent", JsonConvert.SerializeObject(new LoadHelloWorldEvent() { Str = "Hello World" }));
return View(testClass);
}
}
這個代碼看似很簡單,也是最常用的MVC視圖展示方式,但是內建在動态元件系統中之後,你就會得到以下錯誤界面。
這裡看起來似乎依然感覺是
AssemblyLoadContext
的問題。主要的線索是,如果你将插件1的程式集直接引入主程式工程中,重新啟動項目之後,此處代碼能夠正常通路,是以我猜想
Razor
視圖才進行運作時編譯的時候,使用了預設的
AssemblyLoadContext
,而非插件
AssemblyPart
所在的
AssemblyLoadContext
。
由此我做了一個實驗,我在
MystiqueSetup
方法中,在插件加載的時候,也向預設
AssemblyLoadContext
中加載了插件程式集
public static void MystiqueSetup(this IServiceCollection services,
IConfiguration configuration)
{
...
using (IServiceScope scope = provider.CreateScope())
{
MvcRazorRuntimeCompilationOptions option = scope.ServiceProvider.GetService<MvcRazorRuntimeCompilationOptions>();
IUnitOfWork unitOfWork = scope.ServiceProvider.GetService<IUnitOfWork>();
List<ViewModels.PluginListItemViewModel> allEnabledPlugins = unitOfWork.PluginRepository.GetAllEnabledPlugins();
IReferenceLoader loader = scope.ServiceProvider.GetService<IReferenceLoader>();
foreach (ViewModels.PluginListItemViewModel plugin in allEnabledPlugins)
{
...
using (FileStream fs = new FileStream(filePath, FileMode.Open))
{
System.Reflection.Assembly assembly = context.LoadFromStream(fs);
context.SetEntryPoint(assembly);
loader.LoadStreamsIntoContext(context, referenceFolderPath, assembly);
...
fs.Position = 0;
AssemblyLoadContext.Default.LoadFromStream(fs);
}
context.Enable();
}
}
...
}
重新運作程式,通路插件1的路由,你就會得到以下錯誤。
這說明預設
AssemblyLoadContext
中的程式集正常加載了,隻是和視圖中需要的類型不比對,是以此處也可以說明
Razor
視圖的運作時編譯使用的是預設
AssemblyLoadContext
Notes: 這個場景在前幾篇中遇到過,在不同加載相同的程式集,系統會将嚴格的将他們區分開,插件1中的
AssemblyLoadContext
引用是插件1所在
AssemblyPart
中的
AssemblyLoadContext
類型,這與預設
DemoPlugin1.Models.TestClass
中加載的
AssemblyLoadContext
不符。
DemoPlugin1.Models.TestClass
在之前系列文章中,我介紹過兩次,在ASP.NET Core的設計文檔中,針對
AssemblyLoadContext
部分的是這樣設計的
- 每個ASP.NET Core程式啟動後,都會建立出一個唯一的預設
AssemblyLoadContext
- 開發人員可以自定義
, 當在自定義AssemblyLoadContext
加載某個程式集的時候,如果在目前自定義的AssemblyLoadContext
中找不到該程式集,系統會嘗試在預設AssemlyLoadContext
中加載。AssemblyLoadContext
但是這種程式集加載流程隻是單向的,如果預設
AssemblyLoadContext
未加載某個程式集,但某個自定義
AssemblyLoadContext
中加載了該程式集,你是不能從預設
AssemblyLoadContext
中加載到這個程式集的。
這也就是我們現在遇到的問題,如果你有興趣的話,可以去Review一下ASP.NET Core的針對RuntimeCompilation源碼部分,你會發現當ASP.NET Core的Razor視圖引擎會使用Roslyn來編譯視圖,這裡直接使用了預設的
AssemblyLoadContext
加載視圖所需的程式集引用。
綠線是我們期望的加載方式,紅線是實際的加載方式
為什麼不直接用預設 AssemblyLoadContext
來加載插件?
AssemblyLoadContext
可能會有同學問,為什麼不用預設的
AssemblyLoadContext
來加載插件,這裡有2個主要原因。
首先如果都使用預設的
AssemblyLoadContext
來加載插件,當不同插件使用了兩個不同版本、相同名稱的程式集時, 程式加載會出錯,因為一個
AssemblyLoadContext
不能加載不同版本,相同名稱的程式集,是以在之前我們才設計成了這種使用自定義程式集加載不同插件的方式。
其次如果都是用預設的
AssemblyLoadContext
來加載插件,插件的解除安裝和更新會變成一個大問題,但是如果我們使用自定義
AssemblyLoadContext
的加載插件,當更新和解除安裝插件時,我們可以毫不猶豫的Unload目前的自定義
AssemblyLoadContext
臨時的解決方案
既然不能使用預設
AssemblyLoadContext
來加載程式集了,那麼是不是隻能重寫Razor視圖運作時編譯代碼來滿足目前需求呢?
答案當然是否定了,這裡我們可以通過
AssemblyLoadContext
提供的
Resolving
事件來解決這個問題。
AssemblyLoadContext
Resolving
事件是在目前
AssemblyLoadContext
不能加載指定程式集時觸發的。是以當Razor引擎執行運作時視圖編譯的時候,如果在預設
AssemblyLoadContext
中找不到某個程式集,我們可以強制讓它去自定義的
AssemblyLoadContext
中查找,如果能找到,就直接傳回比對的程式。這樣我們的插件1視圖就可以正常展示了。
public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration)
{
...
AssemblyLoadContext.Default.Resolving += (context, assembly) =>
{
Func<CollectibleAssemblyLoadContext, bool> filter = p =>
p.Assemblies.Any(p => p.GetName().Name == assembly.Name
&& p.GetName().Version == assembly.Version);
if (PluginsLoadContexts.All().Any(filter))
{
var ass = PluginsLoadContexts.All().First(filter)
.Assemblies.First(p => p.GetName().Name == assembly.Name
&& p.GetName().Version == assembly.Version);
return ass;
}
return null;
};
...
}
Note: 這裡其實還有一個問題,如果插件1和插件2都引用了相同版本和名稱的程式集,可能會出現插件1的視圖比對到插件2中程式集的問題,就會出現和前面一樣的程式集沖突。這塊最終的解決肯定還是要重寫Razor的運作時編譯代碼,後續如果能完成這部分,再來更新。
臨時的解決方案是,當一個相同版本和名稱的程式集被2個插件共同使用時,我們可以使用預設
來加載,并跳過自定義
AssemblyLoadContext
針對該程式集的加載。
AssemblyLoadContext
現在我們重新啟動項目,通路插件1路由,頁面正常顯示了。
如何動态加載菜單
之前有小夥伴問,能不能動态加載菜單,每次都是手敲連結進入插件界面相當的不友好。答案是肯定的。
這裡我先做一個簡單的實作,如果後續其他的難點都解決了,我會将這裡的實作改為一個單獨的子產品,實作方式也改的更優雅一點。
首先在
Mystique.Core
項目中添加一個特性類
Page
, 這個特性隻允許在方法上使用,
Name
屬性儲存了目前頁面的名稱。
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class Page : Attribute
{
public Page(string name)
{
Name = name;
}
public string Name { get; set; }
}
第二步,建立一個展示導航欄菜單用的視圖模型類
PageRouteViewModel
,我們會在導航部分使用到它。
public class PageRouteViewModel
{
public PageRouteViewModel(string pageName, string area, string controller, string action)
{
PageName = pageName;
Area = area;
Controller = controller;
Action = action;
}
public string PageName { get; set; }
public string Area { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string Url
{
get
{
return $"{Area}/{Controller}/{Action}";
}
}
}
第三步,我們需要使用反射,從所有啟用的插件程式集中加載所有帶有
Page
特性的路由方法,并将他們組合成一個導航欄菜單的視圖模型集合。
public static class CollectibleAssemblyLoadContextExtension
{
public static List<PageRouteViewModel> GetPages(this CollectibleAssemblyLoadContext context)
{
var entryPointAssembly = context.GetEntryPointAssembly();
var result = new List<PageRouteViewModel>();
if (entryPointAssembly == null || !context.IsEnabled)
{
return result;
}
var areaName = context.PluginName;
var types = entryPointAssembly.GetExportedTypes().Where(p => p.BaseType == typeof(Controller));
if (types.Any())
{
foreach (var type in types)
{
var controllerName = type.Name.Replace("Controller", "");
var actions = type.GetMethods().Where(p => p.GetCustomAttributes(false).Any(x => x.GetType() == typeof(Page))).ToList();
foreach (var action in actions)
{
var actionName = action.Name;
var pageAttribute = (Page)action.GetCustomAttributes(false).First(p => p.GetType() == typeof(Page));
result.Add(new PageRouteViewModel(pageAttribute.Name, areaName, controllerName, actionName));
}
}
return result;
}
else
{
return result;
}
}
}
Notes: 這裡其實可以內建MVC的路由系統來生成Url, 這裡為了簡單示範,就采取了手動拼湊Url的方式,有興趣的同學可以自己改寫一下。
最後我們來修改主站點的母版頁
_Layout.cshtml
, 在導航欄尾部追加動态菜單。
@using Mystique.Core.Mvc.Extensions;
@{
var contexts = Mystique.Core.PluginsLoadContexts.All();
var menus = contexts.SelectMany(p => p.GetPages()).ToList();
}
...
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">DynamicPluginsDemoSite</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Plugins" asp-action="Index">Plugins</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Plugins" asp-action="Assemblies">Assemblies</a>
</li>
@foreach (var item in menus)
{
<li class="nav-item">
<a class="nav-link text-dark" href="/Modules/@item.Url">@item.PageName</a>
</li>
}
</ul>
</div>
</div>
</nav>
</header>
這樣基礎設施部分的代碼就完成了,下面我們來嘗試修改插件1的代碼,在
HelloWorld
路由方法上我們添加特性
[Page("Plugin One")]
, 這樣按照我們的預想,當插件1啟動的時候,導航欄中應該出現
Plugin One
的菜單項。
[Area("DemoPlugin1")]
public class Plugin1Controller : Controller
{
private INotificationRegister _notificationRegister;
public Plugin1Controller(INotificationRegister notificationRegister)
{
_notificationRegister = notificationRegister;
}
[Page("Plugin One")]
[HttpGet]
public IActionResult HelloWorld()
{
string content = new Demo().SayHello();
ViewBag.Content = content + "; Plugin2 triggered";
TestClass testClass = new TestClass();
testClass.Message = "Hello World";
_notificationRegister.Publish("LoadHelloWorldEvent", JsonConvert.SerializeObject(new LoadHelloWorldEvent() { Str = "Hello World" }));
return View(testClass);
}
}
最終效果
下面我們啟動程式,來看一下最終的效果,動态菜單功能完成。
總結
本篇給大家示範了處理
Razor
視圖引用問題的一個臨時解決方案和動态菜單的實作,
Razor
視圖引用問題歸根結底還是
AssemblyLoadContext
的問題,這可能就是ASP.NET Core插件開發最常見的問題了。當然視圖部分也有很多其他的問題,其實我一度感覺如果僅停留在控制器部分,僅實作ASP.NET Core Webapi的插件化可能相對更容易一些,一旦牽扯到
Razor
視圖,特别是運作時編譯
Razor
視圖,就有各種各樣的問題,後續編寫部分元件可能會遇到更多的問題,希望能走的下去,有興趣或者遇到問題的小夥伴可以給我發郵件([email protected])或者在Github(https://github.com/lamondlu/Mystique)中提Issues,感謝支援。