在 ASP.NET Core 中使用应用程序模型

作者:Steve Smith

ASP.NET Core MVC 定义表示 MVC 应用的组件 的应用程序模型 。 读取和操控此模型以修改 MVC 元素的行为。 默认情况下,MVC 遵循某些约定来确定哪些类被认为是控制器,这些类上的哪些方法是动作,以及参数和路由的行为方式。 通过创建自定义约定并将其全局应用或作为属性来自定义此行为以满足应用的需求。

模型和供应商 (IApplicationModelProvider

ASP.NET 核心 MVC 应用程序模型包括描述 MVC 应用程序的抽象接口和具体实现类。 此模型是 MVC 根据默认规范识别应用程序的控制器、操作、操作参数、路由和筛选器的结果。 通过使用应用程序模型,修改应用以遵循与默认 MVC 行为不同的约定。 参数、名称、路由和筛选器都用作动作与控制器的配置数据。

ASP.NET 核心 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,该ControllerModel又引用了ActionModelPropertyModelParameterModel实例。 该 DefaultApplicationModelProvider 类是一个内部框架实现详细信息,将来可能会更改。

AuthorizationApplicationModelProvider负责应用与AuthorizeFilterAllowAnonymousFilter属性关联的行为。 有关详细信息,请参阅 ASP.NET Core 中的简单授权

实现与IEnableCorsAttributeIDisableCorsAttribute 相关的行为CorsApplicationModelProvider。 有关详细信息,请参阅 ASP.NET Core 中的启用跨域请求(CORS)。

本部分所述的框架内部提供程序的信息无法通过 .NET API 浏览器获取。 但是,可以在 ASP.NET 核心引用源(dotnet/aspnetcore GitHub 存储库)中检查提供程序。 使用 GitHub 搜索按名称查找提供程序,并使用 Switch 分支/标记 下拉列表选择源的版本。

Conventions

应用程序模型定义约定性抽象,提供比重写整个模型或提供者更为简便的方式来自定义模型的行为。 这些抽象是修改应用行为的推荐方法。 约定提供了一种编写动态应用自定义项的代码的方法。 虽然 筛选器 提供了修改框架行为的方法,但自定义项允许控制整个应用协同工作的方式。

可以使用以下约定:

约定通过将它们添加到 MVC 选项或实现属性,并将其应用于控制器、操作或操作参数(类似于 筛选器)。约定和筛选器不同,仅在应用启动时执行,而不是在每个请求中执行。

注释

有关 Pages 路由和应用程序模型提供程序约定的信息 Razor ,请参阅 Razor ASP.NET Core 中的 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添加到MvcOptionsStartup.ConfigureServices中。

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

Routes

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