Sage.Http 2.0.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package Sage.Http --version 2.0.0.1
                    
NuGet\Install-Package Sage.Http -Version 2.0.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sage.Http" Version="2.0.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sage.Http" Version="2.0.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Sage.Http" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sage.Http --version 2.0.0.1
                    
#r "nuget: Sage.Http, 2.0.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sage.Http@2.0.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sage.Http&version=2.0.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Sage.Http&version=2.0.0.1
                    
Install as a Cake Tool

Sage.Http

一个基于 .NET 9 和 C# 13 的现代化 HTTP 客户端封装库,提供简化的配置、弹性策略和事件监控。

✨ 特性

  • 简化配置 - 统一的配置模型,支持 TLS、HTTP 版本、超时等
  • 弹性策略 - 内置重试、熔断、限流支持(基于 Polly)
  • 事件监控 - 完整的事件系统,支持按客户端订阅
  • DI 友好 - 完美集成 ASP.NET Core 依赖注入
  • Standalone 模式 - 支持 WinForms/WPF/Debug 应用
  • 线程安全 - 并发场景下的安全使用
  • 内存安全 - 弱引用支持,避免内存泄漏
  • AOT 友好 - 支持 Native AOT 编译

📦 安装

# 通过 NuGet 安装
dotnet add package Sage.Http

# 或通过 Package Manager
Install-Package Sage.Http

依赖项:

  • .NET 9.0+
  • Microsoft.Extensions.Http.Resilience 9.0.0+

🚀 快速开始

ASP.NET Core(DI 模式)

// Program.cs
using Sage.Http.Extensions;
using Sage.Http.Configuration;

var builder = WebApplication.CreateBuilder(args);

// 1. 注册 HTTP 客户端管理器
builder.Services.AddHttpClientManager();

// 2. 配置命名客户端
builder.Services.AddHttpClient("github", options =>
{
    options.BaseAddress = new Uri("https://api.github.com");
    options.Timeout = TimeSpan.FromSeconds(30);
    options.AddDefaultHeader("User-Agent", "MyApp/1.0");
    options.UseRetryPolicy(RetryPolicy.Default);
});

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

在服务中使用:

using Sage.Http.Core;
using Sage.Http.Events;

public class GitHubService
{
    private readonly IHttpClientManager _manager;
    private readonly IEventSubscription _retrySubscription;
    private readonly ILogger<GitHubService> _logger;
    
    public GitHubService(IHttpClientManager manager, ILogger<GitHubService> logger)
    {
        _manager = manager;
        _logger = logger;
        
        // 订阅重试事件
        _retrySubscription = _manager.Subscribe<RetryEvent>("github", OnRetry);
    }
    
    private void OnRetry(RetryEvent e)
    {
        _logger.LogWarning("正在重试请求 {Uri},第 {Attempt} 次,延迟 {Delay}ms",
            e.RequestUri, e.AttemptNumber, e.Delay.TotalMilliseconds);
    }
    
    public async Task<User> GetUserAsync(string username)
    {
        var client = _manager.GetClient("github");
        return await client.GetFromJsonAsync<User>($"/users/{username}");
    }
}

WinForms(Standalone 模式)

using Sage.Http.Core;
using Sage.Http.Configuration;
using Sage.Http.Events;

public partial class MainForm : Form
{
    private StandaloneHttpClientManager? _manager;
    private IEventSubscription? _retrySubscription;
    private IEventSubscription? _circuitSubscription;
    
    public MainForm()
    {
        InitializeComponent();
        InitializeHttpClient();
    }
    
    private void InitializeHttpClient()
    {
        _manager = StandaloneHttpClientManager.Create();
        
        // 配置 API 客户端
        _manager.RegisterClient("api", options =>
        {
            options.BaseAddress = new Uri("https://api.example.com");
            options.Timeout = TimeSpan.FromSeconds(30);
            
            // 配置重试
            options.ConfigureRetry(retry =>
            {
                retry.MaxRetryAttempts = 3;
                retry.BaseDelay = TimeSpan.FromSeconds(2);
            });
            
            // 配置熔断器
            options.ConfigureCircuitBreaker(cb =>
            {
                cb.FailureRatio = 0.5;
                cb.BreakDuration = TimeSpan.FromMinutes(1);
            });
        });
        
        // 订阅事件
        _retrySubscription = _manager.Subscribe<RetryEvent>("api", e =>
        {
            Invoke(() =>
            {
                statusLabel.Text = $"重试中... ({e.AttemptNumber}/3)";
                statusLabel.ForeColor = Color.Orange;
            });
        });
        
        _circuitSubscription = _manager.Subscribe<CircuitBreakerEvent>("api", e =>
        {
            Invoke(() =>
            {
                if (e.NewState == CircuitState.Open)
                {
                    statusLabel.Text = "服务暂时不可用";
                    statusLabel.ForeColor = Color.Red;
                }
                else if (e.NewState == CircuitState.Closed)
                {
                    statusLabel.Text = "服务已恢复";
                    statusLabel.ForeColor = Color.Green;
                }
            });
        });
    }
    
