Giter VIP home page Giter VIP logo

reactiveui / refit Goto Github PK

View Code? Open in Web Editor NEW
8.1K 176.0 719.0 15.09 MB

The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.

Home Page: https://reactiveui.github.io/refit/

License: MIT License

C# 97.97% CSS 1.54% Batchfile 0.43% Dockerfile 0.06% Shell 0.01%
dotnet dotnet-core http xamarin c-sharp json xml

refit's Introduction

Refit

Refit: The automatic type-safe REST library for .NET Core, Xamarin and .NET

Build codecov

Refit Refit.HttpClientFactory Refit.Newtonsoft.Json
NuGet NuGet NuGet 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?

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"

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"

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 via StreamContent
  • 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 a StringContent
  • 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
                    }
                }
            }
        )
    });

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.

  1. Add [Headers("Authorization: Bearer")] to the interface or methods which need the token.
  2. Set AuthorizationHeaderValueGetter in the RefitSettings 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 method
  • Header attribute or HeaderCollection 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>>.

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.

refit's People

Contributors

alexandru-calinoiu avatar anaisbetts avatar bennor avatar carl-berg avatar clairernovotny avatar dahlbyk avatar danlyons-softek avatar dashell-james avatar dependabot-preview[bot] avatar dependabot[bot] avatar drakelambert avatar dreamescaper avatar farcasclaudiu avatar flagbug avatar ghuntley avatar glennawatson avatar james-s-tayler avatar jamiehowarth0 avatar mariusvolkhart avatar mburbea avatar ncruces avatar nekresh avatar panetta-net-au avatar renovate[bot] avatar sergio0694 avatar sharwell avatar stevewgh avatar styxxy avatar timothymakkison avatar yuri-voloshyn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

refit's Issues

Baseadress with subdirectory doesn't use the subdirectory when making requests

I have encountered a problem when base adress that is not the same as the domain:
For instance

    public interface IGitHubApi
    {
        [Get("/{username}")]
        Task<User> GetUser(string userName);
    }

    ....
    var fixture = RestService.For<IGitHubApi>("https://api.github.com/users/");
    var result = fixture.GetUser("octocat"); //will make a request for https://api.github.com/octocat, which is not what i want...

same applies if i create an HttpClient and set the base adress myself...

Lists as url parameters

I'm not sure if Refit supports list arguments in the url. As of now i have had to wrap lists in another type and serialize its values with comma separation. And then add some custom handling on the receiving side to turn it back into a list. I would however prefer it if a call like this:

public IMyApi
{
    [Get("api/resource")]
    Task<Response> GetResources(List<int> value);
}

var api = RestService.For<IMyApi>();
api.GetResource(new List<int>{ 1, 2, 3});

would generate a request like GET /api/resource?value=1&value=2&value=3

What do you guys think about that? Good idea or waste of time?

Dealing with non-Refit methods declared on a Refit interface

At the moment methods that are defined on a Refit interface but don't have Refit attributes are stubbed to throw an exception.

Feel free do close this if you think it's A Dumb Idea, but what might be cool is allowing for (or requiring :trollface:) the methods to be implemented manually -- effectively replacing what we're currently using extension methods to implement.

There are two options here:

  1. Just don't implement them at all, requiring people to create a partial class and implement the methods before their project will compile.
  2. Keep the existing virtual method which throws an exception, and rewire RestService.For() to look for any derived classes in the same assembly, allowing people to derive from AutoGeneratedIXXX to override these methods.

Keen to hear your thoughts.

Support Web API routing attributes

It would be nice if refit supported Web API attributes (coming in the next version of Web API) so the same routing can be shared on the server and client.

I think the best way to implement this would be to support overriding the underlying model directly (e.g. like IContractResolver in Json.NET) and then having a seperate Refit.WebApi package.

plug different JsonSerializer or different SerializationSettings

is this possible? From what i saw: no, but i could be wrong.

Do you guys think its worth doing?

We're using refit but we have to serialize stuff with TypeNameHandling.All.

