Refit.HttpClientFactory 7.1.2
前缀已保留
需要 NuGet 2.12 或更高版本。
dotnet add package Refit.HttpClientFactory --version 7.1.2
NuGet\Install-Package Refit.HttpClientFactory -Version 7.1.2
<PackageReference Include="Refit.HttpClientFactory" Version="7.1.2" />
paket add Refit.HttpClientFactory --version 7.1.2
#r "nuget: Refit.HttpClientFactory, 7.1.2"
// Install Refit.HttpClientFactory as a Cake Addin #addin nuget:?package=Refit.HttpClientFactory&version=7.1.2 // Install Refit.HttpClientFactory as a Cake Tool #tool nuget:?package=Refit.HttpClientFactory&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 类生成了使用 HttpClient 进行调用的 IGitHubApi
的实现
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 Platform
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 中的重大变更
Refit 6将System.Text.Json作为默认的JSON序列化程序。如果您想继续使用Newtonsoft.Json
,请添加Refit.Newtonsoft.Json
NuGet包,并在您的RefitSettings
实例上将ContentSerializer
设置为NewtonsoftJsonContentSerializer
。System.Text.Json
速度更快,使用的内存更少,尽管不是所有功能都受支持。《迁移指南》包含更多详细信息。
IContentSerializer
已被重命名为IHttpContentSerializer
,以更好地反映其目的。此外,其中的两个方法已被重命名,SerializeAsync
→ ToHttpContent
和 DeserializeAsync
→ FromHttpContentAsync
。任何现有的实现都需要更新,尽管更改应该是微小的。
6.3版本更新
Refit 6.3将XML序列化通过XmlContentSerializer
分割为单独的包,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参数之间的比较是不区分大小写的,因此如果您在路径 /group/{groupid}/show
中将参数命名为 groupId
,它可以正常工作。
[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
标头。如果您的 API 需要您在请求中发送 Content-Length
标头,您可以通过将 [Body]
属性的 buffered
参数设置为 true
来禁用此流式传输行为。
Task CreateUser([Body(buffered: true)] User user);
JSON内容
JSON 请求和响应使用 IHttpContentSerializer
接口实例进行序列化和反序列化。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 中添加的新的 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 内容,请配置内容序列化器以使用 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);
或者,您可以传递任何对象,并且所有 public, readable 属性都将作为请求中的表单字段序列化。这种方法允许您使用 [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 将也会使用这些别名(当两者同时存在时,[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
中间件管道的一部分注入,从而无需添加大量的 [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 中的操作过滤器类似的方法——**重新定义一个头部将取代它**,以下是有优先级的顺序
- 接口上的
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);
}
Attribute
构造函数可以可选地接受一个字符串,该字符串将成为HttpRequestMessage.Properties
字典中的键。如果没有明确定义键,则参数的名称成为键。如果定义了多个相同的键,则HttpRequestMessage.Properties
中的值将被覆盖。参数本身可以是任何object
。属性可以在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 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);
}
}
注意:在.NET 5中,HttpRequestMessage.Properties
已被标记为过时
,并且Refit将使用新的HttpRequestMessage.Options
来填充值。
支持Polly和Polly.Context
由于Refit支持HttpClientFactory
,因此您可以为您的HttpClient配置Polly策略。如果您的策略使用Polly.Context
,则可以通过Refit传递它,方法是添加[Property("PolicyExecutionContext")] Polly.Context context
,因为幕后Polly.Context
只是简单地在键PolicyExecutionContext
下存储在HttpRequestMessage.Properties
中,并且是类型Polly.Context
。仅在需要用仅在实际运行时才可知的内容初始化Polly.Context
的情况下建议以此方式传递Polly.Context
。如果您的Polly.Context
每次都需要相同的内容(例如,您想在其策略内部使用的ILogger
)则更干净的方法是通过DelegatingHandler
注入Polly.Context
,如此处所述。
目标接口类型和方法信息
有时您可能想知道Refit实例的目标接口类型。一个例子是在您有一个派生接口实现通用基类的情况下。
public interface IGetAPI<TEntity>
{
[Get("/{key}")]
Task<TEntity> Get(long key);
}
public interface IUsersAPI : IGetAPI<User>
{
}
public interface IOrdersAPI : IGetAPI<Order>
{
}
您可以通过处理程序访问接口的实体类型,例如更改请求的URL。
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);
}
}
完整的方法信息(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 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);
}
}
注意:在.NET 5中,HttpRequestMessage.Properties
已被标记为过时
,并且Refit将使用新的HttpRequestMessage.Options
来填充值。Refit提供了HttpRequestMessageOptions.InterfaceTypeKey
和HttpRequestMessageOptions.RestMethodInfoKey
,分别用于从选项中访问接口类型和REST方法信息。
多部分上传
装饰有Multipart
属性的函数将以多部分内容类型提交。到目前为止,多部分方法支持以下参数类型:
- string(参数名称将被用作名称,字符串值作为值)
- byte数组
- Stream
- FileInfo
多部分数据中字段的名称优先级
- 如果指定且不为null,则multipartItem.Name(可选);动态的,允许在执行时命名表单数据部分。
- [AliasAs]属性(可选)装饰方法签名中的streamPart参数(见下文);静态的,在代码中定义。
- 多部分项参数名称(默认值)如方法签名中定义;静态的,在代码中定义。
可以使用可选的字符串参数通过Multipart
属性指定自定义边界。如果为空,则默认为----MyGreatBoundary
。
要为byte[]、Stream和FileInfo参数指定文件名和内容类型,需要使用包装类。这些类型的包装类是ByteArrayPart、StreamPart和FileInfoPart。
public interface ISomeApi
{
[Multipart]
[Post("/users/{id}/photo")]
Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart stream);
}
要将Stream传递给此方法,请构建一个StreamPart对象,如下所示
someApiInstance.UploadPhoto(id, new StreamPart(myPhotoStream, "photo.jpg", "image/jpeg"));
注意:本节中先前描述的AttachmentName属性已被弃用,并不建议使用。
检索响应
请注意,在Refit中与Retrofit不同,没有同步网络请求的选项——所有请求都必须是异步的,无论是通过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未找到)、响应版本、发送的原始请求消息等信息。下面是一些如何检索响应元数据的示例。
//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)。使用DIM,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 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生成的类型。IApiClient
的消费者将调用公共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。将对Refit.HttpClientFactory
的引用添加到配置中,并在您的ConfigureServices
方法中调用提供的扩展方法来配置您的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而不是Refit管理,所以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>
方法来提供它。
RestService.For<ISomeApi>(new HttpClient()
{
BaseAddress = new Uri("https://www.someapi.com/api/")
});
但是,在提供自定义的HttpClient
实例时,以下RefitSettings
属性将不会生效:
AuthorizationHeaderValueGetter
HttpMessageHandlerFactory
如果您仍然想配置Refit
提供的HtttpClient
实例,同时仍然使用上述设置,请简单地在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接口方法返回的是Task<T>
还是Task<IApiResponse>
、Task<IApiResponse<T>>
或Task<ApiResponse<T>>
,Refit有不同的异常处理行为。
<a id="when-returning-taskapiresponset"></a>当返回Task<IApiResponse>
、Task<IApiResponse<T>>
或Task<ApiResponse<T>>
时
Refit在处理响应时捕捉由ExceptionFactory
抛出的任何ApiException
,并将任何传入的响应反序列化到ApiResponse<T>
时发生的错误填充到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规范的问题详情,并且响应内容类型为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通过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 已计算。 |
-
.NETFramework 4.6.2
- Microsoft.Extensions.Http (>= 8.0.0)
- Refit (>= 7.1.2)
-
.NETStandard 2.0
- Microsoft.Extensions.Http (>= 8.0.0)
- Refit (>= 7.1.2)
-
net6.0
- Microsoft.Extensions.Http (>= 8.0.0)
- Refit (>= 7.1.2)
-
net8.0
- Microsoft.Extensions.Http (>= 8.0.0)
- Refit (>= 7.1.2)
NuGet 包 (265)
显示依赖于 Refit.HttpClientFactory 的前 5 个 NuGet 包
包 | 下载 |
---|---|
Chargily.EpayGateway.NET
C# .NET 库,用于使用 Chargily Epay 支付网关 |
|
Uno.Extensions.Http.Refit 为 Uno Platform、UWP 和 WinUI 注册 Refit 端点的 HTTP Refit 扩展 |
|
Apple.Receipt.Verificator
用于验证 App Store 收据的库 |
|
TinyFx
TinyFx 基础库 |
|
Lombiq.Tests.UI Orchard Core Lombiq UI 测试工具箱:主要用于 Orcha Core 应用的 Web UI 测试工具箱。这里包含了使用 Selenium 进行 Orcha 应用的 UI 测试所需的一切。有关详细文档,请参阅项目网站。 |
GitHub 仓库 (33)
显示依赖于 Refit.HttpClientFactory 的前 5 个最受欢迎的 GitHub 仓库
仓库 | 星数 |
---|---|
dotnet/AspNetCore.Docs
ASP.NET Core 文档
|
|
elsa-workflows/elsa-core
一个 .NET 工作流库
|
|
LykosAI/StabilityMatrix
Stable Diffusion 的多平台包管理器
|
|
SciSharp/BotSharp
.NET 中的 AI 代理框架
|
|
dotnetcore/WebApiClient
一个功能更强大、性能更高、可扩展性更好的 REST API 库,优于 refit
|
版本 | 下载 | 最后更新 |
---|---|---|
7.1.2 | 327,138 | 6/30/2024 |
7.1.1 | 67,897 | 6/24/2024 |
7.1.0 | 49,378 | 6/20/2024 |
7.0.0 | 7,070,169 | 6/29/2023 |
7.0.0-beta.1 | 20,160 | 5/14/2023 |
6.3.2 | 14,657,324 | 2/8/2022 |
6.2.16 | 397,053 | 1/26/2022 |
6.1.15 | 2,408,971 | 10/14/2021 |
6.0.94 | 1,184,729 | 8/5/2021 |
6.0.38 | 1,329,045 | 3/19/2021 |
6.0.24 | 234,453 | 2/23/2021 |
6.0.21 | 4,535 | 2/23/2021 |
6.0.15 | 45,924 | 2/11/2021 |
6.0.8 | 25,594 | 2/10/2021 |
6.0.1 | 67,682 | 2/8/2021 |
6.0.0-preview.128 | 681 | 2/4/2021 |
6.0.0-preview.124 | 217 | 2/4/2021 |
6.0.0-preview.121 | 223 | 2/4/2021 |
6.0.0-preview.96 | 2,274 | 1/25/2021 |
6.0.0-preview.94 | 320 | 1/24/2021 |
6.0.0-preview.86 | 268 | 1/24/2021 |
6.0.0-preview.84 | 255 | 1/23/2021 |
6.0.0-preview.37 | 5,655 | 11/26/2020 |
6.0.0-preview.34 | 1,704 | 11/26/2020 |
5.2.4 | 5,016,540 | 11/26/2020 |
5.2.1 | 1,486,932 | 9/5/2020 |
5.1.67 | 2,344,781 | 4/8/2020 |
5.1.54 | 117,095 | 4/1/2020 |
5.1.27 | 143,141 | 3/23/2020 |
5.0.23 | 1,171,959 | 11/18/2019 |
5.0.15 | 44,613 | 11/14/2019 |
4.8.14 | 154,113 | 10/31/2019 |
4.7.51 | 858,262 | 8/20/2019 |
4.7.9 | 960,758 | 6/5/2019 |
4.7.5 | 28,821 | 6/5/2019 |
4.6.107 | 201,887 | 4/12/2019 |
4.6.99 | 148,613 | 3/12/2019 |
4.6.90 | 479,627 | 2/18/2019 |
4.6.58 | 485,897 | 11/23/2018 |
4.6.48 | 96,006 | 10/5/2018 |
4.6.30 | 241,121 | 7/31/2018 |
4.6.16 | 216,729 | 6/20/2018 |