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

推荐订阅源

T
Threatpost
S
Schneier on Security
P
Palo Alto Networks Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Securelist
T
Threat Research - Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
V
Vulnerabilities – Threatpost
AI
AI
C
CERT Recently Published Vulnerability Notes
C
Cyber Attacks, Cyber Crime and Cyber Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
TaoSecurity Blog
TaoSecurity Blog
O
OpenAI News
Cyberwarzone
Cyberwarzone
G
GRAHAM CLULEY
SecWiki News
SecWiki News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
C
CXSECURITY Database RSS Feed - CXSecurity.com
C
Cisco Blogs
K
Kaspersky official blog
Spread Privacy
Spread Privacy
S
Security @ Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
IT之家
IT之家
有赞技术团队
有赞技术团队
B
Blog
T
Tailwind CSS Blog
PCI Perspectives
PCI Perspectives
P
Privacy & Cybersecurity Law Blog
Last Week in AI
Last Week in AI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Webroot Blog
Webroot Blog
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
H
Hacker News: Front Page
The Cloudflare Blog
C
Cybersecurity and Infrastructure Security Agency CISA
美团技术团队
V
V2EX
腾讯CDC
S
SegmentFault 最新的问题
Security Archives - TechRepublic
Security Archives - TechRepublic

博客园 - 我才是银古

第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
第十三章:远程请求与HTTP客户端
我才是银古 · 2026-06-21 · via 博客园 - 我才是银古

第十三章:远程请求与HTTP客户端

一、HttpClient 基础

1.1 .NET HttpClient 概述

在现代微服务和分布式架构中,服务间的 HTTP 通信是必不可少的。.NET 提供了 HttpClient 类用于发送 HTTP 请求,而 Furion 在此基础上进行了大量增强,提供了更简洁、更强大的远程请求方式。

1.2 原生 HttpClient 的问题

问题 说明
套接字耗尽 频繁创建/销毁 HttpClient 可能导致端口耗尽
DNS 更新 单例 HttpClient 不会更新 DNS 解析结果
配置繁琐 每次请求需手动设置请求头、序列化等
缺少拦截器 不支持统一的请求/响应拦截
重试困难 没有内置的重试机制

1.3 IHttpClientFactory

.NET 推荐使用 IHttpClientFactory 管理 HttpClient 实例:

// 注册 HttpClient
builder.Services.AddHttpClient();

// 使用命名客户端
builder.Services.AddHttpClient("GitHubApi", client =>
{
    client.BaseAddress = new Uri("https://api.github.com/");
    client.DefaultRequestHeaders.Add("Accept", "application/json");
    client.DefaultRequestHeaders.Add("User-Agent", "FurionApp");
});

二、Furion 远程请求增强

2.1 注册远程请求服务

var builder = WebApplication.CreateBuilder(args);

// 注册 Furion 远程请求服务
builder.Services.AddRemoteRequest();

var app = builder.Build();
app.Run();

2.2 基本使用方式

Furion 提供了多种远程请求方式:

方式 说明 适用场景
字符串扩展方法 直接在 URL 字符串上调用 快速简单请求
IHttpDispatchProxy 接口代理方式 微服务间通信
HttpClient 增强 增强版 HttpClient 需要完全控制
// 方式1:字符串扩展方法(最简洁)
var result = await "https://api.example.com/users".GetAsAsync<List<UserDto>>();

// 方式2:POST 请求
var user = await "https://api.example.com/users".SetBody(new { Name = "张三", Age = 25 })
    .PostAsAsync<UserDto>();

// 方式3:带请求头
var data = await "https://api.example.com/data"
    .SetHeaders(new Dictionary<string, object>
    {
        { "Authorization", "Bearer xxx" },
        { "X-Request-Id", Guid.NewGuid().ToString() }
    })
    .GetAsAsync<DataDto>();

三、IHttpDispatchProxy 接口代理

3.1 声明式 HTTP 请求

