Refit.Newtonsoft.Json
7.1.2
Prefix Reserved
See the version list below for details.
Requires NuGet 2.12 or higher.
dotnet add package Refit.Newtonsoft.Json --version 7.1.2
NuGet\Install-Package Refit.Newtonsoft.Json -Version 7.1.2
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.1.2" />
paket add Refit.Newtonsoft.Json --version 7.1.2
#r "nuget: Refit.Newtonsoft.Json, 7.1.2"
// Install Refit.Newtonsoft.Json as a Cake Addin #addin nuget:?package=Refit.Newtonsoft.Json&version=7.1.2 // Install Refit.Newtonsoft.Json as a Cake Tool #tool nuget:?package=Refit.Newtonsoft.Json&version=7.1.2
Refit: The automatic type-safe REST library for .NET Core, Xamarin and .NET
Refit | Refit.HttpClientFactory | Refit.Newtonsoft.Json | |
---|---|---|---|
NuGet |
Refit is a library heavily inspired by Square's Retrofit library, and it turns your REST API into a live interface:
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<User> GetUser(string user);
}
The RestService
class generates an implementation of IGitHubApi
that uses
HttpClient
to make its calls:
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
var octocat = await gitHubApi.GetUser("octocat");
.NET Core supports registering via HttpClientFactory
services
.AddRefitClient<IGitHubApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));
Table of Contents
- Where does this work?
- API Attributes
- Querystrings
- Body content
- Setting request headers
- Passing state into DelegatingHandlers
- Multipart uploads
- Retrieving the response
- Using generic interfaces
- Interface inheritance
- Default Interface Methods
- Using HttpClientFactory
- Providing a custom HttpClient
- Handling exceptions
Where does this work?
Refit currently supports the following platforms and any .NET Standard 2.0 target:
- UWP
- Xamarin.Android
- Xamarin.Mac
- Xamarin.iOS
- Desktop .NET 4.6.1
- .NET 5 / .NET Core
- Blazor
- Uno Platform
SDK Requirements
Refit 6 requires Visual Studio 16.8 or higher, or the .NET SDK 5.0.100 or higher. It can target any .NET Standard 2.0 platform.
Refit 6 does not support the old packages.config
format for NuGet references (as they do not support analyzers/source generators). You must
migrate to PackageReference to use Refit v6 and later.
Breaking changes in 6.x
Refit 6 makes System.Text.Json the default JSON serializer. If you'd like to continue to use Newtonsoft.Json
, add the Refit.Newtonsoft.Json
NuGet package and set your ContentSerializer
to NewtonsoftJsonContentSerializer
on your RefitSettings
instance. System.Text.Json
is faster and uses less memory, though not all features are supported. The migration guide contains more details.
IContentSerializer
was renamed to IHttpContentSerializer
to better reflect its purpose. Additionally, two of its methods were renamed, SerializeAsync<T>
→ ToHttpContent<T>
and DeserializeAsync<T>
→ FromHttpContentAsync<T>
. Any existing implementations of these will need to be updated, though the changes should be minor.
Updates in 6.3
Refit 6.3 splits out the XML serialization via XmlContentSerializer
into a separate package, Refit.Xml
. This
is to reduce the dependency size when using Refit with Web Assembly (WASM) applications. If you require XML, add a reference
to Refit.Xml
.
API Attributes
Every method must have an HTTP attribute that provides the request method and relative URL. There are six built-in annotations: Get, Post, Put, Delete, Patch and Head. The relative URL of the resource is specified in the annotation.
[Get("/users/list")]
You can also specify query parameters in the URL:
[Get("/users/list?sort=desc")]
A request URL can be updated dynamically using replacement blocks and parameters on the method. A replacement block is an alphanumeric string surrounded by { and }.
If the name of your parameter doesn't match the name in the URL path, use the
AliasAs
attribute.
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId);
A request url can also bind replacement blocks to a custom object
[Get("/group/{request.groupId}/users/{request.userId}")]
Task<List<User>> GroupList(UserGroupRequest request);
class UserGroupRequest{
int groupId { get;set; }
int userId { get;set; }
}
Parameters that are not specified as a URL substitution will automatically be used as query parameters. This is different than Retrofit, where all parameters must be explicitly specified.
The comparison between parameter name and URL parameter is not
case-sensitive, so it will work correctly if you name your parameter groupId
in the path /group/{groupid}/show
for example.
[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);
GroupList(4, "desc");
>>> "/group/4/users?sort=desc"
Round-tripping route parameter syntax: Forward slashes aren't encoded when using a double-asterisk (**) catch-all parameter syntax.
During link generation, the routing system encodes the value captured in a double-asterisk (**) catch-all parameter (for example, {**myparametername}) except the forward slashes.
The type of round-tripping route parameter must be string.
[Get("/search/{**page}")]
Task<List<Page>> Search(string page);
Search("admin/products");
>>> "/search/admin/products"
Querystrings
Dynamic Querystring Parameters
If you specify an object
as a query parameter, all public properties which are not null are used as query parameters.
This previously only applied to GET requests, but has now been expanded to all HTTP request methods, partly thanks to Twitter's hybrid API that insists on non-GET requests with querystring parameters.
Use the Query
attribute to change the behavior to 'flatten' your query parameter object. If using this Attribute you can specify values for the Delimiter and the Prefix which are used to 'flatten' the object.
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"
A similar behavior exists if using a Dictionary, but without the advantages of the AliasAs
attributes and of course no intellisense and/or type safety.
You can also specify querystring parameters with [Query] and have them flattened in non-GET requests, similar to:
[Post("/statuses/update.json")]
Task<Tweet> PostTweet([Query]TweetParams params);
Where TweetParams
is a POCO, and properties will also support [AliasAs]
attributes.
Collections as Querystring parameters
Use the Query
attribute to specify format in which collections should be formatted in query string
[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"
You can also specify collection format in RefitSettings
, that will be used by default, unless explicitly defined in Query
attribute.
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
CollectionFormat = CollectionFormat.Multi
});
Unescape Querystring parameters
Use the QueryUriFormat
attribute to specify if the query parameters should be url escaped
[Get("/query")]
[QueryUriFormat(UriFormat.Unescaped)]
Task Query(string q);
Query("Select+Id,Name+From+Account")
>>> "/query?q=Select+Id,Name+From+Account"
Custom Querystring parameter formatting
Formatting Keys
To customize the format of query keys, you have two main options:
Using the
AliasAs
Attribute:You can use the
AliasAs
attribute to specify a custom key name for a property. This attribute will always take precedence over any key formatter you specify.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);
This will generate the following request:
/group/1/users?order=desc&Limit=10
Using the
RefitSettings.UrlParameterKeyFormatter
Property:By default, Refit uses the property name as the query key without any additional formatting. If you want to apply a custom format across all your query keys, you can use the
UrlParameterKeyFormatter
property. Remember that if a property has anAliasAs
attribute, it will be used regardless of the formatter.The following example uses the built-in
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;
The request will look like:
/group/users?sortOrder=desc&queryLimit=10
Note: The AliasAs
attribute always takes the top priority. If both the attribute and a custom key formatter are present, the AliasAs
attribute's value will be used.
Formatting URL Parameter Values with the UrlParameterFormatter
In Refit, the UrlParameterFormatter
property within RefitSettings
allows you to customize how parameter values are formatted in the URL. This can be particularly useful when you need to format dates, numbers, or other types in a specific manner that aligns with your API's expectations.
Using UrlParameterFormatter
:
Assign a custom formatter that implements the IUrlParameterFormatter
interface to the UrlParameterFormatter
property.
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()
};
In this example, a custom formatter is created for date values. Whenever a DateTime
parameter is encountered, it formats the date as yyyyMMdd
.
Formatting Dictionary Keys:
When dealing with dictionaries, it's important to note that keys are treated as values. If you need custom formatting for dictionary keys, you should use the UrlParameterFormatter
as well.
For instance, if you have a dictionary parameter and you want to format its keys in a specific way, you can handle that in the custom formatter:
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()
};
In the above example, the dictionary keys will be converted to uppercase.
Body content
One of the parameters in your method can be used as the body, by using the Body attribute:
[Post("/users/new")]
Task CreateUser([Body] User user);
There are four possibilities for supplying the body data, depending on the type of the parameter:
- If the type is
Stream
, the content will be streamed viaStreamContent
- If the type is
string
, the string will be used directly as the content unless[Body(BodySerializationMethod.Json)]
is set which will send it as aStringContent
- If the parameter has the attribute
[Body(BodySerializationMethod.UrlEncoded)]
, the content will be URL-encoded (see form posts below) - For all other types, the object will be serialized using the content serializer specified in RefitSettings (JSON is the default).
Buffering and the Content-Length
header
By default, Refit streams the body content without buffering it. This means you can
stream a file from disk, for example, without incurring the overhead of loading
the whole file into memory. The downside of this is that no Content-Length
header
is set on the request. If your API needs you to send a Content-Length
header with
the request, you can disable this streaming behavior by setting the buffered
argument
of the [Body]
attribute to true
:
Task CreateUser([Body(buffered: true)] User user);
JSON content
JSON requests and responses are serialized/deserialized using an instance of the IHttpContentSerializer
interface. Refit provides two implementations out of the box: SystemTextJsonContentSerializer
(which is the default JSON serializer) and NewtonsoftJsonContentSerializer
. The first uses System.Text.Json
APIs and is focused on high performance and low memory usage, while the latter uses the known Newtonsoft.Json
library and is more versatile and customizable. You can read more about the two serializers and the main differences between the two at this link.
For instance, here is how to create a new RefitSettings
instance using the Newtonsoft.Json
-based serializer (you'll also need to add a PackageReference
to Refit.Newtonsoft.Json
):
var settings = new RefitSettings(new NewtonsoftJsonContentSerializer());
If you're using Newtonsoft.Json
APIs, you can customize their behavior by setting the Newtonsoft.Json.JsonConvert.DefaultSettings
property:
JsonConvert.DefaultSettings =
() => new JsonSerializerSettings() {
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = {new StringEnumConverter()}
};
// Serialized as: {"day":"Saturday"}
await PostSomeStuff(new { Day = DayOfWeek.Saturday });
As these are global settings they will affect your entire application. It
might be beneficial to isolate the settings for calls to a particular API.
When creating a Refit generated live interface, you may optionally pass a
RefitSettings
that will allow you to specify what serializer settings you
would like. This allows you to have different serializer settings for separate
APIs:
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()
}
)});
Property serialization/deserialization can be customised using Json.NET's JsonProperty attribute:
public class Foo
{
// Works like [AliasAs("b")] would in form posts (see below)
[JsonProperty(PropertyName="b")]
public string Bar { get; set; }
}
JSON source generator
To apply the benefits of the new JSON source generator for System.Text.Json added in .NET 6, you can use SystemTextJsonContentSerializer
with a custom instance of RefitSettings
and JsonSerializerOptions
:
var options = new JsonSerializerOptions();
options.AddContext<MyJsonSerializerContext>();
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
ContentSerializer = new SystemTextJsonContentSerializer(options)
});
XML Content
XML requests and responses are serialized/deserialized using System.Xml.Serialization.XmlSerializer.
By default, Refit will use JSON content serialization, to use XML content configure the ContentSerializer to use the XmlContentSerializer
:
var gitHubApi = RestService.For<IXmlApi>("https://www.w3.org/XML",
new RefitSettings {
ContentSerializer = new XmlContentSerializer()
});
Property serialization/deserialization can be customised using attributes found in the System.Xml.Serialization namespace:
public class Foo
{
[XmlElement(Namespace = "https://www.w3.org/XML")]
public string Bar { get; set; }
}
The System.Xml.Serialization.XmlSerializer provides many options for serializing, those options can be set by providing an XmlContentSerializerSettings
to the XmlContentSerializer
constructor:
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>Form posts
For APIs that take form posts (i.e. serialized as application/x-www-form-urlencoded
),
initialize the Body attribute with BodySerializationMethod.UrlEncoded
.
The parameter can be an 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);
Or you can just pass any object and all public, readable properties will
be serialized as form fields in the request. This approach allows you to alias
property names using [AliasAs("whatever")]
which can help if the API has
cryptic field names:
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);
If you have a type that has [JsonProperty(PropertyName)]
attributes setting property aliases, Refit will use those too ([AliasAs]
will take precedence where you have both).
This means that the following type will serialize as 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; }
}
NOTE: This use of AliasAs
applies to querystring parameters and form body posts, but not to response objects; for aliasing fields on response objects, you'll still need to use [JsonProperty("full-property-name")]
.
Setting request headers
Static headers
You can set one or more static request headers for a request applying a Headers
attribute to the method:
[Headers("User-Agent: Awesome Octocat App")]
[Get("/users/{user}")]
Task<User> GetUser(string user);
Static headers can also be added to every request in the API by applying the
Headers
attribute to the interface:
[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);
}
Dynamic headers
If the content of the header needs to be set at runtime, you can add a header
with a dynamic value to a request by applying a Header
attribute to a parameter:
[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");
Adding an Authorization
header is such a common use case that you can add an access token to a request by applying an Authorize
attribute to a parameter and optionally specifying the scheme:
[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
If you need to set multiple headers at runtime, you can add a IDictionary<string, string>
and apply a HeaderCollection
attribute to the parameter and it will inject the headers into the request:
[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 Authentication
Most APIs need some sort of Authentication. The most common is OAuth Bearer authentication. A header is added to each request of the form: Authorization: Bearer <token>
. Refit makes it easy to insert your logic to get the token however your app needs, so you don't have to pass a token into each method.
- Add
[Headers("Authorization: Bearer")]
to the interface or methods which need the token. - Set
AuthorizationHeaderValueGetter
in theRefitSettings
instance. Refit will call your delegate each time it needs to obtain the token, so it's a good idea for your mechanism to cache the token value for some period within the token lifetime.
Reducing header boilerplate with DelegatingHandlers (Authorization headers worked example)
Although we make provisions for adding dynamic headers at runtime directly in Refit,
most use-cases would likely benefit from registering a custom DelegatingHandler
in order to inject the headers as part of the HttpClient
middleware pipeline
thus removing the need to add lots of [Header]
or [HeaderCollection]
attributes.
In the example above we are leveraging a [HeaderCollection]
parameter to inject an Authorization
and X-Tenant-Id
header.
This is quite a common scenario if you are integrating with a 3rd party that uses OAuth2. While it's ok for the occasional endpoint,
it would be quite cumbersome if we had to add that boilerplate to every method in our interface.
In this example we will assume our application is a multi-tenant application that is able to pull information about a tenant through
some interface ITenantProvider
and has a data store IAuthTokenStore
that can be used to retrieve an auth token to attach to the outbound request.
//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
}
}
If you aren't using dependency injection then you could achieve the same thing by doing something like this:
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
Redefining headers
Unlike Retrofit, where headers do not overwrite each other and are all added to the request regardless of how many times the same header is defined, Refit takes a similar approach to the approach ASP.NET MVC takes with action filters — redefining a header will replace it, in the following order of precedence:
Headers
attribute on the interface (lowest priority)Headers
attribute on the methodHeader
attribute orHeaderCollection
attribute on a method parameter (highest priority)
[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:");
Note: This redefining behavior only applies to headers with the same name. Headers with different names are not replaced. The following code will result in all headers being included:
[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);
Removing headers
Headers defined on an interface or method can be removed by redefining
a static header without a value (i.e. without : <value>
) or passing null
for
a dynamic header. Empty strings will be included as empty headers.
[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, "");
Passing state into DelegatingHandlers
If there is runtime state that you need to pass to a DelegatingHandler
you can add a property with a dynamic value to the underlying HttpRequestMessage.Properties
by applying a Property
attribute to a parameter:
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);
}
The attribute constructor optionally takes a string which becomes the key in the HttpRequestMessage.Properties
dictionary.
If no key is explicitly defined then the name of the parameter becomes the key.
If a key is defined multiple times the value in HttpRequestMessage.Properties
will be overwritten.
The parameter itself can be any object
. Properties can be accessed inside a DelegatingHandler
as follows:
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);
}
}
Note: in .NET 5 HttpRequestMessage.Properties
has been marked Obsolete
and Refit will instead populate the value into the new HttpRequestMessage.Options
.
Support for Polly and Polly.Context
Because Refit supports HttpClientFactory
it is possible to configure Polly policies on your HttpClient.
If your policy makes use of Polly.Context
this can be passed via Refit by adding [Property("PolicyExecutionContext")] Polly.Context context
as behind the scenes Polly.Context
is simply stored in HttpRequestMessage.Properties
under the key PolicyExecutionContext
and is of type Polly.Context
. It's only recommended to pass the Polly.Context
this way if your use case requires that the Polly.Context
be initialized with dynamic content only known at runtime. If your Polly.Context
only requires the same content every time (e.g an ILogger
that you want to use to log from inside your policies) a cleaner approach is to inject the Polly.Context
via a DelegatingHandler
as described in #801
Target Interface Type and method info
There may be times when you want to know what the target interface type is of the Refit instance. An example is where you have a derived interface that implements a common base like this:
public interface IGetAPI<TEntity>
{
[Get("/{key}")]
Task<TEntity> Get(long key);
}
public interface IUsersAPI : IGetAPI<User>
{
}
public interface IOrdersAPI : IGetAPI<Order>
{
}
You can access the concrete type of the interface for use in a handler, such as to alter the URL of the request:
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);
}
}
The full method information (RestMethodInfo
) is also always available in the request options. The RestMethodInfo
contains more information about the method being called such as the full MethodInfo
when using reflection is needed:
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);
}
}
Note: in .NET 5 HttpRequestMessage.Properties
has been marked Obsolete
and Refit will instead populate the value into the new HttpRequestMessage.Options
. Refit provides HttpRequestMessageOptions.InterfaceTypeKey
and HttpRequestMessageOptions.RestMethodInfoKey
to respectively access the interface type and REST method info from the options.
Multipart uploads
Methods decorated with Multipart
attribute will be submitted with multipart content type.
At this time, multipart methods support the following parameter types:
- string (parameter name will be used as name and string value as value)
- byte array
- Stream
- FileInfo
Name of the field in the multipart data priority precedence:
- multipartItem.Name if specified and not null (optional); dynamic, allows naming form data part at execution time.
- [AliasAs] attribute (optional) that decorate the streamPart parameter in the method signature (see below); static, defined in code.
- MultipartItem parameter name (default) as defined in the method signature; static, defined in code.
A custom boundary can be specified with an optional string parameter to the Multipart
attribute. If left empty, this defaults to ----MyGreatBoundary
.
To specify the file name and content type for byte array (byte[]
), Stream
and FileInfo
parameters, use of a wrapper class is required.
The wrapper classes for these types are ByteArrayPart
, StreamPart
and FileInfoPart
.
public interface ISomeApi
{
[Multipart]
[Post("/users/{id}/photo")]
Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart stream);
}
To pass a Stream to this method, construct a StreamPart object like so:
someApiInstance.UploadPhoto(id, new StreamPart(myPhotoStream, "photo.jpg", "image/jpeg"));
Note: The AttachmentName attribute that was previously described in this section has been deprecated and its use is not recommended.
Retrieving the response
Note that in Refit unlike in Retrofit, there is no option for a synchronous
network request - all requests must be async, either via Task
or via
IObservable
. There is also no option to create an async method via a Callback
parameter unlike Retrofit, because we live in the async/await future.
Similarly to how body content changes via the parameter type, the return type will determine the content returned.
Returning Task without a type parameter will discard the content and solely tell you whether or not the call succeeded:
[Post("/users/new")]
Task CreateUser([Body] User user);
// This will throw if the network call fails
await CreateUser(someUser);
If the type parameter is 'HttpResponseMessage' or 'string', the raw response message or the content as a string will be returned respectively.
// 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);
There is also a generic wrapper class called ApiResponse<T>
that can be used as a return type. Using this class as a return type allows you to retrieve not just the content as an object, but also any metadata associated with the request/response. This includes information such as response headers, the http status code and reason phrase (e.g. 404 Not Found), the response version, the original request message that was sent and in the case of an error, an ApiException
object containing details of the error. Following are some examples of how you can retrieve the response metadata.
//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;
Using generic interfaces
When using something like ASP.NET Web API, it's a fairly common pattern to have a whole stack of CRUD REST services. Refit now supports these, allowing you to define a single API interface with a generic type:
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);
}
Which can be used like this:
// 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");
Interface inheritance
When multiple services that need to be kept separate share a number of APIs, it is possible to leverage interface inheritance to avoid having to define the same Refit methods multiple times in different services:
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);
}
In this example, the IDerivedServiceA
interface will expose both the GetResource
and DeleteResource
APIs, while IDerivedServiceB
will expose GetResource
and AddResource
.
Headers inheritance
When using inheritance, existing header attributes will be passed along as well, and the inner-most ones will have precedence:
[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();
}
Here, IAmInterfaceB.Pang()
will use PANG
as its user agent, while IAmInterfaceB.Foo
and IAmInterfaceB.Ping
will use BBB
.
Note that if IAmInterfaceB
didn't have a header attribute, Foo
would then use the AAA
value inherited from IAmInterfaceA
.
If an interface is inheriting more than one interface, the order of precedence is the same as the one in which the inherited interfaces are declared:
public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB
{
[Get("/get?result=Foo")]
Task<string> Foo();
}
Here IAmInterfaceC.Foo
would use the header attribute inherited from IAmInterfaceA
, if present, or the one inherited from IAmInterfaceB
, and so on for all the declared interfaces.
Default Interface Methods
Starting with C# 8.0, default interface methods (a.k.a. DIMs) can be defined on interfaces. Refit interfaces can provide additional logic using DIMs, optionally combined with private and/or static helper methods:
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}";
}
The type generated by Refit will implement the method IApiClient.GetInternal
. If additional logic is required immediately before or after its invocation, it shouldn't be exposed directly and can thus be hidden from consumers by being marked as internal
.
The default interface method IApiClient.Get
will be inherited by all types implementing IApiClient
, including - of course - the type generated by Refit.
Consumers of the IApiClient
will call the public Get
method and profit from the additional logic provided in its implementation (optionally, in this case, with the help of the private static helper FormatResponse
).
To support runtimes without DIM-support (.NET Core 2.x and below or .NET Standard 2.0 and below), two additional types would be required for the same solution.
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}";
}
Using HttpClientFactory
Refit has first class support for the ASP.Net Core 2.1 HttpClientFactory. Add a reference to Refit.HttpClientFactory
and call
the provided extension method in your ConfigureServices
method to configure your Refit interface:
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));
Optionally, a RefitSettings
object can be included:
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));
Note that some of the properties of RefitSettings
will be ignored because the HttpClient
and HttpClientHandlers
will be managed by the HttpClientFactory
instead of Refit.
You can then get the api interface using constructor injection:
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);
}
}
Providing a custom HttpClient
You can supply a custom HttpClient
instance by simply passing it as a parameter to the RestService.For<T>
method:
RestService.For<ISomeApi>(new HttpClient()
{
BaseAddress = new Uri("https://www.someapi.com/api/")
});
However, when supplying a custom HttpClient
instance the following RefitSettings
properties will not work:
AuthorizationHeaderValueGetter
HttpMessageHandlerFactory
If you still want to be able to configure the HtttpClient
instance that Refit
provides while still making use of the above settings, simply expose the HttpClient
on the API interface:
interface ISomeApi
{
// This will automagically be populated by Refit if the property exists
HttpClient Client { get; }
[Headers("Authorization: Bearer")]
[Get("/endpoint")]
Task<string> SomeApiEndpoint();
}
Then, after creating the REST service, you can set any HttpClient
property you want, e.g. Timeout
:
SomeApi = RestService.For<ISomeApi>("https://www.someapi.com/api/", new RefitSettings()
{
AuthorizationHeaderValueGetter = (rq, ct) => GetTokenAsync()
});
SomeApi.Client.Timeout = timeout;
Handling exceptions
Refit has different exception handling behavior depending on if your Refit interface methods return Task<T>
or if they return Task<IApiResponse>
, Task<IApiResponse<T>>
, or Task<ApiResponse<T>>
.
<a id="when-returning-taskapiresponset"></a>When returning Task<IApiResponse>
, Task<IApiResponse<T>>
, or Task<ApiResponse<T>>
Refit traps any ApiException
raised by the ExceptionFactory
when processing the response, and any errors that occur when attempting to deserialize the response to ApiResponse<T>
, and populates the exception into the Error
property on ApiResponse<T>
without throwing the exception.
You can then decide what to do like so:
var response = await _myRefitClient.GetSomeStuff();
if(response.IsSuccessStatusCode)
{
//do your thing
}
else
{
_logger.LogError(response.Error, response.Error.Content);
}
When returning Task<T>
Refit throws any ApiException
raised by the ExceptionFactory
when processing the response and any errors that occur when attempting to deserialize the response to Task<T>
.
// ...
try
{
var result = await awesomeApi.GetFooAsync("bar");
}
catch (ApiException exception)
{
//exception handling
}
// ...
Refit can also throw ValidationApiException
instead which in addition to the information present on ApiException
also contains ProblemDetails
when the service implements the RFC 7807 specification for problem details and the response content type is application/problem+json
For specific information on the problem details of the validation exception, simply catch 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
}
// ...
Providing a custom ExceptionFactory
You can also override default exceptions behavior that are raised by the ExceptionFactory
when processing the result by providing a custom exception factory in RefitSettings
. For example, you can suppress all exceptions with the following:
var nullTask = Task.FromResult<Exception>(null);
var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
new RefitSettings {
ExceptionFactory = httpResponse => nullTask;
});
Note that exceptions raised when attempting to deserialize the response are not affected by this.
ApiException
deconstruction with Serilog
For users of Serilog, you can enrich the logging of ApiException
using the
Serilog.Exceptions.Refit NuGet package. Details of how to
integrate this package into your applications can be found here.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
-
.NETStandard 2.0
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
-
net6.0
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
-
net8.0
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
NuGet packages (80)
Showing the top 5 NuGet packages that depend on Refit.Newtonsoft.Json:
Package | Downloads |
---|---|
N3O.Umbraco.Extensions
TODO |
|
Apple.Receipt.Verificator
Library for validating receipts with the App Store |
|
Meraki.Api
Meraki API |
|
Lykke.MarginTrading.BackendSnow.Contracts
Package Description |
|
Alexa.NET.Management
A .NET Core library for handling Alexa Skills Management API |
GitHub repositories (7)
Showing the top 5 popular GitHub repositories that depend on Refit.Newtonsoft.Json:
Repository | Stars |
---|---|
ErsatzTV/ErsatzTV
Stream custom live channels using your own media
|
|
GoldenPotato137/PotatoVN
一款Visual Novel管理软件
|
|
dorisoy/Dorisoy.Pan
Dorisoy.Pan 是基于.net core8 的跨平台文档管理系统,使用 MS SQL 2012 / MySql8.0(或更高版本)后端数据库,您可以在 Windows、Linux 或 Mac 上运行它,项目中的所有方法都是异步的,支持令牌基身份验证,项目体系结构遵循著名的软件模式和最佳安全实践。源代码是完全可定制的,热插拔且清晰的体系结构,使开发定制功能和遵循任何业务需求变得容易。 系统使用最新的 Microsoft 技术,高性能稳定性和安全性
|
|
Strypper/mauisland
MAUIsland 🏝️ is the number 1 controls gallery for .NET MAUI
|
|
whuanle/maomi
Maomi 框架是一个简单的、简洁的开发框架,除了框架本身提供的功能之外,Maomi 还作为一个易于阅读的开源项目,能够给开发者提供设计框架的思路和代码。
|
Version | Downloads | Last updated |
---|---|---|
8.0.0 | 208,638 | 11/3/2024 |
7.2.22 | 22,361 | 11/8/2024 |
7.2.1 | 233,985 | 9/19/2024 |
7.2.0 | 6,257 | 9/16/2024 |
7.1.2 | 579,995 | 6/30/2024 |
7.1.1 | 95,557 | 6/24/2024 |
7.1.0 | 31,597 | 6/20/2024 |
7.0.0 | 3,428,579 | 6/29/2023 |
7.0.0-beta.1 | 5,363 | 5/14/2023 |
6.3.2 | 10,584,721 | 2/8/2022 |
6.2.16 | 238,241 | 1/26/2022 |
6.1.15 | 1,399,574 | 10/14/2021 |
6.0.94 | 666,994 | 8/5/2021 |
6.0.38 | 821,656 | 3/19/2021 |
6.0.24 | 137,850 | 2/23/2021 |
6.0.21 | 608 | 2/23/2021 |
6.0.15 | 41,080 | 2/11/2021 |
6.0.8 | 15,988 | 2/10/2021 |
6.0.1 | 53,110 | 2/8/2021 |
6.0.0-preview.128 | 246 | 2/4/2021 |
6.0.0-preview.124 | 222 | 2/4/2021 |
6.0.0-preview.121 | 231 | 2/4/2021 |
6.0.0-preview.96 | 2,140 | 1/25/2021 |
6.0.0-preview.94 | 235 | 1/24/2021 |
6.0.0-preview.86 | 275 | 1/24/2021 |
6.0.0-preview.84 | 254 | 1/23/2021 |
6.0.0-preview.37 | 1,494 | 11/26/2020 |
6.0.0-preview.34 | 1,703 | 11/26/2020 |