    private async void btnLoad_Click(object sender, EventArgs e)
    {
        try
        {
            var client = _manager!.GetClient("api");
            var data = await client.GetFromJsonAsync<List<Item>>("/items");
            dataGrid.DataSource = data;
            
            statusLabel.Text = "加载成功";
            statusLabel.ForeColor = Color.Green;
        }
        catch (Exception ex)
        {
            MessageBox.Show($"加载失败: {ex.Message}", "错误", 
                MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
    
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        // 释放资源
        _retrySubscription?.Dispose();
        _circuitSubscription?.Dispose();
        _manager?.Dispose();
        
        base.OnFormClosing(e);
    }
}

📖 详细文档

配置选项

基础配置
builder.Services.AddHttpClient("myapi", options =>
{
    // 基础地址
    options.BaseAddress = new Uri("https://api.example.com");
    
    // 超时设置
    options.Timeout = TimeSpan.FromSeconds(30);
    
    // HTTP 版本
    options.HttpVersion = HttpVersion.Version20;  // HTTP/2.0
    
    // TLS 版本
    options.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13;
    
    // 自动重定向
    options.AllowAutoRedirect = true;
    options.MaxAutomaticRedirections = 50;
    
    // 默认请求头
    options.AddDefaultHeader("User-Agent", "MyApp/1.0");
    options.AddDefaultHeader("Accept", "application/json");
});
预设重试策略
// 方式1:使用预设策略
options.UseRetryPolicy(RetryPolicy.Default);
// - Default: 重试 408, 429, 500, 502, 503, 504
// - Aggressive: 重试所有 5xx 错误和 408, 429
// - Conservative: 仅重试 503, 504
// - None: 不重试

// 方式2:自定义配置
options.ConfigureRetry(retry =>
{
    retry.MaxRetryAttempts = 3;
    retry.BaseDelay = TimeSpan.FromSeconds(2);
    retry.BackoffType = BackoffType.Exponential;
    retry.UseJitter = true;
    
    // 指定重试的状态码
    retry.RetryOnStatusCodes = new HashSet<int> { 408, 429, 500, 502, 503, 504 };
    
    // 或指定状态码范围
    retry.RetryOnStatusCodeRanges = new List<StatusCodeRange>
    {
        new(500, 599)  // 所有 5xx 错误
    };
    
    // 排除某些状态码
    retry.ExcludeStatusCodes = new HashSet<int> { 501 };
    
    // 指定重试的异常类型
    retry.RetryOnExceptionTypes = new HashSet<string>
    {
        "System.Net.Http.HttpRequestException",
        "System.Net.Sockets.SocketException"
    };
});
熔断器配置
options.ConfigureCircuitBreaker(cb =>
{
    cb.FailureRatio = 0.5;              // 失败率阈值 50%
    cb.SamplingDuration = TimeSpan.FromSeconds(30);  // 采样时间窗口
    cb.MinimumThroughput = 10;          // 最小请求数
    cb.BreakDuration = TimeSpan.FromMinutes(1);      // 熔断持续时间
});
限流器配置
options.ConfigureRateLimiter(limiter =>
{
    limiter.PermitLimit = 100;          // 每个时间窗口最多 100 个请求
    limiter.Window = TimeSpan.FromMinutes(1);  // 时间窗口
    limiter.QueueLimit = 10;            // 等待队列长度
});

事件系统

订阅事件
// 订阅特定客户端的重试事件
var subscription = _manager.Subscribe<RetryEvent>("myapi", e =>
{
    Debug.WriteLine($"重试 {e.AttemptNumber}: {e.RequestUri}");
});

// 订阅所有客户端的熔断事件
var allSubscription = _manager.Subscribe<CircuitBreakerEvent>(e =>
{
    Debug.WriteLine($"[{e.ClientName}] 熔断器状态: {e.OldState} → {e.NewState}");
});

// 使用 using 自动取消订阅
using var tempSubscription = _manager.Subscribe<RetryEvent>("myapi", HandleRetry);

// 手动取消订阅
subscription.Dispose();
弱引用订阅(避免内存泄漏)
// 使用弱引用,当订阅者被 GC 时自动失效
_manager.Subscribe<RetryEvent>("myapi", OnRetry, useWeakReference: true);
// 即使忘记调用 Dispose(),也不会造成内存泄漏
事件类型
// 重试事件
public class RetryEvent : HttpClientEvent
{
    public int AttemptNumber { get; init; }      // 重试次数
    public Exception? Exception { get; init; }    // 异常信息
    public TimeSpan Delay { get; init; }          // 延迟时间
    public Uri? RequestUri { get; init; }         // 请求 URI
    public int? StatusCode { get; init; }         // 状态码
}

// 熔断器事件
public class CircuitBreakerEvent : HttpClientEvent
{
    public CircuitState OldState { get; init; }   // 旧状态
    public CircuitState NewState { get; init; }   // 新状态
    public Exception? Exception { get; init; }    // 异常信息
    public string? Reason { get; init; }          // 原因
}

// 限流事件
public class RateLimiterEvent : HttpClientEvent
{
    public bool IsRejected { get; init; }         // 请求是否被拒绝
    public Uri? RequestUri { get; init; }         // 请求 URI(如果可用)
}

消息处理器

内置处理器
using Sage.Http.Handlers;

// 日志处理器(可通过 DI 构造)
options.AddHandler<LoggingHandler>();

// 关联 ID(用于分布式追踪)— 建议使用工厂重载
options.AddHandler(_ => new CorrelationIdHandler());

// Bearer Token 认证 — 使用异步 Token 提供器
options.AddHandler(_ => new BearerTokenHandler(GetTokenAsync));

// 自定义请求头 — 使用工厂提供字典
options.AddHandler(_ => new CustomHeaderHandler(new Dictionary<string, string>
{
    ["X-API-Key"] = "your-api-key",
    ["X-Custom-Header"] = "custom-value"
}));

// User-Agent — 支持字符串或产品名+版本
options.AddHandler(_ => new UserAgentHandler("MyApp", "1.0.0"));

#### 为何推荐工厂重载

- 防止 Standalone 模式下处理器实例被多个客户端复用导致并发问题。
- 便于按客户端/环境注入不同参数(如 Header、策略、密钥)。
- 与 DI 生命周期对齐:每次添加时可获得最新的依赖实例。
- 支持需要运行时参数的处理器(例如从配置或服务获取值)。
- 与过时的实例重载区分,避免后续升级引入行为差异。

#### 何时使用泛型 vs 工厂

- 使用 `AddHandler<THandler>()`:处理器构造函数完全由 DI 解析,且无自定义运行时参数。
- 使用 `AddHandler(factory)`:需要传入运行时参数或显式控制实例创建,或在 Standalone 模式中无 DI 时。

示例:在 Standalone 模式使用日志处理器

```csharp
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(b =>
{
    b.AddConsole();
    b.SetMinimumLevel(LogLevel.Information);
});
var logger = loggerFactory.CreateLogger<LoggingHandler>();

options.AddHandler(_ => new LoggingHandler(logger));

示例:在 DI 模式使用日志处理器(推荐)

// 由 DI 注入 ILogger<LoggingHandler>
options.AddHandler<LoggingHandler>();

#### 自定义处理器

```csharp
public class ApiKeyHandler : DelegatingHandler
{
    private readonly string _apiKey;
    
    public ApiKeyHandler(string apiKey)
    {
        _apiKey = apiKey;
    }
    
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        request.Headers.Add("X-API-Key", _apiKey);
        return base.SendAsync(request, cancellationToken);
    }
}

// 使用(建议使用工厂重载)
options.AddHandler(_ => new ApiKeyHandler("your-api-key"));

拦截器

// 仅拦截请求(同步)
options.AddRequestInterceptor(req =>
{
    req.Headers.Add("X-Trace-Id", Activity.Current?.Id ?? Guid.NewGuid().ToString());
});

// 异步拦截响应并记录耗时
options.AddResponseInterceptorAsync(async (res, ct) =>
{
    var elapsed = res.Headers.TryGetValues("X-Elapsed", out var values)
        ? values.FirstOrDefault()
        : null;
    Debug.WriteLine($"Response {res.StatusCode}, Elapsed={elapsed}");
});

// 统一处理异常
options.AddErrorInterceptor(ex =>
{
    Debug.WriteLine($"HTTP 错误: {ex.Message}");
});
选择处理器还是拦截器
  • 处理器适合严格控制管道顺序、与重试/熔断策略协同。
  • 拦截器更轻量,便于观测与简单变更,不依赖 DI。
  • 二者可并存,但应避免重复记录日志;尽量选择一种方案。
  • 无 DI 的 Standalone 模式建议用拦截器进行日志;DI 模式优先 LoggingHandler
  • 如需统一在一个位置处理请求、响应、异常,拦截器更直观。

示例:使用拦截器实现日志(替代 LoggingHandler)

options.AddInterceptor(
    onRequest: req => Debug.WriteLine($"-> {req.Method} {req.RequestUri}"),
    onResponse: res => Debug.WriteLine($"<- {(int)res.StatusCode} {res.ReasonPhrase}"),
    onError: ex => Debug.WriteLine($"!! {ex.Message}")
);

提示:若同时启用 LoggingHandler 与上述拦截器日志,可能出现重复日志,请根据需要选择其一。

JSON 配置

appsettings.json
{
  "HttpClients": {
    "github": {
      "BaseAddress": "https://api.github.com",
      "Timeout": "00:00:30",
      "HttpVersion": "2.0",
      "DefaultHeaders": {
        "User-Agent": "MyApp/1.0",
        "Accept": "application/vnd.github.v3+json"
      },
      "Resilience": {
        "EnableRetry": true,
        "Retry": {
          "MaxRetryAttempts": 3,
          "BaseDelay": "00:00:02",
          "BackoffType": "Exponential",
          "UseJitter": true,
          "RetryOnStatusCodes": [408, 429, 500, 502, 503, 504]
        },
        "EnableCircuitBreaker": true,
        "CircuitBreaker": {
          "FailureRatio": 0.5,
          "SamplingDuration": "00:00:30",
          "MinimumThroughput": 10,
          "BreakDuration": "00:01:00"
        },
        "EnableEvents": true
      }
    }
  }
}
加载配置
// 从配置文件加载
var config = builder.Configuration
    .GetSection("HttpClients:github")
    .Get<HttpClientOptions>();

builder.Services.AddHttpClient("github", config);

🎯 使用场景

场景1:调用第三方 API

builder.Services.AddHttpClient("weather", options =>
{
    options.BaseAddress = new Uri("https://api.weatherapi.com");
    options.AddDefaultHeader("X-API-Key", configuration["WeatherApiKey"]);
    options.UseRetryPolicy(RetryPolicy.Default);
    options.AddHandler<LoggingHandler>();
});

public class WeatherService
{
    private readonly IHttpClientManager _manager;
    
