.NET 中台

ZhonTai.DynamicApi 动态 Web API 生成组件

ZhonTai.DynamicApi 是一款动态 Web API 生成组件,可将符合约定的应用类直接转换为 RESTful 风格的 API。该组件由 ASP.NET Core MVC 原生调用,全程无 Controller 代码与性能损耗。

#Admin Core #Asoka Core #ZhonTai #CAP
ZhonTai.DynamicApi 动态 Web API 生成组件 封面图

简介

ZhonTai.DynamicApi 是一款动态 Web API 生成组件,可将符合约定的应用类直接转换为 RESTful 风格的 API。该组件由 ASP.NET Core MVC 原生调用,全程无 Controller 代码与性能损耗。

组件完美集成 Swagger,能够自动生成与手写 Controller 无任何差异的 API 文档。

应用场景

在 DDD 架构的应用逻辑层中,使用本组件可让应用服务直接暴露为 Web API,彻底省去手动编写 Controller 的重复工作。

配置动态 API

MyApp.Host 项目下的 Program.cs 文件中配置动态 API:

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
47
48
49
50
51
52
53
54
new HostApp(new HostAppOptions
{
ConfigureDynamicApi = options =>
{
// 接口以小驼峰方式命名
options.NamingConvention = NamingConventionEnum.CamelCase;

// 指定全局默认的 api 前缀
options.DefaultApiPrefix = "api";

// 清空 API 结尾,不删除 API 结尾
// 若不清空,CreateUserAsync 将变为 CreateUser
options.RemoveActionPostfixes.Clear();

// 自定义 ActionName 处理函数
options.GetRestFulActionName = (actionName) => actionName;

// 指定程序集,配置所有的 api 请求方式都为 POST
options.AddAssemblyOptions(GetType().Assembly, httpVerb: "POST");

// 指定程序集,配置 url 前缀为 api
// 例如: http://localhost:8010/api/User/CreateUser
options.AddAssemblyOptions(GetType().Assembly, apiPreFix: "api");

// 指定程序集,配置 url 前缀为 api,且所有请求方式都为 POST
// 例如: http://localhost:8010/api/User/CreateUser
options.AddAssemblyOptions(GetType().Assembly, apiPreFix: "api", httpVerb: "POST");

// 根据前缀动词生成请求方式
AppConsts.HttpVerbs = new Dictionary<string, string>()
{
["add"] = "POST",
["create"] = "POST",
["insert"] = "POST",
["submit"] = "POST",
["post"] = "POST",

["get"] = "GET",
["find"] = "GET",
["fetch"] = "GET",
["query"] = "GET",

["update"] = "PUT",
["change"] = "PUT",
["put"] = "PUT",
["batch"] = "PUT",

["delete"] = "DELETE",
["soft"] = "DELETE",
["remove"] = "DELETE",
["clear"] = "DELETE",
};
}
}).Run(args);

[!NOTE]
根据前缀动词未匹配到请求方式时,默认使用 HttpPost 方式请求。

NamingConvention 接口命名可选值

命名方式 示例
CamelCase camelCase
PascalCase PascalCase
SnakeCase snake_case
KebabCase kebab-case
ExtensionCase extension.case
Custom 自定义,通过options.GetRestFulActionName 设置

使用动态 API

在服务上增加 [DynamicApi(Area = ApiConsts.AreaName)] 特性,并继承 IDynamicApi 接口即可自动生成动态 API:

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
/// <summary>
/// 模块服务
/// </summary>
[DynamicApi(Area = ApiConsts.AreaName)]
public class ModuleService : BaseService, IModuleService, IDynamicApi
{
/// <summary>
/// 查询分页
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost]
public async Task<PageOutput<ModuleListOutput>> GetPageAsync(PageInput<ModuleGetPageDto> input)
{
var key = input.Filter?.Name;

var list = await _moduleRepository.Select
.WhereIf(key.NotNull(), a => a.Name.Contains(key))
.Count(out var total)
.OrderByDescending(true, c => c.Id)
.Page(input.CurrentPage, input.PageSize)
.ToListAsync<ModuleListOutput>();

var data = new PageOutput<ModuleListOutput>()
{
List = list,
Total = total
};

return data;
}
}