Currently, we're sending just strings and serializing before sending the request
If you guys this is something worth doing, i can try to implement it :)

Capture redirects instead of throwing 404 or possibly more detail in the errors

This is more of an exploratory question than an "its broken" as I'm trying to get my head around what I might want to happen.

I'm using Refit to allow me to streamline testing an API we have and for the most part its great - but we have one method that returns a redirect (a choice of 3 with additional data depending) so of course the code attempts to follow the redirect and for my test I end up with a 404 and not a lot more information (at least not that I can see in the error analyser).

So... I'd either a) like to suppress the redirect or b) to have more information about the error (this morning I've managed lots of 400s, a few 500s and some 404s - some of which were failing to set the right URL and some of which were "no actually it worked but example.com/success doesn't exist).

Thoughts?

InterfaceStubGenerator causes namespace clash

using My.Large.Namespace;
using OAuth = Thinktecture.IdentityModel.Client;

public interface IAccountClientApi
{
    [Get("/Account/UserInfo")]
    Task<UserInfoResponse> GetUserInfo([Header("Authorization")]string authorization);
}

Above testcode will cause a namespace clash in the RefitStubs.cs since the generator copies the namespaces like:

using My.Large.Namespace;
using Thinktecture.IdentityModel.Client;

which produces a collision on the type UserInfoResponse in the RefitStubs.cs

REST attributes don't handle non-lowercase substitutions

If we have a service like:

 public interface IEmployeeApi
 {        
    [Get("/api/v2/employee/{employeeId}")]
    Task<Employee> Get(int employeeId)
}

If i call ,for example, employeeApi.Get(1) refit will attempt to make a request to /api/v2/employee/{employeeId} instead of /api/v2/employee/1

If I change the string in the Get attribute to "/api/v2/employee/{employeeid}" , it works fine.

refit does not handle JSON return types with variable members

So this is something I've been tracking and trying to figure out in my fork, but I haven't come up with a good solution yet. It's not really a bug in refit, but it's more about the problem with serializing JSON to C# in general.

I'm calling an API using refit that returns a JSON object with a list of properties. The problem is the properties can vary.

{
  "timestamp": 1398877243,
  "base": "USD",
  "rates": {
    "AED": 3.672913,
    "AFN": 56.866025,
    "ALL": 101.06385,
    "AMD": 415.960999,
    /* 165 currencies */
    "YER": 215.036499,
    "ZAR": 10.54031,
    "ZMK": 5253.075255,
    "ZMW": 6.312353,
    "ZWL": 322.355006
  }
}

So, we could hard code a type that contains all 175 currencies, but the currency list could change in the future. Ideally, the API would return the currency list as an array of ISO code: rate object

"rates" : [
{"code": "AED", "rate":"3.6"}
]

So what I'm looking for is a way to tell refit to serialize the "rates" object as a hashtable or List OR a way to create the "rates" member of the C# object as an expando object.

So the question is: Is this something to be solved in refit or in JSON.net?

parsing url template placeholders bug.

Hi guys,
I've encountered a weird issue with the Url template fields matching.
I've a simple REST Interface with the following signature:

    [Delete("/api/attachment/{resourceType}/{resourceId}/{filename}")]
    Task DeleteAttachment([AliasAs("resourceId")] string resourceId, [AliasAs("filename")] string filename, [AliasAs("org")] string org, [AliasAs("resourceType")] string resourceType);

if when I make a call and inspect logs, it seems that the engine don't replace the resourceType and resourceId placeholders which isn't the case for other parameter alias names.
If I change them to id, type respectively, it works fine.

    [Delete("/api/attachment/{type}/{id}/{filename}")]

Just want to know if this is some kind of special case or a possible bug in the API.
Regards,
Oualid

Provide a way to supply authentication attributes

A lot of REST API's require some form of authentication, be it via BASIC or OAuth (Bearer:) HTTP header or some other method.

It would be great if you could expose some way from the factory class to provide authentication credentials. Maybe, for example:

RestService.WithAuthentication.Basic("username","password")
                 .For<IGithubApi>();

and