    public async Task<Weather> GetWeatherAsync(string city)
    {
        var client = _manager.GetClient("weather");
        return await client.GetFromJsonAsync<Weather>($"/current.json?q={city}");
    }
}

场景2:微服务间调用

builder.Services.AddHttpClient("order-service", options =>
{
    options.BaseAddress = new Uri("http://order-service:8080");
    
    // 配置重试(考虑网络波动)
    options.ConfigureRetry(retry =>
    {
        retry.MaxRetryAttempts = 5;
        retry.RetryOnStatusCodes = new HashSet<int> { 408, 503, 504 };
    });
    
    // 配置熔断(防止雪崩)
    options.ConfigureCircuitBreaker(cb =>
    {
        cb.FailureRatio = 0.3;
        cb.BreakDuration = TimeSpan.FromSeconds(30);
    });
    
    // 添加服务间认证(工厂重载)
    options.AddHandler(_ => new BearerTokenHandler(() => GetServiceTokenAsync()));
    
    // 添加追踪 ID(工厂重载)
    options.AddHandler(_ => new CorrelationIdHandler());
});

场景3:批量下载(限流)

builder.Services.AddHttpClient("downloader", options =>
{
    options.Timeout = TimeSpan.FromMinutes(5);
    
    // 限制并发下载数
    options.ConfigureRateLimiter(limiter =>
    {
        limiter.PermitLimit = 5;  // 同时最多 5 个请求
        limiter.QueueLimit = 20;  // 队列最多 20 个
    });
});

public class DownloadService
{
    public async Task DownloadFilesAsync(List<string> urls)
    {
        var client = _manager.GetClient("downloader");
        
        var tasks = urls.Select(url => DownloadFileAsync(client, url));
        await Task.WhenAll(tasks);
    }
    
