使用 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 存储库)中检查提供程序。 使用 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 附带了专用于此的 WebApiCompatShimNuGet 包

注意

有关从 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 操作约定。 此约定指定用作操作参数的简单类型默认来自 URI,而复杂类型来自请求正文。

路由

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 可见性。