IHttpDispatchProxy 允许通过定义接口来声明 HTTP 请求,类似于 Java 中的 Feign:

/// <summary>
/// 用户服务 HTTP 代理接口
/// </summary>
public interface IUserServiceApi : IHttpDispatchProxy
{
    /// <summary>
    /// 获取用户列表
    /// </summary>
    [Get("https://api.userservice.com/api/users")]
    Task<List<UserDto>> GetUsersAsync();

    /// <summary>
    /// 根据Id获取用户
    /// </summary>
    [Get("https://api.userservice.com/api/users/{id}")]
    Task<UserDto> GetUserByIdAsync([RouteParam] long id);

    /// <summary>
    /// 创建用户
    /// </summary>
    [Post("https://api.userservice.com/api/users")]
    Task<UserDto> CreateUserAsync([Body] CreateUserInput input);

    /// <summary>
    /// 更新用户
    /// </summary>
    [Put("https://api.userservice.com/api/users/{id}")]
    Task<UserDto> UpdateUserAsync([RouteParam] long id, [Body] UpdateUserInput input);

    /// <summary>
    /// 删除用户
    /// </summary>
    [Delete("https://api.userservice.com/api/users/{id}")]
    Task DeleteUserAsync([RouteParam] long id);
}

3.2 使用接口代理

[ApiDescriptionSettings("用户管理")]
public class UserAppService : IDynamicApiController
{
    private readonly IUserServiceApi _userServiceApi;

    public UserAppService(IUserServiceApi userServiceApi)
    {
        _userServiceApi = userServiceApi;
    }

    /// <summary>
    /// 获取所有用户
    /// </summary>
    public async Task<List<UserDto>> GetUsers()
    {
        return await _userServiceApi.GetUsersAsync();
    }

    /// <summary>
    /// 获取用户详情
    /// </summary>
    public async Task<UserDto> GetUser(long id)
    {
        return await _userServiceApi.GetUserByIdAsync(id);
    }

    /// <summary>
    /// 创建用户
    /// </summary>
    public async Task<UserDto> CreateUser(CreateUserInput input)
    {
        return await _userServiceApi.CreateUserAsync(input);
    }
}

3.3 配置基础地址

通过 appsettings.json 配置服务地址,避免硬编码:

{
  "RemoteRequest": {
    "UserService": {
      "BaseUrl": "https://api.userservice.com",
      "Timeout": 30
    },
    "OrderService": {
      "BaseUrl": "https://api.orderservice.com",
      "Timeout": 60
    }
  }
}
/// <summary>
/// 使用配置的基础地址
/// </summary>
[Client("UserService")]
public interface IUserServiceApi : IHttpDispatchProxy
{
    [Get("/api/users")]
    Task<List<UserDto>> GetUsersAsync();

    [Get("/api/users/{id}")]
    Task<UserDto> GetUserByIdAsync([RouteParam] long id);

    [Post("/api/users")]
    Task<UserDto> CreateUserAsync([Body] CreateUserInput input);
}

四、RESTful 请求详解

4.1 GET 请求

// 简单 GET 请求
var users = await "https://api.example.com/users".GetAsAsync<List<UserDto>>();

// 带查询参数
var result = await "https://api.example.com/users"
    .SetQueries(new
    {
        page = 1,
        pageSize = 20,
        keyword = "张三",
        status = "active"
    })
    .GetAsAsync<PagedResult<UserDto>>();

// 带请求头的 GET
var data = await "https://api.example.com/protected/data"
    .SetHeaders(new Dictionary<string, object>
    {
        { "Authorization", $"Bearer {token}" }
    })
    .GetAsAsync<ProtectedData>();

// 获取原始字符串
var rawJson = await "https://api.example.com/users".GetAsStringAsync();

// 获取 HttpResponseMessage
var response = await "https://api.example.com/users".GetAsync();

4.2 POST 请求

