惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Scott Helme
Scott Helme
N
Netflix TechBlog - Medium
AI
AI
Security Latest
Security Latest
GbyAI
GbyAI
P
Proofpoint News Feed
Y
Y Combinator Blog
A
Arctic Wolf
G
Google Developers Blog
U
Unit 42
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Vulnerabilities – Threatpost
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
T
Tor Project blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threatpost
L
Lohrmann on Cybersecurity
C
CERT Recently Published Vulnerability Notes
C
Check Point Blog
B
Blog RSS Feed
The GitHub Blog
The GitHub Blog
Microsoft Azure Blog
Microsoft Azure Blog
博客园 - 【当耐特】
博客园 - Franky
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Cisco Blogs
云风的 BLOG
云风的 BLOG
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Microsoft Security Blog
Microsoft Security Blog
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
Latest news
Latest news
L
LINUX DO - 最新话题
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
美团技术团队
WordPress大学
WordPress大学
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
酷 壳 – CoolShell
酷 壳 – CoolShell
大猫的无限游戏
大猫的无限游戏
The Hacker News
The Hacker News
Simon Willison's Weblog
Simon Willison's Weblog
V
V2EX
Project Zero
Project Zero
博客园_首页

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第四章:动态WebAPI开发
我才是银古 · 2026-06-20 · via 博客园 - 我才是银古

第四章:动态WebAPI开发

目录

  1. 动态WebAPI概述
  2. IDynamicApiController接口
  3. DynamicApiController特性标记
  4. 路由规则与约定
  5. HTTP方法约定
  6. 自定义路由规则
  7. API版本管理
  8. Swagger文档集成与配置
  9. 请求参数绑定
  10. 返回值处理
  11. 接口分组与标签
  12. 动态WebAPI高级配置

1. 动态WebAPI概述

1.1 什么是动态WebAPI

动态WebAPI是Furion框架最核心、最具特色的功能之一。它允许开发者 无需手动编写Controller类,只需创建普通的服务类,框架便能在运行时自动将其转换为标准的ASP.NET Core Web API控制器。

传统开发方式 vs Furion动态WebAPI:

// ============ 传统方式 ============
// 需要创建Controller,编写大量模板代码
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    private readonly IUserService _userService;

    public UserController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet]
    public async Task<ActionResult<List<UserDto>>> GetUsers()
    {
        var users = await _userService.GetUsersAsync();
        return Ok(users);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<UserDto>> GetUser(int id)
    {
        var user = await _userService.GetUserByIdAsync(id);
        return Ok(user);
    }

    [HttpPost]
    public async Task<ActionResult<int>> CreateUser(CreateUserInput input)
    {
        var id = await _userService.CreateUserAsync(input);
        return Ok(id);
    }
}

// 还需要定义Service接口和实现
public interface IUserService
{
    Task<List<UserDto>> GetUsersAsync();
    Task<UserDto> GetUserByIdAsync(int id);
    Task<int> CreateUserAsync(CreateUserInput input);
}

public class UserService : IUserService
{
    // 实现...
}
// ============ Furion动态WebAPI方式 ============
// 只需一个类,自动生成API接口
public class UserService : IDynamicApiController
{
    public async Task<List<UserDto>> GetUsersAsync()
    {
        // 自动映射为 GET /api/user/users
        return await _repository.AsQueryable().ToListAsync();
    }

    public async Task<UserDto> GetUserByIdAsync(int id)
    {
        // 自动映射为 GET /api/user/user-by-id?id={id}
        return await _repository.FindAsync(id);
    }

    public async Task<int> CreateUserAsync(CreateUserInput input)
    {
        // 自动映射为 POST /api/user/create-user
        var entity = await _repository.InsertNowAsync(input.Adapt<User>());
        return entity.Entity.Id;
    }
}

1.2 动态WebAPI的优势

优势 说明
减少代码量 无需编写Controller、无需手动定义路由
消除重复 不再需要Controller到Service的参数传递代码
自动路由 根据方法名自动推断HTTP方法和路由
Swagger集成 自动生成完整的API文档
灵活配置 支持自定义路由、分组、版本等
兼容性好 生成的是标准ASP.NET Core控制器,完全兼容现有生态

1.3 工作原理

动态WebAPI的工作原理是在应用启动时,Furion会扫描所有实现了IDynamicApiController接口或标记了[DynamicApiController]特性的类,然后通过ASP.NET Core的ApplicationPartApplicationModel机制,动态将这些类注册为控制器。

