Refit 7.1.2

前缀已预留

需要NuGet 2.12或更高版本。

dotnet add package Refit --version 7.1.2                
NuGet\Install-Package Refit -Version 7.1.2                
此命令旨在在Visual Studio的包管理器控制台中使用,因为它使用NuGet模块版本的Install-Package
<PackageReference Include="Refit" Version="7.1.2" />                
对于支持PackageReference的项目,将此XML节点复制到项目文件中以引用该包。
paket add Refit --version 7.1.2                
#r "nuget: Refit, 7.1.2"                
#r指令可以在F# Interactive和Polyglot Notebooks中使用。将此复制到交互式工具或脚本源代码中以引用包。
// 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

Refit: 为.NET Core、Xamarin和.NET提供的自动类型安全REST库

Build codecov

Refit Refit.HttpClientFactory Refit.Newtonsoft.Json
NuGet NuGet NuGet 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"));

目录

这个功能在哪里起作用?

当前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设置为NewtonsoftJsonContentSerializerSystem.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"
自定义查询字符串参数格式化

格式化键

要自定义查询键的格式,你有两个主要选项

  1. 使用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
    
  2. 使用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.JsonPackageReference

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 源生成器 的优势,您可以使用与自定义实例 RefitSettingsJsonSerializerOptionsSystemTextJsonContentSerializer

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 使你可以轻松地在应用程序需要的方式中插入获取令牌的逻辑,因此你不需要将令牌传递给每个方法。

  1. [Headers("Authorization: Bearer")] 添加到需要令牌的接口或方法上。
  2. RefitSettings 实例中设置 AuthorizationHeaderValueGetter。每次 Refit 需要获取令牌时,它都会调用你的委托,因此最好在令牌有效期内缓存令牌值一段时间。
使用DelegatingHandlers减少头部的模板代码(示例:授权头)

尽管我们为在 Refit 中直接添加运行时动态头提供了便利,但大多数用例可能仍然需要注册一个自定义的 DelegatingHandler,以将其作为 HttpClient 中间件管道的一部分注入头,从而消除添加大量 [Header][HeaderCollection] 属性的需要。

在上面的示例中,我们利用 [HeaderCollection] 参数注入 AuthorizationX-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.InterfaceTypeKeyHttpRequestMessageOptions.RestMethodInfoKey,分别用于从选项中访问接口类型和REST方法信息。

多部分上传

使用 Multipart 属性装饰的方法将以多部分内容类型提交。目前,多部分方法支持以下参数类型

  • string(参数名称将用作名称,字符串值用作值)
  • byte 数组
  • Stream 流
  • FileInfo 文件信息

多部分数据中字段的名称作为优先级顺序

  • 如果指定且非空,则为 multipartItem.Name(可选);动态的,允许在执行时命名表单数据部分。
  • 用于修饰方法签名中 streamPart 参数的 [AliasAs] 特性(可选);静态的,在代码中定义。
  • 方法签名中定义的 MultipartItem 参数名称(默认值);静态的,在代码中定义。

可以使用可选的字符串参数指定自定义边界,用于 Multipart 特性。如果为空,则默认为 ----MyGreatBoundary

要对 byte[]StreamFileInfo 参数指定文件名和内容类型,需要使用包装类。这些类型的包装类分别是 ByteArrayPartStreamPartFileInfoPart

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 接口将公开 GetResourceDeleteResource API,而 IDerivedServiceB 将公开 GetResourceAddResource

头继承

在使用继承时,现有的头属性也将传递,并且最内部的属性将具有优先级。

[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.FooIAmInterfaceB.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));

注意,由于HttpClientHttpClientHandlers将由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 已计算。
兼容的目标框架
包括的目标框架(在包中)
了解更多关于 目标框架.NET Standard

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