從零開始實現ASP.NET Core MVC的外掛式開發(七) - 近期問題彙總及部分問題解決方案
阿新 • • 發佈:2020-05-21
> 標題:從零開始實現ASP.NET Core MVC的外掛式開發(七) - 近期問題彙總及部分問題解決方案
> 作者:Lamond Lu
> 地址:https://www.cnblogs.com/lwqlun/p/12930713.html
> 原始碼:https://github.com/lamondlu/Mystique
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144538765-1489990103.jpg)
# 前景回顧
- [從零開始實現ASP.NET Core MVC的外掛式開發(一) - 使用Application Part動態載入控制器和檢視](https://www.cnblogs.com/lwqlun/p/11137788.html#4310745)
- [從零開始實現ASP.NET Core MVC的外掛式開發(二) - 如何建立專案模板](https://www.cnblogs.com/lwqlun/p/11155666.html)
- [從零開始實現ASP.NET Core MVC的外掛式開發(三) - 如何在執行時啟用元件](https://www.cnblogs.com/lwqlun/p/11260750.html )
- [從零開始實現ASP.NET Core MVC的外掛式開發(四) - 外掛安裝](https://www.cnblogs.com/lwqlun/p/11343141.html )
- [從零開始實現ASP.NET Core MVC的外掛式開發(五) - 使用AssemblyLoadContext實現外掛的升級和刪除](https://www.cnblogs.com/lwqlun/p/11395828.html)
- [從零開始實現ASP.NET Core MVC的外掛式開發(六) - 如何載入外掛引用](https://www.cnblogs.com/lwqlun/p/11717254.html)
# 簡介
在上一篇中,我給大家講解外掛引用程式集的載入問題,在載入外掛的時候,我們不僅需要載入外掛的程式集,還需要載入外掛引用的程式集。在上一篇寫完之後,有許多小夥伴聯絡到我,提出了各種各樣的問題,在這裡謝謝大家的支援,你們就是我前進的動力。本篇呢,我就對這其中的一些主要問題進行一下彙總和解答。
# 如何在Visual Studio中以除錯模式啟動專案?
在所有的問題中,提到最多的問題就是如何在Visual Studio中使用除錯模式啟動專案的問題。當前專案在預設情況下,可以在Visual Studio中啟動除錯模式,但是當你嘗試訪問已安裝外掛路由時,所有的外掛檢視都打不開。
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144556887-1429547582.png)
這裡之前給出臨時的解決方案是在`bin\Debug\netcoreapp3.1`目錄中使用命令列`dotnet Mystique.dll`的方式啟動專案。
## 檢視找不到的原因及解決方案
這個問題的主要原因就是主站點在Visual Studio中以除錯模式啟動的時候,預設的`Working directory`是當前專案的根目錄,而非`bin\Debug\netcoreapp3.1`目錄,所以當主程式查詢外掛檢視的時候,按照所有的內建規則,都找不到指定的檢視檔案, 所以就給出了`The view 'xx' was not found`的錯誤資訊。
因此,這裡我們要做的就是修改一下當前主站點的`Working directory`即可,這裡我們需要將`Working directory`設定為當前主站點下的`bin\Debug\netcoreapp3.1`目錄。
> PS: 我在開發過程中,將.NET Core升級到了3.1版本,如果你還在使用.NET Core 2.2或者.NET Core 3.0,請將`Working directory`配置為相應目錄
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144614261-1910410453.png)
這樣當你在Visual Studio中再次以除錯模式啟動專案之後,就能訪問到外掛檢視了。
## 隨之而來的樣式丟失問題
看完前面的解決方案之後,你不是已經躍躍欲試了?
但是當你啟動專案之後,會心涼半截,你會發現整站的樣式和Javascript指令碼檔案引用都丟失了。
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144622254-549083241.png)
這裡的原因是主站點的預設靜態資原始檔都放置在專案根目錄的`wwwroot`子目錄中,但是現在我們將`Working directory`改為了`bin\Debug\netcoreapp3.1`了,在`bin\Debug\netcoreapp3.1`中並沒有`wwwroot`子目錄,所以在修改`Working directory`後,就不能正常載入靜態資原始檔了。
這裡為了修復這個問題,我們需要對程式碼做兩處修改。
首先呢,我們需要知道當我們使用`app.UseStaticFiles()`新增靜態資原始檔目錄,並以在Visual Studio中以除錯模式啟動專案的時候,專案查詢的預設目錄是當前專案根目錄中的`wwwroot`目錄,所以這裡我們需要將這個地方改為`PhysicalFileProvider`的實現方式,並指定當前靜態資原始檔所在目錄是專案目錄下的`wwwroot`目錄。
其次,因為當前配置只是針對Visual Studio除錯的,所以我們需要使用預編譯指令`#if DEBUG`和`#if !DEBUG針對不同的場景進行不同的靜態資原始檔目錄配置。
所以`Configure()`方法最終的修改結果如下:
```c#
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
#if DEBUG
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(@"G:\D1\Mystique\Mystique\wwwroot")
});
#endif
#if !DEBUG
app.UseStaticFiles();
#endif
app.MystiqueRoute();
}
```
在完成修改之後,重新編譯專案,並以除錯模式啟動專案後,你就會發現,我們熟悉的介面又回來了。
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144634722-1993455786.png)
# 如何實現外掛間的訊息傳遞?
這個問題是去年年底和衣明志大哥討論動態外掛開發的時候,衣哥提出來的功能,本身實現思路不麻煩,但是實踐過程中,我卻讓`AssemblyLoadContext`給絆了一跤。
## 基本思路
這裡因為需要實現兩個不同外掛的訊息通訊,最簡單的方式是使用訊息註冊訂閱。
> PS: 使用第三方訊息佇列也是一種實現方式,但是本次實踐中只是為了簡單,沒有使用額外的訊息註冊訂閱元件,直接使用了程序內的訊息註冊訂閱
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144642020-1142263850.png)
基本思路:
- 定義`INotificationHandler`介面來處理訊息
- 在每個獨立元件中,我們通過`INotificationProvider`介面向主程式公開當前元件訂閱的訊息及處理程式
- 在主站點中,我們通過`INotificationRegister`介面實現一個訊息註冊訂閱容器,當站點啟動,系統可以通過每個元件的`INotificationProvider`介面實現,將訂閱的訊息和處理程式註冊到主站點的訊息釋出訂閱容器中。
- 每個外掛中,使用`INotifcationRegister`介面的`Publish`方法釋出訊息
根據以上思路,我們首先定義一個訊息處理介面`INotification`
```c#
public interface INotificationHandler
{
void Handle(string data);
}
```
這裡我沒有采用強型別的來規範訊息的格式,主要原因是如果使用強型別定義訊息,不同的外掛勢必都要引用一個存放強型別強型別訊息定義的的程式集,這樣會增加外掛之間的耦合度,每個外掛就開發起來變得不那麼獨立了。
> PS: 以上設計只是個人喜好,如果你喜歡使用強型別也完全沒有問題。
接下來,我們再來定義訊息釋出訂閱介面以及訊息處理程式介面
```c#
public interface INotificationProvider
{
Dictionary> GetNotifications();
}
```
```c#
public interface INotificationRegister
{
void Subscribe(string eventName, INotificationHandler handler);
void Publish(string eventName, string data);
}
```
這裡程式碼非常的簡單,`INotificationProvider`介面提供一個訊息處理器的集合,`INotificationRegister`介面定義了訊息訂閱和釋出的方法。
下面我們在`Mystique.Core.Mvc`專案中完成`INotificationRegister`的介面實現。
```c#
public class NotificationRegister : INotificationRegister
{
private static Dictionary>
_containers = new Dictionary>();
public void Publish(string eventName, string data)
{
if (_containers.ContainsKey(eventName))
{
foreach (var item in _containers[eventName])
{
item.Handle(data);
}
}
}
public void Subscribe(string eventName, INotificationHandler handler)
{
if (_containers.ContainsKey(eventName))
{
_containers[eventName].Add(handler);
}
else
{
_containers[eventName] = new List() { handler };
}
}
}
```
最後,我們還需要在專案啟動方法`MystiqueSetup`中配置訊息訂閱器的發現和繫結。
```c#
public static void MystiqueSetup(this IServiceCollection services,
IConfiguration configuration)
{
...
using (IServiceScope scope = provider.CreateScope())
{
...
foreach (ViewModels.PluginListItemViewModel plugin in allEnabledPlugins)
{
...
using (FileStream fs = new FileStream(filePath, FileMode.Open))
{
...
var providers = assembly.GetExportedTypes()
.Where(p => p.GetInterfaces()
.Any(x => x.Name == "INotificationProvider"));
if (providers != null && providers.Count() > 0)
{
var register = scope.ServiceProvider
.GetService();
foreach (var p in providers)
{
var obj = (INotificationProvider)assembly
.CreateInstance(p.FullName);
var result = obj.GetNotifications();
foreach (var item in result)
{
foreach (var i in item.Value)
{
register.Subscribe(item.Key, i);
}
}
}
}
}
}
}
...
}
```
完成以上基礎設定之後,我們就可以嘗試在外掛中釋出訂閱訊息了。
首先這裡我們在DemoPlugin2中建立訊息`LoadHelloWorldEvent`,並建立對應的訊息處理器`LoadHelloWorldEventHandler`.
```c#
public class NotificationProvider : INotificationProvider
{
public Dictionary> GetNotifications()
{
var handlers = new List { new LoadHelloWorldEventHandler() };
var result = new Dictionary>();
result.Add("LoadHelloWorldEvent", handlers);
return result;
}
}
public class LoadHelloWorldEventHandler : INotificationHandler
{
public void Handle(string data)
{
Console.WriteLine("Plugin2 handled hello world events." + data);
}
}
public class LoadHelloWorldEvent
{
public string Str { get; set; }
}
```
然後我們修改DemoPlugin1的HelloWorld方法,在返回檢視之前,釋出一個`LoadHelloWorldEvent`的訊息。
```c#
[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";
_notificationRegister.Publish("LoadHelloWorldEvent", JsonConvert.SerializeObject(new LoadHelloWorldEvent() { Str = "Hello World" }));
return View();
}
}
public class LoadHelloWorldEvent
{
public string Str { get; set; }
}
```
## AssemblyLoadContext產生的靈異問題
上面的程式碼看起來很美好,但是實際執行的時候,你會遇到一個靈異的問題,就是系統不能將DemoPlugin2中的`NotificationProvider`轉換為`INotificationProvider`介面型別的物件。
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144656346-427681659.png)
這個問題困擾了我半天,完全想象不出可能的問題,但是我隱約感覺這是一個`AssemblyLoadContext`引起的問題。
在上一篇中,我們曾經查詢過.NET Core的程式集載入設計文件。
在.NET Core的設計文件中,對於程式集載入有這樣一段描述
>If the assembly was already present in *A1's* context, either because we had successfully loaded it earlier, or because we failed to load it for some reason, we return the corresponding status (and assembly reference for the success case).
>
>However, if *C1* was not found in *A1's* context, the *Load* method override in *A1's* context is invoked.
>
>- For *Custom LoadContext*, this override is an opportunity to load an assembly **before** the fallback (see below) to *Default LoadContext* is attempted to resolve the load.
>- For *Default LoadContext*, this override always returns *null* since *Default Context* cannot override itself.
這裡簡單來說,意思就是當在一個自定義`LoadContext`中載入程式集的時候,如果找不到這個程式集,程式會自動去預設`LoadContext`中查詢,如果預設`LoadContext`中都找不到,就會返回`null`。
這裡我突然想到會不會是因為DemoPlugin1、DemoPlugin2以及主站點的`AssemblyLoadContext`都載入了`Mystique.Core.dll`程式集的緣故,雖然他們載入的是同一個程式集,但是因為`LoadContext`不同,所以系統認為它是2個程式集。
> PS: 主站點的`AssemblyLoadContext`即預設的`LoadContext`
其實對於DemoPlugin1和DemoPlugin2來說,它們完全沒有必須要載入`Mystique.Core.dll`程式集,因為主站點的預設`LoadContext`已經載入了此程式集,所以當DemoPlugin1和DemoPlugin2使用`Mystique.Core.dll`程式集中定義的`INotificationProvider`時,就會去預設的`LoadContext`中載入,這樣他們載入的程式集就都是預設`LoadContext`中的了,就不存在差異了。
於是根據這個思路,我修改了一下外掛程式集載入部分的程式碼,將`Mystique.Core.*`程式集排除在載入列表中。
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144704107-837400980.png)
重新啟動專案之後,專案正常執行,訊息釋出訂閱能正常執行。
![](https://img2020.cnblogs.com/blog/65831/202005/65831-20200521144709890-625544939.png)
# 專案後續嘗試新增的功能
由於篇幅問題,剩餘的其他問題和功能會在下一篇中來完成。以下是專案後續會逐步新增的功能
- 新增/移除外掛後,主站點導航欄自動載入外掛入口頁面(已完成,下一篇中說明)
- 在主站點中,新增頁面管理模組
- 嘗試一個頁面載入多個外掛,當前的外掛只能實現一個外掛一個頁面。
不過如果大家如果有什麼其他想法,也可以給我留言或者在Github上提Issue,你們的建議就是我進步的動力。
# 總結
本篇針對前一陣子Github Issue和文件評論中比較集中的問題進行了說明和解答,主要講解了如何在Visual Studio中除錯執行外掛以及如何實現外掛間的訊息傳輸。後續我會根據反饋,繼續新增新內容,大家敬請