应用启动
  │
  ├── 扫描程序集
  │   └── 查找IDynamicApiController / [DynamicApiController]
  │
  ├── 解析方法信息
  │   ├── 方法名 → HTTP方法 + 路由
  │   ├── 参数 → 请求参数绑定
  │   └── 返回值 → 响应类型
  │
  ├── 创建ControllerModel
  │   └── 注册到ASP.NET Core路由系统
  │
  └── API可用
      └── Swagger文档自动生成

2. IDynamicApiController接口

2.1 基本用法

IDynamicApiController是一个标记接口(Marker Interface),不包含任何方法定义,实现该接口的类会被自动识别为动态API控制器:

using Furion.DynamicApiController;

namespace MyApp.Application.Services;

/// <summary>
/// 产品管理服务
/// </summary>
public class ProductService : IDynamicApiController
{
    /// <summary>
    /// 获取所有产品
    /// </summary>
    /// <returns>产品列表</returns>
    public List<ProductDto> GetAll()
    {
        return new List<ProductDto>
        {
            new ProductDto { Id = 1, Name = "笔记本电脑", Price = 5999 },
            new ProductDto { Id = 2, Name = "机械键盘", Price = 399 },
            new ProductDto { Id = 3, Name = "无线鼠标", Price = 129 }
        };
    }

    /// <summary>
    /// 根据ID获取产品
    /// </summary>
    /// <param name="id">产品ID</param>
    /// <returns>产品信息</returns>
    public ProductDto GetById(int id)
    {
        return new ProductDto { Id = id, Name = "示例产品", Price = 99 };
    }

    /// <summary>
    /// 创建产品
    /// </summary>
    /// <param name="input">产品信息</param>
    /// <returns>新产品ID</returns>
    public int CreateProduct(CreateProductInput input)
    {
        // 业务逻辑...
        return 1;
    }

    /// <summary>
    /// 更新产品
    /// </summary>
    /// <param name="id">产品ID</param>
    /// <param name="input">更新信息</param>
    public void UpdateProduct(int id, UpdateProductInput input)
    {
        // 业务逻辑...
    }

    /// <summary>
    /// 删除产品
    /// </summary>
    /// <param name="id">产品ID</param>
    public void DeleteProduct(int id)
    {
        // 业务逻辑...
    }
}

上述代码自动生成以下API接口:

HTTP方法 路由 对应方法
GET /api/product/all GetAll()
GET /api/product/by-id?id={id} GetById(int id)
POST /api/product/create-product CreateProduct(...)
PUT /api/product/update-product?id={id} UpdateProduct(...)
DELETE /api/product/delete-product?id={id} DeleteProduct(...)

2.2 构造函数注入

动态API控制器完全支持构造函数依赖注入:

public class OrderService : IDynamicApiController
{
    private readonly IRepository<Order> _orderRepo;
    private readonly ILogger<OrderService> _logger;
    private readonly IMapper _mapper;

    public OrderService(
        IRepository<Order> orderRepo,
        ILogger<OrderService> logger,
        IMapper mapper)
    {
        _orderRepo = orderRepo;
        _logger = logger;
        _mapper = mapper;
    }

    public async Task<List<OrderDto>> GetAllAsync()
    {
        _logger.LogInformation("正在查询所有订单");
        var orders = await _orderRepo.AsQueryable().ToListAsync();
        return _mapper.Map<List<OrderDto>>(orders);
    }
}

2.3 排除方法

如果不希望某个公开方法被暴露为API接口,可以使用[NonAction]特性:

using Microsoft.AspNetCore.Mvc;

public class HelperService : IDynamicApiController
{
    // ✅ 这个方法会暴露为API
    public string GetVersion()
    {
        return "1.0.0";
    }

    // ❌ 这个方法不会暴露为API
    [NonAction]
    public string InternalHelper()
    {
        return "内部辅助方法";
    }
}

3. DynamicApiController特性标记

3.1 使用特性标记

除了实现IDynamicApiController接口外,还可以使用[DynamicApiController]特性来标记动态API控制器:

using Furion.DynamicApiController;

/// <summary>
/// 使用特性标记方式
/// </summary>
[DynamicApiController]
public class CategoryService
{
    public List<string> GetCategories()
    {
        return new List<string> { "电子产品", "家居用品", "服装鞋帽" };
    }

    public string GetCategory(int id)
    {
        return $"分类 {id}";
    }
}

3.2 两种方式的对比

