Refit.Xml 7.1.2
前缀已保留
需要NuGet 2.12或更高版本。
dotnet add package Refit.Xml --version 7.1.2
NuGet\Install-Package Refit.Xml -Version 7.1.2
<PackageReference Include="Refit.Xml" Version="7.1.2" />
paket add Refit.Xml --version 7.1.2
#r "nuget: Refit.Xml, 7.1.2"
// Install Refit.Xml as a Cake Addin #addin nuget:?package=Refit.Xml&version=7.1.2 // Install Refit.Xml as a Cake Tool #tool nuget:?package=Refit.Xml&version=7.1.2
Refit:为.NET Core、Xamarin和.NET提供的自动类型安全REST库
Refit | Refit.HttpClientFactory | Refit.Newtonsoft.Json | |
---|---|---|---|
NuGet |
Refit受Square的Retrofit库的强烈启发,并可以将您的REST API转换为实时接口
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<User> GetUser(string user);
}
RestService
类生成实现了IGitHubApi
的实例,它使用HttpClient
进行调用
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
var octocat = await gitHubApi.GetUser("octocat");
.NET Core 支持通过 HttpClientFactory 注册
services
.AddRefitClient<IGitHubApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));
目录
- 这个功能在哪里使用?
- API 属性
- 查询字符串
- 请求体内容
- 设置请求头
- 将状态传递到 DelegatingHandlers
- 多部分上传
- 检索响应
- 使用泛型接口
- 接口继承
- 默认接口方法
- 使用 HttpClientFactory
- 提供自定义 HttpClient
- 处理异常
这个功能在哪里使用?
Refit 目前支持以下平台以及任何 .NET Standard 2.0 目标平台
- UWP
- Xamarin.Android
- Xamarin.Mac
- Xamarin.iOS
- 桌面 .NET 4.6.1
- .NET 5 / .NET Core
- Blazor
- Uno 平台
SDK 要求
Refit 6 需要 Visual Studio 16.8 或更高版本,或 .NET SDK 5.0.100 或更高版本。它可以针对任何 .NET Standard 2.0 平台。
Refit 6 不支持旧版的 packages.config
格式用于 NuGet 引用(因为它们不支持分析器/源生成器)。您必须迁移到 PackageReference 才能使用 Refit v6 及更高版本。
6.x 版本的 breaking changes
Refit 6 将 System.Text.Json 作为默认 JSON 序列化器。如果您想继续使用 Newtonsoft.Json
,请添加 Refit.Newtonsoft.Json
NuGet 包,并在您的 RefitSettings
实例中将 ContentSerializer
设置为 NewtonsoftJsonContentSerializer
。《System.Text.Json》更快且内存使用更少,但并非所有功能都受支持。《迁移指南 包含更多详细信息。
IContentSerializer
已重命名为 IHttpContentSerializer
,更好地体现了其目的。此外,其两个方法也已重命名,SerializeAsync<T>
→ ToHttpContent<T>
和 DeserializeAsync<T>
→ FromHttpContentAsync<T>
。任何现有的实现都需要更新,但这些更改应该是轻微的。
6.3 版本的更新
Refit 6.3 将通过 XmlContentSerializer
的 XML 序列化拆分到单独的包中,即 Refit.Xml
。这是为了在使用 Refit 与 Web Assembly(WASM)应用程序时减小依赖项大小。如果您需要 XML,请添加对 Refit.Xml
的引用。
API 属性
每个方法都必须有一个 HTTP 属性,该属性提供请求方法和相对 URL。有六个内置注解:Get、Post、Put、Delete、Patch 和 Head。资源的相对 URL 由注解指定。
[Get("/users/list")]
您也可以在 URL 中指定查询参数
[Get("/users/list?sort=desc")]
请求 URL 可以使用方法上的替换块和参数动态更新。替换块是包围在大括号 {} 中的字母数字字符串。
如果您的参数名称与 URL 路径中的名称不匹配,请使用 AliasAs
属性。
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId);
请求 URL 还可以将替换块绑定到自定义对象
[Get("/group/{request.groupId}/users/{request.userId}")]
Task<List<User>> GroupList(UserGroupRequest request);
class UserGroupRequest{
int groupId { get;set; }
int userId { get;set; }
}
未指定为 URL 替换的参数将自动用作查询参数。这与 Retrofit 不同,在 Retrofit 中,必须显式指定所有参数。
参数名称与URL参数的比较不区分大小写,因此如果您的参数名为groupId
,例如在路径/group/{groupid}/show
中使用它,将会正常工作。
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);
GroupList(4, "desc");
>>> "/group/4/users?sort=desc"
回环路由参数语法:使用双星号(*)通配符参数语法时,正斜杠不会被编码。
在生成链接时,路由系统会对双星号(*)通配符参数中的值进行编码(例如,{**myparametername}),除了正斜杠。
回环路由参数的类型必须是字符串。
[Get("/search/{**page}")]
Task<List<Page>> Search(string page);
Search("admin/products");
>>> "/search/admin/products"
查询字符串
动态查询字符串参数
如果您指定一个object
作为查询参数,所有非null的公共属性都将用作查询参数。这以前仅适用于GET请求,但现在扩展到了所有HTTP请求方法,这主要归功于Twitter的混合API坚持使用带有查询字符串参数的非GET请求。使用Query
属性将行为更改为'扁平化'查询参数对象。如果您使用此属性,可以指定用于'扁平化'对象的分隔符和前缀。
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
}
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, MyQueryParams params);
[Get("/group/{id}/users")]
Task<List<User>> GroupListWithAttribute([AliasAs("id")] int groupId, [Query(".","search")] MyQueryParams params);
params.SortOrder = "desc";
params.Limit = 10;
params.Kind = KindOptions.Bar;
GroupList(4, params)
>>> "/group/4/users?order=desc&Limit=10&Kind=bar"
GroupListWithAttribute(4, params)
>>> "/group/4/users?search.order=desc&search.Limit=10&search.Kind=bar"
如果使用字典,也存在类似的行为,但没有AliasAs
属性的优点,当然也没有智能感知和/或类型安全。
您还可以使用[Query]指定查询字符串参数,并在非GET请求中将其扁平化,类似于:
[Post("/statuses/update.json")]
Task<Tweet> PostTweet([Query]TweetParams params);
其中TweetParams
是一个POCO,属性也将支持[AliasAs]
属性。
将集合作为查询字符串参数
使用Query
属性指定集合在查询字符串中应该使用的格式。
[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"
您还可以在RefitSettings
中指定集合格式,在默认情况下,除非在Query
属性中明确指定,否则将使用该格式。
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
CollectionFormat = CollectionFormat.Multi
});
转义查询字符串参数
使用QueryUriFormat
属性指定查询参数是否应该进行URL转义。
[Get("/query")]
[QueryUriFormat(UriFormat.Unescaped)]
Task Query(string q);
Query("Select+Id,Name+From+Account")
>>> "/query?q=Select+Id,Name+From+Account"
自定义查询字符串参数格式化
格式化键
要自定义查询键的格式,您有两个主要选项
使用
AliasAs
属性:您可以使用
AliasAs
属性来指定属性的特定自定义键名。此属性将始终优先于您指定的任何键格式化程序。public class MyQueryParams { [AliasAs("order")] public string SortOrder { get; set; } public int Limit { get; set; } } [Get("/group/{id}/users")] Task<List<User>> GroupList([AliasAs("id")] int groupId, [Query] MyQueryParams params); params.SortOrder = "desc"; params.Limit = 10; GroupList(1, params);
这将生成以下请求
/group/1/users?order=desc&Limit=10
使用
RefitSettings.UrlParameterKeyFormatter
属性:默认情况下,Refit使用属性名作为查询键,而不进行任何其他格式化。如果您想在整个查询键中应用自定义格式,可以使用
UrlParameterKeyFormatter
属性。请记住,如果属性具有AliasAs
属性,它将始终被使用,而不论格式化程序如何。以下示例使用了内置的
CamelCaseUrlParameterKeyFormatter
public class MyQueryParams { public string SortOrder { get; set; } [AliasAs("queryLimit")] public int Limit { get; set; } } [Get("/group/users")] Task<List<User>> GroupList([Query] MyQueryParams params); params.SortOrder = "desc"; params.Limit = 10;
请求将看起来像
/group/users?sortOrder=desc&queryLimit=10
注意:AliasAs
属性始终具有最高优先级。如果同时存在属性和自定义键格式化程序,则使用AliasAs
属性的值。
使用UrlParameterFormatter
格式化URL参数值
在Refit中,RefitSettings
中的UrlParameterFormatter
属性允许您自定义如何格式化URL中的参数值。这特别有用,当你需要以特定方式格式化日期、数字或其他类型以符合API的期望。
使用UrlParameterFormatter
:
将实现IUrlParameterFormatter
接口的自定义格式化程序分配给UrlParameterFormatter
属性。
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();
}
}
var settings = new RefitSettings
{
UrlParameterFormatter = new CustomDateUrlParameterFormatter()
};
在此示例中,为日期值创建了一个自定义格式化程序。每当遇到DateTime
参数时,它将以yyyyMMdd
格式化日期。
格式化字典键:
在处理字典时,请注意键被视为值。如果您需要为字典键定制格式化,则应使用UrlParameterFormatter
。
例如,如果您有一个字典参数,并且希望以特定方式格式化其键,您可以在自定义格式化程序中处理它
public class CustomDictionaryKeyFormatter : IUrlParameterFormatter
{
public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
{
// Handle dictionary keys
if (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
// Custom formatting logic for dictionary keys
return value?.ToString().ToUpperInvariant();
}
return value?.ToString();
}
}
var settings = new RefitSettings
{
UrlParameterFormatter = new CustomDictionaryKeyFormatter()
};
在上面的示例中,字典键将被转换为大写。
请求体内容
您的方法中的参数之一可以用作正文,通过使用Body属性。
[Post("/users/new")]
Task CreateUser([Body] User user);
根据参数类型,提供体数据的可能性有四种:
- 如果类型是
Stream
,内容将通过StreamContent
进行流式传输 - 如果类型是
string
,字符串将直接用作内容,除非设置了[Body(BodySerializationMethod.Json)]
,这将将其作为StringContent
发送 - 如果参数具有属性
[Body(BodySerializationMethod.UrlEncoded)]
,则内容将被 URL 编码(见下文 表单提交) - 对于所有其他类型,对象将使用在 RefitSettings 中指定的内容序列化程序进行序列化(JSON 是默认值)
缓冲和 Content-Length
标头
默认情况下,Refit 会流式传输体内容而不会对其进行缓冲。这意味着您可以从磁盘中流式传输文件,例如,而不需要将整个文件加载到内存中。这种做法的坏处是,请求中没有设置 Content-Length
标头。如果您需要在请求中发送 Content-Length
标头,可以通过将 [Body]
属性的 buffered
参数设置为 true
来禁用此流式传输行为
Task CreateUser([Body(buffered: true)] User user);
JSON 内容
使用 IHttpContentSerializer
接口的实例来序列化和反序列化 JSON 请求和响应。Refit 提供了两种默认实现:SystemTextJsonContentSerializer
(这是默认的 JSON 序列化器)和 NewtonsoftJsonContentSerializer
。前者使用 System.Text.Json
API,专注于高性能和低内存使用,而后者使用知名的 Newtonsoft.Json
库,更灵活且可自定义。您可以在此链接上了解更多关于这两个序列化器和两者之间主要区别的信息。
例如,这里是如何使用基于 Newtonsoft.Json
的序列化器创建一个新的 RefitSettings
实例(您还需要将 PackageReference
添加到 Refit.Newtonsoft.Json
):
var settings = new RefitSettings(new NewtonsoftJsonContentSerializer());
如果您使用 Newtonsoft.Json
API,您可以通过设置 Newtonsoft.Json.JsonConvert.DefaultSettings
属性来自定义其行为
JsonConvert.DefaultSettings =
() => new JsonSerializerSettings() {
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = {new StringEnumConverter()}
};
// Serialized as: {"day":"Saturday"}
await PostSomeStuff(new { Day = DayOfWeek.Saturday });
由于这些是全局设置,它们将影响您的整个应用程序。可能最好将特定 API 的调用隔离设置。在创建由 Refit 生成的实时接口时,您可以选择传递一个 RefitSettings
,这将允许您指定所希望的自定义序列化器设置。这允许您为不同的 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()
}
)});
可以使用 Json.NET 的 JsonProperty 属性来自定义属性序列化和反序列化
public class Foo
{
// Works like [AliasAs("b")] would in form posts (see below)
[JsonProperty(PropertyName="b")]
public string Bar { get; set; }
}
JSON 源生成器
要利用 .NET 6 中添加的新的 System.Text.Json 源生成器 的优势,您可以结合使用 SystemTextJsonContentSerializer
和自定义实例的 RefitSettings
和 JsonSerializerOptions
var options = new JsonSerializerOptions();
options.AddContext<MyJsonSerializerContext>();
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
ContentSerializer = new SystemTextJsonContentSerializer(options)
});
XML 内容
XML 请求和响应使用 System.Xml.Serialization.XmlSerializer 进行序列化和反序列化。默认情况下,Refit 将使用 JSON 内容序列化,要使用 XML 内容可配置 ContentSerializer
使用 XmlContentSerializer
var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML",
new RefitSettings {
ContentSerializer = new XmlContentSerializer()
});
可以使用 System.Xml.Serialization 命名空间中找到的属性来自定义属性序列化和反序列化
public class Foo
{
[XmlElement(Namespace = "https://www.w3.org/XML")]
public string Bar { get; set; }
}
System.Xml.Serialization.XmlSerializer 提供了多种序列化选项,可以通过为 XmlContentSerializer 构造函数提供一个 XmlContentSerializerSettings 来设置这些选项
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
}
}
}
)
});
<a name="form-posts"></a>表单提交
对于需要表单提交的 API(即作为 application/x-www-form-urlencoded
序列化),使用 BodySerializationMethod.UrlEncoded
初始化 Body 属性。
参数可以是 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);
或者您可以传递任何对象,而所有 公共、可读取 的属性将被序列化为请求中的表单字段。此方法允许您使用 [AliasAs(" whatever ")
来别名属性名称,这可能有助于 API 具有难以理解的字段名称
public interface IMeasurementProtocolApi
{
[Post("/collect")]
Task Collect([Body(BodySerializationMethod.UrlEncoded)] Measurement measurement);
}
public class Measurement
{
// Properties can be read-only and [AliasAs] isn't required
public int v { get { return 1; } }
[AliasAs("tid")]
public string WebPropertyId { get; set; }
[AliasAs("cid")]
public Guid ClientId { get; set; }
[AliasAs("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);
如果您有一个带有关联属性别名设置的属性的类型(如使用[JsonProperty(PropertyName)]
),则Refit也将使用这些别名(当您同时使用 [JsonProperty(PropertyName)]
和 [AliasAs]
时,[AliasAs]
优先)。这意味着以下类型将序列化为:one=value1&two=value2
public class SomeObject
{
[JsonProperty(PropertyName = "one")]
public string FirstProperty { get; set; }
[JsonProperty(PropertyName = "notTwo")]
[AliasAs("two")]
public string SecondProperty { get; set; }
}
注意:此处的[AliasAs]
用法适用于查询字符串参数和表单正文提交,但不适用于响应对象;对于响应对象中的字段别名,您仍然需要使用[JsonProperty("full-property-name")]
。
设置请求头
静态头
您可以通过为方法添加Headers
属性来自定义向请求添加一个或多个静态请求头。
[Headers("User-Agent: Awesome Octocat App")]
[Get("/users/{user}")]
Task<User> GetUser(string user);
您还可以通过将Headers
属性应用于接口来将静态头添加到API中每个请求。
[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);
}
动态头
如果头的内容需要在运行时设置,您可以通过将Header
属性应用于参数向请求中添加一个具有动态值的头。
[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");
添加Authorization
头是如此常见的一个用例,您可以通过将Authorize
属性应用于参数并将访问令牌添加到请求中来做到这一点,并且可以选择指定方案。
[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");
//note: the scheme defaults to Bearer if none provided
如果您需要在运行时设置多个头,则可以将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);
Bearer 认证
大多数API都需要某种类型的身份验证。最常见的是OAuth Bearer身份验证。每次请求都会添加一个形式为Authorization: Bearer <token>
的头。Refit简化了获取令牌的逻辑,以便根据您的应用程序需求获取令牌,因此不需要将令牌传递到每个方法中。
- 将
[Headers("Authorization: Bearer")]
添加到需要令牌的接口或方法中。 - 在
RefitSettings
实例中设置AuthorizationHeaderValueGetter
。每次Refit需要获取令牌时都会调用您的委托,因此在令牌生命周期的某个时期内缓存令牌值是个好主意。
使用 DelegatingHandlers 减少头部的样板代码(Authorization 头示例)
尽管我们在Refit中提供了在运行时直接添加动态头的功能,但大多数用例可能仍会从注册自定义DelegatingHandler
中受益,以便作为HttpClient
Middleware pipeline的一部分添加头,从而消除添加大量[Header]
或[HeaderCollection]
属性的需求。
在上述示例中,我们利用了[HeaderCollection]
参数来注入Authorization
和X-Tenant-Id
头。如果您正在与使用OAuth2的第三方集成,这是一个相当常见的场景。虽然是偶尔的端点,但如果我们不得不将样板代码添加到接口中的每个方法,那将会非常麻烦。
在此示例中,我们假设我们的应用程序是一个多租户应用程序,可以通过某种接口ITenantProvider
检索有关租户的信息,并有一个数据存储库IAuthTokenStore
,可以用于检索要附加到出站请求的授权令牌。
//Custom delegating handler for adding Auth headers to outbound requests
class AuthHeaderHandler : DelegatingHandler
{
private readonly ITenantProvider tenantProvider;
private readonly IAuthTokenStore authTokenStore;
public AuthHeaderHandler(ITenantProvider tenantProvider, IAuthTokenStore authTokenStore)
{
this.tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider));
this.authTokenStore = authTokenStore ?? throw new ArgumentNullException(nameof(authTokenStore));
// InnerHandler must be left as null when using DI, but must be assigned a value when
// using RestService.For<IMyApi>
// InnerHandler = new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var token = await authTokenStore.GetToken();
//potentially refresh token here if it has expired etc.
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
request.Headers.Add("X-Tenant-Id", tenantProvider.GetTenantId());
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ITenantProvider, TenantProvider>();
services.AddTransient<IAuthTokenStore, AuthTokenStore>();
services.AddTransient<AuthHeaderHandler>();
//this will add our refit api implementation with an HttpClient
//that is configured to add auth headers to all requests
//note: AddRefitClient<T> requires a reference to Refit.HttpClientFactory
//note: the order of delegating handlers is important and they run in the order they are added!
services.AddRefitClient<ISomeThirdPartyApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
.AddHttpMessageHandler<AuthHeaderHandler>();
//you could add Polly here to handle HTTP 429 / HTTP 503 etc
}
//Your application code
public class SomeImportantBusinessLogic
{
private ISomeThirdPartyApi thirdPartyApi;
public SomeImportantBusinessLogic(ISomeThirdPartyApi thirdPartyApi)
{
this.thirdPartyApi = thirdPartyApi;
}
public async Task DoStuffWithUser(string username)
{
var user = await thirdPartyApi.GetUser(username);
//do your thing
}
}
如果您没有使用依赖注入,则可以通过类似这样做来实现相同的结果
var api = RestService.For<ISomeThirdPartyApi>(new HttpClient(new AuthHeaderHandler(tenantProvider, authTokenStore))
{
BaseAddress = new Uri("https://api.example.com")
}
);
var user = await thirdPartyApi.GetUser(username);
//do your thing
重定义头
与Retrofit不同,Retrofit中的头不会相互覆盖,并且无论定义了多少次相同的头,都会添加到请求中,Refit采用类似于ASP.NET MVC中action filters的方法——重新定义一个头将替换它,以下顺序以此为优先级
Headers
属性在接口上的(最低优先级)Headers
属性在方法上Header
属性或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:");
注意:此重新定义行为仅适用于具有相同名称的头。不同名称的头不会被替换。以下代码将结果为包括所有头
[Headers("Header-A: 1")]
public interface ISomeApi
{
[Headers("Header-B: 2")]
[Post("/post")]
Task PostTheThing([Header("Header-C")] int c);
}
// Header-A: 1
// Header-B: 2
// Header-C: 3
var user = await api.PostTheThing(3);
删除头
可以通过重新定义不带值(即没有 : <value>
)的静态头或传递null
来删除接口或方法上定义的头。对于动态头,空字符串将被视为空头。
[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
[Get("/users/list")]
[Headers("X-Emoji")] // Remove the X-Emoji header
Task<List> GetUsers();
[Get("/users/{user}")]
[Headers("X-Emoji:")] // Redefine the X-Emoji header as empty
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, "");
将状态传递到 DelegatingHandlers
如果需要将运行时状态传递给DelegatingHandler
,可以通过在参数上应用Property
属性,将具有动态值的属性添加到底层HttpRequestMessage.Properties
中。
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);
}
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 property
if(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);
}
}
对 Polly 和 Polly.Context 的支持
目标接口类型和方法信息
public interface IGetAPI<TEntity>
{
[Get("/{key}")]
Task<TEntity> Get(long key);
}
public interface IUsersAPI : IGetAPI<User>
{
}
public interface IOrdersAPI : IGetAPI<Order>
{
}
class RequestPropertyHandler : DelegatingHandler
{
public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Get the type of the target interface
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 it
builder.Path = $"/{interfaceType.Name}{builder.Path}";
// Set the new Uri on the outgoing message
request.RequestUri = builder.Uri;
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
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 info
if (request.Options.TryGetValue(HttpRequestMessageOptions.RestMethodInfoKey, out RestMethodInfo restMethodInfo))
{
var builder = new UriBuilder(request.RequestUri);
// Alter the Path in some way based on the method info or an attribute on it
builder.Path = $"/{restMethodInfo.MethodInfo.Name}{builder.Path}";
// Set the new Uri on the outgoing message
request.RequestUri = builder.Uri;
}
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
多部分上传
- 字符串(参数名称将用作名称,字符串值作为值)
- 字节数组
- 流
- 文件信息
多部分数据中的字段名称优先级
- multipartItem.Name如果指定且非null(可选);动态的,允许在执行时命名表单数据部分。
- 可选的[AliasAs]属性,用于装饰方法签名中的streamPart参数(见下文);静态的,在代码中定义。
- 多部分项参数名称(默认)如方法签名中所定义;静态的,在代码中定义。
public interface ISomeApi
{
[Multipart]
[Post("/users/{id}/photo")]
Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart stream);
}
someApiInstance.UploadPhoto(id, new StreamPart(myPhotoStream, "photo.jpg", "image/jpeg"));
注意:本节中先前描述的AttachmentName属性已被弃用,不推荐使用。
检索响应
值得注意的是,与Retrofit不同,在Refit中没有同步网络请求选项 - 所有请求都必须是异步的,无论是通过Task
还是通过IObservable
。与Retrofit不同,您无法通过回调参数创建异步方法,因为我们生活在异步/await的未来。
同样地,与通过参数类型更改正文内容的方式类似,返回类型将决定返回的内容。
不使用类型参数返回Task将丢弃内容,只会告诉您调用是否成功。
[Post("/users/new")]
Task CreateUser([Body] User user);
// This will throw if the network call fails
await CreateUser(someUser);
如果类型参数是'HttpResponseMessage'或'string',则分别返回原始响应消息或字符串形式的内容。
// Returns the content as a string (i.e. the JSON data)
[Get("/users/{user}")]
Task<string> GetUser(string user);
// Returns the raw response, as an IObservable that can be used with the
// Reactive Extensions
[Get("/users/{user}")]
IObservable<HttpResponseMessage> GetUser(string user);
还有一个名为ApiResponse<T>
的泛型包装类,可以用作返回类型。使用此类作为返回类型允许您检索不仅作为对象的内容,还可以检索与请求/响应相关联的任何元数据。这包括诸如响应头、http状态码和原因短语(例如,404未找到)的信息、响应版本、发送的原始请求消息,以及发生错误的情况下,包含错误详细信息的ApiException
对象。以下是您如何检索响应元数据的示例。
//Returns the content within a wrapper class containing metadata about the request/response
[Get("/users/{user}")]
Task<ApiResponse<User>> GetUser(string user);
//Calling the API
var response = await gitHubApi.GetUser("octocat");
//Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration)
var httpStatus = response.StatusCode;
//Determining if a success status code was received
if(response.IsSuccessStatusCode)
{
//YAY! Do the thing...
}
//Retrieving a well-known header value (e.g. "Server" header)
var serverHeaderValue = response.Headers.Server != null ? response.Headers.Server.ToString() : string.Empty;
//Retrieving a custom header value
var customHeaderValue = string.Join(',', response.Headers.GetValues("A-Custom-Header"));
//Looping through all the headers
foreach(var header in response.Headers)
{
var headerName = header.Key;
var headerValue = string.Join(',', header.Value);
}
//Finally, retrieving the content in the response body as a strongly-typed object
var user = response.Content;
使用泛型接口
当使用类似ASP.NET Web API时,有一个相当常见的模式,即有一堆CRUD REST服务。现在Refit支持这些,允许您使用泛型类型定义单个API接口。
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);
}
它可以这样使用
// The "/users" part here is kind of important if you want it to work for more
// than one type (unless you have a different domain for each type)
var api = RestService.For<IReallyExcitingCrudApi<User, string>>("http://api.example.com/users");
接口继承
当需要单独保存的多个服务共享一些API时,可以通过接口继承来利用,以避免在多个服务中多次定义相同的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);
}
在这个例子中,IDerivedServiceA
接口将公开GetResource
和DeleteResource
API,而IDerivedServiceB
将公开GetResource
和AddResource
。
头继承
在使用继承时,现有的头属性也会被传递,并且最内层的属性将具有优先权。
[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();
}
在这里,IAmInterfaceB.Pang()
将使用PANG
作为它的用户代理,而IAmInterfaceB.Foo
和IAmInterfaceB.Ping
将使用BBB
。请注意,如果IAmInterfaceB
没有头属性,那么Foo
将使用从IAmInterfaceA
继承而来的AAA
值。如果一个接口继承了不止一个接口,则优先级顺序与声明继承接口的顺序相同。
public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB
{
[Get("/get?result=Foo")]
Task<string> Foo();
}
在这里,如果存在,IAmInterfaceC.Foo
将使用从IAmInterfaceA
继承的头属性,或者是从IAmInterfaceB
继承的,等等。
默认接口方法
C# 8.0开始,可以在接口上定义默认接口方法(简称DIM)。Refit接口可以使用DIM提供额外的逻辑,可选地结合使用私有和/或静态辅助方法。
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 call
public async Task<string> Get()
=> FormatResponse(await GetInternal());
private static String FormatResponse(string response)
=> $"The response is: {response}";
}
Refit生成的类型将实现IApiClient.GetInternal
方法。如果需要在调用之前或之后立即执行额外的逻辑,则不应该直接公开,因此可以通过标记为internal
来隐藏它,以使消费者无法访问。默认接口方法IApiClient.Get
将被所有实现IApiClient
的类型继承,包括当然——Refit生成的类型。消费者将调用公共的Get
方法,并从其实现中获得提供的额外逻辑的好处(在这种情况下,可选地,借助私有静态辅助FormatResponse
)。为了支持没有DIM支持(.NET Core 2.x以下或.NET Standard 2.0以下)的运行时,该解决方案需要额外的两个类型。
internal interface IApiClientInternal
{
[Get("/get")]
Task<string> Get();
}
public interface IApiClient
{
public Task<string> Get();
}
internal class ApiClient : IApiClient
{
private readonly IApiClientInternal client;
public ApiClient(IApiClientInternal client) => this.client = client;
public async Task<string> Get()
=> FormatResponse(await client.Get());
private static String FormatResponse(string response)
=> $"The response is: {response}";
}
使用 HttpClientFactory
Refit 对 ASP.Net Core 2.1 HttpClientFactory 提供了一级支持。在您的 ConfigureServices
方法中添加对 Refit.HttpClientFactory
的引用,并通过调用提供的扩展方法来配置您的 Refit 接口。
services.AddRefitClient<IWebApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
// Add additional IHttpClientBuilder chained methods as required here:
// .AddHttpMessageHandler<MyHandler>()
// .SetHandlerLifetime(TimeSpan.FromMinutes(2));
可选地,可以包含一个 RefitSettings
对象。
var settings = new RefitSettings();
// Configure refit settings here
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));
// or injected from the container
services.AddRefitClient<IWebApi>(provider => new RefitSettings() { /* configure 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));
请注意,因为 HttpClient
和 HttpClientHandlers
将由 HttpClientFactory
管理,所以 RefitSettings
的一些属性将被忽略。
然后,您可以通过构造函数注入来获取 API 接口。
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);
}
}
提供自定义 HttpClient
您可以通过简单地将自定义的 HttpClient
实例作为参数传递给 RestService.For<T>
方法来提供自定义的 HttpClient
实例。
RestService.For<ISomeApi>(new HttpClient()
{
BaseAddress = new Uri("https://www.someapi.com/api/")
});
但是,当提供自定义的 HttpClient
实例时,以下 RefitSettings
属性将不起作用:
AuthorizationHeaderValueGetter
HttpMessageHandlerFactory
如果您仍然希望能够在使用上述设置的同时配置 Refit
提供的 HttpClient
实例,只需在 API 接口上公开 HttpClient
即可。
interface ISomeApi
{
// This will automagically be populated by Refit if the property exists
HttpClient Client { get; }
[Headers("Authorization: Bearer")]
[Get("/endpoint")]
Task<string> SomeApiEndpoint();
}
然后,在创建 REST 服务后,您可以设置任何您想要的 HttpClient
属性,例如 Timeout
。
SomeApi = RestService.For<ISomeApi>("https://www.someapi.com/api/", new RefitSettings()
{
AuthorizationHeaderValueGetter = (rq, ct) => GetTokenAsync()
});
SomeApi.Client.Timeout = timeout;
处理异常
Refit 根据 Refit 接口方法是否返回 Task<T>
或返回 Task<IApiResponse>
、Task<IApiResponse<T>>
或 Task<ApiResponse<T>>
,具有不同的异常处理行为。
<a id="when-returning-taskapiresponset"></a>当返回 Task<IApiResponse>
、Task<IApiResponse<T>>
或 Task<ApiResponse<T>>
时
在处理响应时,如果 ExceptionFactory
抛出 ApiException
或在尝试将响应反序列化为 ApiResponse<T>
时发生错误,Refit 会捕获这些错误并将其填充到 ApiResponse<T>
的 Error
属性中,而不会抛出异常。
然后,您可以决定如何处理如下:
var response = await _myRefitClient.GetSomeStuff();
if(response.IsSuccessStatusCode)
{
//do your thing
}
else
{
_logger.LogError(response.Error, response.Error.Content);
}
当返回 Task<T>
时
Refit 会抛出在处理响应时 ExceptionFactory
抛出的任何 ApiException
或尝试将响应反序列化为 Task<T>
时发生的任何错误。
// ...
try
{
var result = await awesomeApi.GetFooAsync("bar");
}
catch (ApiException exception)
{
//exception handling
}
// ...
Refit 还可以抛出 ValidationApiException
,这除了包含在 ApiException
上的信息外,还包含 ProblemDetails
,当服务实现了 RFC 7807 规范的 problem details,并且响应内容类型为 application/problem+json
时。
有关验证异常的详细信息,请简单捕获 ValidationApiException
。
// ...
try
{
var result = await awesomeApi.GetFooAsync("bar");
}
catch (ValidationApiException validationException)
{
// handle validation here by using validationException.Content,
// which is type of ProblemDetails according to RFC 7807
// If the response contains additional properties on the problem details,
// they will be added to the validationException.Content.Extensions collection.
}
catch (ApiException exception)
{
// other exception handling
}
// ...
提供自定义的 ExceptionFactory
您可以通过在 RefitSettings
中提供自定义异常工厂来覆盖 ExceptionFactory
在处理结果时引发的自定义异常行为。例如,您可以使用以下方式抑制所有异常
var nullTask = Task.FromResult<Exception>(null);
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
ExceptionFactory = httpResponse => nullTask;
});
请注意,在尝试反序列化响应时引发的异常不受此影响。
ApiException
使用 Serilog 的结构化
对于 Serilog 的用户,您可以使用 Serilog.Exceptions.Refit NuGet 包来丰富 ApiException
的日志记录。有关如何将此包集成到您的应用程序中的详细信息,请参见 这里。
产品 | 版本 兼容的和额外的计算目标框架版本。 |
---|---|
.NET | net5.0 已计算。 net5.0-windows 已计算。 net6.0 兼容。 net6.0-android 已计算。 net6.0-ios 已计算。 net6.0-maccatalyst 已计算。 net6.0-macos 已计算。 net6.0-tvos 已计算。 net6.0-windows 已计算。 net7.0 已计算。 net7.0-android 已计算。 net7.0-ios 已计算。 net7.0-maccatalyst 已计算。 net7.0-macos 已计算。 net7.0-tvos 已计算。 net7.0-windows 已计算。 net8.0 兼容。 net8.0-android 已计算。 net8.0-browser 已计算。 net8.0-ios 已计算。 net8.0-maccatalyst 已计算。 net8.0-macos 已计算。 net8.0-tvos 已计算。 net8.0-windows 已计算。 |
.NET Core | netcoreapp2.0 已计算。 netcoreapp2.1 已计算。 netcoreapp2.2 已计算。 netcoreapp3.0 已计算。 netcoreapp3.1 已计算。 |
.NET Standard | netstandard2.0 兼容。 netstandard2.1 已计算。 |
.NET Framework | net461 已计算。 net462 兼容。 net463 已计算。 net47 已计算。 net471 已计算。 net472 已计算。 net48 已计算。 net481 已计算。 |
MonoAndroid | monoandroid 已计算。 |
MonoMac | monomac 已计算。 |
MonoTouch | monotouch 已计算。 |
Tizen | tizen40 已计算。 tizen60 已计算。 |
Xamarin.iOS | xamarinios 已计算。 |
Xamarin.Mac | xamarinmac 已计算。 |
Xamarin.TVOS | xamarintvos 已计算。 |
Xamarin.WatchOS | xamarinwatchos 已计算。 |
NuGet 包 (3)
显示依赖于 Refit.Xml 的前 3 个 NuGet 包
包 | 下载 |
---|---|
MASTA.Camunda.Api.Client
为 .NET 平台设计的 Camunda REST API 客户端 |
|
NEnvoy
Enphase Envoy 客户端库 |
|
SSDDO.ECF_DGII.SDK
使用 Refit 定义的多米尼加共和国电子发票 API 服务 |
GitHub 仓库 (1)
显示依赖于 Refit.Xml 的前 1 个最受欢迎的 GitHub 仓库
仓库 | 星级 |
---|---|
ErsatzTV/ErsatzTV
使用您自己的媒体流式传输自定义直播频道
|