动态WebAPI
实际上就是将普通的类变为 Controller
,也就是 动态WebAPI
就是控制器,支持控制器一切功能。
5.1.1 什么是控制器
简单来说,控制器是一个承上启下的作用,根据用户输入,执行响应行为(动作方法),同时在行为中调用模型的业务逻辑,返回给用户结果(视图)。
在 ASP.NET Core
中,控制器有两种表现形式:
Mvc
(带视图)WebAPI
(RESTful API)
- Mvc 控制器
1 2 3 4 5 6 7 8 9 10 11 12 |
using Microsoft.AspNetCore.Mvc; namespace Furion.Web.Entry.Controllers { public class MvcController : Controller { public IActionResult Index() { return View(); } } } |
WebApi控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Microsoft.AspNetCore.Mvc; namespace Furion.Web.Entry.Controllers { [Route("api/[controller]")] public class WebApiController : ControllerBase { [HttpGet] public IActionResult Get() { return Content(nameof(Furion)); } } } |
Mvc
控制器和 WebAPI
控制器最大的区别是 WebAPI
控制器不带 视图 和通过 请求谓词和路由地址响应行为。
5.1.2 Mvc 控制器
约定和缺点
在学习动态 WebAPI
控制器之前,首先了解 ASP.NET Core
中 WebAPI
的一些约定和注意事项。
5.1.2.1 WebAPI
约定
在 ASP.NET Core
应用中,一个 WebAPI
控制器需遵循以下约定:
- 控制器类必须继承
ControllerBase
或间接继承 - 动作方法必须贴有
[HttpMethod]
特性,如:[HttpGet]
- 控制器或动作方法至少有一个配置
[Route]
特性 - 生成
WebAPI
路由地址时会自动去掉控制器名称Controller
后缀,同时也会去掉动作方法匹配的HttpVerb
谓词,如GET,POST,DELETE,PUT
等 - 不支持返回非
IEnumerable<T>
泛型对象 - 不支持类类型参数在
GET,HEAD
请求下生成Query
参数
除了上述约定外,WebAPI
路由地址基本靠手工完成,不利于书写,不利于维护,再者,在移动应用对接中难以进行多版本控制。
5.1.2.2 .NET Core WebAPI
缺点
通过上一章节可以看出,ASP.NET Core
应用实现 WebAPI
需要遵循种种约定,而且容易出错。
除了这些约定,.NET Core WebAPI
有以下缺点:
- 路由地址基本靠手工完成
- 在现在移动为王的时代,不利于进行多版本控制
- 对接
Swagger
文档分组比较复杂 - 实现
Policy
策略授权也比较复杂 - 不支持控制器热插拔插件化
- 难以实现复杂自定义的
RESTful API
风格
5.1.3 动态 WebAPI
控制器
针对以上 ASP.NET Core
提供的 WebAPI
必须遵循的约定和不可避免的缺点,Furion
框架创造出一种更加灵活创建 WebAPI
控制器的方式。
这个方式在继承了 ASP.NET Core WebAPI
所有优点,同时进行了大量拓展和优化。优化后的 WebAPI
具有以下优点:
- 具备原有的
ControllerBase
所有功能 - 支持任意公开 非静态 非抽象 非泛型类转控制器
- 提供更加灵活方便的
IDynamicApiController
空接口或[DynamicApiController]
特性替代ControllerBase
抽象类 - 可直接在任意公开 非静态 非抽象 非泛型类贴
[Route]
特性自动转控制器 - 无需手动配置
[HttpMethod]
特性,同时支持一个动作方法多个HttpVerb
- 无需手动配置
[Route]
特性,支持更加灵活的配置及自动路由生成 - 支持返回泛型接口,泛型类
- 和
Swagger
深度结合,提供极其方便的创建Swagger
分组配置 - 支持
Basic Auth,Jwt,ApiKey
等多种权限灵活配置 - 支持控制器、动作方法版本控制功能
- 支持
GET、HEAD
请求自动转换类类型参数
- 支持生成
OAS3
接口规范
5.1.4 注册动态 WebAPI
服务
.AddDynamicApiControllers()
默认已经集成在 AddInject()
中了,无需再次注册。也就是下列代码可不配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Microsoft.Extensions.DependencyInjection; namespace Furion.Web.Core { [AppStartup(800)] public sealed class FurWebCoreStartup : AppStartup { public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddDynamicApiControllers(); } } } |
.AddDynamicApiControllers()
必须在 services.AddControllers()
之后注册。
5.1.5 第一个例子
创建一个 FurionAppService
类继承 IDynamicApiController
接口 或 贴 [DynamicApiController]
特性,并在这个类中编写一个 Get
方法。
IDynamicApiController
方式
123456789101112using Furion.DynamicApiController;namespace Furion.Application{public class FurionAppService : IDynamicApiController{public string Get(){return $"Hello {nameof(Furion)}";}}}
[DynamicApiController]
方式
12345678910111213using Furion.DynamicApiController;namespace Furion.Application{[DynamicApiController]public class FurionAppService{public string Get(){return $"Hello {nameof(Furion)}";}}}
- 如下图所示,一个
WebAPI
接口就这么生成了。
5.1.6 动态 WebAPI
原理解析
5.1.6.1 控制器特性提供器
Furion
框架会在应用启动时注册 DynamicApiControllerFeatureProvider
控制器特性提供器,该提供器继承自 ControllerFeatureProvider
类。
接着重写 bool IsController(TypeInfo typeInfo)
方法,用来标识控制器类型。在 Furion
框架中,继承自 ControllerBase
类或 IDynamicApiController
接口或 [DynamicApiController]
特性都会被标记为控制器类型。
5.1.6.2 应用模型转换器
Furion
框架同时在应用启动时注册 DynamicApiControllerApplicationModelConvention
应用模型转换器,该转换器继承自 IApplicationModelConvention
接口。
接着实现 void Apply(ApplicationModel application)
接口方法。在该方法中配置控制器名称、路由、导出可见性及动作方法名称、路由、导出可见性等。
实际上该方法做的就是按照 WebAPI 约定 提前帮我们配置好路由、请求谓词等信息。避免了手动配置的同时还增加了许多新特性,如版本控制。
5.1.7 动态 WebAPI
配置约定
5.1.7.1 控制器默认约定
- 生成控制器名称默认去除以
AppServices,AppService,ApiController,Controller,Services,Service
作为前后缀的字符串。见第一个例子中的FurionAppService -> Furion
支持自定义配置 - 控制器名称带
V[0-9_]
结尾的,会自动生成控制器版本号,如FurionAppServiceV2 -> Furion@2
,FurionAppServiceV1_1_0 -> Furion@1.1.0
。支持版本分隔符配置 - 控制名称以
骆驼命名(CamelCase)
会自动切割成多个单词-
连接。支持自定义配置
5.1.7.2 动作方法默认约定
- 生成的动作方法名称默认去除以
Post/Add/Create/Insert/Submit,GetAll/GetList/Get/Find/Fetch/Query/Search,Put/Update,Delete/Remove/Clear,Patch
开头的字符串。支持自定义配置 - 生成的动作方法名称默认去除以
Async
作为前后缀的字符串。支持自定义配置 - 动作方法名称带
V[0-9_]
结尾的,会自动生成动作方法版本号,如ChangePasswordV2 -> ChangePassword@2
,ChangePasswordV1_1_0 -> ChangePassword@1.1.0
。支持版本分隔符配置 - 动作方法名称以
骆驼(驼峰)/帕斯卡命名(CamelCase/Pascal)
会自动切割成多个单词-
连接。支持自定义配置 - 动作方法参数将自动转为小写。支持自定义配置
5.1.7.3 请求谓词默认约定
- 动作方法名
- 以
Post/Add/Create/Insert/Submit/Change
开头,则添加[HttpPost]
特性。 - 以
GetAll/GetList/Get/Find/Fetch/Query
开头,则添加[HttpGet]
特性。 - 以
Put/Update
开头,则添加[HttpPut]
特性。 - 以
Delete/Remove/Clear
开头,则添加[HttpDelete]
特性。 - 以
Patch
开头,则添加[HttpPatch]
特性 - 支持自定义配置
- 以
- 如果不在上面约定中,则默认添加
[HttpPost]
特性。支持自定义配置
5.1.7.4 路由地址默认约定
- 默认以
api
开头。支持自定义配置 - 默认转换为小写路由地址。支持自定义配置
- 生成控制器路由模板格式为:
api/前置参数列表/模块名或默认区域名/[controller@版本号]/后置参数列表
- 生成动作方法路由模板格式为:
前置参数列表/模块名/[action@版本号]/后置参数列表
5.1.7.5 其他约定
- 默认不处理
ControllerBase
控制器类型。支持自定义配置 - 默认不处理
GET,HEAD
请求的引用类型参数。支持自定义配置
5.1.8 更多例子
5.1.8.1 多种请求谓词方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public string Get() { return $"GET 请求"; } public string Post() { return $"POST 请求"; } public string Delete() { return $"DELETE 请求"; } public string Put() { return $"PUT 请求"; } public string Patch() { return $"PATCH 请求"; } } } |
如下图所示:
5.1.8.2 多个自定义动作方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public string GetVersion() { return $"v1.0.0"; } public string ChangeProfile() { return "修改成功"; } public string DeleteUser() { return "删除成功"; } } } |
如下图所示:
5.1.8.3 带参数动作方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public string GetUser(int id) { return $"{id}"; } public string GetUser(int id, string name) { return $"{id} {name}"; } public TestDto Add(TestDto testDto) { return testDto; } } } |
如下图所示:
5.1.8.4 GET/HEAD
类类型参数
默认情况下,ASP.NET Core
会将 GET/HEAD
请求中的 类类型参数
设置为 [FromBody]
绑定,如:
1 2 3 4 5 6 7 8 9 10 11 12 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public TestDto GetTest(TestDto testDto) { return testDto; } } } |
如下图所示:
但是,GET、HEAD
请求不支持 From Body
绑定。所以我们需要转换为 Query
查询参数。
Furion
框架支持以下两种方式配置:
- [FromQuery] 特性
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public TestDto GetTest([FromQuery] TestDto testDto) { return testDto; } } } |
- 配置 DynamicApiControllerSettings, Furion.Web.Entry/appsettings.json
12345{"DynamicApiControllerSettings": {"ModelToQuery": true}}
如下图所示:
5.1.8.5 自定义参数位置
Furion
框架提供了非常方便的自定义参数位置的特性 [ApiSeat]
,通过 [ApiSeat]
可配置参数位置,支持以下四种位置:
ApiSeats.ControllerStart
:控制器之前ApiSeats.ControllerEnd
:控制器之后ApiSeats.ActionStart
:动作方法之前ApiSeats.ActionEnd
:动作方法之后。默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using Furion.DynamicApiController; using System; namespace Furion.Application { public class FurionAppService : IDynamicApiController { // 参数默认为 ApiSeats.ActionEnd public string RouteSeat(int id, string name) { return "配置路由参数位置"; } public string RouteSeat( [ApiSeat(ApiSeats.ControllerStart)] int id, // 控制器名称之前 [ApiSeat(ApiSeats.ControllerEnd)] string name, // 控制器名称之后 [ApiSeat(ApiSeats.ControllerEnd)] int age, // 控制器名称之后 [ApiSeat(ApiSeats.ActionStart)] decimal weight, // 动作方法名称之前 [ApiSeat(ApiSeats.ActionStart)] float height, // 动作方法名称之前 [ApiSeat(ApiSeats.ActionEnd)] DateTime birthday) // 动作方法名称之后(默认值) { return "配置路由参数位置"; } } } |
如下图所示:
多个 同位置
配置的参数将按照 定义参数顺序
进行排序。
[ApiSeat]
只能应用于贴了 [FromRoute]
特性的参数或 基元类型、值类型、可空基元类型和可空值类型
。
5.1.8.6 自定义请求谓词
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { public class FurionAppService : IDynamicApiController { [HttpPost] public string GetVersion() { return "1.0.0"; } } } |
如下图所示:
5.1.8.7 支持多个谓词
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { public class FurionAppService : IDynamicApiController { [HttpPost, HttpGet, AcceptVerbs("PUT", "DELETE")] public string GetVersion() { return "1.0.0"; } } } |
如下图所示:
如果动作方法中含有 类类型参数
,且含有 POST/PUT/DELETE
任意请求谓词,那么该参数会自动添加 [FromBody]
参数,即使在 GET/HEAD
请求中不支持。
5.1.8.8 支持自定义路由
支持控制器和动作方法自定义路由:
- 自定义控制器路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { [Route("customapi/mobile/[controller]")] public class FurionAppService : IDynamicApiController { public string GetVersion() { return "1.0.0"; } } } |
- 自定义动作方法路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { public class FurionAppService : IDynamicApiController { [Route("customapi/[action]")] public string GetVersion() { return "1.0.0"; } } } |
- 同时自定义路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { [Route("customapi/mobile/[controller]")] public class FurionAppService : IDynamicApiController { [Route("get/[action]")] public string GetVersion() { return "1.0.0"; } } } |
- 谓词自定义路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { [Route("api/[controller]")] public class FurionAppService : IDynamicApiController { [HttpGet("get/[action]")] public string GetVersion() { return "1.0.0"; } } } |
如下图所示:
动作方法自定义路由如果以 /
开头,则不会合并控制器路由。
自定义路由如果需要用到 控制器/动作方法名称,推荐使用 [controller]
或 [action]
占位符,因为该占位符已经自动处理了 前后缀、版本号、模块名称等。
5.1.8.9 多路由随意组合
Furion
框架提供了非常灵活的各种路由组合方式,支持一对多,多对多路由组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace Furion.Application { [Route("api/[controller]")] [Route("api/[controller]/second")] [Route("api/[controller]/three")] public class FurionAppService : IDynamicApiController { [HttpGet] [HttpGet("get/[action]")] [HttpPost] [HttpPost("post/cus-version")] public string GetVersion() { return "1.0.0"; } } } |
如下图所示:
动作方法不能同时贴 [Route]
和 [HttpMethod]
特性,只能二取一。
在 Furion 4.8.5.7+
版本提供更加强大的路由组合方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using Furion.DynamicApiController; using Microsoft.AspNetCore.Mvc; namespace WebApplication38; [Route("api/[controller]")] [Route("api2/[controller]")] public class Test1Service : IDynamicApiController { [HttpGet("test")] [HttpPost] [AcceptVerbs("PUT", "PATCH")] public async Task GetTestName() { await Task.CompletedTask; } } public class Test2Service : IDynamicApiController { [HttpGet("/root/test")] [HttpGet("test")] [HttpGet(Name = "other-test")] [HttpGet("template-test", Name = "other-test")] [HttpPost] [AcceptVerbs("PUT", "PATCH")] public async Task GetTestName() { await Task.CompletedTask; } } [Route("api/[controller]")] [Route("api2/[controller]/second")] [Route("api3/[controller]/three")] public class Test3Service : IDynamicApiController { [HttpGet] [HttpGet("get/[action]")] [HttpPost] [HttpPost("post/cus-version")] public string GetVersion() { return "1.0.0"; } } |
5.1.8.10 支持版本控制
- 控制器版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppServiceV1 : IDynamicApiController { public string Get() { return nameof(Furion); } } public class FurionAppServiceV1_2 : IDynamicApiController { public string Get() { return nameof(Furion); } } public class FurionAppServiceV1_2_1 : IDynamicApiController { public string Get() { return nameof(Furion); } } } |
- 动作方法版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public string Get() { return nameof(Furion); } public string GetV1() { return nameof(Furion); } public string GetV2_1() { return nameof(Furion); } } } |
如下图所示:
V[0-9_]
结尾的命名自动解析成版本号,如 FurionAppServiceV2 -> Furion@2
。
除了通过特定后缀方式以外,版本还直接通过 [ApiDescriptionSettings]
进行复写。如:
1 2 3 4 5 |
[ApiDescriptionSettings(Version = "4.0")] public string GetV1() { return nameof(Furion); } |
这时,生成版本将采用 4.0
替代 1
5.1.8.11 不公开控制器或动作方法
有些时候,我们无需导出某个动作方法或控制器(不显示到 Swagger),只需要添加 [ApiDescriptionSettings(false)]
或 [ApiDescriptionSettings(IgnoreApi = true)]
即可。
另外动作方法还支持 [NonAction]
标记不是一个有效的控制器或 Action。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class FurionAppService: IDynamicApiController { public string Export() { // .... } [ApiDescriptionSettings(false)] // 不在 Swagger 上显示 public string NoExport() { // ... } [ApiDescriptionSettings(IgnoreApi = true)] // 不在 Swagger 上显示 public string NoExport2() { // ... } [NonAction] // 不是一个 API public string IsNotAPI() { // ... } } [ApiDescriptionSettings(false)] // 不导出 public class NoExportServices: IDynamicApiController { // .... } |
5.1.8.12 保持控制器和方法命名
默认情况下,动态 API 会将控制器和方法名输出为 RESTFul
风格的路由,如需保留原有设计,只需配置:
1 2 3 4 5 6 7 |
{ "DynamicApiControllerSettings": { "KeepName": true, "KeepVerb": true, "LowercaseRoute": false } } |
5.1.8.13 方法参数 [FromQuery]
化/参数非必填/参数可选
默认情况下,所有的基元类型参数都会贴上 [FromRoute]
特性,如果需要将参数调整为 [FromQuery]
修饰,只需要在方法上面贴 [QueryParameters]
特性即可,如:
1 2 3 4 5 6 |
<span class="token-line theme-code-block-highlighted-line codeLine_lJS_"><span class="codeLineContent_feaV"><span class="token punctuation">[</span><span class="token attribute class-name">QueryParameters</span><span class="token punctuation">]</span></span> </span><span class="token-line codeLine_lJS_"><span class="codeLineContent_feaV"><span class="token keyword">public</span> <span class="token return-type class-name keyword">string</span> <span class="token function">Get</span><span class="token punctuation">(</span><span class="token class-name keyword">int</span><span class="token plain"> id</span><span class="token punctuation">,</span> <span class="token class-name keyword">string</span><span class="token plain"> name</span><span class="token punctuation">)</span></span> </span><span class="token-line codeLine_lJS_"><span class="codeLineContent_feaV"><span class="token punctuation">{</span></span> </span><span class="token-line codeLine_lJS_"><span class="codeLineContent_feaV"> <span class="token keyword">return</span> <span class="token keyword">nameof</span><span class="token punctuation">(</span><span class="token interpolation-string string">$"</span><span class="token interpolation-string interpolation punctuation">{</span><span class="token interpolation-string interpolation expression language-csharp">id</span><span class="token interpolation-string interpolation punctuation">}</span> <span class="token interpolation-string interpolation punctuation">{</span><span class="token interpolation-string interpolation expression language-csharp">name</span><span class="token interpolation-string interpolation punctuation">}</span><span class="token interpolation-string string">"</span><span class="token punctuation">)</span><span class="token punctuation">;</span></span> </span><span class="token-line codeLine_lJS_"><span class="codeLineContent_feaV"><span class="token punctuation">}</span></span> </span> |
生成的路由为:https://xxx.com?id=1&name=Furion
如果不喜欢每个都配置,也可以全局配置(只会影响基元类型的参数):
1 2 3 4 5 |
{ "DynamicApiControllerSettings": { "UrlParameterization": true } } |
特别注意
贴了 [QueryParameters]
之后,会对所有参数影响,包括类类型参数,如果不需要处理某个参数,只需要贴 [FromXXX]
特性即可。
5.1.8.14 参数绑定配置
Furion
框架提供了多种参数特性配置参数绑定规则:
[FromRoute]
:通过路由参数绑定值[FromQuery]
:通过Url
地址参数绑定值[FromBody]
:通过Request Body
参数绑定值[FromForm]
:通过表单提交绑定值[FromHeader]
:通过Request Header
参数绑定值
5.1.8.15 自定义根据方法名生成 [HttpMethod]
规则
在 Furion
框架中,在没有配置 [HttpMethod]
特性的情况下,会自动根据方法名第一个参数进行分析,并生成对应的 [HttpMethod]
特性,规则如下:
- 动作方法名
- 以
Post/Add/Create/Insert/Submit
开头,则添加[HttpPost]
特性。 - 以
GetAll/GetList/Get/Find/Fetch/Query
开头,则添加[HttpGet]
特性。 - 以
Put/Update
开头,则添加[HttpPut]
特性。 - 以
Delete/Remove/Clear
开头,则添加[HttpDelete]
特性。 - 以
Patch
开头,则添加[HttpPatch]
特性 - 以
Head
开头,则添加[HttpHead]
特性 - 支持自定义配置
- 以
- 如果不在上面约定中,则默认添加
[HttpPost]
特性。支持自定义配置
但是,有些时候这不是我们想要的规则,这时我们只需要在 appsettings.json
中配置即可:
1 2 3 4 5 6 7 8 |
{ "DynamicApiControllerSettings": { "VerbToHttpMethods": [ ["getall", "HEAD"], // => getall 会被复写为 `[HttpHead]` ["other", "PUT"] // => 新增一条新规则,比如,一 `[other]` 开头会转换为 `[HttpPut]` 请求 ] } } |
特别注意
二维数组中的每一个元素的第一个元素必须是全小写,第二个元素必须是全大写大写,第二个元素取值有:HEAD, GET, PUT, POST, PATCH, DELETE
5.1.8.16 路由参数非必填/选填
在 Furion v2.8.6
版本中实现了 [FromRoute]
参数非必填功能,支持以下几种方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 方式一,通过可空 ? public object Method1(int id, Datetime? dateTime) { } // 方式二,通过默认值 public object Method1(int id, int age = 10) { } // 方式三,默认值 + 可空 ? public object Method1(int id, int? age = 10) { } // 方式四,[FromQuery] 修饰 public object Method1(int id, [FromQuery]string keyword) { } |
5.1.8.17 [FormRoute]
路由约束
在 Furion v2.8.6
版本中,添加了 [RouteConstraint]
特性,可配置路由约束,如:[RouteConstraint(":min(10)")]
1 2 3 4 |
// 最小值 10 public object Method1([RouteConstraint(":min(10)")] int id) { } |
[RouteConstraint]
支持路由约束符号如下:
符号 | 描述 | 例子 |
---|---|---|
* |
匹配路由 0-n 长度,Furion 4.8.6.2+ 支持 | :* |
alpha |
匹配大写或小写拉丁字母字符(a-z、A-Z) | :alpha |
bool |
bool 类型 | :bool |
datetime |
DateTime 类型 | :datetime |
decimal |
decimal 类型 | :decimal |
double |
double 类型 | :double |
float |
float 类型 | :float |
guid |
guid 类型 | :guid |
int |
int 类型 | :int |
long |
long 类型 | :long |
length |
匹配长度(字符串) | :length(6) 或 :length(1,20) |
max |
最大值 | :max(10) |
maxlength |
最大长度(字符串) | :maxlength(10) |
min |
最小值 | :min(10) |
minlength |
最小长度(字符串) | :minlength(10) |
range |
取值范围 | :range(10,50) |
regex |
正则表达式 | :regex(^\d{3}-\d{3}-\d{4}$) |
5.1.8.18 小驼峰
路由路径
1 2 3 4 5 6 7 |
{ "DynamicApiControllerSettings": { "LowercaseRoute": false, "KeepName": true, "AsLowerCamelCase": true } } |
5.1.8.19 application/xml
报文参数支持
- 在
Startup.cs
中启用XML
请求报文格式支持
1 2 3 |
services.AddControllers() // .AddControllersWithViews() .AddXmlSerializerFormatters() .AddXmlDataContractSerializerFormatters() |
如果出现 XmlSerializer
异常,那么只需要移除 .AddXmlSerializerFormatters()
即可,如:
1 2 |
services.AddControllers() // .AddControllersWithViews() .AddXmlDataContractSerializerFormatters() |
- 定义实体类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 实体类型 public class People { // 基础类型 public int Age { get; set; } public string Name { get; set; } public bool IsDeleted { get; set; } // 数组类型 public string[] Address { get; set; } // 集合类型 public List<string> Emails { get; set; } // 类类型 public Child Child { get; set; } } public class Child { public string Name { get; set; } } |
- 在动态
WebAPI
中使用
1 2 3 4 5 6 7 8 |
public class XmlDemo : IDynamicApiController { //[Consumes("application/xml")] // 如果设置了 [Consumes] 那么就表示只能传递 `accept= application/xml` 格式,不设置则支持多种(XML/JSON) public People Test(People people) { return people; } } |
[Consumes]
和 [Produces]
说明[Consumes]
特性是用来定义输入参数格式,对应请求报文的 accept
,[Produces]
特性是用来定义返回值格式,对应响应报文的 content-type
。
XML
格式说明及注意事项
支持两种 XML
格式报文,如:
XML
标签区分大小写,必须严格对照 C#
类型定义声明。对于集合/数组类型,必须遵循下列格式:
<属性名>
<C#类型></C#类型>
</属性名>
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<Strings> <string>monksoul@outlook.com</string> <string>rustln@outlook.com</string> </Strings> <Ints> <int>1</int> <int>2</int> </Ints> <Bools> <boolean>true</boolean> <boolean>false</boolean> </Bools> <Decimals> <decimal>1.0</decimal> <decimal>2.0</decimal> </Decimals> <Floats> <float>1.0</float> <float>2.0</float> </Floats> <Childs> <Child> <Name>Furion</Name> </Child> </Childs> |
- 第一种(常用格式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<?xml version="1.0" encoding="UTF-8"?> <People> <Age>30</Age> <Name>百小僧</Name> <IsDeleted>true</IsDeleted> <Address> <string>广东省中山市</string> <string>广东省珠海市</string> </Address> <Emails> <string>monksoul@outlook.com</string> <string>rustln@outlook.com</string> </Emails> <Child> <Name>Furion</Name> </Child> </People> |
- 第二种(标准格式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<People xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Age>30</Age> <Name>百小僧</Name> <IsDeleted>true</IsDeleted> <Address> <string>广东省中山市</string> <string>广东省珠海市</string> </Address> <Emails> <string>monksoul@outlook.com</string> <string>rustln@outlook.com</string> </Emails> <Child> <Name>Furion</Name> </Child> </People> |
5.1.9 [ApiDescriptionSettings]
除了上述 ASP.NET Core
提供的配置外,Furion
框架还提供了非常强大且灵活的 [ApiDescriptionSettings]
特性。
5.1.9.1 内置配置
Name
:自定义控制器/动作方法名称,string
,默认null
KeepName
:是否保持原有名称不处理,bool
,默认false
SplitCamelCase
:切割骆驼(驼峰)/帕斯卡命名,bool
,默认true
KeepVerb
:是否保留动作方法请求谓词,bool
,默认false
Enabled
:是否导出接口,bool
,默认true
Module
:模块名,string
,默认null
Version
:版本号,string
,默认null
Groups
:接口分组,可结合Swagger
一起使用,string[]
,默认null
Tags
:接口标签,可结合Swagger
一起使用,string[]
,默认null
Order
:配置控制器/动作方法排序,数值越大越靠前LowercaseRoute
:是否采用小写路由,bool
类型,默认true
AsLowerCamelCase
:启用小驼峰命名(首字母小写),默认false
Area
:配置区域名称,默认空,只作用于类中贴Description
:配置单一接口更多描述功能,只在方法
中有效,仅限 v3.3.5+版本有效ForceWithRoutePrefix
:配置是否强制添加DefaultRoutePrefix
,当控制器自定义了[Route]
有效,默认false
,仅限 v3.4.1+版本有效
5.1.9.2 Name
配置
Name
参数可以覆盖动态 WebAPI
自动生成的控制器或动作方法名称。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings(Name = "MyFur")] public class FurionAppService : IDynamicApiController { [ApiDescriptionSettings(Name = "MyGet")] public string Get() { return nameof(Furion); } [ActionName("MyTest")] // Furion 4.8.4.12+ 支持 public string Test() { return nameof(Furion); } [HttpGet(Name = "MyTest")] // Furion 4.8.4.12+ 支持,此配置有效的前提是控制器贴有 [Route] 特性 public string Test2() { return nameof(Furion); } } } |
如下图所示:
5.1.9.3 KeepName
配置
KeepName
参数可以保留原有的控制器或动作方法名称。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings(KeepName = true)] public class FurionAppService : IDynamicApiController { [ApiDescriptionSettings(KeepName = true)] public string Get() { return nameof(Furion); } } } |
如下图所示:
5.1.9.4 SplitCamelCase
配置
SplitCamelCase
参数默认将骆驼(驼峰)命名切割成多个单词并通过指定 占位符
连接起来。默认 占位符
为 -
。默认为 true
。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings(SplitCamelCase = false)] public class MyFurionAppService : IDynamicApiController { [ApiDescriptionSettings(SplitCamelCase = true)] public string ChangeUserName() { return nameof(Furion); } } } |
如下图所示:
KeepName
优先级高于 SplitCamelCase
,也就是 KeepName
设置为 true
,则不会处理 SplitCamelCase
参数。
5.1.9.5 KeepVerb
配置
KeepVerb
参数作用于动作方法,标识是否保留动作谓词。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { [ApiDescriptionSettings(KeepVerb = true)] public string GetVersion() { return nameof(Furion); } } } |
如下图所示:
5.1.9.6 Enabled
配置
Enabled
参数配置接口是否导出。通常用于动作方法,如果用于控制器实际作用不大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public string GetVersion() { return nameof(Furion); } [ApiDescriptionSettings(false)] public string NoExport() { return nameof(Furion); } } } |
如下图所示:
5.1.9.7 Module
配置
Module
参数可以配置路由分离,类似于 Mvc 区域
的作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings(Module = "mobile")] public class FurionAppService : IDynamicApiController { [ApiDescriptionSettings(Module = "user")] public string GetVersion() { return nameof(Furion); } } } |
如下图所示:
5.1.9.8 Version
配置
Version
参数可以配置接口版本,同时又可以复写特殊版本命名配置。默认版本分隔符为 @
。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings(Version = "1.0")] public class FurionAppService : IDynamicApiController { // V2.0.0 被复写成 V2.1.1 [ApiDescriptionSettings(Version = "2.1.1")] public string GetVersionV2_0_0() { return nameof(Furion); } } } |
如下图所示:
5.1.9.9 Groups
配置
Groups
配置主要用于配置 Swagger
分组信息。
通过配置 Groups
参数可以将控制器和动作方法
进行归类和多个分组直接共享。可通过 [ApiDescriptionSettings(params Groups)]
构造函数传入或指定 Groups
参数配置接口是否导出。通常用于动作方法,如果用于控制器实际作用不大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings("Default", "Common")] public class FurionAppService : IDynamicApiController { public string Get() { return nameof(Furion); } [ApiDescriptionSettings("Custom")] public int Get(int id) { return id; } } } |
如下图所示:
5.1.9.10 Tag
配置
Tag
配置主要用于配置 Swagger
标签分组信息及合并标签。也就是 组中组
:
- 标签命名
未贴标签之前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using Furion.DynamicApiController; namespace Furion.Application { public class FurionAppService : IDynamicApiController { public string Get() { return nameof(Furion); } public int Get(int id) { return id; } } public class TestAppService : IDynamicApiController { public string Get() { return nameof(Furion); } public int Get(int id) { return id; } } } |
贴标签之后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings(Tag = "分组一")] public class FurionAppService : IDynamicApiController { public string Get() { return nameof(Furion); } public int Get(int id) { return id; } } [ApiDescriptionSettings(Tag = "分组二")] public class TestAppService : IDynamicApiController { public string Get() { return nameof(Furion); } public int Get(int id) { return id; } } } |
- 合并标签
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using Furion.DynamicApiController; namespace Furion.Application { [ApiDescriptionSettings(Tag = "合并所有标签")] public class FurionAppService : IDynamicApiController { public string Get() { return nameof(Furion); } public int Get(int id) { return id; } } [ApiDescriptionSettings(Tag = "合并所有标签")] public class TestAppService : IDynamicApiController { public string Get() { return nameof(Furion); } public int Get(int id) { return id; } } } |
如下图所示:
如果 Tag
名字一样,则会自动合并,否则只是命名。
5.1.10 DynamicApiControllerSettings
配置
Furion
还提供动态 WebAPI
接口一些全局配置选项,如:
DefaultRoutePrefix
:默认路由前缀,string
,默认api
DefaultHttpMethod
:默认请求谓词,string
,默认:POST
DefaultModule
:默认模块名称(区域),可用作接口版本,string
,默认:v1
LowercaseRoute
:小写路由格式,bool
,默认:true
AsLowerCamelCase
:启用小驼峰命名(首字母小写),默认false
KeepVerb
:是否保留动作谓词,bool
,默认:false
KeepName
:是否保留默认名称,bool
,默认:fasle
CamelCaseSeparator
:骆驼(驼峰)/帕斯卡命名分隔符,string
,默认:-
VersionSeparator
:版本分隔符,string
,默认:@
ModelToQuery
:GET/HEAD
请求将类类型参数转查询参数
,bool
,默认false
SupportedMvcController
:是否支持Mvc Controller
动态配置,bool
,默认false
UrlParameterization
:路由参数采用[FromQuery]
化,默认false
([FromRoute]
方式)DefaultArea
:配置默认区域,默认null
ForceWithRoutePrefix
:配置是否强制添加DefaultRoutePrefix
,当控制器自定义了[Route]
有效,仅限 v3.4.1+版本有效AbandonControllerAffixes
:默认去除控制器名称前后缀列表名,string[]
,默认:AppServices
AppService
ApiController
Controller
Services
Service
AbandonActionAffixes
:默认去除动作方法名称前后缀列表名,string[]
,默认:Async
VerbToHttpMethods
:复写默认方法名转[HttpMethod]
规则,string[][]
二维数组类型,内置匹配规则为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
["post"] = "POST", ["add"] = "POST", ["create"] = "POST", ["insert"] = "POST", ["submit"] = "POST", ["get"] = "GET", ["find"] = "GET", ["fetch"] = "GET", ["query"] = "GET", ["put"] = "PUT", ["update"] = "PUT", ["delete"] = "DELETE", ["remove"] = "DELETE", ["clear"] = "DELETE", ["patch"] = "PATCH" |
- 复写示例
1 2 3 4 5 6 |
"DynamicApiControllerSettings": { "VerbToHttpMethods": [ [ "getall", "HEAD" ], // => getall 会被复写为 `[HttpHead]` [ "other", "PUT" ] // => 新增一条新规则,比如,一 `[other]` 开头会转换为 `[HttpPut]` 请求 ] } |
5.1.10.1 支持 Mvc 控制器
动态配置
默认情况下,Furion
动态 WebAPI
接口不对 ControllerBase
类型进行任何处理。当然,我们也可以手动启用 ControllerBase
支持。
1 2 3 4 5 |
{ "DynamicApiControllerSettings": { "SupportedMvcController": true } } |
设置 SupportedMvcController: true
后,Mvc ControllerBase
类型也能和动态 WebAPI
一样的灵活了。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
using Microsoft.AspNetCore.Mvc; namespace Furion.Web.Entry.Controllers { public class MvcController : ControllerBase { public string Get() { return nameof(Furion); } } } |
启用该配置后,如果 Mvc 控制器
没有任何 [Route]
特性,但是贴了 [ApiController]
特性将会报错。原因是 [ApiController]
特性内部做了路由特性检测。所以建议使用 [ApiDataValidation]
代替。
5.1.11 关于 AOP 拦截
动态WebAPI
支持 Controller
的所有过滤器/筛选器拦截,也就是可以通过 ActionFilter
,ResultFilter
进行拦截操作。如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class SampleAsyncActionFilter : IAsyncActionFilter { public async Task OnActionExecutionAsync(ActionExecutingContext context,ActionExecutionDelegate next) { // 拦截之前 var resultContext = await next(); // 拦截之后 // 异常拦截 if(resultContext.Exception != null) { } } } |
详细用法可参见 ASP.NET Core 5.0 – 筛选器
5.1.12 设置 api
超时请求时间
在 Program.cs
中添加 .UseKestrel
配置即可,如:
.NET5 版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.Inject() .UseStartup<Startup>() .UseKestrel(option => { option.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(20); option.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(20); }); }); } |
.NET6 版本
1 2 3 4 5 6 7 |
var app = builder.Build(); app.Configuration.Get<WebHostBuilder>().ConfigureKestrel(x => { x.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(20); x.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(20); }); |
5.1.13 获取路由/控制器/Action
列表
有时候我们需要获取当前路由信息,或所有控制器、Action
列表,而已通过以下代码获取:
- 获取当前路由表信息(简易)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class MetadataService : IDynamicApiController { private readonly IHttpContextAccessor _httpContextAccessor; public MetadataService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public RouteValueDictionary Print() { var routeValuesFeature = _httpContextAccessor.HttpContext.Features.Get<IRouteValuesFeature>(); // 获取路由信息 var routeValues = routeValuesFeature.RouteValues; return routeValues; } } |
输出:
1 2 3 4 |
{ "action": "print", "controller": "metadata" } |
- 获取当前终点路由信息(简易)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class MetadataService : IDynamicApiController { private readonly IHttpContextAccessor _httpContextAccessor; public MetadataService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } [Description("这是一段描述")] public void Print() { var endpointFeature = _httpContextAccessor.HttpContext.Features.Get<IEndpointFeature>(); // 获取路由终点信息 var routeEndpoint = endpointFeature.Endpoint as RouteEndpoint; var displayName = routeEndpoint.DisplayName; // 路由映射方法 FullName var routePattern = routeEndpoint.RoutePattern; // 路由表达式(路径) // 获取路由元数据(特性) var metadata = routeEndpoint.Metadata; var attribute = metadata.GetMetadata<DescriptionAttribute>(); // 获取 [Description] 特性 } } |
- 获取所有控制器列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class MetadataService : IDynamicApiController { private readonly ApplicationPartManager _applicationPartManager; public MetadataService(ApplicationPartManager applicationPartManager) { _applicationPartManager = applicationPartManager; } public void Print() { var controllerFeature = new ControllerFeature(); _applicationPartManager.PopulateFeature(controllerFeature); // 获取所有控制器列表 IList<TypeInfo> controllers = controllerFeature.Controllers; } } |
- 获取所有
Action
列表(强大)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
public class MetadataService : IDynamicApiController { private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; public MetadataService(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) { _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; } public void Print() { // 获取所有 Action 列表 var actionDescriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items; foreach (ActionDescriptor actionDescriptor in actionDescriptors) { // 获取请求的方法 var method = (actionDescriptor as ControllerActionDescriptor).MethodInfo; // 获取路由地址 var route = actionDescriptor.AttributeRouteInfo.Template; // 获取 HttpMethod var httpMethod = actionDescriptor.ActionConstraints?.OfType<HttpMethodActionConstraint>().FirstOrDefault()?.HttpMethods.First(); // 任何关于这个路由/方法/控制器/特性的信息都有 } } } |
- 获取所有
Action
列表带分组信息(强大)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class MetadataService : IDynamicApiController { private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionGroupCollectionProvider; public MetadataService(IApiDescriptionGroupCollectionProvider apiDescriptionGroupCollectionProvider) { _apiDescriptionGroupCollectionProvider = apiDescriptionGroupCollectionProvider; } public void Print() { // 获取所有控制器列表 var apiDescriptionGroups = _apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items; foreach (ApiDescriptionGroup group in apiDescriptionGroups) { // 获取当前分组的所有 Actions var actions = group.Items; foreach (ApiDescription action in actions) { // 路由地址 var route = action.RelativePath; // HttpMethod var httpMethod = action.HttpMethod; // 分组名 var groupName = action.GroupName; // Action 描述器 var actionDescriptor = action.ActionDescriptor; // 获取请求的方法 var method = (actionDescriptor as ControllerActionDescriptor).MethodInfo; // 任何关于这个路由/方法/控制器/特性的信息都有 } } } } |