    private async Task DownloadFileAsync(HttpClient client, string url)
    {
        var response = await client.GetAsync(url);
        var content = await response.Content.ReadAsByteArrayAsync();
        // 保存文件...
    }
}

🔍 高级用法

动态创建客户端

// 运行时动态创建客户端(不需要预注册)
var client = _manager.CreateClient("temp", options =>
{
    options.BaseAddress = new Uri("https://dynamic-api.com");
    options.Timeout = TimeSpan.FromSeconds(10);
});

var result = await client.GetStringAsync("/data");

多环境配置

// appsettings.Development.json
{
  "HttpClients": {
    "api": {
      "BaseAddress": "https://dev-api.example.com",
      "Resilience": {
        "EnableRetry": false  // 开发环境禁用重试,便于调试
      }
    }
  }
}

// appsettings.Production.json
{
  "HttpClients": {
    "api": {
      "BaseAddress": "https://api.example.com",
      "Resilience": {
        "EnableRetry": true,
        "EnableCircuitBreaker": true
      }
    }
  }
}

条件订阅

// 只在开发环境订阅事件
if (builder.Environment.IsDevelopment())
{
    _manager.Subscribe<RetryEvent>(e =>
    {
        Debug.WriteLine($"[DEV] Retry: {e.ClientName} - {e.RequestUri}");
    });
}

📊 性能优化建议