特性 IDynamicApiController接口 [DynamicApiController]特性
使用方式 实现接口 添加特性
侵入性 需要引用Furion 需要引用Furion
继承传递 子类自动继承 子类不自动继承
适用场景 推荐用于标准服务类 适用于不想改变继承关系的类
语义性 语义更强 更灵活

3.3 基类方式

可以创建一个基类来统一实现IDynamicApiController接口:

/// <summary>
/// 应用服务基类
/// </summary>
public abstract class BaseAppService : IDynamicApiController
{
    /// <summary>
    /// 获取当前登录用户ID
    /// </summary>
    protected long CurrentUserId => App.User?.FindFirst("UserId")?.Value.ToLong() ?? 0;

    /// <summary>
    /// 获取当前租户ID
    /// </summary>
    protected long CurrentTenantId => App.User?.FindFirst("TenantId")?.Value.ToLong() ?? 0;
}

/// <summary>
/// 订单服务 - 继承基类,自动成为动态API
/// </summary>
public class OrderAppService : BaseAppService
{
    public List<OrderDto> GetMyOrders()
    {
        var userId = CurrentUserId;
        // 查询当前用户的订单...
        return new List<OrderDto>();
    }
}

4. 路由规则与约定

4.1 默认路由规则

Furion的动态WebAPI有一套完善的默认路由生成规则:

规则1:控制器名称

类名会被自动处理为路由前缀:

  • 移除常见后缀:ServiceAppServiceApplication
  • 转换为小写kebab-case格式
UserService          → /api/user/...
ProductAppService    → /api/product/...
OrderApplication     → /api/order/...
CategoryService      → /api/category/...

规则2:方法名称转换

方法名会经过以下处理:

  • 移除HTTP方法前缀(Get、Post、Put、Delete等)
  • 移除Async后缀
  • 转换为小写kebab-case格式
public class UserService : IDynamicApiController
{
    // GET /api/user/all
    public List<User> GetAll() { }

    // GET /api/user/by-id
    public User GetById(int id) { }

    // POST /api/user
    public void Post(CreateUserInput input) { }

    // PUT /api/user/info
    public void UpdateInfo(UpdateUserInput input) { }

    // DELETE /api/user
    public void Delete(int id) { }

    // GET /api/user/active-users(异步方法自动移除Async后缀)
    public Task<List<User>> GetActiveUsersAsync() { }
}

4.2 路由转换规则详解

方法名 转换后路由 HTTP方法
GetAll /all GET
GetById /by-id GET
GetUserInfo /user-info GET
PostUser /user POST
CreateOrder /create-order POST
UpdateUserName /update-user-name PUT
DeleteById /by-id DELETE
GetActiveUsersAsync /active-users GET
Post / POST
Get / GET

4.3 PascalCase到kebab-case的转换

Furion会自动将方法名从PascalCase转换为kebab-case:

GetUserList       → get-user-list    → /api/user/user-list (去掉Get前缀)
CreateNewOrder    → create-new-order → /api/order/create-new-order
FindByNameAndAge  → find-by-name-and-age
UpdateUserProfile → update-user-profile

4.4 默认路由模板

默认的路由模板为:

api/[controller]/[action]

其中:

  • api 是固定前缀
  • [controller] 是控制器名称(类名去掉后缀)
  • [action] 是操作名称(方法名去掉HTTP前缀)

5. HTTP方法约定

5.1 方法名前缀约定

Furion通过方法名的前缀来自动推断HTTP方法:

方法名前缀 对应HTTP方法 常见用途
GetFindFetchQuerySearch GET 查询数据
PostCreateAddInsertSubmit POST 创建数据
PutUpdateModifyChangeEdit PUT 更新数据
DeleteRemoveClearCancel DELETE 删除数据
Patch PATCH 部分更新
public class ArticleService : IDynamicApiController
{
    // ===== GET 方法 =====
    public List<Article> GetAll() { }              // GET /api/article/all
    public Article FindById(int id) { }            // GET /api/article/by-id
    public List<Article> FetchLatest() { }         // GET /api/article/latest
    public List<Article> QueryByCategory(int c) {} // GET /api/article/by-category
    public List<Article> SearchByTitle(string t) {} // GET /api/article/by-title

    // ===== POST 方法 =====
    public int CreateArticle(ArticleInput i) { }   // POST /api/article/article
    public int AddComment(CommentInput i) { }      // POST /api/article/comment
    public void SubmitReview(int id) { }            // POST /api/article/review