[!TIP]
ApiConsts.AreaName 定义 /api/app/module/get 路径中的 app

  • 动态 API 根据前缀动词自动生成请求方式,使用 [HttpPost] 特性可覆盖默认请求方式。
  • 新接口文档地址:http://localhost:8010/admin/index.html
  • Swagger 接口文档地址:http://localhost:8010/admin/swagger/index.html
  • 如果服务是动态 API,开启事务的方法务必定义为 virtual 虚方法,才能正常使用事务拦截。

高级功能

禁用动态 API

在服务方法上添加 [NonAction] 特性,可禁用生成动态 API,变为服务公共方法:

1
2
3
4
5
[NonAction]
public async Task BatchSoftDeleteAsync(long[] ids)
{
await _moduleRepository.SoftDeleteAsync(ids);
}

重命名

在服务方法上添加特性重命名。API 路径需使用绝对路径:

1
2
3
4
5
[HttpPost(template: "/api/[area]/[controller]/[action]")]
public async Task BatchSoftDeleteAsync(long[] ids)
{
await _moduleRepository.SoftDeleteAsync(ids);
}

排序

服务级别排序:

1
2
3
4
5
6
7
8
/// <summary>
/// 模块服务
/// </summary>
[Order(1010)]
[DynamicApi(Area = ApiConsts.AreaName)]
public class ModuleService : BaseService, IModuleService, IDynamicApi
{
}

[!NOTE]
值越小该服务越靠前。

方法级别排序:

1
2
3
4
5
[Order(1010)]
public async Task BatchSoftDeleteAsync(long[] ids)
{
await _moduleRepository.SoftDeleteAsync(ids);
}

[!NOTE]
值越大该服务方法越靠前。

多分组

服务级别分组:

1
2
3
4
5
6
7
/// <summary>
/// 模块服务
/// </summary>
[DynamicApi(Area = ApiConsts.AreaName, GroupNames = new string[] { AdminConsts.AreaName })]
public class ModuleService : BaseService, IModuleService, IDynamicApi
{
}

方法级别分组:

1
2
3
4
5
6
7
[ApiGroup(ApiConsts.AreaName, AdminConsts.AreaName)]
// 或使用 GroupNames 参数
// [ApiGroup(GroupNames = new string[] { ApiConsts.AreaName, AdminConsts.AreaName })]
public async Task BatchSoftDeleteAsync(long[] ids)
{
await _moduleRepository.SoftDeleteAsync(ids);
}

[!TIP]
接口不分组:[ApiGroup(NonGroup = true)]

返回当前数据

添加 [NonFormatResult] 特性可禁用统一包装的数据格式,直接返回当前数据:

1
2
3
4
5
6
7
8
9
10
// 统一包装格式
{
"success": true,
"code": null,
"msg": null,
"data": 1
}

// 使用 NonFormatResult 后直接返回
1
1
2
3
4
5
6
7
[NonFormatResult]
public async Task<long> AddAsync(ModuleAddInput input)
{
var entity = input.Adapt<ModuleEntity>();
await _moduleRepository.InsertAsync(entity);
return entity.Id;
}

授权(v8.3.0+)

在服务方法上添加 [ApiAccess("权限点编码")] 特性:

1
2
3
4
5
[ApiAccess("admin:dict:export")]
public async Task<ActionResult> ExportListAsync()
{
// ...
}

[!TIP]

  • [ApiAccess("权限点编码1", "权限点编码2", All = true)]
  • 权限点编码可设置一个或多个
  • All 默认为 false,表示满足任意一个权限点编码即可访问
  • 设置为 true 时,需要全部满足才可访问

隐藏 Swagger 文档

添加 [ApiExplorerSettings(IgnoreApi = true)] 特性,接口不会在文档中显示:

1
2
3
4
5
[ApiExplorerSettings(IgnoreApi = true)]
public async Task GetInternalDataForPublicAsync()
{
// ...
}

禁用操作日志

添加 [NoOperationLog] 特性,接口不会被记录操作日志:

1
2
3
4
5
[NoOperationLog]
public async Task GetDataListAsync()
{
// ...
}

#中台 #中台/特性注解 #中台/搭建项目框架 #中台/.NET模板 #中台/分布式微服务