在 ASP.NET Core 中使用應用程式模型

作者:Steve Smith

ASP.NET Core MVC 定義了一個「應用程式模型」,代表 MVC 應用程式的元件。 讀取及操作此模型來修改 MVC 項目的行為方式。 根據預設,MVC 遵循特定慣例來判斷哪些類別會被視為控制器、對這些類別的哪些方法是動作,以及參數和路由的行為方式。 自訂此行為,以符合應用程式的需求,方法是建立自訂慣例,並將其全域套用或作為屬性來套用。

模型和提供者 (IApplicationModelProvider)

ASP.NET Core MVC 應用程式模型包含抽象介面和描述 MVC 應用程式的具象實作類別。 此模型是 MVC 根據預設慣例來探索應用程式控制器、動作、動作參數、路由和篩選條件的結果。 藉由使用應用程式模型,修改應用程式以遵循與預設 MVC 行為不同的慣例。 參數、名稱、路由和篩選條件全都用作動作與控制器的組態資料。

ASP.NET Core MVC 應用程式模型具有下列結構:

  • ApplicationModel
    • 控制器 (ControllerModel)
      • 動作 (ActionModel)
        • 參數 (ParameterModel)

模型的每個層級都可存取共同的 Properties 集合,而較低層級可以存取並覆寫階層架構中較高層級所設定的屬性值。 屬性會在建立動作時保存到 ActionDescriptor.Properties。 然後當處理要求時,可以透過 ActionContext.ActionDescriptor 來存取慣例所新增或修改的任何屬性。 使用屬性是根據每個動作設定篩選條件、模型繫結器和其他應用程式模型層面的好方法。

注意

在應用程式啟動之後,ActionDescriptor.Properties 集合不是安全執行緒 (適用於寫入)。 慣例是安全地將資料新增至此集合的最佳方式。

ASP.NET Core MVC 使用 IApplicationModelProvider 介面定義的提供者模式來載入應用程式模型。 本節涵蓋此提供者運作方式的一些內部實作詳細資料。 使用提供者模式是進階主體,主要供架構使用。 大部分的應用程式應該使用慣例,而不是提供者模式。

IApplicationModelProvider 介面的實作會彼此「包裝」,其中每個實作根據其 Order 屬性以遞增順序呼叫 OnProvidersExecuting。 然後以相反順序呼叫 OnProvidersExecuted 方法。 架構會定義數個提供者:

先是 (Order=-1000):

  • DefaultApplicationModelProvider

然後 (Order=-990):

  • AuthorizationApplicationModelProvider
  • CorsApplicationModelProvider

注意

具有相同 Order 值的兩個提供者的呼叫順序未定義,而且不應依賴該順序。

注意

IApplicationModelProvider 是讓架構作者擴充的進階概念。 一般情況下,應用程式應該使用慣例,而架構應該使用提供者。 主要的差別是提供者一律在慣例之前先執行。

DefaultApplicationModelProvider 建立了 ASP.NET Core MVC 使用的許多預設行為。 其負責的部分包括:

  • 將全域篩選條件新增至內容
  • 將控制器新增至內容
  • 將公用控制器方法新增作為動作
  • 將動作方法參數新增至內容
  • 套用路由和其他屬性

某些內建行為由 DefaultApplicationModelProvider 實作。 此提供者負責建構 ControllerModel,而它則會參考 ActionModelPropertyModelParameterModel 執行個體。 DefaultApplicationModelProvider 類別是內部架構實作詳細資料,未來可能會變更。

AuthorizationApplicationModelProvider 負責套用與 AuthorizeFilterAllowAnonymousFilter 屬性建立關聯的行為。 如需詳細資訊,請參閱 ASP.NET Core 中的簡單授權

CorsApplicationModelProvider 會實作與 IEnableCorsAttributeIDisableCorsAttribute 相關聯的行為。 如需詳細資訊,請參閱在 ASP.NET Core 中啟用跨原始來源要求 (CORS)

本節所述的架構內部提供者相關資訊無法透過 .NET API 瀏覽器取得。 不過,您可以在 ASP.NET Core 參考來源 (dotnet/aspnetcore GitHub repository) 中檢查這些提供者。 使用 GitHub 搜尋依名稱尋找提供者,並使用 [切換分支/標籤] 下拉式清單選取來源版本。

慣例

應用程式模型定義了慣例抽象化,提供比覆寫整個模型或提供者更簡單的方式,來自訂模型的行為。 這些抽象是修改應用程式行為的建議方式。 慣例提供一種方式,讓您撰寫動態套用自訂的程式碼。 雖然篩選條件提供一種修改架構行為的方法,但自訂可讓您控制整個應用程式如何一起運作的方式。

可用的慣例如下:

套用慣例的方式是將其新增至 MVC 選項,或實作屬性並將其套用至控制器、動作或動作參數 (類似於篩選條件)。不同於篩選條件,慣例只會在應用程式啟動時才執行,而不是作為每個要求的一部分。

