從零開始實現ASP.NET Core MVC的外掛式開發(九) - 升級.NET 5及啟用預編譯檢視
阿新 • • 發佈:2020-11-17
> 標題:從零開始實現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`
![](https://img2020.cnblogs.com/blog/65831/202011/65831-20201117054232604-1334243390.jpg)
# 前景回顧
- [從零開始實現ASP.NET Core MVC的外掛式開發(一) - 使用Application Part動態載入控制器和檢視](https://www.cnblogs.com/lwqlun/p/11137788.html)
- [從零開始實現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)
- [從零開始實現ASP.NET Core MVC的外掛式開發(七) - 近期問題彙總及部分解決方案](https://www.cnblogs.com/lwqlun/p/12930713.html)
- [從零開始實現ASP.NET Core MVC的外掛式開發(八) - Razor檢視相關問題及解決方案](https://www.cnblogs.com/lwqlun/p/13208980.html)
# 簡介
在這個專案建立的時候,專案的初衷是使用預編譯檢視來呈現介面,但是由於多次嘗試失敗,最後改用了執行時編譯檢視,這種方式在第一次載入的時候非常的慢,所有的外掛檢視都要在執行時編譯,而且從便攜性上來說,預編譯檢視更好。近日,在幾位同道的共同努力下,終於實現了這種載入方式。
---------------------------------------------------------------------------------------------------------------------------------------
此篇要鳴謝網友 [j4587698](https://github.com/j4587698) 和 [yang-er](https://github.com/yang-er) 對針對當前專案的支援,你們的思路幫我解決了當前專案針對不能啟用**預編譯檢視**的2個主要的問題
- 在當前專案目錄結構下,啟動時載入元件,元件預編譯檢視不能正常使用
- 執行時載入元件之後,元件中的預編譯檢視不能正常使用
# 升級.NET 5
隨著.NET 5的釋出,當前專案也升級到了.NET 5版本。
![](https://img2020.cnblogs.com/blog/65831/202011/65831-20201117054305055-1097465901.png)
整個升級的過程比我預想的簡單的多,只是修改了一下專案使用的`Target fremework`。重新編譯打包了一下外掛程式,專案就可以正常運行了,整個過程中沒有產生任何因為版本升級導致的編譯問題。
![](https://img2020.cnblogs.com/blog/65831/202011/65831-20201117054317619-842189531.png)
# 預編譯檢視不能使用的問題
在升級了.NET 5之後,我重新嘗試在啟動時關閉了執行時編譯,載入預編譯檢視View, 藉此測試.NET 5對預編譯檢視的支援情況。
```c#
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();
...
}
```
執行專案之後,你會發現專案竟然會得到一個無法找到檢視的錯誤。
![](https://img2020.cnblogs.com/blog/65831/202011/65831-20201117054353255-1065032420.gif)
這裡的結果很奇怪,因為參考[第一章](https://www.cnblogs.com/lwqlun/p/11137788.html)的場景,ASP.NET Core預設是支援啟動時載入預編譯檢視的。在[第一章](https://www.cnblogs.com/lwqlun/p/11137788.html)的時候,我們建立了1個元件,在啟動時,直接載入到主`AssemblyLoadContext`中,啟動之後,我們是可以正常訪問到檢視的。
在仔細思考之後,我想到的兩種可能性。
- 一種可能是因為我們的元件載入在獨立的`AssemblyLoadContext`中,而非主`AssemblyLoadContext`中,所以可能導致載入失敗
- 外掛的目錄結構與[第一章](https://www.cnblogs.com/lwqlun/p/11137788.html#4310745)不符合,導致載入失敗
但是苦於不能除錯ASP.NET Core的原始碼,所以這一部分就暫時擱置了。直到前幾天,網友[j4587698](https://github.com/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(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`
```c#
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原始碼中`DefaultViewCompilerProvider `和`DefaultViewCompiler`2個類的程式碼,稍作修改,保證編譯通過。
```c#
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`的主頁。
![](https://img2020.cnblogs.com/blog/65831/202011/65831-20201117054419340-884351543.gif)
看完這個除錯過程,你是不是發現了點什麼,當我們訪問`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`的目錄結構
![](https://img2020.cnblogs.com/blog/65831/202011/65831-20201117054429032-1751416108.png)
由此我們推斷出,預編譯檢視在生成的時候,會記錄當前檢視的相對路徑,而在主程式載入的外掛的過程中,由於我們使用了`Area`來區分模組,多出的一級目錄,所以導致目錄對映失敗了。因此如果我們將`DemoPlugin1`的外掛檢視目錄結構改為以上提示的6個地址之一,問題應該就解決了。
那麼這裡有沒有辦法,在不改變路徑的情況下,讓檢視正常載入呢,答案是有的。參照之前的程式碼,在載入檢視元件的時候,我們使用了內建類`CompiledRazorAssemblyPart`, 那麼讓我們來看看它的原始碼。
```c#
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`類的集合裡。
```c#
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`
```c#
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`
```c#
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`
```c#
public class MystiqueModuleViewCompiledItem : RazorCompiledItem
{
public override string Identifier { get; }
public override string Kind { get; }
public override IReadOnlyList