Skip to content

Commit

Permalink
Merge pull request #629 from benjaminhowarth1/master
Browse files Browse the repository at this point in the history
Fixes #627, #637 and relates to #573
  • Loading branch information
Oren Novotny authored Jun 5, 2019
2 parents f7854e0 + 1be0034 commit a1bfe49
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 35 deletions.
43 changes: 41 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ 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.
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 the 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.

```csharp
Expand Down Expand Up @@ -135,6 +136,14 @@ GroupListWithAttribute(4, params)

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:
```csharp
[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
Expand Down Expand Up @@ -409,7 +418,9 @@ var user = await GetUser("octocat", "token OAUTH-TOKEN");
#### Authorization (Dynamic Headers redux)
The most common reason to use headers is for authorization. Today most API's use some flavor of oAuth with access tokens that expire and refresh tokens that are longer lived.

One way to encapsulate these kinds of token usage, a custom `HttpClientHandler` can be inserted instead.
One way to encapsulate these kinds of token usage, a custom `HttpClientHandler` can be inserted instead.
There are two classes for doing this: one is `AuthenticatedHttpClientHandler`, which takes a `Func<Task<string>>` parameter, where a signature can be generated without knowing about the request.
The other is `AuthenticatedParameterizedHttpClientHandler`, which takes a `Func<HttpRequestMessage, Task<string>>` parameter, where the signature requires information about the request (see earlier notes about Twitter's API)

For example:
```csharp
Expand Down Expand Up @@ -438,6 +449,34 @@ class AuthenticatedHttpClientHandler : HttpClientHandler
}
```

Or:

```csharp
class AuthenticatedParameterizedHttpClientHandler : DelegatingHandler
{
readonly Func<HttpRequestMessage, Task<string>> getToken;

public AuthenticatedParameterizedHttpClientHandler(Func<HttpRequestMessage, Task<string>> getToken, HttpMessageHandler innerHandler = null)
: base(innerHandler ?? new HttpClientHandler())
{
this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken));
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// See if the request has an authorize header
var auth = request.Headers.Authorization;
if (auth != null)
{
var token = await getToken(request).ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token);
}

return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
```

While HttpClient contains a nearly identical method signature, it is used differently. HttpClient.SendAsync is not called by Refit. The HttpClientHandler must be modified instead.

This class is used like so (example uses the [ADAL](http://msdn.microsoft.com/en-us/library/azure/jj573266.aspx) library to manage auto-token refresh but the principal holds for Xamarin.Auth or any other library:
Expand Down
37 changes: 37 additions & 0 deletions Refit.Tests/AuthenticatedClientHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,26 @@ public void DefaultHandlerIsHttpClientHandler()
Assert.IsType<HttpClientHandler>(handler.InnerHandler);
}

[Fact]
public void DefaultHandlerIsHttpClientHandlerWithParam()
{
var handler = new AuthenticatedParameterizedHttpClientHandler(((request) => Task.FromResult(string.Empty)));

Assert.IsType<HttpClientHandler>(handler.InnerHandler);
}

[Fact]
public void NullTokenGetterThrows()
{
Assert.Throws<ArgumentNullException>(() => new AuthenticatedHttpClientHandler(null));
}

[Fact]
public void NullTokenGetterThrowsWithParam()
{
Assert.Throws<ArgumentNullException>(() => new AuthenticatedParameterizedHttpClientHandler(null));
}

[Fact]
public async void AuthenticatedHandlerIgnoresUnAuth()
{
Expand Down Expand Up @@ -83,5 +97,28 @@ public async void AuthenticatedHandlerUsesAuth()

Assert.Equal("Ok", result);
}

[Fact]
public async void AuthenticatedHandlerWithParamUsesAuth()
{
var handler = new MockHttpMessageHandler();
var settings = new RefitSettings()
{
AuthorizationHeaderValueWithParamGetter = (request) => Task.FromResult("tokenValue"),
HttpMessageHandlerFactory = () => handler
};

handler.Expect(HttpMethod.Get, "http://api/auth")
.WithHeaders("Authorization", "Bearer tokenValue")
.Respond("text/plain", "Ok");

var fixture = RestService.For<IMyAuthenticatedService>("http://api", settings);

var result = await fixture.GetAuthenticated();

handler.VerifyNoOutstandingExpectation();

Assert.Equal("Ok", result);
}
}
}
38 changes: 38 additions & 0 deletions Refit.Tests/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ public interface IRestMethodInfoTests

[Post("/foo")]
Task ManyComplexTypes(Dictionary<int, string> theData, [Body] Dictionary<int, string> theData1);

[Post("/foo")]
Task PostWithDictionaryQuery([Query]Dictionary<int, string> theData);

[Post("/foo")]
Task PostWithComplexTypeQuery([Query]ComplexQueryObject queryParams);

[Post("/foo")]
Task ImpliedComplexQueryType(ComplexQueryObject queryParams, [Body] Dictionary<int, string> theData1);
}

public class ComplexQueryObject
{
[AliasAs("test-query-alias")]
public string TestAlias1 {get; set;}

public string TestAlias2 {get; set;}
}

public class RestMethodInfoTests
Expand Down Expand Up @@ -150,6 +167,27 @@ public void DefaultBodyParameterDetectedForPut()
Assert.NotNull(fixture.BodyParameterInfo);
}