    // ===== PUT 方法 =====
    public void UpdateTitle(int id, string t) { }  // PUT /api/article/title
    public void ModifyContent(int id, string c) { } // PUT /api/article/content
    public void EditArticle(ArticleInput i) { }     // PUT /api/article/article

    // ===== DELETE 方法 =====
    public void DeleteArticle(int id) { }           // DELETE /api/article/article
    public void RemoveComment(int id) { }           // DELETE /api/article/comment
    public void ClearDrafts() { }                    // DELETE /api/article/drafts
}

5.2 显式指定HTTP方法

如果默认约定不满足需求,可以使用ASP.NET Core的HTTP方法特性显式指定:

using Microsoft.AspNetCore.Mvc;

public class PaymentService : IDynamicApiController
{
    // 显式指定为POST(虽然以Get开头)
    [HttpPost]
    public PaymentResult GetPaymentUrl(PaymentInput input)
    {
        // 获取支付链接通常需要POST
        return new PaymentResult { Url = "https://pay.example.com/..." };
    }

    // 显式指定为GET(虽然以Check开头,默认会是POST)
    [HttpGet]
    public bool CheckPaymentStatus(string orderId)
    {
        return true;
    }

    // 方法名不以约定前缀开头时,默认为POST
    // 可以显式指定为其他方法
    [HttpPut]
    public void Process(int orderId)
    {
        // 处理订单
    }
}

5.3 无前缀方法名的默认行为

如果方法名不以任何约定前缀开头,Furion默认将其视为 POST 方法:

public class TaskService : IDynamicApiController
{
    public void Execute(int taskId) { }       // POST /api/task/execute
    public string Process(string data) { }    // POST /api/task/process
    public bool Validate(ValidateInput i) { } // POST /api/task/validate
}

6. 自定义路由规则

6.1 使用ApiDescriptionSettings自定义控制器路由

[ApiDescriptionSettings]特性允许自定义控制器级别的路由配置:

using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;

// 自定义控制器名称
[ApiDescriptionSettings(Name = "Users")]
public class UserManagementService : IDynamicApiController
{
    // 生成路由:GET /api/users/list 而不是 /api/user-management/list
    public List<UserDto> GetList() { return new(); }
}

// 自定义路由前缀
[ApiDescriptionSettings(Name = "Admin/Users")]
public class AdminUserService : IDynamicApiController
{
    // 生成路由:GET /api/admin/users/list
    public List<UserDto> GetList() { return new(); }
}

6.2 使用Route特性自定义路由

可以使用ASP.NET Core标准的[Route]特性来完全控制路由:

public class CustomRouteService : IDynamicApiController
{
    // 完全自定义路由
    [HttpGet("api/v1/users/{id}")]
    public UserDto GetUserById(int id)
    {
        return new UserDto { Id = id, Name = "自定义路由用户" };
    }

    // 使用路由模板参数
    [HttpGet("api/departments/{deptId}/users")]
    public List<UserDto> GetDepartmentUsers(int deptId)
    {
        return new List<UserDto>();
    }

    // 多路由映射到同一方法
    [HttpGet("api/users/search")]
    [HttpGet("api/users/find")]
    public List<UserDto> SearchUsers(string keyword)
    {
        return new List<UserDto>();
    }
}

6.3 配置全局路由前缀

appsettings.json中可以配置全局路由前缀:

{
  "DynamicApiControllerSettings": {
    "DefaultRoutePrefix": "api",
    "DefaultHttpMethod": "POST",
    "LowercaseRoute": true,
    "KeepVerb": false,
    "KeepName": false,
    "AsLowerCamelCase": false,
    "SplitCamelCase": true,
    "AbandonControllerAffixes": ["Service", "AppService", "Application"],
    "AbandonActionAffixes": ["Async"]
  }
}
配置项 默认值 说明
DefaultRoutePrefix "api" 全局路由前缀
DefaultHttpMethod "POST" 无法推断HTTP方法时的默认方法
LowercaseRoute true 路由是否转为小写
KeepVerb false 是否保留方法名中的HTTP动词前缀
KeepName false 是否保留完整方法名
SplitCamelCase true 是否将PascalCase拆分为kebab-case
AbandonControllerAffixes [...] 要移除的控制器名称后缀
AbandonActionAffixes ["Async"] 要移除的方法名后缀

6.4 路由规则示例对照

