Refit 是 .NET 平台上一个非常流行的声明式 REST 客户端库,受到 Square 公司的 Retrofit 启发。它的核心思路是:你只需要定义一个 C# 接口,用特性标注每个方法对应的 HTTP 方法和路由,Refit 就会在运行时帮你生成具体的实现代码。相比手动使用 HttpClient 拼接请求地址、序列化参数、解析响应,Refit 让 API 调用代码变得简洁且类型安全。这篇文章带你从零开始掌握 Refit 的使用方法。

一、Refit 的安装与第一个请求
1. 安装 NuGet 包
在 Visual Studio 的 NuGet 包管理器中搜索 Refit,或者直接在包管理器控制台执行安装命令:
Install-Package Refit // 或者使用 dotnet CLI // dotnet add package Refit</code>
Refit 本身依赖 System.Text.Json 作为默认序列化器,如果你习惯用 Newtonsoft.Json,可以额外安装 Refit.Newtonsoft.Json 包,两者可以并存使用。
2. 定义接口并发起请求
假设我们要调用一个用户管理接口,先定义模型类和接口:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public interface IUserApi
{
// GET /api/users/{id}
[Get("/api/users/{id}")]
Task<User> GetUserAsync(int id);
// GET /api/users?page=1&pageSize=20
[Get("/api/users")]
Task<List<User>> GetUsersAsync([Query] int page, [Query] int pageSize);
}注意接口中的泛型 Task<T>,Refit 会自动把响应的 JSON 反序列化为对应类型。路径中的占位符 {id} 会与方法参数自动匹配,参数名必须一致。
接下来生成客户端实例并发起调用:
var api = RestService.For<IUserApi>("https://api.ipipp.com");
var user = await api.GetUserAsync(1);
Console.WriteLine($"用户名:{user.Name}");三行代码就完成了一次完整的 GET 请求,不需要手动处理 HttpResponseMessage,也不需要自己写反序列化逻辑,这就是 Refit 最大的魅力。
二、常见的请求场景
1. POST 提交与 PUT 更新
提交数据时使用 [Post] 或 [Put] 特性,复杂对象默认会以 JSON 形式放入请求体:
public interface IUserApi
{
[Post("/api/users")]
Task<User> CreateUserAsync([Body] User user);
[Put("/api/users/{id}")]
Task<User> UpdateUserAsync(int id, [Body] User user);
[Delete("/api/users/{id}")]
Task DeleteUserAsync(int id);
}[Body] 特性标记的参数会被序列化为请求体,默认使用 JSON 格式。如果服务端要求表单提交,可以写成 [Body(BodySerializationMethod.UrlEncoded)],Refit 会把对象转成 application/x-www-form-urlencoded 格式。
2. 动态 Header 与鉴权 Token
很多 API 需要 Authorization 头携带 Token。固定头部可以直接标在方法上,动态头部则用参数传入:
public interface IUserApi
{
[Get("/api/users/me")]
[Headers("Accept: application/json")]
Task<User> GetCurrentUserAsync([Header("Authorization")] string token);
}
// 调用时动态传入
var user = await api.GetCurrentUserAsync("Bearer eyJhbGciOi...");也可以把 [Headers] 标在整个接口上,这样所有方法都会携带该头部,适合设置统一的 API 版本号或租户标识。
3. 文件上传与流式下载
上传文件时使用 StreamPart 或 ByteArrayPart,Refit 会自动以 multipart/form-data 形式发送:
public interface IFileApi
{
[Post("/api/files/upload")]
Task UploadFileAsync([AliasAs("file")] StreamPart stream);
}
// 调用
using var fileStream = File.OpenRead(@"C:\data\report.pdf");
var part = new StreamPart(fileStream, "report.pdf", "application/pdf");
await fileApi.UploadFileAsync(part);下载文件时,把返回类型声明为 Stream 或 HttpContent 即可获得原始流,适合处理大文件或非 JSON 响应。
三、在 ASP.NET Core 中与依赖注入集成
1. 注册 Refit 客户端
实际项目中很少直接用 RestService.For,更推荐的做法是通过 HttpClientFactory 集成,这样能享受连接池管理和生命周期控制:
builder.Services
.AddRefitClient<IUserApi>()
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://api.ipipp.com");
c.Timeout = TimeSpan.FromSeconds(30);
});注册之后,在控制器或服务中直接通过构造函数注入 IUserApi 即可使用,整个调用链都是接口化的,方便单元测试时替换成 Mock。
2. 结合 Polly 实现重试与熔断
网络请求难免出现瞬时故障,可以借助 Microsoft.Extensions.Http.Polly 做重试策略:
builder.Services
.AddRefitClient<IUserApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.ipipp.com"))
.AddPolicyHandler(
HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(Math.Pow(2, retry))));这段配置实现了指数退避重试:第一次失败等 2 秒,第二次等 4 秒,第三次等 8 秒,最多重试三次。对于关键的支付、订单类接口,还可以追加熔断策略,避免故障扩散。
四、错误处理与高级用法
1. 捕获 ApiException 获取错误信息
当服务端返回非成功状态码时,Refit 会抛出 ApiException,其中包含状态码和原始响应内容:
try
{
var user = await api.GetUserAsync(999);
}
catch (ApiException ex)
{
var statusCode = ex.StatusCode; // 例如 HttpStatusCode.NotFound
var errorContent = ex.Content; // 服务端返回的错误正文
Console.WriteLine($"请求失败:{statusCode},{errorContent}");
}对于验证类接口,服务端往往在错误响应中返回结构化的错误描述,可以把 ex.Content 手动反序列化成错误模型,给用户展示更友好的提示。
2. 动态 BaseAddress 与多环境切换
有些场景需要在运行时决定请求地址,比如多租户系统每个租户有自己的域名。Refit 提供了每次请求设置地址的能力:
public interface ITenantApi
{
[Get("/api/info")]
Task<TenantInfo> GetInfoAsync([Host] string host, string tenantId);
}使用 [Host] 特性后,每次调用可以传入不同的主机地址,配合配置中心可以轻松实现多环境、多租户的切换,不需要为每个地址重复注册一套客户端。
3. 自定义序列化行为
默认的 System.Text.Json 采用驼峰命名策略,如果服务端要求下划线命名或日期格式,可以通过 RefitSettings 定制:
var settings = new RefitSettings
{
ContentSerializer = new SystemTextJsonContentSerializer(
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
})
};
var api = RestService.For<IUserApi>("https://api.ipipp.com", settings);与依赖注入配合时,使用 AddRefitClient<IUserApi>(settings) 的重载传入即可,全局生效。
五、Refit 的优缺点与选型建议
Refit 的优势非常明显:接口即文档,调用代码可读性高;强类型约束让参数和返回值在编译期就能发现问题;与 HttpClientFactory、Polly、Mock 测试框架无缝集成。特别是接口定义文件可以单独抽成一个类库项目,多个项目共享同一份 API 定义,前后端协作时甚至可以根据 OpenAPI 规范自动生成 Refit 接口,进一步减少手写工作量。
它的局限性也需要了解:反射生成的代理在极高性能场景下比手写 HttpRequestMessage 略慢,不过绝大多数业务系统感知不到;接口定义要求和服务端路由严格对齐,服务端接口变更时需要同步修改接口文件;对于需要精细控制请求头顺序、自定义传输层的场景,Refit 的抽象层反而会碍事,这时直接用 HttpClient 更合适。
总体来说,如果你的项目需要频繁调用第三方 REST API,或者微服务之间通信想统一封装,Refit 都是值得引入的选择。从定义第一个接口到集成依赖注入和重试策略,整个过程通常不超过半小时就能跑通,学习成本远低于它带来的代码整洁度收益。建议新项目直接采用 Refit 管理所有出站 HTTP 请求,老项目则可以在新增模块中逐步试点,验证稳定后再全面推广。