Refit 7.1.2
前缀已预留需要NuGet 2.12或更高版本。
dotnet add package Refit --version 7.1.2
NuGet\Install-Package Refit -Version 7.1.2
<PackageReference Include="Refit" Version="7.1.2" />
paket add Refit --version 7.1.2
#r "nuget: Refit, 7.1.2"
// Install Refit as a Cake Addin #addin nuget:?package=Refit&version=7.1.2 // Install Refit as a Cake Tool #tool nuget:?package=Refit&version=7.1.2
Refit: 为.NET Core、Xamarin和.NET提供的自动类型安全REST库
Refit | Refit.HttpClientFactory | Refit.Newtonsoft.Json | |
---|---|---|---|
NuGet |
重装是受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 Platform
SDK需求
Refit 6需要Visual Studio 16.8或更高版本,或.NET SDK 5.0.100或更高版本。它可以针对任何.NET Standard 2.0平台。
Refit 6不支持旧的packages.config
格式(因为这些不支持分析器/源生成器)。你必须将它们迁移到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<T>
→ ToHttpContent<T>
和DeserializeAsync<T>
→ FromHttpContentAsync<T>
。任何现有的实现都需要更新,尽管更改应该是微小的。
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参数的比较不区分大小写,所以如果你将参数命名为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,该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
实例的(您还需要添加对 Refit.Newtonsoft.Json
的 PackageReference
)
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 源生成器 的优势,您可以使用与自定义实例 RefitSettings
和 JsonSerializerOptions
的 SystemTextJsonContentSerializer
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 控件提供了许多序列化的选项,可以通过将 XmlContentSerializerSettings
实例传递给 XmlContentSerializer
构造函数来设置这些选项。
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>表单帖子
对于接受表单帖子(即序列化为 application/x-www-form-urlencoded
)的 API,请将 Body
属性初始化为 BodySerializationMethod.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);
或者你只需要传递任何对象,所有的 公共、可读 属性都将序列化为请求中的表单字段。此方法允许你使用 [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
属性应用在接口上,将静态头添加到 所有请求 中。
[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
属性应用到参数上并将访问令牌添加到请求中,在参数上应用 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 携带者身份验证。每个请求都会添加一个头的形式:Authorization: Bearer <token>
。Refit 使你可以轻松地在应用程序需要的方式中插入获取令牌的逻辑,因此你不需要将令牌传递给每个方法。
- 将
[Headers("Authorization: Bearer")]
添加到需要令牌的接口或方法上。 - 在
RefitSettings
实例中设置AuthorizationHeaderValueGetter
。每次 Refit 需要获取令牌时,它都会调用你的委托,因此最好在令牌有效期内缓存令牌值一段时间。
使用DelegatingHandlers减少头部的模板代码(示例:授权头)
尽管我们为在 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不同,其后置处理的头部不会相互覆盖,无论定义了多少次相同的头部,所有的头部都会添加到请求中,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
中传递运行时状态,可以为参数应用一个带有动态值的属性,并将其添加到底层的 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);
}
属性构造函数可以可选地接受一个字符串,该字符串将成为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
),则一种更干净的方法是使用如#801中描述的方式通过DelegatingHandler
注入。
目标接口类型和方法信息
有时您可能想知道Refit实例的目标接口类型。一个例子是您有一个派生接口,该接口实现了一个公共基类,如下所示
public interface IGetAPI<TEntity>
{
[Get("/{key}")]
Task<TEntity> Get(long key);
}
public interface IUsersAPI : IGetAPI<User>
{
}
public interface IOrdersAPI : IGetAPI<Order>
{
}
您可以通过访问接口的Concrete类型来在处理程序中使用它,例如修改请求的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 文件信息
多部分数据中字段的名称作为优先级顺序
- 如果指定且非空,则为 multipartItem.Name(可选);动态的,允许在执行时命名表单数据部分。
- 用于修饰方法签名中 streamPart 参数的 [AliasAs] 特性(可选);静态的,在代码中定义。
- 方法签名中定义的 MultipartItem 参数名称(默认值);静态的,在代码中定义。
可以使用可选的字符串参数指定自定义边界,用于 Multipart
特性。如果为空,则默认为 ----MyGreatBoundary
。
要对 byte[]
、Stream
和 FileInfo
参数指定文件名和内容类型,需要使用包装类。这些类型的包装类分别是 ByteArrayPart
、StreamPart
和 FileInfoPart
。
public interface ISomeApi
{
[Multipart]
[Post("/users/{id}/photo")]
Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart 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 Not Found”)的信息、响应版本、发送的原始请求消息,以及在出现错误的情况下,包含错误详细信息的 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 开始,可以在接口上定义默认接口方法(又称 DIMs)。Refit 接口可以使用 DIMs 提供额外的逻辑,可选地结合私有和/或静态辅助方法。
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提供了一级支持。在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
管理,而不是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
可以通过简单地传递参数到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接口方法返回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的NuGet包Serilog.Exceptions.Refit来丰富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
- System.Net.Http (>= 4.3.4)
- System.Net.Http.Json (>= 8.0.0)
- System.Text.Json (>= 8.0.3)
-
.NETStandard 2.0
- System.Net.Http.Json (>= 8.0.0)
- System.Text.Json (>= 8.0.3)
-
net6.0
- System.Net.Http.Json (>= 8.0.0)
-
net8.0
- System.Net.Http.Json (>= 8.0.0)
NuGet 包 (823)
显示依赖 Refit 的前 5 个 NuGet 包
包 | 下载 |
---|---|
Refit.HttpClientFactory Refit HTTP 客户端工厂扩展 |
|
Refit.Newtonsoft.Json Refit 序列化器 برای Newtonsoft.Json |
|
N3O.Umbraco.Extensions
待办事项 |
|
Mixandblend.Century21.SharedModel
包描述 |
|
Lykke.HttpClientGenerator
此包使用 Refit 库为 api 生成客户端代理。它为生成的代理添加了使用 Polly 的缓存和重试。用户逻辑也可以作为包装器添加到请求或方法调用中。 |
GitHub 仓库 (51)
显示依赖 Refit 的前 5 个最受欢迎的 GitHub 仓库
仓库 | 星星 |
---|---|
unoplatform/uno
使用 C# 和 XAML 构建移动、桌面和 WebAssembly 应用。今天即可开始。开源且专业支持。
|
|
reactiveui/refit
专为 .NET Core、Xamarin 和 .NET 设计的自动类型安全 REST 库。深受 Square 的 Retrofit 库启发,Refit 将您的 REST API 转换为实时接口。
|
|
microsoft/ailab
使用 Microsoft AI 体验、学习和编写最新的突破性创新。
|
|
elsa-workflows/elsa-core
一个 .NET 工作流库
|
|
LykosAI/StabilityMatrix
稳定扩散的多平台包管理器
|
版本 | 下载 | 最后更新 |
---|---|---|
7.1.2 | 729,481 | 6/30/2024 |
7.0.0 | 9,562,539 | 6/29/2023 |
6.3.2 | 22,383,814 | 2/8/2022 |
6.2.16 | 516,271 | 1/26/2022 |
6.1.15 | 3,620,547 | 10/14/2021 |
6.0.94 | 1,640,308 | 8/5/2021 |
6.0.38 | 2,274,561 | 3/19/2021 |
6.0.24 | 948,751 | 2/23/2021 |
6.0.21 | 11,605 | 2/23/2021 |
6.0.15 | 199,148 | 2/11/2021 |
6.0.8 | 75,940 | 2/10/2021 |
6.0.1 | 91,780 | 2/8/2021 |
6.0.0-preview.128 | 872 | 2/4/2021 |
6.0.0-preview.96 | 3,605 | 1/25/2021 |
6.0.0-preview.37 | 14,016 | 11/26/2020 |
5.2.4 | 7,889,846 | 11/26/2020 |
5.2.1 | 2,973,592 | 9/5/2020 |
5.1.67 | 4,601,547 | 4/8/2020 |
5.1.54 | 173,937 | 4/1/2020 |
5.1.27 | 353,062 | 3/23/2020 |
5.0.23 | 9,278,194 | 11/18/2019 |
5.0.15 | 187,976 | 11/14/2019 |
4.8.14 | 635,654 | 10/31/2019 |
4.7.51 | 1,626,162 | 8/20/2019 |
4.7.9 | 1,966,927 | 6/5/2019 |
4.6.107 | 1,401,669 | 4/12/2019 |
4.6.99 | 541,633 | 3/12/2019 |
4.6.90 | 1,244,235 | 2/18/2019 |
4.6.58 | 1,529,856 | 11/23/2018 |
4.6.48 | 2,044,550 | 10/5/2018 |
4.6.30 | 970,577 | 7/31/2018 |
4.6.16 | 640,378 | 6/20/2018 |
4.5.6 | 412,037 | 5/23/2018 |
4.4.17 | 316,809 | 5/15/2018 |
4.3.0 | 355,543 | 2/17/2018 |
4.2.0 | 204,827 | 1/31/2018 |
4.1.0 | 163,298 | 1/14/2018 |
4.0.1 | 451,554 | 8/27/2017 |
4.0.0 | 955,802 | 8/15/2017 |
3.1.0 | 237,785 | 6/6/2017 |
3.0.1 | 110,311 | 10/3/2016 |
3.0.0 | 57,646 | 8/15/2016 |
2.4.1 | 642,347 | 9/29/2015 |
2.4.0 | 13,797 | 9/24/2015 |
2.3.0 | 16,699 | 6/1/2015 |
2.2.1 | 26,111 | 2/8/2015 |
2.2.0 | 8,693 | 1/3/2015 |
2.1.0 | 4,683 | 11/14/2014 |
2.0.2 | 3,472 | 10/13/2014 |
2.0.1 | 3,133 | 10/13/2014 |
2.0.0 | 4,165 | 10/12/2014 |
1.3.0 | 4,260 | 7/25/2014 |
1.2.0 | 3,399 | 6/20/2014 |
1.1.0 | 3,354 | 5/2/2014 |
1.0.0 | 49,602 | 7/30/2013 |