public class ProductService : IDynamicApiController
{
    // KeepVerb=false(默认): GET /api/product/list
    // KeepVerb=true:         GET /api/product/get-list
    public List<Product> GetList() { }

    // KeepName=false(默认): POST /api/product/product
    // KeepName=true:         POST /api/product/create-product
    public int CreateProduct(ProductInput input) { }

    // SplitCamelCase=true(默认): GET /api/product/by-category-id
    // SplitCamelCase=false:        GET /api/product/bycategoryid
    public List<Product> GetByCategoryId(int categoryId) { }
}

7. API版本管理

7.1 基于路由的版本管理

// V1版本
[ApiDescriptionSettings(Name = "v1/User")]
public class UserServiceV1 : IDynamicApiController
{
    // GET /api/v1/user/info
    public UserDto GetInfo(int id)
    {
        return new UserDto { Id = id, Name = "V1接口" };
    }
}

// V2版本
[ApiDescriptionSettings(Name = "v2/User")]
public class UserServiceV2 : IDynamicApiController
{
    // GET /api/v2/user/info
    public UserDtoV2 GetInfo(int id)
    {
        return new UserDtoV2 { Id = id, Name = "V2接口", Avatar = "avatar.png" };
    }
}

7.2 基于分组的版本管理

// V1分组
[ApiDescriptionSettings("V1")]
public class UserServiceV1 : IDynamicApiController
{
    public UserDto GetInfo(int id) { return new(); }
}

// V2分组
[ApiDescriptionSettings("V2")]
public class UserServiceV2 : IDynamicApiController
{
    public UserDtoV2 GetInfo(int id) { return new(); }
}

在Swagger中会根据分组分别显示不同版本的接口。

7.3 集成ASP.NET Core API版本控制

// 安装版本控制包
// dotnet add package Microsoft.AspNetCore.Mvc.Versioning

// 在Startup中配置
services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
});

8. Swagger文档集成与配置

8.1 基本Swagger配置

Furion通过AddInject()UseInject()自动集成了Swagger。默认配置下,访问应用根路径即可看到Swagger UI。

8.2 详细Swagger配置

// appsettings.json
{
  "SpecificationDocumentSettings": {
    "DocumentTitle": "MyApp API 文档",
    "DefaultGroupName": "Default",
    "FormatAsV2": false,
    "DocExpansionState": "List",
    "XmlComments": true,
    "RoutePrefix": "api-docs",
    "ServerDir": "",
    "LoginInfo": {
      "Enabled": true
    },
    "GroupOpenApiInfos": [
      {
        "Group": "Default",
        "Title": "通用接口",
        "Description": "通用业务接口",
        "Version": "v1.0",
        "TermsOfService": "https://furion.net",
        "Contact": {
          "Name": "开发团队",
          "Email": "dev@example.com",
          "Url": "https://example.com"
        }
      },
      {
        "Group": "System",
        "Title": "系统管理接口",
        "Description": "系统管理和配置相关接口",
        "Version": "v1.0"
      }
    ],
    "EnableAuthorized": true,
    "SecurityDefinitions": [
      {
        "Id": "Bearer",
        "Type": "Http",
        "Name": "Authorization",
        "Description": "JWT授权。在下方输入Token(不需要加Bearer前缀)",
        "BearerFormat": "JWT",
        "Scheme": "bearer",
        "In": "Header"
      }
    ]
  }
}

8.3 XML注释显示

确保在所有包含动态API的项目中启用XML文档生成:

<!-- 在每个项目的.csproj中添加 -->
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
/// <summary>
/// 订单管理服务
/// </summary>
public class OrderService : IDynamicApiController
{
    /// <summary>
    /// 创建订单
    /// </summary>
    /// <remarks>
    /// 请求示例:
    ///
    ///     POST /api/order/create
    ///     {
    ///         "productId": 1,
    ///         "quantity": 2,
    ///         "address": "北京市朝阳区"
    ///     }
    ///
    /// </remarks>
    /// <param name="input">订单创建参数</param>
    /// <returns>订单编号</returns>
    /// <response code="200">创建成功</response>
    /// <response code="400">参数验证失败</response>
    public string CreateOrder(CreateOrderInput input)
    {
        return "ORD20240115001";
    }
}

9. 请求参数绑定

9.1 参数绑定特性

Furion支持ASP.NET Core的所有参数绑定方式:

特性 说明 适用场景
[FromQuery] 从URL查询字符串获取 GET请求的简单参数
[FromBody] 从请求体获取 POST/PUT的复杂对象
[FromRoute] 从路由模板获取 路径参数
[FromHeader] 从请求头获取 认证信息、自定义头
[FromForm] 从表单数据获取 文件上传、表单提交

9.2 默认绑定规则

Furion对不同HTTP方法有不同的默认参数绑定规则:

public class BindingDemoService : IDynamicApiController
{
    // GET方法:简单类型默认从Query获取
    // GET /api/binding-demo/user?id=1&name=张三
    public UserDto GetUser(int id, string name)
    {
        // id 和 name 自动从QueryString获取
        return new UserDto { Id = id, Name = name };
    }

    // POST方法:复杂类型默认从Body获取
    // POST /api/binding-demo/user
    // Body: { "name": "张三", "email": "..." }
    public int CreateUser(CreateUserInput input)
    {
        // input 自动从RequestBody获取
        return 1;
    }

    // PUT方法:复杂类型默认从Body获取
    // PUT /api/binding-demo/user?id=1
    // Body: { "name": "新名称" }
    public void UpdateUser(int id, UpdateUserInput input)
    {
        // id 从QueryString获取
        // input 从RequestBody获取
    }
}

9.3 显式指定参数绑定

using Microsoft.AspNetCore.Mvc;

public class ExplicitBindingService : IDynamicApiController
{
    // 从路由获取参数
    [HttpGet("api/users/{userId}/orders/{orderId}")]
    public OrderDto GetOrder(
        [FromRoute] int userId,
        [FromRoute] int orderId)
    {
        return new OrderDto();
    }

    // 从请求头获取参数
    [HttpGet]
    public UserDto GetCurrentUser(
        [FromHeader(Name = "Authorization")] string token,
        [FromHeader(Name = "X-Tenant-Id")] string tenantId)
    {
        return new UserDto();
    }

    // 混合绑定
    [HttpPost("api/products/{categoryId}")]
    public int CreateProduct(
        [FromRoute] int categoryId,
        [FromQuery] string source,
        [FromBody] CreateProductInput input)
    {
        return 1;
    }

    // 文件上传
    [HttpPost]
    public string UploadFile(
        [FromForm] string description,
        [FromForm] IFormFile file)
    {
        return file.FileName;
    }
}

9.4 复杂查询参数

// 分页查询DTO
public class PagedQueryInput
{
    public int PageIndex { get; set; } = 1;
    public int PageSize { get; set; } = 20;
    public string Keyword { get; set; }
    public string OrderBy { get; set; }
    public bool IsDesc { get; set; }
}

public class UserQueryService : IDynamicApiController
{
    // GET /api/user-query/paged?pageIndex=1&pageSize=20&keyword=张
    // 复杂类型在GET方法中需要显式标记[FromQuery]
    public PagedResult<UserDto> GetPaged([FromQuery] PagedQueryInput input)
    {
        return new PagedResult<UserDto>
        {
            Total = 100,
            PageIndex = input.PageIndex,
            PageSize = input.PageSize,
            Items = new List<UserDto>()
        };
    }
}

10. 返回值处理

10.1 基本返回类型

Furion的动态WebAPI支持各种返回类型:

public class ReturnDemoService : IDynamicApiController
{
    // 返回简单类型
    public string GetString() => "Hello";
    public int GetNumber() => 42;
    public bool GetBool() => true;

    // 返回对象
    public UserDto GetUser()
    {
        return new UserDto { Id = 1, Name = "张三" };
    }

    // 返回集合
    public List<UserDto> GetUsers()
    {
        return new List<UserDto>();
    }

    // 返回匿名对象
    public object GetDashboard()
    {
        return new
        {
            UserCount = 1000,
            OrderCount = 5000,
            Revenue = 1234567.89
        };
    }

    // 无返回值(void)
    public void PostAction()
    {
        // 返回204 No Content
    }

    // 返回Task
    public async Task PostAsyncAction()
    {
        await Task.Delay(100);
    }

    // 返回Task<T>
    public async Task<UserDto> GetUserAsync()
    {
        return await Task.FromResult(new UserDto());
    }
}

10.2 规范化结果

Furion提供了规范化结果功能,将所有API返回值包装为统一格式:

// 启用规范化结果
services.AddControllers()
    .AddInject();

// 规范化返回格式示例:
// {
//     "statusCode": 200,
//     "succeeded": true,
//     "data": { ... },       // 实际返回数据
//     "errors": null,
//     "extras": null,
//     "timestamp": 1705312000
// }

