前后端契约

一份 [WebApi] 接口如何同时充当服务端路由源与客户端强类型 HTTP 门面。

最后更新

ServiceProxy<T> 是 Known "前后端一体"的核心机制:客户端不写 HttpClient、不拼 URL、不做反序列化样板代码, 只注入 T(比如 IOrderService),运行时自动生成一个把方法调用转成 HTTP 请求的代理。

契约 = 接口 + [WebApi]

[WebApi]
public interface IOrderService
{
    Task<PagingResult<Order>> QueryOrdersAsync(PagingCriteria criteria);
    Task<Order> GetOrderAsync(string id);
    Task<Result> SaveOrderAsync(Order order);
    Task<Result> DeleteOrdersAsync(List<string> ids);
}

接口与它的 [Service] 默认实现同处一个文件(框架约定),这个文件放在服务端与客户端共同引用的模块程序集里——客户端只用到接口那一半。

服务端:AddKnownWeb 把接口暴露成路由

AddKnownWeb 在 Config.AddModule 的扫描结果里挑出带 [WebApi] 的接口与对应 [Service] 实现, 在 ASP.NET Core 上以最小 API 端点方式注册路由:

  • HTTP 方法约定:Known 只用 GET / POST 两个动词——方法名以 Get 开头且所有参数都是基元或 string 才走 GET,其余一律 POST(PagingCriteria、实体、List<> 都算复杂参数 → POST)。
  • 路径:/api/{接口名去 I 去 Service}/{方法名去 Async},如 IOrderService.QueryOrdersAsync → /api/Order/QueryOrders;可用 [Route("...")] 覆写。
  • 参数绑定:单个复杂类型 → body;多个基元 / string → 查询串。
  • 返回:Task<T>、Task<Result>、Task<PagingResult<T>> 都直接序列化为 JSON。Result 在 Known 里非泛型(IsValid + Message),分页结果用 PagingResult<T>(Total + Data),没有 Result<T> 这种包装。

客户端:AddKnownClient 把接口变成可用服务

客户端调用 AddKnownClient(option => option.BaseAddress = ...) 后,IOrderService 直接 @inject:

@inject IOrderService Orders

@code {
    private List<Order> orders = [];

    protected override async Task OnInitializedAsync()
    {
        // 分页查询:criteria 承载页码/排序/过滤,框架拼 SQL
        var page = await Orders.QueryOrdersAsync(new PagingCriteria { PageIndex = 1, PageSize = 20 });
        orders = page.Data;

        // 按主键取单条:直接返回实体本身,无数据时为 null
        var one = await Orders.GetOrderAsync("…id…");
    }
}

ServiceProxy<T> 用 DispatchProxy 生成运行时类,Invoke 里根据方法名与参数拼出 URL、 序列化 body、发送请求、反序列化 Result<T>——所有 HttpClient 细节被完全屏蔽。

为什么值得这么做

  • 一份类型定义三处用:编译期客户端、服务端路由契约、DTO 结构都指向同一份接口。
  • 改造成本低:重命名方法 = 前后端同步失效(编译期就报错),不会出现"改了服务端忘了客户端"。
  • 测试友好:单测里替换成假实现(FakeOrderService),无需 mock HTTP。

边界

  • 目前只覆盖同步方法调用 → HTTP 请求/响应;SSE / WebSocket / 长轮询走另外的通道(IN 侧规划)。
  • 分页查询返回 PagingResult<T>、写操作返回非泛型 Result(带 IsValid / Message);直接返回 Task<T> 也支持,但会丢掉业务错误信息。
  • 文件上传下载:走独立的 /api/upload 端点,代理不自动处理 Stream/IFormFile 语义。
  • API 版本管理(v1/v2)目前由路由段决定,接口本身不带版本;跨版本兼容性需在应用侧演进时自行保证。

未来

前后端一体能力属规划中的 Path C(不锁死、按 Batch 推进):IAuthProvider / ISyncApi 等新契约 落地时同样沿用 ServiceProxy<T> 代理机制,客户端零改造。

微信公众号二维码 扫码关注公众号
QQ 交流群二维码 扫码加入 QQ 群