// JSON Body
var newUser = await "https://api.example.com/users"
    .SetBody(new CreateUserInput
    {
        UserName = "张三",
        Email = "zhangsan@example.com",
        Password = "SecurePass123"
    })
    .PostAsAsync<UserDto>();

// Form 表单提交
var loginResult = await "https://api.example.com/auth/token"
    .SetBody(new Dictionary<string, string>
    {
        { "grant_type", "password" },
        { "username", "admin" },
        { "password", "123456" }
    }, "application/x-www-form-urlencoded")
    .PostAsAsync<TokenResult>();

// 带查询参数的 POST
var result = await "https://api.example.com/api/batch"
    .SetQueries(new { type = "import" })
    .SetBody(dataList)
    .PostAsAsync<BatchResult>();

4.3 PUT 请求

// 更新资源
var updatedUser = await "https://api.example.com/users/1"
    .SetBody(new UpdateUserInput
    {
        UserName = "张三(已更新)",
        Email = "zhangsan_new@example.com"
    })
    .PutAsAsync<UserDto>();

4.4 DELETE 请求

// 删除资源
await "https://api.example.com/users/1".DeleteAsync();

// 带条件删除
await "https://api.example.com/users"
    .SetQueries(new { ids = "1,2,3" })
    .DeleteAsync();

4.5 PATCH 请求

// 部分更新
var patchedUser = await "https://api.example.com/users/1"
    .SetBody(new { Email = "new_email@example.com" })
    .PatchAsAsync<UserDto>();

五、请求拦截器

5.1 请求拦截

/// <summary>
/// 请求拦截器:统一添加认证信息
/// </summary>
public class AuthorizationInterceptor : IHttpDispatchProxy
{
    // 在接口代理中使用拦截器
}

// 使用 OnRequesting 回调
var result = await "https://api.example.com/data"
    .OnRequesting((client, request) =>
    {
        // 添加认证头
        request.Headers.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", GetAccessToken());

        // 添加自定义头
        request.Headers.Add("X-Correlation-Id", Guid.NewGuid().ToString());
        request.Headers.Add("X-Client-Version", "1.0.0");
    })
    .GetAsAsync<DataResult>();

private static string GetAccessToken() => "your-access-token";

5.2 全局请求拦截

/// <summary>
/// 全局 HTTP 请求拦截处理器
/// </summary>
public class GlobalHttpMessageHandler : DelegatingHandler
{
    private readonly ILogger<GlobalHttpMessageHandler> _logger;

    public GlobalHttpMessageHandler(ILogger<GlobalHttpMessageHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // 请求前处理
        var requestId = Guid.NewGuid().ToString("N");
        request.Headers.Add("X-Request-Id", requestId);

        _logger.LogInformation("[{RequestId}] {Method} {Url}",
            requestId, request.Method, request.RequestUri);

        var stopwatch = System.Diagnostics.Stopwatch.StartNew();

        // 发送请求
        var response = await base.SendAsync(request, cancellationToken);

        stopwatch.Stop();

        // 响应后处理
        _logger.LogInformation("[{RequestId}] 响应状态:{StatusCode},耗时:{Elapsed}ms",
            requestId, (int)response.StatusCode, stopwatch.ElapsedMilliseconds);

        return response;
    }
}

// 注册全局拦截器
builder.Services.AddTransient<GlobalHttpMessageHandler>();
builder.Services.AddHttpClient("Default")
    .AddHttpMessageHandler<GlobalHttpMessageHandler>();

六、响应拦截器

6.1 响应处理

// 响应拦截
var result = await "https://api.example.com/data"
    .OnResponsing((client, response) =>
    {
        // 检查响应状态
        if (!response.IsSuccessStatusCode)
        {
            // 记录错误日志
            Console.WriteLine($"请求失败:{response.StatusCode}");
        }

        // 检查特定响应头
        if (response.Headers.Contains("X-RateLimit-Remaining"))
        {
            var remaining = response.Headers.GetValues("X-RateLimit-Remaining").First();
            Console.WriteLine($"API 剩余调用次数:{remaining}");
        }
    })
    .GetAsAsync<DataResult>();