[Fact]
public void PostWithDictionaryQueryParameter()
{
var input = typeof(IRestMethodInfoTests);
var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "PostWithDictionaryQuery"));

Assert.Single(fixture.QueryParameterMap);
Assert.Null(fixture.BodyParameterInfo);
}

[Fact]
public void PostWithObjectQueryParameter()
{
var input = typeof(IRestMethodInfoTests);
var fixtureParams = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "PostWithComplexTypeQuery"));

Assert.Equal(2, fixtureParams.QueryParameterMap.Count);
Assert.Equal("test-query-alias", fixtureParams.QueryParameterMap[0]);
Assert.Null(fixtureParams.BodyParameterInfo);
}

[Fact]
public void DefaultBodyParameterDetectedForPatch()
{
Expand Down
42 changes: 42 additions & 0 deletions Refit.Tests/RestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public interface IHttpBinApi<TResponse, in TParam, in THeader>
[Get("/get?hardcoded=true")]
Task<TResponse> GetQuery([Query("_")]TParam param);

[Post("/post?hardcoded=true")]
Task<TResponse> PostQuery([Query("_")]TParam param);

[Get("")]
Task<TResponse> GetQueryWithIncludeParameterName([Query(".", "search")]TParam param);

Expand Down Expand Up @@ -1016,6 +1019,45 @@ public async Task ComplexDynamicQueryparametersTest()
Assert.Equal("9999", resp.Args["Addr_Zip"]);
}

[Fact]
public async Task ComplexPostDynamicQueryparametersTest()
{
var mockHttp = new MockHttpMessageHandler();

var settings = new RefitSettings
{
HttpMessageHandlerFactory = () => mockHttp
};

mockHttp.Expect(HttpMethod.Post, "https://httpbin.org/post")
.Respond("application/json", "{'url': 'https://httpbin.org/post?hardcoded=true&FirstName=John&LastName=Rambo&Addr_Zip=9999&Addr_Street=HomeStreet 99&MetaData_Age=99&MetaData_Initials=JR&MetaData_Birthday=10%2F31%2F1918 4%3A21%3A16 PM&Other=12345&Other=10%2F31%2F2017 4%3A21%3A17 PM&Other=696e8653-6671-4484-a65f-9485af95fd3a', 'args': { 'Addr_Street': 'HomeStreet 99', 'Addr_Zip': '9999', 'FirstName': 'John', 'LastName': 'Rambo', 'MetaData_Age': '99', 'MetaData_Birthday': '10/31/1981 4:32:59 PM', 'MetaData_Initials': 'JR', 'Other': ['12345','10/31/2017 4:32:59 PM','60282dd2-f79a-4400-be01-bcb0e86e7bc6'], 'hardcoded': 'true'}}");

var myParams = new MyComplexQueryParams
{
FirstName = "John",
LastName = "Rambo"
};
myParams.Address.Postcode = 9999;
myParams.Address.Street = "HomeStreet 99";

myParams.MetaData.Add("Age", 99);
myParams.MetaData.Add("Initials", "JR");
myParams.MetaData.Add("Birthday", new DateTime(1981, 10, 31, 16, 24, 59));

myParams.Other.Add(12345);
myParams.Other.Add(new DateTime(2017, 10, 31, 16, 24, 59));
myParams.Other.Add(new Guid("60282dd2-f79a-4400-be01-bcb0e86e7bc6"));


var fixture = RestService.For<IHttpBinApi<HttpBinGet, MyComplexQueryParams, int>>("https://httpbin.org", settings);

var resp = await fixture.PostQuery(myParams);

Assert.Equal("John", resp.Args["FirstName"]);
Assert.Equal("Rambo", resp.Args["LastName"]);
Assert.Equal("9999", resp.Args["Addr_Zip"]);
}

[Fact]
public async Task GenericMethodTest()
{
Expand Down
4 changes: 2 additions & 2 deletions Refit.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.3
# Visual Studio Version 16
VisualStudioVersion = 16.0.28621.142
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4F1C8991-7097-4471-A9A6-A72005AB594D}"
ProjectSection(SolutionItems) = preProject
Expand Down
34 changes: 34 additions & 0 deletions Refit/AuthenticatedParameterizedHttpClientHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;

namespace Refit
{
public class AuthenticatedParameterizedHttpClientHandler : DelegatingHandler
{
readonly Func<HttpRequestMessage, Task<string>> getToken;

public AuthenticatedParameterizedHttpClientHandler(Func<HttpRequestMessage, Task<string>> getToken, HttpMessageHandler innerHandler = null)
: base(innerHandler ?? new HttpClientHandler())
{
this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken));
}

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// See if the request has an authorize header
var auth = request.Headers.Authorization;
if (auth != null)
{
var token = await getToken(request).ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token);
}

return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
}
3 changes: 3 additions & 0 deletions Refit/RefitSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public RefitSettings()
}

public Func<Task<string>> AuthorizationHeaderValueGetter { get; set; }

public Func<HttpRequestMessage, Task<string>> AuthorizationHeaderValueWithParamGetter { get; set; }

public Func<HttpMessageHandler> HttpMessageHandlerFactory { get; set; }

[Obsolete("Set RefitSettings.ContentSerializer = new JsonContentSerializer(JsonSerializerSettings) instead.", false)]
Expand Down
Loading

0 comments on commit a1bfe49

Please sign in to comment.