注意

如需 Razor Pages 路由和應用程式模型提供者慣例的相關資訊,請參閱 ASP.NET Core 中的 Razor Pages 路由和應用程式慣例

修改 ApplicationModel

下列慣例用來將屬性新增至應用程式模型:

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ApplicationDescription : IApplicationModelConvention
    {
        private readonly string _description;

        public ApplicationDescription(string description)
        {
            _description = description;
        }

        public void Apply(ApplicationModel application)
        {
            application.Properties["description"] = _description;
        }
    }
}

Startup.ConfigureServices 中新增 MVC 時,應用程式模型慣例會套用為選項:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Conventions.Add(new ApplicationDescription("My Application Description"));
        options.Conventions.Add(new NamespaceRoutingConvention());
    });
}

屬性可從控制器動作內的 ActionDescriptor.Properties 集合中存取:

public class AppModelController : Controller
{
    public string Description()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }
}

修改 ControllerModel 描述

控制器模型也可以包含自訂屬性。 自訂屬性會使用應用程式模型中指定的相同名稱,來覆寫現有的屬性。 下列慣例屬性會在控制器層級新增描述:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ControllerDescriptionAttribute : Attribute, IControllerModelConvention
    {
        private readonly string _description;

        public ControllerDescriptionAttribute(string description)
        {
            _description = description;
        }

        public void Apply(ControllerModel controllerModel)
        {
            controllerModel.Properties["description"] = _description;
        }
    }
}

這個慣例會套用為控制器上的屬性:

[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
    public string Index()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }

修改 ActionModel 描述

個別的屬性規格可以套用至個別的動作,且已在應用程式或控制器層級套用覆寫行為:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class ActionDescriptionAttribute : Attribute, IActionModelConvention
    {
        private readonly string _description;

        public ActionDescriptionAttribute(string description)
        {
            _description = description;
        }

        public void Apply(ActionModel actionModel)
        {
            actionModel.Properties["description"] = _description;
        }
    }
}

將此慣例套用至控制器內的動作,示範其如何覆寫控制器層級慣例:

[ControllerDescription("Controller Description")]
public class DescriptionAttributesController : Controller
{
    public string Index()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }

    [ActionDescription("Action Description")]
    public string UseActionDescriptionAttribute()
    {
        return "Description: " + ControllerContext.ActionDescriptor.Properties["description"];
    }
}

修改 ParameterModel

下列的慣例可以套用至動作參數,以修改其 BindingInfo。 下列慣例要求參數須為路由參數。 其他潛在的繫結來源 (例如查詢字串值) 會被忽略:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace AppModelSample.Conventions
{
    public class MustBeInRouteParameterModelConvention : Attribute, IParameterModelConvention
    {
        public void Apply(ParameterModel model)
        {
            if (model.BindingInfo == null)
            {
                model.BindingInfo = new BindingInfo();
            }
            model.BindingInfo.BindingSource = BindingSource.Path;
        }
    }
}

屬性可套用至任何動作參數:

public class ParameterModelController : Controller
{
    // Will bind:  /ParameterModel/GetById/123
    // WON'T bind: /ParameterModel/GetById?id=123
    public string GetById([MustBeInRouteParameterModelConvention]int id)
    {
        return $"Bound to id: {id}";
    }
}

若要將慣例套用至所有動作參數,請將 MustBeInRouteParameterModelConvention 新增至 Startup.ConfigureServices 中的 MvcOptions

options.Conventions.Add(new MustBeInRouteParameterModelConvention());

修改 ActionModel 名稱

下列慣例會修改 ActionModel 以更新其所套用之動作的「名稱」。 新的名稱會當作傳給屬性的參數。 這個新名稱由路由使用,因此其會影響用來到達此動作方法的路由:

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class CustomActionNameAttribute : Attribute, IActionModelConvention
    {
        private readonly string _actionName;

        public CustomActionNameAttribute(string actionName)
        {
            _actionName = actionName;
        }

        public void Apply(ActionModel actionModel)
        {
            // this name will be used by routing
            actionModel.ActionName = _actionName;
        }
    }
}

這個屬性會套用至 HomeController 中的動作方法:

// Route: /Home/MyCoolAction
[CustomActionName("MyCoolAction")]
public string SomeName()
{
    return ControllerContext.ActionDescriptor.ActionName;
}

即使方法名稱是 SomeName,屬性仍會覆寫使用方法名稱的 MVC 慣例,並將動作名稱取代為 MyCoolAction。 因此,用來連線到此動作的路由是 /Home/MyCoolAction

注意

本節中的這個範例基本上與使用內建 ActionNameAttribute 的範例相同。

自訂路由慣例

使用 IApplicationModelConvention 來自訂路由的運作方式。 例如,下列慣例會將控制器的命名空間納入路由中,從而將命名空間中的 . 取代為路由中的 /

using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;