  1. 使用 GetClient 而非 CreateClient(如果客户端配置固定)
  2. 启用 HTTP/2(默认已启用)
  3. 合理配置超时和重试次数
  4. 使用弱引用订阅(长期运行的应用)
  5. 避免在循环中创建临时客户端

🐛 故障排查

常见问题

Q: 订阅的事件没有触发?

// 确保在配置中启用了事件
options.Resilience = new ResilienceOptions
{
    EnableEvents = true,  // 必须设置为 true
    EnableRetry = true
};

Q: 重试没有生效?

// 检查状态码是否在重试列表中
options.ConfigureRetry(retry =>
{
    retry.RetryOnStatusCodes = new HashSet<int> { 500, 502, 503 };
    // 确保包含你遇到的状态码
});

Q: 内存泄漏?

// 确保在窗体关闭时释放资源
protected override void OnFormClosing(FormClosingEventArgs e)
{
    _subscription?.Dispose();
    _manager?.Dispose();
    base.OnFormClosing(e);
}

// 或使用弱引用订阅
_manager.Subscribe<RetryEvent>("api", OnRetry, useWeakReference: true);

📝 完整示例项目

参考 /Examples 目录下的完整示例:

🔗 相关链接


许可证

MIT License - 详见 LICENSE 文件。

贡献

欢迎提交 Issue 和 Pull Request!

更新日志

详见 CHANGELOG.md

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Sage.Http:

Package Downloads
Sage.CloudStorage.Qiniu

Sage.CloudStorage.Qiniu 是一个基于 .NET 平台的现代化七牛云存储 SDK,采用完全异步设计,提供了对七牛云对象存储、CDN 等服务的简单易用的 API 封装。该库基于 Sage.Http 构建,具有高性能、可扩展的七牛云服务访问能力,特别适合企业级应用和大文件处理场景。 ## 核心优势 - **现代化API设计**:完全异步,符合.NET最佳实践 - **模块化架构**:各组件职责明确,易于扩展和维护 - **丰富的事件机制**:提供上传进度通知和完成事件 - **智能上传策略**:自动选择最佳上传方式和分片大小 - **完善的错误处理**:提供详细的错误信息和恢复机制 ## 功能特性 - **完整的对象存储支持**:上传、下载、管理、删除等操作 - **高级上传功能**: - 智能分片上传(自动优化分片大小) - 断点续传支持 - 并发控制 - 实时进度监控 - **CDN管理**:刷新、预取、带宽查询、日志下载 - **数据处理**:图片处理、音视频转码等 - **批量操作**:批量上传、删除等

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0.11 104 1/14/2026
2.0.0.10 150 12/12/2025
2.0.0.9 440 11/18/2025
2.0.0.8 225 10/27/2025
2.0.0.7 142 10/25/2025
2.0.0.6 149 10/25/2025
2.0.0.5 145 10/25/2025
2.0.0.4 138 10/25/2025
2.0.0.3 150 10/25/2025
2.0.0.2 178 10/24/2025
2.0.0.1 210 10/23/2025
2.0.0 211 10/23/2025
1.0.1.25 209 10/19/2025
1.0.1.24 211 10/19/2025
1.0.1.23 292 10/19/2025 1.0.1.23 is deprecated because it has critical bugs.
1.0.1.22 294 10/19/2025 1.0.1.22 is deprecated because it has critical bugs.
1.0.1.21 298 10/19/2025 1.0.1.21 is deprecated because it has critical bugs.
1.0.1.20 300 10/19/2025 1.0.1.20 is deprecated because it has critical bugs.
1.0.1.19 209 10/15/2025
1.0.1.18 208 10/13/2025
Loading failed

增加Net10 rc2的支持