自定义规范化结果提供器:

using Furion.DataValidation;
using Furion.UnifyResult;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

/// <summary>
/// 自定义规范化结果提供器
/// </summary>
[UnifyModel(typeof(ApiResult<>))]
public class CustomResultProvider : IUnifyResultProvider
{
    public IActionResult OnSucceeded(ActionExecutedContext context, object data)
    {
        return new JsonResult(new ApiResult<object>
        {
            Code = 200,
            Success = true,
            Message = "请求成功",
            Data = data
        });
    }

    public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata)
    {
        return new JsonResult(new ApiResult<object>
        {
            Code = 400,
            Success = false,
            Message = metadata.Message,
            Data = null
        });
    }

    public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings = null)
    {
        switch (statusCode)
        {
            case 401:
                await context.Response.WriteAsJsonAsync(new ApiResult<object>
                {
                    Code = 401,
                    Success = false,
                    Message = "未授权",
                    Data = null
                });
                break;
            case 403:
                await context.Response.WriteAsJsonAsync(new ApiResult<object>
                {
                    Code = 403,
                    Success = false,
                    Message = "禁止访问",
                    Data = null
                });
                break;
        }
    }

    public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata)
    {
        return new JsonResult(new ApiResult<object>
        {
            Code = 500,
            Success = false,
            Message = metadata.Message,
            Data = null
        });
    }
}

/// <summary>
/// 统一返回结果模型
/// </summary>
public class ApiResult<T>
{
    public int Code { get; set; }
    public bool Success { get; set; }
    public string Message { get; set; }
    public T Data { get; set; }
}

10.3 跳过规范化结果

某些接口不需要规范化结果包装时,可以使用特性跳过:

using Furion.UnifyResult;

public class FileService : IDynamicApiController
{
    // 文件下载不需要规范化结果包装
    [NonUnify]
    [HttpGet]
    public IActionResult DownloadFile(string fileName)
    {
        var bytes = System.IO.File.ReadAllBytes($"uploads/{fileName}");
        return new FileContentResult(bytes, "application/octet-stream")
        {
            FileDownloadName = fileName
        };
    }

    // 健康检查不需要包装
    [NonUnify]
    public string HealthCheck() => "OK";
}

11. 接口分组与标签

11.1 使用ApiDescriptionSettings分组

// 系统管理分组
[ApiDescriptionSettings("System", Name = "SysUser", Order = 1)]
public class SysUserService : IDynamicApiController
{
    /// <summary>
    /// 获取系统用户列表
    /// </summary>
    public List<UserDto> GetList() => new();

    /// <summary>
    /// 创建系统用户
    /// </summary>
    public int Create(CreateUserInput input) => 1;
}

// 业务管理分组
[ApiDescriptionSettings("Business", Name = "Order", Order = 1)]
public class OrderService : IDynamicApiController
{
    /// <summary>
    /// 获取订单列表
    /// </summary>
    public List<OrderDto> GetList() => new();
}

// 报表分组
[ApiDescriptionSettings("Report", Name = "SalesReport", Order = 1)]
public class SalesReportService : IDynamicApiController
{
    /// <summary>
    /// 获取销售报表
    /// </summary>
    public object GetSalesReport() => new { };
}

11.2 Swagger分组配置

{
  "SpecificationDocumentSettings": {
    "GroupOpenApiInfos": [
      {
        "Group": "Default",
        "Title": "默认分组",
        "Description": "未分类的接口",
        "Version": "v1.0"
      },
      {
        "Group": "System",
        "Title": "系统管理",
        "Description": "系统管理相关接口,包括用户、角色、菜单等",
        "Version": "v1.0"
      },
      {
        "Group": "Business",
        "Title": "业务管理",
        "Description": "核心业务相关接口",
        "Version": "v1.0"
      },
      {
        "Group": "Report",
        "Title": "报表统计",
        "Description": "数据报表和统计分析接口",
        "Version": "v1.0"
      }
    ]
  }
}

11.3 方法级别的分组

public class MixedService : IDynamicApiController
{
    // 同一个服务中的方法可以属于不同分组
    [ApiDescriptionSettings("System")]
    public string GetSystemInfo() => "系统信息";

    [ApiDescriptionSettings("Business")]
    public string GetBusinessData() => "业务数据";

    // 可以同时出现在多个分组中
    [ApiDescriptionSettings("System,Business")]
    public string GetCommonData() => "公共数据";
}