6.2 统一响应处理

/// <summary>
/// 远程请求帮助类
/// </summary>
public static class RemoteRequestHelper
{
    /// <summary>
    /// 带统一错误处理的 GET 请求
    /// </summary>
    public static async Task<T> SafeGetAsync<T>(string url, Dictionary<string, object> headers = null)
    {
        try
        {
            var request = url;
            if (headers != null)
            {
                request = url;
            }

            var response = await url
                .SetHeaders(headers ?? new Dictionary<string, object>())
                .OnResponsing((client, resp) =>
                {
                    if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
                    {
                        throw new UnauthorizedAccessException("远程服务认证失败");
                    }

                    if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
                    {
                        throw new InvalidOperationException($"远程资源不存在:{url}");
                    }
                })
                .GetAsAsync<ApiResponse<T>>();

            if (response.Code != 200)
            {
                throw new Exception($"远程请求业务异常:{response.Message}");
            }

            return response.Data;
        }
        catch (HttpRequestException ex)
        {
            throw new Exception($"远程请求网络异常:{ex.Message}", ex);
        }
        catch (TaskCanceledException)
        {
            throw new TimeoutException($"远程请求超时:{url}");
        }
    }
}

/// <summary>
/// 统一 API 响应模型
/// </summary>
public class ApiResponse<T>
{
    public int Code { get; set; }
    public string Message { get; set; }
    public T Data { get; set; }
    public bool Success => Code == 200;
}

七、文件上传与下载

7.1 文件上传

/// <summary>
/// 文件上传服务
/// </summary>
public class FileUploadService
{
    /// <summary>
    /// 单文件上传
    /// </summary>
    public async Task<FileUploadResult> UploadFileAsync(Stream fileStream, string fileName)
    {
        var content = new MultipartFormDataContent();
        var streamContent = new StreamContent(fileStream);
        content.Add(streamContent, "file", fileName);

        var result = await "https://api.example.com/files/upload"
            .SetBody(content)
            .PostAsAsync<FileUploadResult>();

        return result;
    }

    /// <summary>
    /// 多文件上传
    /// </summary>
    public async Task<List<FileUploadResult>> UploadFilesAsync(List<(Stream Stream, string FileName)> files)
    {
        var content = new MultipartFormDataContent();

        foreach (var (stream, fileName) in files)
        {
            var streamContent = new StreamContent(stream);
            content.Add(streamContent, "files", fileName);
        }

        // 添加额外表单字段
        content.Add(new StringContent("images"), "category");

        var result = await "https://api.example.com/files/batch-upload"
            .SetBody(content)
            .PostAsAsync<List<FileUploadResult>>();

        return result;
    }
}

/// <summary>
/// 文件上传结果
/// </summary>
public class FileUploadResult
{
    public string FileId { get; set; }
    public string FileName { get; set; }
    public string Url { get; set; }
    public long Size { get; set; }
}

7.2 文件下载

/// <summary>
/// 文件下载服务
/// </summary>
public class FileDownloadService
{
    /// <summary>
    /// 下载文件到本地
    /// </summary>
    public async Task DownloadFileAsync(string fileUrl, string savePath)
    {
        var response = await fileUrl.GetAsync();
        response.EnsureSuccessStatusCode();

        await using var fileStream = File.Create(savePath);
        await response.Content.CopyToAsync(fileStream);
    }

    /// <summary>
    /// 下载文件为字节数组
    /// </summary>
    public async Task<byte[]> DownloadFileBytesAsync(string fileUrl)
    {
        var response = await fileUrl.GetAsync();
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsByteArrayAsync();
    }

    /// <summary>
    /// 下载大文件(流式处理)
    /// </summary>
    public async Task DownloadLargeFileAsync(string fileUrl, string savePath, IProgress<long> progress = null)
    {
        using var httpClient = new HttpClient();
        using var response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead);
        response.EnsureSuccessStatusCode();

