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

作者:Steve Smith

ASP.NET Core MVC 会定义一个应用程序模型,用于表示 MVC 应用的各个组件。 通过读取和处理此模型可修改 MVC 元素的行为方式。 默认情况下,MVC 遵循特定的约定,以确定将哪些类视作控制器,这些类上的哪些方法是操作,以及参数和路由的行为方式。 你可以自定义此行为以满足应用的需要,方法如下:创建自己的约定,并将它们应用于全局或作为属性应用。

模型和提供程序

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

ASP.NET Core MVC 应用程序模型具有以下结构:

  • ApplicationModel
    • 控制器 (ControllerModel)
      • 操作 (ActionModel)
        • 参数 (ParameterModel)

该模型的每个级别都有权访问公用 Properties 集合,层次结构中的较低级别可以访问和覆盖由较高级别设置的属性值。 创建操作时,属性保存到 ActionDescriptor.Properties 中。 之后,当处理请求时,可通过 ActionContext.ActionDescriptor.Properties 访问某个约定添加或修改的任何属性。 若要基于每项操作对筛选器、模型绑定器等进行配置,使用属性不失为一个好办法。

备注

一旦完成应用启动,ActionDescriptor.Properties 集合就不再是线程安全的(针对写入)。 约定是将数据安全添加到此集合的最佳方式。

IApplicationModelProvider

ASP.NET Core MVC 使用提供程序模式(由 IApplicationModelProvider 接口定义)加载应用程序模型。 此部分介绍此提供程序的工作原理的一些内部实现细节。 这是一项高级主题 — 利用应用程序模型的大多数应用应使用约定来执行此操作。

IApplicationModelProvider 接口的实现相互“包装”,每个实现都基于其 Order 属性以升序调用 OnProvidersExecuting 然后,按相反的顺序调用 OnProvidersExecuted 方法。 该框架定义了多个提供程序:

首先 (Order=-1000):

然后 (Order=-990):

备注

未定义具有相同 Order 值的两个提供程序的调用顺序,因此不应依赖此顺序。

备注

IApplicationModelProvider 是一种高级概念,框架创建者可对其进行扩展。 一般情况下,应用应使用约定,而框架应使用提供程序。 主要不同之处在于提供程序始终先于约定运行。

DefaultApplicationModelProvider 建立了由 ASP.NET Core MVC 使用的许多默认行为。 其职责包括:

  • 将全局筛选器添加到上下文
  • 将控制器添加到上下文
  • 将公共控制器方法作为操作添加
  • 将操作方法参数添加到上下文
  • 应用路由和其他属性

某些内置行为由 DefaultApplicationModelProvider 实现。 此提供程序负责构造 ControllerModel,后者又引用 ActionModelPropertyModelParameterModel 实例。 DefaultApplicationModelProvider 类是一个内部框架实现细节,未来将对其进行更改。

AuthorizationApplicationModelProvider 负责应用与 AuthorizeFilterAllowAnonymousFilter 属性关联的行为。 详细了解这些属性

CorsApplicationModelProvider 可实现与 IEnableCorsAttributeIDisableCorsAttributeDisableCorsAuthorizationFilter 关联的行为。 详细了解 CORS

约定

应用程序模型定义了约定抽象,通过约定抽象来自定义模型行为比重写整个模型或提供程序更简单。 建议使用这些抽象来修改应用的行为。 通过使用约定,可以编写能动态应用自定义项的代码。 使用筛选器可修改框架的行为,而利用自定义项可控制整个应用协同工作的方式。

可用约定如下:

可通过以下方式应用约定:将它们添加到 MVC 选项,或实现 Attribute 并将它们应用于控制器、操作或操作参数(类似于 Filters)。 与筛选器不同的是,约定仅在应用启动时执行,而不作为每个请求的一部分执行。

示例:修改 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;
        }
    }
}

当在 StartupConfigureServices 中添加 MVC 时,应用程序模型约定作为选项应用。

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

可从控制器操作内的 ActionDescriptor 属性集合中访问属性:

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"];
    }

访问“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}";
    }
}

示例:修改 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

备注

此示例本质上与使用内置 ActionName 属性相同。

示例:自定义路由约定

可以使用 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 中。

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

提示

可以使用 services.Configure<MvcOptions>(c => c.Conventions.Add(YOURCONVENTION)); 来访问 MvcOptions,以将约定添加到中间件

此示例将此约定应用于未使用属性路由的路由,其中,控制器名称包含“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

备注

详细了解从 ASP.NET Web API 迁移

若要使用 Web API Compatibility Shim,需将该包添加到项目中,然后通过调用 Startup 中的 AddWebApiConventions,将约定添加到 MVC:

services.AddMvc().AddWebApiConventions();

该填充程序提供的约定仅适用于应用中已应用特定属性的部分。 以下四个属性用于控制哪些控制器应使用该填充程序的约定来修改自己的约定:

操作约定

UseWebApiActionConventionsAttribute 用于根据名称将 HTTP 方法映射到操作(例如,Get 将映射到 HttpGet)。 它仅适用于不使用属性路由的操作。

重载

UseWebApiOverloadingAttribute 用于应用 WebApiOverloadingApplicationModelConvention 约定。 此约定可向操作选择过程添加 OverloadActionConstraint,以将候选操作限制为其请求满足所有非可选参数的操作。

参数约定

UseWebApiParameterConventionsAttribute 用于应用 WebApiParameterConventionsApplicationModelConvention 操作约定。 此约定指定用作操作参数的简单类型默认来自 URI,而复杂类型来自请求正文。

路由

UseWebApiRoutesAttribute 控制是否应用 WebApiApplicationModelConvention 控制器约定。 启用后,此约定用于向路由添加对区域的支持。

除了一组约定外,该兼容性包还包含一个 System.Web.Http.ApiController 基类,用于替换 Web API 提供的等效项。 这允许针对 Web API 编写并且继承自 ApiController 的控制器在 ASP.NET Core MVC 上运行时,能够按照设计的方式运行。 前面列出的所有 UseWebApi* 属性将应用于基本控制器类。 ApiController 公开了与在 Web API 中找到的属性、方法和结果类型兼容的属性、方法和结果类型。

使用 ApiExplorer 记录应用

应用程序模型在每个级别公开了 ApiExplorer 属性,该属性可用于遍历应用的结构。 这可用于使用 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 可见性。

上一篇:使用应用程序部件共享控制器、视图、Razor Pages 等

下一篇:ASP.NET Core 中的区域

关注微信小程序
程序员编程王-随时随地学编程

扫描二维码
程序员编程王

扫一扫关注最新编程教程