深入解析ASP.NET Core MVC的模組化設計[下篇]

Artech發表於2024-03-06

ASP.NET Core MVC的“模組化”設計使我們可以構成應用的基本單元Controller定義在任意的模組(程式集)中,並在執行時動態載入和解除安裝。《設計篇》介紹了這種為“飛行中的飛機加油”的方案的實現原理?本篇我們將演示將介紹“分散定義Controller”的N種實現方案。原始碼從這裡下載。

一、標註ApplicationPartAttribute特性
二、標註RelatedAssemblyAttribute特性
三、註冊ApplicationPartManager
四、新增ApplicationPart到現有ApplicationPartManager

一、標註ApplicationPartAttribute特性

接下來我們就透過幾個簡單的例項來演示如何將Controller型別定義在非入口應用所在的專案中。我們建立如圖1所示的解決方案,其中App是一個MVC應用型別的專案,而Foo則是一個普通的類庫專案,App具有針對Foo的專案引用。我們希望將部分Controller型別定義在Foo這個類庫專案中。

image_thumb[12]
圖1 將部分Controller型別定義在Foo專案中

我們在App專案中定義瞭如下這個HomeController。如程式碼片段所示,我們在建構函式中注入了ApplicationPartManager物件,並利用它得到當前應用範圍內所有有效Controller型別。在執行應用根路徑的Action方法Index中,我們將得到的有效Controller型別名稱呈現出來。如下所示的FooController型別是我們在Foo專案中定義的Controller型別。

public class HomeController : Controller
{
    private readonly IEnumerable<Type> _controllerTypes;
    public HomeController(ApplicationPartManager manager)
    {
        var feature = new ControllerFeature();
        manager.PopulateFeature(feature);
        _controllerTypes = feature.Controllers;
    }

    [HttpGet("/")]
    public string Index()
    {
        var lines = _controllerTypes.Select(it => it.Name);
        return string.Join(Environment.NewLine, lines.ToArray());
    }
}

public class FooController
{
    public void Index() => throw new NotImplementedException();
}

在啟動這個演示程式後,如果利用瀏覽器透過根路徑訪問定義在HomeController型別中的Action方法Index,我們會得到如圖2所示的輸出結果。從輸出結果可以看出,定義在非MVC應用專案Foo中的Controller型別在預設情況下是不會被解析的。

image_thumb[14]
圖2 預設只解析MVC應用所在專案定義的Controller

如果希望MVC應用在進行Controller型別解析的時候將專案Foo編譯後的程式集(預設為Foo.dll)包括進來,我們可以在應用所在專案中標註ApplicationPartAttribute特性將程式集Foo作為應用的組成部分。所以我們在Program.cs中針對ApplicationPartAttribute特性進行了如下的標記。

[assembly:ApplicationPart("Foo")]

修改後的程式集啟動之後,再次利用瀏覽器按照按照相同的路徑對它發起請求,我們將得到如圖3所示的輸出結果。由於程式集Foo成為了當前應用的有效組成部分,定義在它裡面的BarController自然也成為了當前應用有效的Controller型別。

image_thumb[16]
圖3 解析ApplicationPartAttribute特性指向程式集中的Controller型別

二、標註RelatedAssemblyAttribute特性

除了在入口程式集上標註ApplicationPartAttribute特性將某個程式集作為當前應用的有效組成部分之外,我們也可以透過標註RelatedAssemblyAttribute達到相同的目的。根據前面的介紹,我們知道RelatedAssemblyAttribute特性只能標註到入口程式集或者ApplicationPartAttribute特性指向的程式集中,所以我們可以將RelatedAssemblyAttribute特性標註到Foo專案中將另一個程式集包含進行。為此我們在解決方案中新增了另一個類庫專案Bar(如圖4所示),併為App新增針對Bar的專案引用,然後在Bar專案中定義一個類似於FooController的BarController型別。

image_thumb[19]
圖4 將部分Controller型別定義在Foo和Bar專案中

為了將專案Bar編譯後生成的程式集(預設為Bar.dll)作為當前應用的組成部分,我們可以選擇在App或者Foo專案中標註一個指向它的RelatedAssemblyAttribute特性。對於我們演示的例項來說,我們選擇在FooController.cs中以如下形式標註一個指向程式集Bar的RelatedAssemblyAttribute特性。

[assembly: RelatedAssembly("Bar")]

修改後的程式集啟動之後,再次利用瀏覽器按照按照相同的路徑對它發起請求,我們將得到如圖5所示的輸出結果。由於程式集Bar成為了當前應用的有效組成部分,定義在它裡面的BazController自然也成為了當前應用有效的Controller型別。

image_thumb[21]
圖5 解析RelatedAssemblyAttribute特性指向程式集中的Controller型別

三、註冊ApplicationPartManager

由於針對有效Controller型別的解析是利用註冊的ApplicationPartManager物件實現的,所以我們完全可以透過註冊一個ApplicationPartManager物件的方式達到相同的目的。接下來我們將上一個演示例項中標註的ApplicationPartAttribute和RelatedAssemblyAttribute特性刪除,並將承載程式修改為如下的形式。

var manager = new ApplicationPartManager();
var entry = Assembly.GetEntryAssembly()!;
var foo = Assembly.Load(new AssemblyName("Foo"));
var bar = Assembly.Load(new AssemblyName("Bar"));

manager.ApplicationParts.Add(new AssemblyPart(entry));
manager.ApplicationParts.Add(new AssemblyPart(foo));
manager.ApplicationParts.Add(new AssemblyPart(bar));
manager.FeatureProviders.Add(new ControllerFeatureProvider());


var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddSingleton(manager)
    .AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

如上面的程式碼片段所示,我們建立了一個ApplicationPartManager物件,並在其ApplicationParts屬性中顯式新增了指向入口程式集以及Foo和Bar程式集的AssemblyPart物件。為了能夠讓這個ApplicationPartManager物件具有解析Controller型別的能力,我們在其FeatureProviders中新增了一個ControllerFeatureProvider物件。在後續的應用承載程式中,我們將這個ApplicationPartManager物件作為服務註冊到依賴注入框架中。修改後的程式集啟動之後,再次利用瀏覽器按照按照相同的路徑對它發起請求,我們依然會得到如圖5所示的輸出結果。

四、新增ApplicationPart到現有ApplicationPartManager

其實我們沒有必要註冊一個新的,按照如下的方式將Foo、Bar程式集轉換成AssemblyPart並將其新增到現有的ApplicationPartManager之中也可以達到相同的目的。

var builder = WebApplication.CreateBuilder(args);
builder.Services
    .AddControllers()
    .AddApplicationPart(Assembly.Load(new AssemblyName("Foo")))
    .AddApplicationPart(Assembly.Load(new AssemblyName("Bar")));
var app = builder.Build();
app.MapControllers();
app.Run();

深入解析ASP.NET Core MVC的模組化設計[上篇]
深入解析ASP.NET Core MVC的模組化設計[下篇]
如何實現執行時動態定義Controller型別?

相關文章