        var totalBytes = response.Content.Headers.ContentLength ?? -1L;

        await using var contentStream = await response.Content.ReadAsStreamAsync();
        await using var fileStream = File.Create(savePath);

        var buffer = new byte[8192];
        var totalBytesRead = 0L;
        int bytesRead;

        while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0)
        {
            await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead));
            totalBytesRead += bytesRead;
            progress?.Report(totalBytesRead);
        }
    }
}

八、超时与重试

8.1 超时配置

// 设置请求超时
var result = await "https://api.example.com/slow-api"
    .SetHttpClientTimeout(TimeSpan.FromSeconds(60))
    .GetAsAsync<SlowResult>();

// 在命名客户端中配置超时
builder.Services.AddHttpClient("SlowService", client =>
{
    client.BaseAddress = new Uri("https://api.slowservice.com");
    client.Timeout = TimeSpan.FromMinutes(2);
});

8.2 重试策略

使用 Polly 实现重试策略:

// 安装 Microsoft.Extensions.Http.Polly 包
builder.Services.AddHttpClient("ResilientService")
    .AddTransientHttpErrorPolicy(policy =>
        policy.WaitAndRetryAsync(3, retryAttempt =>
            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
    .AddTransientHttpErrorPolicy(policy =>
        policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

8.3 自定义重试逻辑

/// <summary>
/// 带重试的 HTTP 请求帮助类
/// </summary>
public static class ResilientHttpHelper
{
    /// <summary>
    /// 带重试的 GET 请求
    /// </summary>
    public static async Task<T> GetWithRetryAsync<T>(
        string url,
        int maxRetries = 3,
        int baseDelayMs = 1000)
    {
        Exception lastException = null;

        for (int i = 0; i <= maxRetries; i++)
        {
            try
            {
                return await url.GetAsAsync<T>();
            }
            catch (HttpRequestException ex) when (i < maxRetries)
            {
                lastException = ex;
                var delay = baseDelayMs * (int)Math.Pow(2, i);
                await Task.Delay(delay);
            }
            catch (TaskCanceledException ex) when (i < maxRetries)
            {
                lastException = ex;
                var delay = baseDelayMs * (int)Math.Pow(2, i);
                await Task.Delay(delay);
            }
        }

        throw new Exception($"请求 {url} 失败,已重试 {maxRetries} 次", lastException);
    }
}

九、请求日志

9.1 日志中间件

/// <summary>
/// HTTP 请求日志处理器
/// </summary>
public class HttpLoggingHandler : DelegatingHandler
{
    private readonly ILogger<HttpLoggingHandler> _logger;

    public HttpLoggingHandler(ILogger<HttpLoggingHandler> logger) => _logger = logger;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var requestId = Guid.NewGuid().ToString("N")[..8];
        var stopwatch = System.Diagnostics.Stopwatch.StartNew();

        // 记录请求信息
        _logger.LogInformation(
            "[{RequestId}] >>> {Method} {Url}",
            requestId, request.Method, request.RequestUri);

        if (request.Content != null)
        {
            var body = await request.Content.ReadAsStringAsync(cancellationToken);
            if (body.Length <= 1000) // 限制日志长度
            {
                _logger.LogDebug("[{RequestId}] 请求体:{Body}", requestId, body);
            }
        }

        // 发送请求
        var response = await base.SendAsync(request, cancellationToken);

        stopwatch.Stop();

        // 记录响应信息
        var level = response.IsSuccessStatusCode ? LogLevel.Information : LogLevel.Warning;
        _logger.Log(level,
            "[{RequestId}] <<< {StatusCode} {ReasonPhrase} ({Elapsed}ms)",
            requestId, (int)response.StatusCode, response.ReasonPhrase,
            stopwatch.ElapsedMilliseconds);

        return response;
    }
}

9.2 注册日志处理器

builder.Services.AddTransient<HttpLoggingHandler>();

builder.Services.AddHttpClient("LoggedClient")
    .AddHttpMessageHandler<HttpLoggingHandler>();

十、微服务间通信

10.1 服务发现与请求

/// <summary>
/// 微服务 HTTP 代理集合
/// </summary>
[Client("OrderService")]
public interface IOrderServiceApi : IHttpDispatchProxy
{
    [Get("/api/orders")]
    Task<PagedResult<OrderDto>> GetOrdersAsync(
        [QueryString] int page = 1,
        [QueryString] int pageSize = 20);

    [Get("/api/orders/{orderId}")]
    Task<OrderDto> GetOrderAsync([RouteParam] long orderId);

    [Post("/api/orders")]
    Task<OrderDto> CreateOrderAsync([Body] CreateOrderInput input);

    [Put("/api/orders/{orderId}/status")]
    Task UpdateOrderStatusAsync(
        [RouteParam] long orderId,
        [Body] UpdateStatusInput input);
}

[Client("InventoryService")]
public interface IInventoryServiceApi : IHttpDispatchProxy
{
    [Get("/api/inventory/{productId}")]
    Task<InventoryDto> GetInventoryAsync([RouteParam] long productId);

    [Post("/api/inventory/deduct")]
    Task<bool> DeductInventoryAsync([Body] DeductInventoryInput input);

    [Post("/api/inventory/restore")]
    Task<bool> RestoreInventoryAsync([Body] RestoreInventoryInput input);
}

10.2 编排多个服务调用

/// <summary>
/// 订单编排服务
/// </summary>
[ApiDescriptionSettings("订单编排")]
public class OrderOrchestrationService : IDynamicApiController
{
    private readonly IOrderServiceApi _orderApi;
    private readonly IInventoryServiceApi _inventoryApi;
    private readonly ILogger<OrderOrchestrationService> _logger;

    public OrderOrchestrationService(
        IOrderServiceApi orderApi,
        IInventoryServiceApi inventoryApi,
        ILogger<OrderOrchestrationService> logger)
    {
        _orderApi = orderApi;
        _inventoryApi = inventoryApi;
        _logger = logger;
    }

    /// <summary>
    /// 创建订单(编排多个服务)
    /// </summary>
    public async Task<OrderDto> CreateOrder(CreateOrderInput input)
    {
        // 1. 检查库存
        var inventory = await _inventoryApi.GetInventoryAsync(input.ProductId);
        if (inventory.Available < input.Quantity)
        {
            throw new Exception("库存不足");
        }

        // 2. 扣减库存
        var deducted = await _inventoryApi.DeductInventoryAsync(new DeductInventoryInput
        {
            ProductId = input.ProductId,
            Quantity = input.Quantity
        });

        if (!deducted)
        {
            throw new Exception("扣减库存失败");
        }

        try
        {
            // 3. 创建订单
            var order = await _orderApi.CreateOrderAsync(input);
            return order;
        }
        catch (Exception ex)
        {
            // 4. 补偿:恢复库存
            _logger.LogError(ex, "创建订单失败,开始恢复库存");
            await _inventoryApi.RestoreInventoryAsync(new RestoreInventoryInput
            {
                ProductId = input.ProductId,
                Quantity = input.Quantity
            });

            throw;
        }
    }
}

十一、请求转发

11.1 网关模式转发

/// <summary>
/// API 网关转发控制器
/// </summary>
[ApiDescriptionSettings("网关")]
public class GatewayAppService : IDynamicApiController
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IConfiguration _configuration;

    public GatewayAppService(
        IHttpClientFactory httpClientFactory,
        IConfiguration configuration)
    {
        _httpClientFactory = httpClientFactory;
        _configuration = configuration;
    }

    /// <summary>
    /// 转发请求到目标服务
    /// </summary>
    [HttpGet("{serviceName}/{**path}")]
    public async Task<IActionResult> ForwardRequest(
        string serviceName, string path,
        [FromServices] HttpContext httpContext)
    {
        var serviceUrl = _configuration[$"Services:{serviceName}:BaseUrl"];
        if (string.IsNullOrEmpty(serviceUrl))
        {
            return new NotFoundObjectResult($"服务 {serviceName} 未配置");
        }

        var targetUrl = $"{serviceUrl}/{path}{httpContext.Request.QueryString}";
        var client = _httpClientFactory.CreateClient();

        // 复制请求头
        foreach (var header in httpContext.Request.Headers)
        {
            if (!header.Key.StartsWith("Host", StringComparison.OrdinalIgnoreCase))
            {
                client.DefaultRequestHeaders.TryAddWithoutValidation(header.Key, header.Value.ToArray());
            }
        }

        var response = await client.GetAsync(targetUrl);
        var content = await response.Content.ReadAsStringAsync();

        return new ContentResult
        {
            Content = content,
            ContentType = response.Content.Headers.ContentType?.ToString(),
            StatusCode = (int)response.StatusCode
        };
    }
}

十二、最佳实践

12.1 远程请求设计原则

原则 说明
使用 IHttpClientFactory 避免直接 new HttpClient
设置合理超时 根据业务场景设置超时时间
添加重试策略 网络不稳定场景添加重试
断路器模式 避免雪崩效应
请求日志 记录请求和响应便于排查
接口代理 使用 IHttpDispatchProxy 提升可维护性
配置外置 服务地址放配置文件
异常处理 统一处理远程请求异常

12.2 完整的远程请求服务示例

/// <summary>
/// 第三方支付服务代理
/// </summary>
[Client("PaymentService")]
public interface IPaymentApi : IHttpDispatchProxy
{
    /// <summary>
    /// 创建支付订单
    /// </summary>
    [Post("/api/payment/create")]
    Task<PaymentResult> CreatePaymentAsync([Body] CreatePaymentInput input);

    /// <summary>
    /// 查询支付状态
    /// </summary>
    [Get("/api/payment/{paymentId}/status")]
    Task<PaymentStatusResult> GetPaymentStatusAsync([RouteParam] string paymentId);

    /// <summary>
    /// 退款
    /// </summary>
    [Post("/api/payment/{paymentId}/refund")]
    Task<RefundResult> RefundAsync([RouteParam] string paymentId, [Body] RefundInput input);
}

/// <summary>
/// 支付服务封装
/// </summary>
public class PaymentService
{
    private readonly IPaymentApi _paymentApi;
    private readonly ILogger<PaymentService> _logger;

    public PaymentService(IPaymentApi paymentApi, ILogger<PaymentService> logger)
    {
        _paymentApi = paymentApi;
        _logger = logger;
    }

    /// <summary>
    /// 发起支付(带重试和异常处理)
    /// </summary>
    public async Task<PaymentResult> CreatePaymentWithRetry(CreatePaymentInput input)
    {
        const int maxRetries = 3;

        for (int i = 0; i <= maxRetries; i++)
        {
            try
            {
                var result = await _paymentApi.CreatePaymentAsync(input);
                _logger.LogInformation("支付订单创建成功:{PaymentId}", result.PaymentId);
                return result;
            }
            catch (Exception ex) when (i < maxRetries)
            {
                _logger.LogWarning(ex, "支付请求失败,第 {Retry} 次重试", i + 1);
                await Task.Delay(1000 * (i + 1));
            }
        }

        throw new Exception("支付服务暂时不可用,请稍后重试");
    }
}

通过 Furion 的远程请求增强功能,可以极大简化微服务间的 HTTP 通信代码,提升代码的可读性和可维护性。结合拦截器、重试策略和日志记录,可以构建出健壮可靠的分布式系统。


上一章: 第12章-定时任务 | 返回目录 | 下一章: 第14章-对象映射