RestService.WithAuthentication.OAuth2(accessToken, "Bearer") // second argument is header name, default it to Bearer
                 .For<IGithubApi>();

Support for getting response headers

(This issue is at the request of @paulcbetts over twitter, in which I stated I'm willing to give a pull req a try, but some discussion on scenarios first.)

I would like to see support for getting response headers out of the remote api call while still using a strongly typed response (or none at all). This would be for making calls against APIs (ex the azure management api) that return a unique request-id as a response header, which can be used in support cases when something unexpected/bad happens, or more frequently in the case of asynchronous calls in which a call is made against the api such as swapping an azure deployment, in order to take that returned request-id and query its status in a future api call.

Given the way refit is exposed to it's user, the most natural way I feel would be to use attributes i.e.

// async api that returns no body
[Post("/some/asyncapi/{someId}")]
Task SomeAsyncRemoteApiCall(int someId, [ResponseHeader("x-request-id")] out string requestId);

// async api that returns body and requestid header
[Post("/some/asyncapi/{someId}")]
Task<SomeResponse> SomeAsyncRemoteApiCall(int someId, [ResponseHeader("x-request-id")] out string requestId);

I suppose that an alternative to the first one could be

[Post("/some/asyncapi/{someId}")]
[return: ResponseHeader("x-request-id")]
Task<string> SomeAsyncRemoteApiCall(int someId);

where the returned Task is for the header, not the string content because the request doesn't return a body. But that seems a little wonky IMO.

Thoughts/comments/insults?

Silverlight support?

This library would nicely remove some code we have in our LOB Silverlight app. Is adding support for SL (at least 4, probably 5 would be ok) something that is on the radar, or probably pretty easy for someone to add and submit a pull request for?

Works on Mono?

I'm wondering whether Refit works on Mono on Linux? The readme mentions Xamarin.Mac but I don't know whether this is the same as the open-source Mono.

Customize url/querystring serialization

Hi,

I have an issue where a web UI project runs different UI cultures for different users. This has a side effect of for example DateTime being serialized differently depending on the culture. What i would like is to customize how refit serializes objects into the url/querystring. Is there any possible extension points for that, or could there be? If so, I'd be happy to provide a PR with a suggestion.

RefitInternalGenerated.PreserveAttribute.X is never assigned to, and will always have its default value false

Howdy guys, a discussion before the pull request.

It seems that any interface w/ Visual Studio upon compilation results in two CS0649 warnings. It's unclean and results in build failures if Visual Studio is configured to fail builds on warning (as it should be)

Example interface:

[Headers("User-Agent: " + AppSettings.ApiClientUserAgent)]
public interface IEndlessCatsApi
{
    /// <remarks>
    /// - You can redefine the user-agent header on a per request basis via: [Headers("User-Agent: ELinks/0.9.3 (textmode; Linux 2.6.11 i686; 79x24)"]
    /// - You can remove the user-agent header on a per request basis via: [Headers("User-Agent")]
    /// </remarks>
    [Get("/cats")]
    Task<List<Cat>> GetMoreCats([Header("X-Auth-Token")] string apiKey);
}

The offending fields:

Warning 1   Field 'RefitInternalGenerated.PreserveAttribute.AllMembers' is never
assigned to, and will always have its default value false

Warning 2   Field 'RefitInternalGenerated.PreserveAttribute.Conditional' is never
assigned to, and will always have its default value false

Possible solution is a PR that edits tools/GeneratedInterfaceStubTemplate.mustache as follows.

From:

    //
    // Field
    //
    public bool AllMembers;

    public bool Conditional;

To:

#pragma warning disable 0649
    //
    // Field
    //
    public bool AllMembers;

    public bool Conditional;
#pragma warning enable 0649

๐Ÿ‘ or ๐Ÿ‘Ž or ๐Ÿ‘Š?

Using Refit with a cross platform library.

I am putting together a PCL that is, essentially, a client for a rest based service. When its working I want to push it to github and into nuget...

So rather than code all the usual stuff by hand I thought I'd give Refit a try.

My question is: what is the best way to put together the client? I started with a PCL targeting the usual suspects (WinPhone, Xamarin Android, etc) but Refit has platform specific assemblies.

I assume that I would need to follow Refit's pattern and have a platform specific project even if all I do is include the same source files (and platform specific Refit?)...

Any advice very much appreciated ๐Ÿ˜„

Conflicting types in RefitStubs.cs

I have two different sets of REST API in my project, both enabled by Refit. Both API reference a type named Project, but these are separate and unrelated Project classes that live in different namespaces. Unfortunately, when Refit puts stubs for both API into the same file, with one set of usings, I get a naming conflict.

One way to resolve this (short of me renaming my classes, which may not always be possible) is to have the InterfaceStubGenerator.cs generate fully qualified type names rather than reuse the names it finds in the interface file. To do that, the Roslyn syntax model needs to be converted to a semantic model and queried.

Possibly useful reading:
http://stackoverflow.com/a/9336238/56499
http://blogs.msdn.com/b/csharpfaq/archive/2011/11/23/using-the-roslyn-symbol-api.aspx

Stub implementations not created with pre-paren attribute whitespace

I haven't nailed down exactly what caused this (updating Refit to 2.1.0, updating Xamarin Studio, putting more air in my car's tires for cold weather, who knows?), but somewhere along the line the API in my Xamarin.Android app stopped getting a stubbed out version in RefitStubs.cs when I built the project. The symptom was triggering the well-worded exception from v2.1.0 telling me my interface didn't "look like a Refit interface". After lots of digging/reverting/cursing, here's the resulting difference.