11.4 排序控制

// 通过Order控制接口在Swagger中的显示顺序
[ApiDescriptionSettings("System", Order = 1)]  // 第一个显示
public class UserService : IDynamicApiController { }

[ApiDescriptionSettings("System", Order = 2)]  // 第二个显示
public class RoleService : IDynamicApiController { }

[ApiDescriptionSettings("System", Order = 3)]  // 第三个显示
public class MenuService : IDynamicApiController { }

12. 动态WebAPI高级配置

12.1 控制器命名规则配置

// 自定义控制器名称
[ApiDescriptionSettings(
    Name = "UserMgmt",           // 自定义名称
    Tag = "用户管理",             // Swagger标签
    Order = 1,                   // 排序
    Description = "用户管理相关接口" // 描述
)]
public class UserManagementService : IDynamicApiController
{
    public List<UserDto> GetList() => new();
}

12.2 隐藏接口

public class InternalService : IDynamicApiController
{
    // 公开接口
    public string GetPublicData() => "公开数据";

    // 隐藏接口(在Swagger中不显示,但仍可访问)
    [ApiDescriptionSettings(false)]
    public string GetHiddenData() => "隐藏数据";
}

12.3 自定义模型绑定

// 自定义日期格式绑定
public class DateRangeInput
{
    [ModelBinder(BinderType = typeof(DateTimeModelBinder))]
    public DateTime StartDate { get; set; }

    [ModelBinder(BinderType = typeof(DateTimeModelBinder))]
    public DateTime EndDate { get; set; }
}

12.4 动态WebAPI与传统Controller共存

Furion的动态WebAPI可以与传统的ASP.NET Core Controller共存,互不影响:

// 传统Controller
[ApiController]
[Route("api/[controller]")]
public class LegacyController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok("传统Controller");
}

// 动态WebAPI
public class ModernService : IDynamicApiController
{
    public string GetData() => "动态WebAPI";
}

// 两者可以在同一个项目中同时使用
// GET /api/legacy         → 传统Controller
// GET /api/modern/data    → 动态WebAPI

12.5 性能优化建议

建议 说明
合理使用异步方法 IO密集型操作使用async/await
避免大对象返回 使用分页和投影减少返回数据量
利用缓存 频繁查询的数据使用缓存
压缩响应 启用gzip/brotli压缩
限制请求大小 配置最大请求体大小
// 异步方法示例
public class OptimizedService : IDynamicApiController
{
    private readonly IRepository<Product> _repo;
    private readonly IMemoryCache _cache;

    public OptimizedService(IRepository<Product> repo, IMemoryCache cache)
    {
        _repo = repo;
        _cache = cache;
    }

    /// <summary>
    /// 分页查询(高性能)
    /// </summary>
    public async Task<PagedResult<ProductDto>> GetPagedAsync([FromQuery] PagedInput input)
    {
        var cacheKey = $"products:{input.PageIndex}:{input.PageSize}:{input.Keyword}";

        return await _cache.GetOrCreateAsync(cacheKey, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);

            var query = _repo.AsQueryable();

            if (!string.IsNullOrEmpty(input.Keyword))
            {
                query = query.Where(p => p.Name.Contains(input.Keyword));
            }

            var total = await query.CountAsync();
            var items = await query
                .Skip((input.PageIndex - 1) * input.PageSize)
                .Take(input.PageSize)
                .Select(p => new ProductDto { Id = p.Id, Name = p.Name })
                .ToListAsync();

            return new PagedResult<ProductDto>
            {
                Total = total,
                Items = items
            };
        });
    }
}

总结

本章全面介绍了Furion动态WebAPI的开发方法,这是Furion框架最核心的特色功能。通过动态WebAPI,开发者可以大幅减少样板代码,专注于业务逻辑的实现。

关键要点:

  • 实现IDynamicApiController接口或使用[DynamicApiController]特性即可创建动态API
  • 方法名前缀约定自动推断HTTP方法(Get→GET, Create→POST, Update→PUT, Delete→DELETE)
  • 路由自动生成遵循api/[controller]/[action]的模板,支持kebab-case转换
  • 参数绑定遵循HTTP方法的默认规则,也可以显式指定
  • 规范化结果提供统一的API返回格式
  • 接口分组通过ApiDescriptionSettings实现,方便API文档组织

下一章预告:第五章将详细介绍Furion的依赖注入与服务注册机制,包括接口标记注入、构造函数注入、属性注入等多种注入方式。