namespace AppModelSample.Conventions
{
    public class NamespaceRoutingConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            foreach (var controller in application.Controllers)
            {
                var hasAttributeRouteModels = controller.Selectors
                    .Any(selector => selector.AttributeRouteModel != null);

                if (!hasAttributeRouteModels
                    && controller.ControllerName.Contains("Namespace")) // affect one controller in this sample
                {
                    // Replace the . in the namespace with a / to create the attribute route
                    // Ex: MySite.Admin namespace will correspond to MySite/Admin attribute route
                    // Then attach [controller], [action] and optional {id?} token.
                    // [Controller] and [action] is replaced with the controller and action
                    // name to generate the final template
                    controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel()
                    {
                        Template = controller.ControllerType.Namespace.Replace('.', '/') + "/[controller]/[action]/{id?}"
                    };
                }
            }

            // You can continue to put attribute route templates for the controller actions depending on the way you want them to behave
        }
    }
}

慣例會新增為 Startup.ConfigureServices 中的選項:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.Conventions.Add(new ApplicationDescription("My Application Description"));
        options.Conventions.Add(new NamespaceRoutingConvention());
    });
}

提示

使用下列方法,透過 MvcOptions 將慣例新增至 中介軟體{CONVENTION} 預留位置是要新增的慣例:

services.Configure<MvcOptions>(c => c.Conventions.Add({CONVENTION}));

下列範例會將慣例套用至未使用屬性路由的路由,其中控制器在其名稱中具有 Namespace 屬性:

using Microsoft.AspNetCore.Mvc;

namespace AppModelSample.Controllers
{
    public class NamespaceRoutingController : Controller
    {
        // using NamespaceRoutingConvention
        // route: /AppModelSample/Controllers/NamespaceRouting/Index
        public string Index()
        {
            return "This demonstrates namespace routing.";
        }
    }
}

WebApiCompatShim 中的應用程式模型使用方式

ASP.NET Core MVC 會使用與 ASP.NET Web API 2 不同的一組慣例。 您可以使用自訂慣例,來修改 ASP.NET Core MVC 應用程式的行為,以便與 Web API 應用程式一致。 Microsoft 特別針對這個用途而隨附 WebApiCompatShim NuGet 套件

注意

如需從 ASP.NET Web API 移轉的詳細資訊,請參閱從 ASP.NET Web API 移轉至 ASP.NET Core

若要使用 Web API 相容性填充碼:

  • Microsoft.AspNetCore.Mvc.WebApiCompatShim 套件新增至專案。
  • Startup.ConfigureServices 中呼叫 AddWebApiConventions,將慣例新增至 MVC:
services.AddMvc().AddWebApiConventions();

填充碼提供的慣例僅會套用到已套用特定屬性的應用程式組件。 下列四個屬性用來控制哪些控制器應該讓其慣例由填充碼修改:

動作慣例

UseWebApiActionConventionsAttribute 用來根據名稱將 HTTP 方法對應到動作 (例如,Get 會對應至 HttpGet)。 它只適用於不使用屬性路由的動作。

多載化

UseWebApiOverloadingAttribute 用來套用 WebApiOverloadingApplicationModelConvention 慣例。 這個慣例會將 OverloadActionConstraint 新增至動作選取程序,這將候選項目動作限制為要求符合其所有非選擇性參數的動作。

參數慣例

UseWebApiParameterConventionsAttribute 用來套用 WebApiParameterConventionsApplicationModelConvention 動作慣例。 這個慣例指定用來作為動作參數的簡單類型預設會從 URL 繫結,而複雜類型則會從要求主體繫結。

路由

UseWebApiRoutesAttribute 控制是否套用 WebApiApplicationModelConvention 控制器慣例。 啟用時,這個慣例用來將區域的支援新增到路由,並指出控制器位於 api 區域中。

除了一組慣例之外,相容性套件還包括 System.Web.Http.ApiController 基底類別,其會取代 Web API 提供的類別。 這可讓您針對 Web API 撰寫且繼承自其 ApiController 的 Web API 控制器運作,同時在 ASP.NET Core MVC 上執行。 先前所列的所有 UseWebApi* 屬性都會套用至基底控制器類別。 ApiController 會公開屬性、方法和結果類型,這些與 Web API 中找到的屬性、方法和結果類型相容。

使用 ApiExplorer 來記載應用程式

應用程式模型會在可用來周遊應用程式結構的每個層級公開 ApiExplorerModel 屬性。 這可以用來使用 Swagger 等工具為 Web API 產生說明頁面ApiExplorer 屬性會公開 IsVisible 屬性,其可以設定為指定應該公開應用程式模型的哪些部分。 請使用慣例來設定這項設定:

using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace AppModelSample.Conventions
{
    public class EnableApiExplorerApplicationConvention : IApplicationModelConvention
    {
        public void Apply(ApplicationModel application)
        {
            application.ApiExplorer.IsVisible = true;
        }
    }
}

使用此方法 (如有必要,以及其他慣例),應用程式內的任何層級會啟用或停用 API 可見度。