ASP.NET Core 开发实践汇总
本文索引:
- ASP.NET Core 的入口点在哪?
- 使用 ASP.NET Core 2.1 开发 Api Controller
- DI 的 Scoped 生存期是什么?
- 你真的了解中间件吗?
- IFilter 和 Attribute,我该用哪一个?
ASP.NET Core 的入口点在哪?
正常情况下,ASP.NET Core 应用的 Program
类型看上去大概如下:
1 | public class Program |
ASP.NET Core 的 Main
方法实际是启动了一个 WebHost
的实例,该 WebHost
通过一个 Builder
对其进行配置,以下是 CreateWebHostBuilder
方法的实现:
1 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => |
多次调用
ConfigureServices
将追加到前一个调用,多次调用Configure
将基于上一个调用。
Startup 从何而来?
微软认为,调用 IWebHostBuilder
的配置方法,诸如 ConfigureServices
以及 Configure
应该交由单独的类型来控制,于是对 IWebHostBuilder
接口添加了扩展方法 UseStartup<TStartup>
。TStartup
可以不实现任何接口,但必须以约定命名的方式命名并实现框架要求的方法:
- 必须包括
Configure
方法,以创建应用的请求处理管道。 - 可选择性地包括
ConfigureServices
方法。
可以为不同的环境定义对应的 Startup
类型(例如 StartupDevelopment
),同样基于约定,如果应用在开发环境中运行并包含 Startup
类和 StartupDevelopment
类,则使用 StartupDevelopment
类。详情参考Startup 类约定
IStartupFilter 是什么?
通过其命名可以看出,该接口是一种 Filter
,用于在 Startup
类型外部添加中间件注册,这意味着:
- 在
StartupFilter
中注册的中间件将先于Startup.Configure
方法注册的任何中间件执行 - 多个
IStartupFilter
接口将按照注册顺序决定中间件的执行顺序 - 可利用外部程序集增强 ASP.NET Core 的功能而不影响其原本的业务逻辑
IStartupFilter
仅定义了一个方法:
1 | public class RequestSetOptionsStartupFilter : IStartupFilter |
通过 IWebHostBuilder.ConfigureServices
方法来注册该 Filter:
1 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) |
使用 ASP.NET Core 2.1 开发 Api Controller
ASP.NET Core 2.1 版本新增了部分便于开发 Web Api 的功能。现在,当创建用于 Web Api 的控制器时:
- 从
ControllerBase
类继承: Api 控制器类型不再需要继承自传统的 Mvc 控制器Controller
类型,ControllerBase
提供了诸如BadRequest()
、CreateAtAction()
等分别返回相应状态码的行为 - 对类型标注
ApiController
特性: ASP.NET Core 2.1 版本引入了ApiController
特性,该特性通常结合ControllerBase
来为控制器启用 REST 的行为。为了确保在控制器级别该特性能够正常工作,需要在Startup.ConfigureServices
方法设置兼容版本:1
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
ApiController
添加了以下行为:
自动 HTTP 400 相应
当模型验证失败时,ModelState.IsValid
的计算结果为 false
,并自动返回包含问题详细信息的 HTTP 400 响应。因此,不再需要以下类似代码:
1 | if (!ModelState.IsValid) |
自动参数源推定
应用了 ApiController
的控制器类,将根据下表隐式推定参数的绑定源:
FromBody | 请求正文 |
---|---|
FromForm | 表单数据 |
FromHeader | 请求头 |
FromQuery | 查询字符串参数 |
FromRoute | 路由数据 |
FromServices | 作为操作参数插入的请求服务 |
没有 ApiController
特性时,需要为入站请求参数显式定义绑定源特性。在下例中,FromQuery
特性指示 discontinuedOnly 参数值在请求 URL 的查询字符串中提供:
1 | [ ] |
Route 特性现在是必须的
使用 ApiController
特性标注的控制器类型将不再采用 Startup.Configure
方法中为 Mvc 程序定义的诸如 UseMvc().UseMvcWithDefaultRoute
的约定式路由策略。转而要求必须为每个标注了 ApiController
特性的控制器类型添加 Route
特性标注。
ApiBehaviorOptions
上述讨论的默认行为可通过 ApiBehaviorOptions
对象启用或禁用,在 Startup.ConfigureServices
方法中修改这些配置的值:
1 | services.Configure<ApiBehaviorOptions>(options => |
具体每项配置的影响可参考 ApiBehaviorOptions
DI 的 Scoped 生存期是什么?
初次接触 ASP.NET Core 的开发人员可能对 Scoped
生存期心存疑惑:
- 作用域生存期的服务以每个请求一次的方式创建,即
instance per HttpContext
- 默认情况下,在中间件内使用有作用域的服务必须在
Invoke
或InvokeAsync
方法参数注入服务,而不要通过构造函数进行注入,因为「按约定激活」的中间件总是以单例创建,在构造函数中注入Scoped
生存期的服务将导致不一致的状态。
你真的了解中间件吗?
中间件的特点是:
- 选择是否将请求传递到管道中的下一个组件
- 可在调用管道中的下一个组件前后执行工作
中间件可定义为匿名委托或创建单独的类型实现,可借助以下方法应用中间件:
Run
: 该方法进传入一个HttpContext
参数,指示该中间件总在管道的最后执行Map
: 传入匹配条件,使得中间件在某些条件下执行Use
: 传入参数中包含下一个中间件的引用,如果不调用next.Invoke
,可短路管道
如何定义中间件?
ASP.NET Core 大量使用了命名约定的方式来隐式定义组件,中间件也不例外,默认情况下,以 Middleware
后缀结尾的类型被认为是隐式定义的中间件,该类型必须遵循:
- 构造函数接收一个代表下一个中间件的
RequestDelegate
参数注入 - 包含一个
Task InvokeAsync(HttpContext context)
的方法签名
如前文所述,中间件实例在应用程序启动时创建,如果定义的中间件需要使用
Scoped
生存期的服务,则必须在Task InvokeAsync(HttpContext context)
方法的参数列表中添加注入。
随后,框架在 IStartupFilter
和 Startup.Configure
方法中调用 IApplicationBuilder.Use<TMiddleware>
以激活中间件。通常,为了方便使用,会专门为中间件定义 IApplicationBuilder.Use<Middlware>
的扩展方法。例如 IApplicationBuilder.UseMvc()
。
IMiddleware 接口有什么用?
除了基于命名约定的方式定义和激活中间件,微软提供了 IMiddleware
接口用于显式定义中间件并以工厂模式激活中间件。UseMiddleware
方法检查中间件的注册类型是否实现了 IMiddleware
接口,如果是,则从服务容器中解析 IMiddlewareFactory
实例激活中间件,这样做的好处在于:
- 可实现按作用域管理中间件的生存期
- 中间件强类型化
IMiddleware
接口定义了 InvokeAsync(HttpContext, RequestDelegate)
方法,其实现类可在构造函数中注入 Scoped
或 Singleton
生存期的服务。同时,基于工厂的中间件在调用 IApplicationBuilder.UseMiddleware<TMiddleware>
时不再支持传入参数。
IFilter 和 Attribute,我该用哪一个?
首先要明确一点,Filter
是组件,而 Attribute
支持以标注方式使用 Filter
的一种方式。
Filter
在 ASP.NET 中由来已久,ASP.NET Core 中定义了诸多内置 Filter
,例如 IActionFilter
, IResultFilter
等等,可通过创建实现这些接口的类型或继承现有类型来定义 Filter
。定义好 Filter
之后,便可在 ConfigureServices
中注册该 Filter
,随后便可在 AddMvc
方法中以服务或实例的方式对该 Filter
进行应用。
1 | services.AddMvc(options => { |
通过 Attribute 将 Filter 应用到 Controller 和 Action 级别
上述方式展示了在「全局级别」应用 Filter
的案例,ASP.NET Core 还提供了在 Controller
和 Action
级别应用 Filter
的方式,这是结合 Attribute
和 IFilterMetadata
接口实现的。
IFilterMetadata
用于标记 Filter
类型,该接口未定义任何方法签名,仅仅是告知框架该类型是一个 Filter
。另一个接口 IFilterFactory
派生自 IFilterMetadata
接口,进一步告知框架该 Filter
的实例应该如何创建。与 Middleware
的工厂激活模式相似,这种模式不必在应用程序中显式指定 Filter
的创建时机,转而通过引入 Attribute
来扮演 IFilterFactory
的角色。所以定义的 FilterAttribute
通常都实现了 IFilterFactory
接口,并在接口方法中返回一个与之对应的真正的 Filter
实例。此处的 Attribute
实际上是 Filter
的提供器。
ServiceFilter 和 TypeFilters 又是什么?
由于 Attribute
在语言层面的限制(必须显式传入构造参数),它无法通过自身提供有 DI 依赖的 Filter
实例,为了解决这个问题,微软提供了 ServiceFilterAttribute
和 TypeFilterAttribute
:
ServiceFilterAttribute
: 通过Attribute
的方式 从 DI 解析目标Filter
类型的实例,该Filter
必须要在Startup
的ConfigureServices
中注册,否则将抛出异常TypeFilterAttribute
: 与ServiceFilterAttribute
类似,但引入的Filter
无需事先在Startup
的ConfigureServices
中注册,且可以部分传入构造函数的参数。
Middleware 还是 Filters?
中间件在请求抵达 Mvc 之前对请求进行处理,在这里还访问不到任何有关 Mvc 的内容,所以如果不依赖 Mvc 的组件,如 Routes
, Actions
, Controllers
等,优先使用 Middleware
,反之使用 Filters
。另外,ResourceFilter
是抵达 Mvc 之前的最后一种 Filter
,此时它具有最完整的 HttpContext
信息。ResourceFilter
是将 Middleware
以 Filter
的形式应用到指定 Controller
或 Action
的最佳选择。