Working

[Get("/users/{user}")]
Task<User> GetUser(string user);

Not so much with the working

[Get ("/users/{user}")]
Task<User> GetUser(string user);

In case you missed it, the only difference is the space between the Get and the open parenthesis
([Get ( vs. [Get().

Two methods

Oddly, if there are two methods, one with a space and one without the space (order doesn't matter), it generates the stub just fine. If all the methods have the space (tested to three), nothing is generated.

Setup (since this could be outside Refit's control)

  • OS X 10.10.1
  • Xamarin Studio 5.5.3
  • Xamarin.Android 4.18.1

InterfaceStubGenerator not working on build

I'm getting the following error on my build:

C:\Program Files (x86)\MSBuild\12.0\bin\amd64\Microsoft.Common.CurrentVersion.targets(1069, 5): error MSB3073: The command ""..\packages\refit.2.1.0\tools\InterfaceStubGenerator.exe" "C:\TeamCity\buildAgent\work\5c154ea65fba2a12\src\Website\RefitStubs.cs" "App_Start\IoC.cs;App_Start\LessTransform.cs;App_Start\RouteConfig.cs;App_Start\BundleConfig.cs;Classes\BundleHelper.cs;Classes\FormHelper.cs;Controllers\CallbackController.cs;Controllers\T101AsyncController.cs;Models\ConfirmationViewModel.cs;Orchestrators\ISecurePaymentOrchestrator.cs;Controllers\SecureController.cs;Orchestrators\SecurePaymentOrchestrator.cs;Controllers\T101Controller.cs;Global.asax.cs;Models\PaymentViewModel.cs;Properties\AssemblyInfo.cs"" exited with code 3.

I don't have any refit stub classes in that project (they're all in a different one that is referenced in this one) so I'm not sure why the generator is trying to check all these.

Or am I wrong in what this .exe is trying to achieve?

Should specify that I'm using teamcity for build.

Multipart requests

Hey Paul,

I saw on the roadmap that there are plans to handle Multipart requests. What was your idea on how to implement it as I need it for file uploads and maybe I can help out with implementing it?

Alternatively would it make sense to allow passing in a parameter of HttpContent and in such a case assign it directly to the Content property of the HttpRequestMessage? This is sort of similar to how you handle Streams....? This will also allow for a lot of flexibility to allow users to structure the request exactly the way they want to.

Thanks

json.net dependency and refit dependency

This is my 'Client' project

public interface IClientApi {
    [Get("/")]
    Task<string> Home();

    [Get("/{id}")]
    Task<string> GetById(int id);
}

public class ClientApi : IClientApi {
    private IClientApi _client;

    public ClientApi() {
        var c = new HttpClient {BaseAddress = new Uri("http://localhost:7820")};
        _client = RestService.For<IClientApi>(c);
    }

    public async Task<string> Home() {
        return await _client.Home();
    }

    public async Task<string> GetById(int id) {
        return await _client.GetById(id);
    }
}

This is my 'Consumer' project:

private static void Main(string[] args) {
    var client = new ClientApi();
    Console.WriteLine(client.Home().Result);
    Console.WriteLine(client.GetById(1).Result);
}

Why i have to install json.net and refit in my consumer project? And why isn't json.net a dependency of refit nuget package?

PCL Support

Hi Paul,

Any thoughts on the PCL support?

The latest Nuget fails under profile portable-net45+win+wp80 due to Castle.Core dependency.

Revert to single solution now that PCL is OSS and RTM?

When I went down the path of adding Silverlight 5 support, my initial pull request was rejected as I implemented it as a PCL but took a dependency on some MS libraries that weren't OSS and multi-platform friendly at the time.

Given the recent liberalisation of licensing around PCL and HttpClient, etc, can a simplification of the project source code be achieved? (i.e. does there still need to be Refit-VisualStudio.sln, Refit-XamarinStudio.sln, + a number of platform-specific .csproj files?)

More convention: complex type should default to [Body]

Right now it seems like you need to put [Body] on a parameter you want to be in the body. How about reversing that or at least making the defaults based on the verb.

I.e., for PUT/POST, a complex type would go in the body and any other parameters to the query string.
for GET, a complex type would translate to a query string parameter along with any other parameters.

Composing multiple interfaces for one API.

Hi,

Firstly, sorry if this is a obvious question, but is it possible to have one big interface/api defined in multiple pieces?

Maybe a folder structure like this? Where each interface is in its own folder inside a class.

/Acme
  AcmeApi.cs (IAcmeApi interface)
  /photos
    /IPhotosApi (acme.com/api/photos)
  /videos
   /IVideosApi (acme.com/api/videos)

then using just the IAcmeApi interface

var acmeApi = RestService.For<IAcmeApi>("https://api.acme.com");

var stuff = await acmeApi.DoStuff("banana");
// or maybe
var stuff = await acmeApi.Videos.DoStuff("banana");

I hope it makes sense.
Thank you.

Package as Xamarin Studio component

It would be nice to have this library packaged as Xamarin Studio component and make easy to add Refit in any Xamarin.Android or Xamarin.iOS projects.

Stubs aren't generated unless service is instantiated in the same project

InterfaceStubGenerator needs there to be a call to RestService.For<ISomeApi>() in the same project that ISomeApi is defined or it won't generate a stub for it.

We should be able to fix this by looking for all interfaces that have an attribute derived from HttpMethodAttribute on a method and making stubs for them.

This awesome error shows up when you try to create the service later too: https://gist.github.com/screamish/ccc46cb849cc4497ea8e

Usage of alternative (JSON / De-)Serializers

Good morning,

I just started playing with refit and I was wondering whether it was possible to use an alternative Json serializer and deserializer than Json.Net? As far as I can see it its hard-wired mostly in RequestBuilderImplementation.cs, but I've come to love Jil, been using NetJson in another project and some might use protobuf, MessagePack or some other formats.

While those might not provide support for the exact same amount of platforms as refit does, it might be nice to have a choice and I was wondering, whether the hard-coded usage of Json.net could be de-coupled and one could provide custom (de)serialization providers for refit?

Basically something like providing a Serializer registration point in https://github.com/paulcbetts/refit/blob/master/Refit/RefitSettings.cs, use that one instead of Json.Net directly?

I've forked away refit and started dabbling with this idea but wanted to check upfront whether this is something you might be ok with?

Cheers,
-J

Releases

Hi,
I'm stuck with issue #21 that seems to have been fixed some time ago, yet the package up on nuget is older. Are there any plans to release a new version or should i just use a local reference in my case? I prefer not to use local references, but i don't really see a way out rather than pushing up a new package on private nuget feed, but that kinda takes me out of the loop for potential new releases. Any advice?

Overloaded method support

As of now, refit doesn't handle method overload.
The methods dictionary can't be built because of DuplicateKeyException.

Would it be interesting to support WebApi's style method overload ?

IEnumerable<X> Get();
X Get(int id);

JsonConverters not being used?

I'm not sure if I missed something obvious, but I'm not seeing my DateTime JsonConverter being used in outbound parameters.

While poking at an API that expects dates like this: "yyyy-MM-dd HH:mm:ss" (e.g., "2014-11-07 16:32:54", or "2014-11-07+16%3A32%3A54" in querystring), I kept getting errors about invalid date format. It looks like the JsonConverter I am trying to apply isn't actually getting used. Instead, my DateTimes look like this: "11/7/2014+4:32:54+PM".

screen shot 2014-11-12 at 15 26 49

I was seeing this running against both v2.0.2 via NuGet and a version built from master grabbed yesterday evening (Nov. 11 MDT).

Approach 1

I tried the Json.NET defaults approach:

JsonConvert.DefaultSettings = 
    () => new JsonSerializerSettings () { 
        Converters = { new SomeDateTimeConverter () },
    };

Just to confirm the converter itself works, I gave it a [successful] run right after that.

var testDate = new DateTime (2014, 11, 7, 16, 32, 54);
string testDateJson = JsonConvert.SerializeObject (testDate);
System.Diagnostics.Debug.Assert (testDateJson == "2014-11-07 16:32:54"); // success

Approach 2

I also tried adding the converter to the parameter. Totally a shot in the dark, though, since I wasn't even sure that was supposed to be a supported approach.

[Get ("/test/something1/")]
Task<SomeResult> GetResult ([JsonConverter (typeof(SomeDateTimeConverter))]DateTime someDate);

Repro Code

All the relevant code mentioned here is pulled from a repro project I put together and stuck in a Gist.

Repro steps:

  1. New Xamarin.Android project
  2. Add Refit via NuGet (v2.0.2)
  3. Add Microsoft.Net.Http via NuGet to get it to build.
  4. Replace content of MainActivity.cs with Gist code.
  5. Insert your own Runscope debug bucket ID in the RestService.For... call.
  6. Run on device; click the big ol' button to send off requests.
  7. Check resulting outbound URLs in Runscope and see DateTimes in this form: "11/7/2014+4:32:54+PM" (URL decoded from "11%2f7%2f2014+4%3a32%3a54+PM")

Please provide support for WinRT and/or Xamarin.iOS

This library is brilliant, and to become more perfect than it already is, just need to provide support for WinRT and Xamarin.iOS platforms.

Please, could you help me? I need WinRT version of that library.

Thank you very much.

error with return type of Task (no generics)

Repro: http://1drv.ms/1itgwUu

Hi, I'm trying to do a simple test with refit like this:

public interface IClientApi {
    [Post("/")]
    Task PostHome();
}

When i call RestService.For<IClientApi>("http://localhost:7820"); i have the following error:

An unhandled exception of type 'System.InvalidOperationException' occurred in mscorlib.dll
Additional information: This operation is only valid on generic types.

   at System.RuntimeType.GetGenericTypeDefinition()
   at Refit.RestMethodInfo.determineReturnTypeInfo(MethodInfo methodInfo) in c:\Users\Paul\github\refit\Refit\RequestBuilderImplementation.cs:line 412
   at Refit.RestMethodInfo..ctor(Type targetInterface, MethodInfo methodInfo) in c:\Users\Paul\github\refit\Refit\RequestBuilderImplementation.cs:line 321
   at Refit.RequestBuilderImplementation.<>c__DisplayClass8.<.ctor>b__0(MethodInfo x) in c:\Users\Paul\github\refit\Refit\RequestBuilderImplementation.cs:line 42
   at System.Linq.Enumerable.<SelectManyIterator>d__14`2.MoveNext()
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)
   at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector)
   at Refit.RequestBuilderImplementation..ctor(Type targetInterface) in c:\Users\Paul\github\refit\Refit\RequestBuilderImplementation.cs:line 36
   at Refit.RequestBuilderFactory.Create(Type interfaceType) in c:\Users\Paul\github\refit\Refit\RequestBuilderImplementation.cs:line 20
   at Refit.RequestBuilder.ForType(Type interfaceType) in c:\Users\Paul\github\refit\Refit\RequestBuilder.cs:line 37
   at Refit.RequestBuilder.ForType[T]() in c:\Users\Paul\github\refit\Refit\RequestBuilder.cs:line 42
   at Refit.CastleRestService.For[T](HttpClient client) in c:\Users\Paul\github\refit\Refit\CastleRestService.cs:line 16
   at Refit.RestService.For[T](HttpClient client) in c:\Users\Paul\github\refit\Refit\RestService.cs:line 34
   at Refit.RestService.For[T](String hostUrl) in c:\Users\Paul\github\refit\Refit\RestService.cs:line 40
   at TesteClient.Client.ClientApi..ctor() in c:\Users\deschamps\Documents\Visual Studio 2013\Projects\TesteClient\TesteClient.Client\ClientApi.cs:line 9
   at TesteClient.Consumidor.Program.Main(String[] args) in c:\Users\deschamps\Documents\Visual Studio 2013\Projects\TesteClient\TesteClient.Consumidor\Program.cs:line 7
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

Support returning IQueryable to save much boilerplate

I heart refit. And with IQueryable it'd be ๐Ÿ‘! Would you accept a PR that accomplishes this?

(1) Get pagination (and sorting) for free! This is important since any good API will force pagination on almost every returned array.

// Instead of:
var users = await api.SearchUsers("Joe", startIndex: 0, maxResults: 25).ToList();

// Do this:
var users = await api.SearchUsers("Joe").Skip(0).Take(25);

(2) Allow flexible filtering, reducing the number of obscure interface methods.

// Instead of:
IList<User> GetUsersWithNameSinceLogInTime(string name, DateTime time);

// Do this:
var users = 
    from u in api.GetUsers()
    where u.FirstName == "Joe" && u.LastLoggedIn < time
    orderby u.LastLoggedIn
    select u;

These can be accomplished by simply returning Task<IQueryable<T>> from the interface method.

public interface INeatoApi
{
    [Get("/users")]
    Task<IQueryable<User>> GetUsers();
}

Under the hood refit would need to generate rest-like query string params for the extra LINQ goodies. The OData spec would be great for this. In fact, OData brings more than just paging and filtering. There are expand/include and projection potentials too. Even other fun goodies like api.GetUsers().AsAutomaticPaging() which will lazy-fetch the next 25 as you foreach through the Enumerable.

Refit causes TFS Builds to fail

Issue is that it errors out if the target file (the RefitSubs.cs) is read-only. TFS Team Build (and possibly other build agents) will check files out as read-only. This causes the build to fail.

Suggestion: Generate the file in-memory. Read the target file and only complain if they don't match. In the case of a build server, hopefully a local user already ran the build step and the stubs are up-to-date anyway.

OAuth2 flow

Hi,

Do you have a suggestion for a best practice on implementing the OAuth2 authorization flow, especially the refresh token usage.

It will need to do something like:

original request -> repond: 401 -> request refresh_token -> respond: 200 -> original request -> respond: 200

Thank you,

Missing Dependency or problem in the nuget definition?

Not sure if it is something I am doing or something missing in the nuspec .

Create a console app (4.5.1 is fine or an iOS app if you prefer). Use nuget to add refit. Compile the app.

Error: The type or namespace name 'Http' does not exist in the namespace 'System.Net' (are you missing an assembly reference?) in RefitStubs.cs

This seems to happen for everything except winphone.
Note: it seems to go away if I add a reference to System.Net.Http...

Thoughts?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.