Git官网:https://github.com/reactiveui/refit
Refit 是一个针对 .NET 应用程序的 REST API 客户端库,它通过接口定义 API 调用,从而简化与 RESTful 服务的交互。其核心理念是利用声明性编程的方式来创建 HttpClient 客户端,使得 API 调用更加简洁和易于维护。
目前Refit所支持的平台如下:
- .NET 6 / 8
- Blazor
- Desktop .NET 4.6.1
- UWP
- Xamarin.Android
- Xamarin.Mac
- Xamarin.iOS
- Uno Platform
简单使用
依赖库的安装
可以从Nuget中直接下载依赖库,在Nuget中搜索Refit可以看到有两个相关的依赖库可以下载,分别是Refit
和Refit.HttpClientFactory
Refit
是一个用于 .NET 的库,它通过接口定义 REST API 调用,允许开发者以声明性方式创建 HTTP 客户端。Refit 负责自动序列化和反序列化请求和响应,并简化与 RESTful 服务的交互。其核心功能如下:
- 提供强类型的 API 调用
- 自动处理 JSON 和其他格式的序列化
- 支持异步调用和中间件
Refit.HttpClientFactory
是一个扩展库,它与 ASP.NET Core 的 HttpClientFactory
结合使用,提供了使用 Refit 创建 API 客户端的便利方式。其核心功能如下:
- 使用 ASP.NET Core 的
HttpClientFactory
管管理HttpClient
的生命周期,避免了潜在的性能问题(如 DNS 缓存问题)和过度创建HttpClient
实例 - 主要用于 ASP.NET Core 应用,依赖于 ASP.NET Core 的
HttpClientFactory
,可以更好地与 ASP.NET Core 的依赖注入框架集成
可以根据自己的项目选择使用,如果是ASP.NET Core 应用中使用 Refit,那么直接安装Refit.HttpClientFactory
就可以了,如果是一些其他项目,例如WPF啥的,那么安装Refit
然后自己管理HttpClient
实例就可以了。
定义接口
要使用Refit,第一步先要根据要请求的RestAPI进行接口的定义。
-
示例
public interface IGitHubApi {[Get("/users/{user}")]Task<User> GetUser(string user); }public class User {public string Id { get; set; }public string Name { get; set; } }
进行Http请求
Refit提供了RestService
服务,可以帮助我们生成指定接口的实现,其内部调用了HttpClient
进行Http请求
-
示例
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com"); var octocat = await gitHubApi.GetUser("octocat");
Asp.Net Core中的注册
如果是Asp.Net Core项目,Refit支持通过HttpClientFactory
进行注册(安装Refit.HttpClientFactory
),后面直接使用我们定义的接口进行依赖注入直接使用就可以了
-
示例-
Program.cs
...... services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com")); ......
使用详解
一、路由特性
在进行接口定义时,每个方法都必须有一个HTTP特性,该特性提供请求方式和相对URL。
Refit提供了六个内置特性:Get
、Post
、Put
、Delete
、Patch
和Head
,资源的相对URL在特性中指定。
-
示例
public interface IGitHubApi {[Get("/users/list")]Task<List<User>> GetUserList();//Get请求可以直接在请求地址中携带一些参数[Get("/users/list?sort=desc")]Task<List<User>> GetUserListWithSort(); }
参数占位符
Refit的方法特性支持使用参数占位符,通过{}
占位符可以将URL模板中的指定参数与参数列表中对应名称的参数进行关联
-
示例
public interface IGitHubApi {[Get("/users/{user}")]Task<User> GetUser(string user); }
如果参数列表中的参数名称与ULR模板中{}
所指定的参数名称不一致,可以在参数列表中使用AliasAs(paramName)
进行指定
-
示例
public interface IGitHubApi {[Get("/group/{id}/users")]Task<List<User>> GroupList([AliasAs("id")] int groupId); }
在{}
中还可以直接访问参数列表中的对象成员
-
示例
public interface IGitHubApi {[Get("/group/{request.groupId}/users/{request.userId}")]Task<List<User>> GroupList(UserGroupRequest request); }class UserGroupRequest{int groupId { get;set; }int userId { get;set; } }
需要注意的是,方法的参数列表中未匹配的参数一律会作为请求参数
-
示例
public interface IGitHubApi {[Get("/group/{id}/users")]Task<List<User>> GroupListA([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);[Get("/group/{id}/users")]Task<List<User>> GroupListB([AliasAs("id")] int groupId, string sortOrder); }GroupListA(4, "desc"); //请求路径为 /group/4/users?sort=desc GroupListB(4, "desc"); //请求路径为 /group/4/users?sortOrder=desc
路由转换
在声明方法时,如果希望方法的参数作为URL的一部分,那么可以使用{** paramName}
进行匹配,使用**
可以将匹配到的参数中所携带的斜杠 /
保持原样,不进行编码
-
示例
public interface IGitHubApi {[Get("/search/{**page}")]Task<List<Page>> Search(string page); }Search("admin/products"); // 请求的URL为 /search/admin/products
需要注意的是{** paramName}
所匹配的参数,其类型必须为string
二、查询字符串
这里指出的查询字符串是指在请求的URL中通过?
分隔开的查询参数
1、动态查询字符串
对象作为查询字符串
在Get请求中,如果使用一个引用类型作为方法参数,那么类型对象中所有公共且不为null
的属性将为自动成为查询字符串。
-
自定义类型中,可以使用
[AliasAs]
特性设置属性序列化和反序列化时键的名称。如果不设置,默认会使用属性名 -
自定义类中可以使用
[Query]
特性设置指定属性的前缀和前缀与参数名之间的分隔符。不过通常更多是在接口的参数列表中使用,对类型的所有属性统一设置 -
枚举类型则可以使用
[EnumMember]
特性设置序列化和序列化时的对应值 -
示例
public class MyQueryParams {[AliasAs("order")]public string SortOrder { get; set; }public int Limit { get; set; }public KindOptions Kind { get; set; } }public enum KindOptions {Foo,[EnumMember(Value = "bar")] //定义序列化和反序列化时对应的值Bar }public interface IGitHubApi {[Get("/group/{id}/users")]Task<List<User>> GroupList([AliasAs("id")] int groupId, MyQueryParams param);[Get("/group/{id}/users")]Task<List<User>> GroupListWithAttribute([AliasAs("id")] int groupId, [Query(".","search")] MyQueryParams param); }var myParams = new MyQueryParams(); myParams.SortOrder = "desc"; myParams.Limit = 10; myParams.Kind = KindOptions.Bar;GroupList(4, myParams) //请求的相对URL为 /group/4/users?order=desc&objA.Limit=10&Kind=bar GroupListWithAttribute(4, myParams) //请求的相对URL为/group/4/users?search.order=desc&search.Limit=10&search.Kind=bar
字典作为查询字符串
在Get请求中,可以使用字典Dictionary
作为参数,其内容会自动序列化为查询字符串,只是无法通过[AliasAs]
去指定键名
在非Get请求中的查询字符串
在非Get请求中,如果希望将指定的参数对象作为查询字符串,可以使用[Query]
特性来声明对参数进行扁平化处理。实际上不论是Get还是非Get请求,如果要将方法参数对象作为查询字符串,那么建议在参数列表中都使用[Query]
特性进行声明。
-
示例
[Post("/statuses/update.json")] Task<Tweet> PostTweet([Query]TweetParams myParams);
2、集合参数作为查询字符串
如果方法参数是一个集合对象,并且希望其内容作为查询字符串来使用,可以在参数列表中使用[Query]
特性配合CollectionFormat
枚举对集合进行格式化设置,其有效的格式如下:
-
CollectionFormat.Multi
:根据参数名称,将集合中的每一个元素作为单独的查询字符串参数 -
CollectionFormat.Csv
:将集合中的所有元素使用逗号连接,作为一个查询字符串 -
CollectionFormat.Ssv
:将集合中的所有元素使用空格连接,作为一个查询字符串 -
CollectionFormat.Tsv
:将集合中的所有元素使用制表符(\t
)连接,作为一个查询字符串 -
CollectionFormat.Pipes
:将集合中的所有元素使用竖线(|
)连接,作为一个查询字符串 -
示例
[Get("/users/list")] Task Search([Query(CollectionFormat.Multi)]int[] ages);Search(new [] {10, 20, 30}) //"/users/list?ages=10&ages=20&ages=30"[Get("/users/list")] Task Search([Query(CollectionFormat.Csv)]int[] ages);Search(new [] {10, 20, 30}) //"/users/list?ages=10%2C20%2C30"
如果向进行全局设置,可以在创建接口对象或进行容器注册时进行设置
-
示例
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",new RefitSettings {CollectionFormat = CollectionFormat.Multi});
3、查询字符串参数解码
解码查询字符串其实就是不对查询字符串进行编码,保留原有的字符串内容,refit支持在进行接口方法定义时,通过使用[QueryUriFormat(UriFormat.Unescaped)]
特性声明不对此方法中的查询字符串内容进行编码
-
示例
[Get("/query")] [QueryUriFormat(UriFormat.Unescaped)] Task Query(string q);Query("Select+Id,Name+From+Account") // 请求的相对URL为 /query?q=Select+Id,Name+From+Account
4、URL参数的自定义格式化
Refit提供了IUrlParameterFormatter
接口来帮助我们对URL参数进行自定义的格式化,这在我们需要对日期、数值等数据进行格式化时十分有效。
实现接口
只需要实现IUrlParameterFormatter
接口,并实现Format
()方法,在方法中进行自定义格式化即可
-
示例
//对日期进行格式化 public class CustomDateUrlParameterFormatter : IUrlParameterFormatter {public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type){if (value is DateTime dt){return dt.ToString("yyyyMMdd");}return value?.ToString();} } //对字典进行格式化 public class CustomDictionaryKeyFormatter : IUrlParameterFormatter {public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type){// Handle dictionary keysif (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)){// Custom formatting logic for dictionary keysreturn value?.ToString().ToUpperInvariant();}return value?.ToString();} }
使用自定义格式化
在创建接口对象或进行容器注册时进行设置即可
-
示例
var gitHubApi = RestService.For<IGitHubApi>("https://localhost:7234",new RefitSettings {UrlParameterFormatter = new CustomDateUrlParameterFormatter() });
三、请求体(Body)
在进行Post请求时,是需要携带请求体的,此时可以在声明方法时,在参数列表中对希望作为请求体的参数使用[Body]
特性。
-
示例
[Post("/users/new")] Task CreateUser([Body] User user);
Refit对请求体的处理主要有如下四种情况
- 当参数为
Stream
类型,那么数据会通过StreamContent
进行流式传输 - 当参数为
string
类型,默认情况下会直接将参数作为请求体(也就是Content-Type=text/plain
);如果使用了特性[Body(BodySerializationMethod.Json)]
,字符串将被作为StringContent
发送,并以 JSON 格式提供(Content-Type = application/json
),这通常适用于需要符合 JSON 格式的 API 接口 - 使用了
[Body(BodySerializationMethod.UrlEncoded)]
特性的参数,其内容将被 URL 编码。这种情况通常用于表单提交(Content-Type=application/x-www-form-urlencoded
) - 当参数是其他类型(即不属于上面的三种情况)时,则会使用
RefitSettings
中指定的内容序列化器进行序列化,默认情况下是 JSON 格式(也就是[Body(BodySerializationMethod.Json)]
)
1、流缓存及Content-Length
默认情况下,Refit对请求体进行流处理时,是不进行缓存的(针对所有请求体,不仅仅是文件),这意味着可以从磁盘流式传输文件,而不会产生将整个文件加载到内存中的开销。这样做的缺点是请求上没有设置Content-Length
报头。如果需要Refit在进行API请求中发送一个Content-Length
头,可以使用[Body(buffered:true)]
特性来开启缓存
-
原因在于,要得到
Content-Length
就必须先把要进行传输的文件加载到内存中才能正确的计算出来 -
示例
这里只是以文件上传为例子,实际上refit对所有的请求体默认都是不进行缓存的
public interface IMyApi { [Post("/upload")] Task UploadFile([Body(buffered: true)] Stream fileStream); } // 使用时 var api = RestService.For<IMyApi>("https://example.com"); using (var fileStream = File.OpenRead("largefile.txt")) { await api.UploadFile(fileStream); // Content-Length 会被设置 }
2、Json的序列化管理
关于Json的请求和响应,Refit使用IHttpContentSerializer
接口对象其进行序列化和反序列化。
Refit提供了两种Json序列化的实现方式:
SystemTextJsonContentSerializer
:这是默认的 JSON 序列化器,基于 .NET 内置的System.Text.Json
库。这个实现专注于高性能和低内存占用,适合对性能有严格要求的应用(默认)NewtonsoftJsonContentSerializer
:基于流行的Newtonsoft.Json
库,这个实现更灵活,可以处理更多复杂的序列化需求,支持配置选项和定制化。
使用Newtonsoft.Json
进行序列化
由于默认使用的是SystemTextJsonContentSerializer
,所以这里学习一下如何使用NewtonsoftJsonContentSerializer
就好了。
如果使用NewtonsoftJsonContentSerializer
进行序列化,需要在项目中安装Refit.Newtonsoft.Json
依赖库
然后在创建Refit接口对象或进行容器注册时进行设置即可
-
示例
var gitHubApi = RestService.For<IGitHubApi>("https://localhost:7234", new RefitSettings() {ContentSerializer = new NewtonsoftJsonContentSerializer() });
使用Newtonsoft.Json
,可以通过JsonConvert.DefaultSettings
对其默认的序列化行为进行全局配置。
-
示例
JsonConvert.DefaultSettings = () => new JsonSerializerSettings() {ContractResolver = new CamelCasePropertyNamesContractResolver(),Converters = { new StringEnumConverter() } };
也可以在创建Refit接口对象或进行容器注册针对指定的API接口进行配置
-
示例
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",new RefitSettings {ContentSerializer = new NewtonsoftJsonContentSerializer(new JsonSerializerSettings { ContractResolver = new SnakeCasePropertyNamesContractResolver()})});var otherApi = RestService.For<IOtherApi>("https://api.example.com",new RefitSettings {ContentSerializer = new NewtonsoftJsonContentSerializer(new JsonSerializerSettings {ContractResolver = new CamelCasePropertyNamesContractResolver()})});
属性的序列化别名设置
如果使用的是Newtonsoft.Json
,那么在类型的属性上,可以通过[JsonProperty]
对属性进行自定义序列化设置,例如[JsonProperty(PropertyName="b")
,其效果与[AliasAs("b")]
一样
-
示例
public class Foo {// Works like [AliasAs("b")] would in form posts (see below)[JsonProperty(PropertyName="b")]public string Bar { get; set; } }
如果使用的是默认的System.Text.Json
,则可以使用[JsonPropertyName]
进行属性序列化别名的设置
-
示例
public class User { [JsonPropertyName("full_name")] public string FullName { get; set; } public int Age { get; set; } }
3、XML序列化管理
Refit默认使用Json进行序列化,因此如果不进行配置,所有的请求和响应都会被处理为Json格式。
Refit的XML序列化使用System.Xml.Serialization.XmlSerializer
,要配置XML序列化,需要将ContentSerializer
设置为XmlContentSerializer
对象
-
示例
var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML",new RefitSettings {ContentSerializer = new XmlContentSerializer()});
System.Xml.Serialization.XmlSerializer
提供了许多序列化配置项,可以在创建或注册Refit接口时候进行设置。
-
示例
var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML",new RefitSettings {ContentSerializer = new XmlContentSerializer(new XmlContentSerializerSettings{XmlReaderWriterSettings = new XmlReaderWriterSettings(){ReaderSettings = new XmlReaderSettings{IgnoreWhitespace = true}}})});
此外,System.Xml.Serialization.XmlSerializer
还提供了一些可以对类型属性的序列化进行自定义设置的特性
-
示例
public class Foo {[XmlElement(Namespace = "https://www.w3.org/XML")]public string Bar { get; set; } }
4、表单提交
如果需要进行表单提交,那么需要使用[Body(BodySerializationMethod.UrlEncoded)]
特性对方法参数进行声明,其会使用application/x-www-form-urlencoded
格式进行序列化。
字典参数
可以使用IDictionary
类型作为参数进行表单提交。
-
示例
public interface IMeasurementProtocolApi {[Post("/collect")]Task Collect([Body(BodySerializationMethod.UrlEncoded)] Dictionary<string, object> data); }var data = new Dictionary<string, object> {{"v", 1},{"tid", "UA-1234-5"},{"cid", new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")},{"t", "event"}, };// Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event await api.Collect(data);
object类型参数
可以使用object
类型作为参数进行表单提交,其公开可读的属性会被序列化为表单字段。且类型中的属性同样支持使用[AliasAs("whatever")]
、[JsonProperty(PropertyName = "whatever")]
或[JsonPropertyName("whatever")]
进行属性的别名设置,但是[AliasAs("whatever")]
的优先级更高
-
示例
public interface IMeasurementProtocolApi {[Post("/collect")]Task Collect([Body(BodySerializationMethod.UrlEncoded)] Measurement measurement); }public class Measurement {public int v { get { return 1; } }[AliasAs("tid")]public string WebPropertyId { get; set; }[JsonProperty(PropertyName = "one")][AliasAs("cid")]public Guid ClientId { get; set; }[JsonProperty(PropertyName = "t")]public string Type { get; set; }public object IgnoreMe { private get; set; } }var measurement = new Measurement {WebPropertyId = "UA-1234-5",ClientId = new Guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"),Type = "event" };// Serialized as: v=1&tid=UA-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event await api.Collect(measurement);
需要注意的是,[AliasAs]
特性对查询字符串参数和表单数据POST 请求的序列化有效,但对响应数据的反序列化无效,因此如果响应内容中的字段名称与接纳类型的属性名不同时,还是需要使用[JsonProperty]
或[JsonPropertyName]
特性进行别名指定。
四、请求头设置
1、静态请求头
Refit提供了[Headers]
特性,可以在声明接口方法时,设置一个或多个请求头信息。
-
示例
public interface IGitHubApi {[Headers("User-Agent: Awesome Octocat App")][Get("/users/{user}")]Task<User> GetUser(string user); }
如果希望对接口中的所有方法都设置同样的头信息,那么直接在接口上使用[Headers]
特性就可以了
-
示例
[Headers("User-Agent: Awesome Octocat App")] public interface IGitHubApi {[Get("/users/{user}")]Task<User> GetUser(string user);[Post("/users/new")]Task CreateUser([Body] User user); }
2、动态请求头
如果请求头中的信息需要根据运行时动态变化,那么可以将[Headers]
特性直接在参数列表中对参数使用即可。
-
示例
[Get("/users/{user}")] Task<User> GetUser(string user, [Header("Authorization")] string authorization);// Will add the header "Authorization: token OAUTH-TOKEN" to the request var user = await GetUser("octocat", "token OAUTH-TOKEN");
批量请求头
如果需要设置多个动态请求头,可以使用IDictionary<string, string>
类型作为参数,并对其使用[HeaderCollection]
特性。
-
示例
[Get("/users/{user}")] Task<User> GetUser(string user, [HeaderCollection] IDictionary<string, string> headers);var headers = new Dictionary<string, string> {{"Authorization","Bearer tokenGoesHere"}, {"X-Tenant-Id","123"}}; var user = await GetUser("octocat", headers);
3、授权头信息
添加授权头信息是很常见的操作,因此Refit提供了[Authorize]
特性专门用于声明授权令牌参数,并且可以设置授权策略,例如JWT可以使用[Authorize("Bearer")]
。
-
示例
[Get("/users/{user}")] Task<User> GetUser(string user, [Authorize("Bearer")] string token);// Will add the header "Authorization: Bearer OAUTH-TOKEN}" to the request var user = await GetUser("octocat", "OAUTH-TOKEN");
授权令牌简化方案
通常,在API请求中携带授权令牌是很常见的需求,如果对每一个方法都要传入授权令牌,并对参数使用[Authorize("Bearer")]
就太麻烦了,针对此,Refit提供了更为简介的方案。
-
第一步,在创建Refit接口或注册时,通过
RefitSettings
,配置AuthorizationHeaderValueGetter
委托,每次需要授权头信时候都会从此委托中获取授权令牌信息 -
第二步,在需要携带授权令牌的接口或方法上使用
[Headers("Authorization: Bearer")]
特性即可 -
示例
[Headers("Authorization: Bearer")] public interface IGitHubApi {[Get("/users/{user}")]Task<string> GetUser(string user); }
var gitHubApi = RestService.For<IGitHubApi>("https://localhost:7234", new RefitSettings() {AuthorizationHeaderValueGetter = (request,token) => Task.FromResult("返回授权令牌") });
4、请求头的全局配置
如果有一些头部信息是必须在所有的请求都携带的,对每一个接口或方法都进行配置未免显得繁琐和冗余,此时可以借助DelegatingHandler
中间件,在 HTTP 请求和响应的处理管道中插入自定义逻辑,通过继承DelegatingHandler
并重写SendAsync()方法,在SendAsync()
方法中就可以在每个请求中自动添加所需的头部,而不需要在每个 API 方法中手动添加。对接口或方法特有的请求头,再另外使用特性去配置即可。
DelegatingHandler
是 .NET 中用于处理 HTTP 请求和响应的一个抽象类,属于System.Net.Http
命名空间。它可以在 HTTP 请求的处理管道中插入自定义逻辑,是实现 HTTP 客户端自定义行为的一个重要组件
实现DelegatingHandler
这里假设我们在项目中已经实现了ITenantProvider
接口来获取当前租户的信息,以及一个 IAuthTokenStore
接口来获取授权令牌,可以进行如下DelegatingHandler
实现。
-
示例
public class CustomHeaderHandler : DelegatingHandler { private readonly ITenantProvider _tenantProvider; private readonly IAuthTokenStore _authTokenStore; public CustomHeaderHandler(ITenantProvider tenantProvider, IAuthTokenStore authTokenStore) { _tenantProvider = tenantProvider; _authTokenStore = authTokenStore; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // 获取租户 ID 和授权令牌 var tenantId = _tenantProvider.GetCurrentTenantId(); var authToken = await _authTokenStore.GetAuthTokenAsync(); // 添加头部 request.Headers.Add("X-Tenant-Id", tenantId); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authToken); // 继续处理请求 return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); } }
定义API接口
-
示例
public interface IGitHubApi {[Get("/users/{user}")]Task<User> GetUser(string user); }public class User {public string Id { get; set; }public string Name { get; set; } }
注册中间件及API接口
-
示例-
Program.cs
...... builder.Services.AddTransient<ITenantProvider, TenantProvider>(); builder.Services.AddTransient<IAuthTokenStore, AuthTokenStore>(); builder.Services.AddTransient<CustomHeaderHandler>();builder.Services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com")).AddHttpMessageHandler<CustomHeaderHandler>(); ......
如果没有使用依赖容器,那么直接在创建Refit接口时候传入对应的接口对象即可
-
示例
var api = RestService.For<IGitHubApi>(new HttpClient(new CustomHeaderHandler(tenantProvider, authTokenStore)){BaseAddress = new Uri("https://api.example.com")} );
5、请求头的覆盖
在Refit中,如果同一个请求上对同一个请求头设置了多次,那么会根据如下优先级对请求头进行覆盖(仅覆盖同名的请求头):
-
在接口上使用
[Headers]
特性进行设置(最低优先级) -
在方法上使用
[Headers]
特性进行设置 -
在方法的参数上使用
[Headers]
或[HeaderCollection]
进行设置 -
示例
[Headers("X-Emoji: :rocket:")] public interface IGitHubApi {[Get("/users/list")]Task<List> GetUsers();[Get("/users/{user}")][Headers("X-Emoji: :smile_cat:")]Task<User> GetUser(string user);[Post("/users/new")][Headers("X-Emoji: :metal:")]Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji); }// X-Emoji: :rocket: var users = await GetUsers();// X-Emoji: :smile_cat: var user = await GetUser("octocat");// X-Emoji: :trollface: await CreateUser(user, ":trollface:");
6、删除请求头
如果希望对某个接口或方法删除指定的请求头,可以通过如下两种方式:
- 静态请求头的方式设置请求头,且不设置请求头的值
- 动态请求头的方式设置请求头,且将值设置为
null
需要注意删除和设置为空""
的区别
-
示例
[Headers("X-Emoji: :rocket:")] public interface IGitHubApi {[Get("/users/list")][Headers("X-Emoji")] // 删除 X-Emoji 请求头Task<List> GetUsers();[Get("/users/{user}")][Headers("X-Emoji:")] // 设置 X-Emoji 请求头为 ""Task<User> GetUser(string user);[Post("/users/new")]Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji); }// No X-Emoji header var users = await GetUsers();// X-Emoji: var user = await GetUser("octocat");// No X-Emoji header await CreateUser(user, null);// X-Emoji: await CreateUser(user, "");
五、中间件的数据传递
1、参数数据传输
如果有些运行过程中的数据需要传递给DelegatingHandler
中间件,需要对方法参数使用[Property]
特性进行声明。Refit会将使用[Property]
特性进行声明的方法参数传入到HttpRequestMessage.Properties
或HttpRequestMessage.Options
中
- NET 5 以后
HttpRequestMessage.Properties
已被标记为过时,Refit会将数据放入到HttpRequestMessage.Options
中
声明需要传递的参数
-
示例
public interface IGitHubApi {[Post("/users/new")]Task CreateUser([Body] User user, [Property("SomeKey")] string someValue);[Post("/users/new")]Task CreateUser([Body] User user, [Property] string someOtherKey); }
读取传递的数据
在DelegatingHandler
中间件中进行读取
-
示例
class RequestPropertyHandler : DelegatingHandler {public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken){// See if the request has a the propertyif(request.Properties.ContainsKey("SomeKey")){var someProperty = request.Properties["SomeKey"];//do stuff}if(request.Properties.ContainsKey("someOtherKey")){var someOtherProperty = request.Properties["someOtherKey"];//do stuff}return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);} }
注册中间件
这里多提一嘴,记得向依赖容器注册中间件
-
示例-
Program.cs
...... builder.Services.AddTransient<RequestPropertyHandler>();builder.Services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com")).AddHttpMessageHandler<RequestPropertyHandler>(); ......
如果没有使用依赖容器,那么则记得要在创建Refit接口时使用中间件
-
示例
var api = RestService.For<IGitHubApi>(new HttpClient(new RequestPropertyHandler()){BaseAddress = new Uri("https://api.example.com")} );
2、接口及方法信息的获取
这里指的是为了使用Refit所定义的接口及其方法。
在实际开发时,有时候可能需要知道当前所调用的方法是来自于哪个接口,特别是在使用的接口继承了某个公共接口的情况时。例如下列情况:
-
示例
public interface IGetAPI<TEntity> {[Get("/{key}")]Task<TEntity> Get(long key); }public interface IUsersAPI : IGetAPI<User> { }public interface IOrdersAPI : IGetAPI<Order> { }
获取接口信息
Refit提供了HttpRequestMessageOptions.InterfaceType
静态字符串专门用于从HttpRequestMessage.Properties
或HttpRequestMessage.Options
中获取对应接口的Type
类型
- .Net5之后,从
HttpRequestMessage.Properties
改为HttpRequestMessage.Options
通过获取接口信息,可以在中间件中进行一些业务处理,例如更换访问的URL
-
示例
class RequestPropertyHandler : DelegatingHandler {public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken){//通过 HttpMessageRequestOptions.InterfaceType 获取到接口的Type类型Type interfaceType = (Type)request.Properties[HttpMessageRequestOptions.InterfaceType];var builder = new UriBuilder(request.RequestUri);// Alter the Path in some way based on the interface or an attribute on itbuilder.Path = $"/{interfaceType.Name}{builder.Path}";// Set the new Uri on the outgoing messagerequest.RequestUri = builder.Uri;return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);} }
获取方法信息
Refit提供了HttpRequestMessageOptions.RestMethodInfo
静态字符串专门用于从HttpRequestMessage.Properties
或HttpRequestMessage.Options
中获取对应方法的RestMethodInfo
类型,所有方法所相关的信息都封装在RestMethodInfo
中,特别是在需要使用反射时,可以访问到完整的MethodInfo
对象。
-
示例
class RequestPropertyHandler : DelegatingHandler {public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken){// Get the method infoif (request.Options.TryGetValue(HttpRequestMessageOptions.RestMethodInfo,out RestMethodInfo restMethodInfo)){var builder = new UriBuilder(request.RequestUri);// Alter the Path in some way based on the method info or an attribute on itbuilder.Path = $"/{restMethodInfo.MethodInfo.Name}{builder.Path}";// Set the new Uri on the outgoing messagerequest.RequestUri = builder.Uri;}return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);} }
六、Multipart uploads
Multipart uploads指的是多部份上传,类似于表单中携带一个或多个文件进行上传的情况。
Refit支持使用[Multipart]
特性声明方法,使用[Multipart]
特性的方法将会以Content-Type=multipart/form-data
类型提交。
多部份上传所支持的参数类型
此时该方法所支持的参数类型如下:
string
:字符串类型,参数名将用作表单数据的名称,字符串值作为其值byte[]
:字节数组,通常用于已经将内容加载到内存中的情况Stream
:流,用于处理文件或数据流,常用于大文件FileInfo
:文件信息,表示文件的元数据,会自动根据文件信息获取文件进行上传
字段名称的优先级
在多部份上传的数据中,字段名称的优先级如下:
- multipartItem.Name:如果在运行时动态的指定了名称并且不为 null,则优先使用此名称,可以在执行时为表单数据部分命名
[AliasAs]
特性:可用于装饰方法签名中的流参数,为其提供一个静态名称- MultipartItem参数名称:这是默认的字段名称,直接根据方法签名中定义的参数名称使用
边界设置
边界指的是在多部份上传时,对不同部分进行分割的标识符,可以通过[Multipart(boundaryText)]
进行设置,如果没有设置则默认使用----MyGreatBoundary
-
边界效果示意
------MyGreatBoundary Content-Disposition: form-data; name="field1" value1 ------MyGreatBoundary Content-Disposition: form-data; name="file"; filename="example.txt" Content-Type: text/plain (文件内容) ------MyGreatBoundary--
边界字符串可以帮助服务器解析请求中的不同部分,服务器用边界来识别这些部分的开始和结束。
文件名和内容类型的指定
对于 byte[]
、Stream
和 FileInfo
参数,必须使用包装类来指定文件名和内容类型。具体的包装类包括:
-
ByteArrayPart
:用于字节数组 -
StreamPart
:用于流 -
FileInfoPart
:用于文件信息 -
示例-
Stream
public interface IMyApi { [Multipart] [Post("/users/textFile")]Task UploadFile([AliasAs("file")] StreamPart fileStream, string description); } // 调用示例 using (var stream = File.OpenRead("path/to/largeFile.txt")) { var part = new StreamPart(stream, "largeFile.txt", "text/plain"); await api.UploadFile(part, "Large file description"); }
-
示例-
byte[]
public interface IMyApi { [Multipart] [Post("/users/textFile")]Task UploadFile([AliasAs("file")] ByteArrayPart file,string description); } // 调用示例 var byteArray = File.ReadAllBytes("path/to/file.txt"); var part = new ByteArrayPart(byteArray, "file.txt", "text/plain"); await api.UploadFile(part, "File description");
-
示例-
FileInfo
public interface IMyApi { [Multipart] [Post("/users/textFile")]Task UploadFile([AliasAs("file")]FileInfoPart file,string description); } // 调用示例 var fileInfo = new FileInfo("path/to/file.txt"); var part = new FileInfoPart(fileInfo); await api.UploadFile(part, "File description");
七、响应类型
在Refit中,所有的网络请求必须是异步的,所有请求都要返回一个 Task
(表示正在进行的异步操作)或 IObservable
(用于响应式编程)。
返回Task
如果方法返回 Task
而不带类型参数,这意味着调用只关注请求是否成功,而不关心返回的具体内容。示例中 CreateUser
方法创建用户,但不返回任何数据,只确认请求是否成功。
-
示例
[Post("/users/new")] Task CreateUser([Body] User user);
返回Task<T>
如果返回类型是Task<T>
,则表示可以接收响应的内容,通常是从服务器返回的 JSON 数据(自动进行反序列化),或是一些基础的基本类型,例如string
、int
等等,这种情况下也是不关注响应的元数据的。
-
示例
// 获取用户内容作为字符串 [Get("/users/{user}")] Task<string> GetUser(string user);
返回ApiResponse<T>
使用 ApiResponse<T>
作为返回类型能够获取请求和响应的元数据,例如 HTTP 状态码、响应头等,同时也能获得反序列化后的内容
-
通过
ApiResponse<T>
的response.StatusCode
获取状态码 -
通过
ApiResponse<T>
的IsSuccessful
属性来判断请求是否成功,并进一步处理响应 -
通过
ApiResponse<T>
的Content
属性可以获取到T
对象 -
通过
ApiResponse<T>
的Headers
属性可以获取到响应头信息 -
通过
ApiResponse<T>
的Headers.Server
属性可以获取到服务器信息 -
示例
[Get("/users/{user}")] Task<ApiResponse<User>> GetUser(string user);var response = await gitHubApi.GetUser("octocat"); var httpStatus = response.StatusCode;if(response.IsSuccessful) {//YAY! Do the thing... }var serverHeaderValue = response.Headers.Server != null ? response.Headers.Server.ToString() : string.Empty;var customHeaderValue = string.Join(',', response.Headers.GetValues("A-Custom-Header"));foreach(var header in response.Headers) {var headerName = header.Key;var headerValue = string.Join(',', header.Value); }var user = response.Content;
返回IObservable<HttpResponseMessage>
当发出网络请求并返回一个 IObservable<HttpResponseMessage>
时,将接收到一个包含 HTTP 响应的对象,这个对象可以被用来获取请求的详细信息,比如状态码、响应头和响应体等。
由于很少用到IObservable<HttpResponseMessage>
类型,这里就不展开说明了。哪天用到了再补充IObservable
接口的用法
八、接口的使用
1、泛型接口
Refit允许使用泛型接口,这一点跟常规泛型接口的使用是一样的
-
示例
public interface IReallyExcitingCrudApi<T, in TKey> where T : class {[Post("")]Task<T> Create([Body] T payload);[Get("")]Task<List<T>> ReadAll();[Get("/{key}")]Task<T> ReadOne(TKey key);[Put("/{key}")]Task Update(TKey key, [Body]T payload);[Delete("/{key}")]Task Delete(TKey key); }var api = RestService.For<IReallyExcitingCrudApi<User, string>>("http://api.example.com/users");
2、接口继承
Refit支持接口继承,从而避免重复声明一样的方法
-
示例
public interface IBaseService {[Get("/resources")]Task<Resource> GetResource(string id); }public interface IDerivedServiceA : IBaseService {[Delete("/resources")]Task DeleteResource(string id); }public interface IDerivedServiceB : IBaseService {[Post("/resources")]Task<string> AddResource([Body] Resource resource); }
需要注意的是,使用继承时,请求头的配置也会被继承。
-
示例
[Headers("User-Agent: AAA")] public interface IAmInterfaceA {[Get("/get?result=Ping")]Task<string> Ping(); }[Headers("User-Agent: BBB")] public interface IAmInterfaceB : IAmInterfaceA {[Get("/get?result=Pang")][Headers("User-Agent: PANG")]Task<string> Pang();[Get("/get?result=Foo")]Task<string> Foo(); }public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB {[Get("/get?result=Foo")]Task<string> Foo(); }
上述示例中:
- IAmInterfaceB 的Ping()和Foo()方法将会使用 User-Agent: BBB 请求头
- IAmInterfaceB 的Pang()方法将会使用 User-Agent: PANG 请求头
- 如果IAmInterfaceB 接口上没有设定请求头,那么Foo()方法将会使用 User-Agent: AAA 请求头
- IAmInterfaceC 的 Foo()方法会先查看IAmInterfaceA所定义的请求头,如果有则直接使用,如果没有则查看IAmInterfaceB的请求头,这个跟继承的顺序有关,按顺序往下,找到第一个就直接使用
3、接口的默认实现
从C# 8.0开始,接口中可以定义默认实现方法。Refit支持在接口中通过默认实现为接口提供额外的逻辑。
-
示例
public interface IApiClient {// implemented by Refit but not exposed publicly[Get("/get")]internal Task<string> GetInternal();// Publicly available with added logic applied to the result from the API callpublic async Task<string> Get()=> FormatResponse(await GetInternal());private static String FormatResponse(string response)=> $"The response is: {response}"; }
九、使用HttpClientFactory
Refit对ASP.Net Core 2.1 以后出现的HttpClientFactory
有着一流的支持。
在ASP.Net Core中使用Refit,直接安装Refit.HttpClientFactory
依赖库即可。
注册Refit接口
ASP.Net Core 项目中,可以在Program.cs中,通过AddRefitClient<IWebApi>()
注册Refit接口并进行基RUL的配置
-
示例-
Program.cs
...... builder.Services.AddRefitClient<IGitHubApi>().ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));//.AddHttpMessageHandler<MyHandler>() //添加中间件//.SetHandlerLifetime(TimeSpan.FromMinutes(2)); //设置中间件的声明周期 ......
进行Refit配置
如果需要对Refit进行自定义设置,可以有如下两种方式:
-
先进行
RefitSettings
对象的配置,然后再直接传入到AddRefitClient<IWebApi>(RefitSettings settings)
方法中 -
示例-
Program.cs
var settings = new RefitSettings(); builder.Services.AddRefitClient<IWebApi>(settings).ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));// Add additional IHttpClientBuilder chained methods as required here:// .AddHttpMessageHandler<MyHandler>()// .SetHandlerLifetime(TimeSpan.FromMinutes(2));
-
使用
AddRefitClient<IWebApi>(Func<IServiceProvider, RefitSettings?>? settingsAction)
方法获取容器注入的服务提供对象后再进行配置 -
示例-
Program.cs
...... //从容器中注入服务提供对象后在进行设置 builder.Services.AddRefitClient<IWebApi>(provider => new RefitSettings() { /* configure settings */ }).ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));// .AddHttpMessageHandler<MyHandler>()// .SetHandlerLifetime(TimeSpan.FromMinutes(2)); ......
需要注意的是,RefitSettings
中的一些属性会被忽略,因为HttpClient
和HttpClientHandlers
是由HttpClientFactory
管理而不是Refit。
依赖注入获得API接口对象
注册完成后,就可以再需要使用的地方通过依赖注入获得Refit接口对象了。
-
示例
public class HomeController : Controller {public HomeController(IWebApi webApi){_webApi = webApi;}private readonly IWebApi _webApi;public async Task<IActionResult> Index(CancellationToken cancellationToken){var thing = await _webApi.GetSomethingWeNeed(cancellationToken);return View(thing);} }
十、异常处理
1、Refit对异常的处理
Refit对于异常处理的方式会根据方法返回的类型而有所不同,例如返回Task<T>
、Task<IApiResponse>
、Task<IApiResponse<T>>
、Task<ApiResponse<T>>
等类型。
返回 Task<T>
如果接口方法返回 Task<T>
,Refit将会抛出任何由ExceptionFactory
产生的 ApiException
异常或在尝试反序列化为 Task<T>
时产生的异常。
-
示例
try {var result = await awesomeApi.GetFooAsync("bar"); } catch (ApiException exception) {// 异常处理逻辑 }
返回 Task<IApiResponse>
、Task<IApiResponse<T>>
或 Task<ApiResponse<T>>
当接口方法返回Task<IApiResponse>
、Task<IApiResponse<T>>
或 Task<ApiResponse<T>>
时,Refit将捕获所有由ExceptionFactory
产生的ApiException
异常以及在尝试反序列化ApiResponse<T>
时发生的异常,并将捕获到的异常填充到 ApiResponse<T>
的 Error
属性中,而不会直接抛出。
-
示例
var response = await _myRefitClient.GetSomeStuff(); if (response.IsSuccessful) { // 执行业务逻辑 } else { _logger.LogError(response.Error, response.Error.Content); }
需要注意的是,ApiResponse<T>
的IsSuccessful
所检查的是状态是否在200~299内且没有任何其他异常(例如反序列化时产生的异常),如果只是想检查HTTP响应状态,可以使用IsSuccessStatusCode
属性。
2、自定义异常工厂
Refit允许用户提供自定义的异常处理逻辑。
处理响应的自定义异常工厂
如果是想自定义处理响应时发生异常,可以通过设置RefitSettings
中的ExceptionFactory
属性来实现,例如可以选择在处理响应时忽略所有异常。
-
示例
var nullTask = Task.FromResult<Exception>(null); var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com", new RefitSettings { ExceptionFactory = httpResponse => nullTask; });
处理反序列化的自定义异常工厂
如果希望自定义处理反序列化时产生的异常,可以通过设置RefitSettings
中的DeserializationExceptionFactory
属性来实现
-
示例
var nullTask = Task.FromResult<Exception>(null);var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",new RefitSettings {DeserializationExceptionFactory = (httpResponse, exception) => nullTask;});