From 0d2b8b84ee7aab3aa2e25baf3197dbb1d3433632 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 27 Feb 2019 11:41:56 +0000 Subject: [PATCH 1/9] Updated AuthorizationHeaderValueGetter to support passing HttpRequestMessage into getToken() as OAuth signature generation may rely on having access to properties in the request (example: Twitter reuest signing) --- Refit/AuthenticatedHttpClientHandler.cs | 6 +++--- Refit/RefitSettings.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Refit/AuthenticatedHttpClientHandler.cs b/Refit/AuthenticatedHttpClientHandler.cs index 5568aba8b..ec73673e7 100644 --- a/Refit/AuthenticatedHttpClientHandler.cs +++ b/Refit/AuthenticatedHttpClientHandler.cs @@ -10,9 +10,9 @@ namespace Refit { class AuthenticatedHttpClientHandler : DelegatingHandler { - readonly Func> getToken; + readonly Func> getToken; - public AuthenticatedHttpClientHandler(Func> getToken, HttpMessageHandler innerHandler = null) + public AuthenticatedHttpClientHandler(Func> getToken, HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) { this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken)); @@ -24,7 +24,7 @@ protected override async Task SendAsync(HttpRequestMessage var auth = request.Headers.Authorization; if (auth != null) { - var token = await getToken().ConfigureAwait(false); + var token = await getToken(request).ConfigureAwait(false); request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token); } diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 20226adf0..cc4f4cea1 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -21,7 +21,7 @@ public RefitSettings() ContentSerializer = new JsonContentSerializer(); } - public Func> AuthorizationHeaderValueGetter { get; set; } + public Func> AuthorizationHeaderValueGetter { get; set; } public Func HttpMessageHandlerFactory { get; set; } [Obsolete("Set RefitSettings.ContentSerializer = new JsonContentSerializer(JsonSerializerSettings) instead.", false)] From f5c75af73e9a88eeb0ea731874d08c08d0ad8851 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 27 Feb 2019 11:49:28 +0000 Subject: [PATCH 2/9] Updated test signature --- Refit.Tests/AuthenticatedClientHandlerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Refit.Tests/AuthenticatedClientHandlerTests.cs b/Refit.Tests/AuthenticatedClientHandlerTests.cs index 67f05e55f..2712ad3cd 100644 --- a/Refit.Tests/AuthenticatedClientHandlerTests.cs +++ b/Refit.Tests/AuthenticatedClientHandlerTests.cs @@ -27,7 +27,7 @@ public interface IMyAuthenticatedService [Fact] public void DefaultHandlerIsHttpClientHandler() { - var handler = new AuthenticatedHttpClientHandler((() => Task.FromResult(string.Empty))); + var handler = new AuthenticatedHttpClientHandler(((rqMsg) => Task.FromResult(string.Empty))); Assert.IsType(handler.InnerHandler); } @@ -44,7 +44,7 @@ public async void AuthenticatedHandlerIgnoresUnAuth() var handler = new MockHttpMessageHandler(); var settings = new RefitSettings() { - AuthorizationHeaderValueGetter = () => Task.FromResult("tokenValue"), + AuthorizationHeaderValueGetter = (rqMsg) => Task.FromResult("tokenValue"), HttpMessageHandlerFactory = () => handler }; @@ -67,7 +67,7 @@ public async void AuthenticatedHandlerUsesAuth() var handler = new MockHttpMessageHandler(); var settings = new RefitSettings() { - AuthorizationHeaderValueGetter = () => Task.FromResult("tokenValue"), + AuthorizationHeaderValueGetter = (rqMsg) => Task.FromResult("tokenValue"), HttpMessageHandlerFactory = () => handler }; From 79552402894710b0bb04f5d82755018d32671430 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Fri, 15 Mar 2019 18:21:01 +0000 Subject: [PATCH 3/9] Created parameterized HttpClientHandler as requested change to be additive, which is not possible without modifying the handler. So, creatingf a handler explicitly with a method type to support passing HttpRequestMessage in, and then users can choose to use this whenever their getToken method requires access to the HttpRequestMessage. --- Refit.sln | 4 +-- Refit/AuthenticatedHttpClientHandler.cs | 6 ++-- ...enticatedParameterizedHttpClientHandler.cs | 34 +++++++++++++++++++ Refit/RefitSettings.cs | 2 +- 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 Refit/AuthenticatedParameterizedHttpClientHandler.cs diff --git a/Refit.sln b/Refit.sln index 5869061f5..affbc8ab6 100644 --- a/Refit.sln +++ b/Refit.sln @@ -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 diff --git a/Refit/AuthenticatedHttpClientHandler.cs b/Refit/AuthenticatedHttpClientHandler.cs index ec73673e7..5568aba8b 100644 --- a/Refit/AuthenticatedHttpClientHandler.cs +++ b/Refit/AuthenticatedHttpClientHandler.cs @@ -10,9 +10,9 @@ namespace Refit { class AuthenticatedHttpClientHandler : DelegatingHandler { - readonly Func> getToken; + readonly Func> getToken; - public AuthenticatedHttpClientHandler(Func> getToken, HttpMessageHandler innerHandler = null) + public AuthenticatedHttpClientHandler(Func> getToken, HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) { this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken)); @@ -24,7 +24,7 @@ protected override async Task SendAsync(HttpRequestMessage var auth = request.Headers.Authorization; if (auth != null) { - var token = await getToken(request).ConfigureAwait(false); + var token = await getToken().ConfigureAwait(false); request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token); } diff --git a/Refit/AuthenticatedParameterizedHttpClientHandler.cs b/Refit/AuthenticatedParameterizedHttpClientHandler.cs new file mode 100644 index 000000000..f15509486 --- /dev/null +++ b/Refit/AuthenticatedParameterizedHttpClientHandler.cs @@ -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> getToken; + + public AuthenticatedParameterizedHttpClientHandler(Func> getToken, HttpMessageHandler innerHandler = null) + : base(innerHandler ?? new HttpClientHandler()) + { + this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken)); + } + + protected override async Task 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); + } + } +} diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index cc4f4cea1..20226adf0 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -21,7 +21,7 @@ public RefitSettings() ContentSerializer = new JsonContentSerializer(); } - public Func> AuthorizationHeaderValueGetter { get; set; } + public Func> AuthorizationHeaderValueGetter { get; set; } public Func HttpMessageHandlerFactory { get; set; } [Obsolete("Set RefitSettings.ContentSerializer = new JsonContentSerializer(JsonSerializerSettings) instead.", false)] From 248d33325b8927f70f7a104b2066d9644a19cf6d Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 20 Mar 2019 10:14:24 -0700 Subject: [PATCH 4/9] Created new class for passing HttpRequestMessage to extra param, additive as @onovotny requested. --- .../AuthenticatedClientHandlerTests.cs | 43 +++++++++++++++++-- Refit/RefitSettings.cs | 3 ++ Refit/RestService.cs | 4 ++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/Refit.Tests/AuthenticatedClientHandlerTests.cs b/Refit.Tests/AuthenticatedClientHandlerTests.cs index 2712ad3cd..5806fa27c 100644 --- a/Refit.Tests/AuthenticatedClientHandlerTests.cs +++ b/Refit.Tests/AuthenticatedClientHandlerTests.cs @@ -27,7 +27,15 @@ public interface IMyAuthenticatedService [Fact] public void DefaultHandlerIsHttpClientHandler() { - var handler = new AuthenticatedHttpClientHandler(((rqMsg) => Task.FromResult(string.Empty))); + var handler = new AuthenticatedHttpClientHandler((() => Task.FromResult(string.Empty))); + + Assert.IsType(handler.InnerHandler); + } + + [Fact] + public void DefaultHandlerIsHttpClientHandlerWithParam() + { + var handler = new AuthenticatedParameterizedHttpClientHandler(((request) => Task.FromResult(string.Empty))); Assert.IsType(handler.InnerHandler); } @@ -38,13 +46,19 @@ public void NullTokenGetterThrows() Assert.Throws(() => new AuthenticatedHttpClientHandler(null)); } + [Fact] + public void NullTokenGetterThrowsWithParam() + { + Assert.Throws(() => new AuthenticatedParameterizedHttpClientHandler(null)); + } + [Fact] public async void AuthenticatedHandlerIgnoresUnAuth() { var handler = new MockHttpMessageHandler(); var settings = new RefitSettings() { - AuthorizationHeaderValueGetter = (rqMsg) => Task.FromResult("tokenValue"), + AuthorizationHeaderValueGetter = () => Task.FromResult("tokenValue"), HttpMessageHandlerFactory = () => handler }; @@ -67,7 +81,7 @@ public async void AuthenticatedHandlerUsesAuth() var handler = new MockHttpMessageHandler(); var settings = new RefitSettings() { - AuthorizationHeaderValueGetter = (rqMsg) => Task.FromResult("tokenValue"), + AuthorizationHeaderValueGetter = () => Task.FromResult("tokenValue"), HttpMessageHandlerFactory = () => handler }; @@ -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("http://api", settings); + + var result = await fixture.GetAuthenticated(); + + handler.VerifyNoOutstandingExpectation(); + + Assert.Equal("Ok", result); + } } } diff --git a/Refit/RefitSettings.cs b/Refit/RefitSettings.cs index 20226adf0..86c7c3aed 100644 --- a/Refit/RefitSettings.cs +++ b/Refit/RefitSettings.cs @@ -22,6 +22,9 @@ public RefitSettings() } public Func> AuthorizationHeaderValueGetter { get; set; } + + public Func> AuthorizationHeaderValueWithParamGetter { get; set; } + public Func HttpMessageHandlerFactory { get; set; } [Obsolete("Set RefitSettings.ContentSerializer = new JsonContentSerializer(JsonSerializerSettings) instead.", false)] diff --git a/Refit/RestService.cs b/Refit/RestService.cs index d219b842a..4a5922693 100644 --- a/Refit/RestService.cs +++ b/Refit/RestService.cs @@ -54,6 +54,10 @@ public static T For(string hostUrl, RefitSettings settings) { innerHandler = new AuthenticatedHttpClientHandler(settings.AuthorizationHeaderValueGetter, innerHandler); } + else if (settings.AuthorizationHeaderValueWithParamGetter != null) + { + innerHandler = new AuthenticatedParameterizedHttpClientHandler(settings.AuthorizationHeaderValueWithParamGetter, innerHandler); + } } var client = new HttpClient(innerHandler ?? new HttpClientHandler()) { BaseAddress = new Uri(hostUrl.TrimEnd('/')) }; From c528c293170aa0265c24f2d70f434209ba733bf7 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 20 Mar 2019 15:31:20 -0700 Subject: [PATCH 5/9] #637 Test demonstrating failure of [Query] to be recognised in non-get methods. --- Refit.Tests/RequestBuilder.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index fc12ab710..46274057b 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -94,6 +94,9 @@ public interface IRestMethodInfoTests [Post("/foo")] Task ManyComplexTypes(Dictionary theData, [Body] Dictionary theData1); + + [Post("/foo")] + Task QueryComplexTypes([Query]Dictionary theData); } public class RestMethodInfoTests @@ -144,6 +147,16 @@ public void DefaultBodyParameterDetectedForPut() Assert.NotNull(fixture.BodyParameterInfo); } + [Fact] + public void ExplicitQueryParameterForPost() + { + var input = typeof(IRestMethodInfoTests); + var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "QueryComplexTypes")); + + Assert.Single(fixture.QueryParameterMap); + Assert.Null(fixture.BodyParameterInfo); + } + [Fact] public void DefaultBodyParameterDetectedForPatch() { From 8301de0539c3d28e2ff37d6c29084bcf33e92be7 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 5 Jun 2019 13:05:05 +0100 Subject: [PATCH 6/9] Fixes #637 by supporting POCO query parameters on all HTTP methods. --- Refit.Tests/RequestBuilder.cs | 31 +++++++++++++++++-- Refit.Tests/RestService.cs | 42 ++++++++++++++++++++++++++ Refit/RestMethodInfo.cs | 56 ++++++++++++++++++++++++++--------- 3 files changed, 112 insertions(+), 17 deletions(-) diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index 46274057b..6f3fffc69 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -96,7 +96,21 @@ public interface IRestMethodInfoTests Task ManyComplexTypes(Dictionary theData, [Body] Dictionary theData1); [Post("/foo")] - Task QueryComplexTypes([Query]Dictionary theData); + Task PostWithDictionaryQuery([Query]Dictionary theData); + + [Post("/foo")] + Task PostWithComplexTypeQuery([Query]ComplexQueryObject queryParams); + + [Post("/foo")] + Task ImpliedComplexQueryType(ComplexQueryObject queryParams, [Body] Dictionary theData1); + } + + public class ComplexQueryObject + { + [AliasAs("test-query-alias")] + public string TestAlias1 {get; set;} + + public string TestAlias2 {get; set;} } public class RestMethodInfoTests @@ -148,15 +162,26 @@ public void DefaultBodyParameterDetectedForPut() } [Fact] - public void ExplicitQueryParameterForPost() + public void PostWithDictionaryQueryParameter() { var input = typeof(IRestMethodInfoTests); - var fixture = new RestMethodInfo(input, input.GetMethods().First(x => x.Name == "QueryComplexTypes")); + 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() { diff --git a/Refit.Tests/RestService.cs b/Refit.Tests/RestService.cs index ab8da5111..ee011366a 100644 --- a/Refit.Tests/RestService.cs +++ b/Refit.Tests/RestService.cs @@ -77,6 +77,9 @@ public interface IHttpBinApi [Get("/get?hardcoded=true")] Task GetQuery([Query("_")]TParam param); + [Post("/post?hardcoded=true")] + Task PostQuery([Query("_")]TParam param); + [Get("")] Task GetQueryWithIncludeParameterName([Query(".", "search")]TParam param); @@ -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>("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() { diff --git a/Refit/RestMethodInfo.cs b/Refit/RestMethodInfo.cs index 429d084da..46129892f 100644 --- a/Refit/RestMethodInfo.cs +++ b/Refit/RestMethodInfo.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Net.Http; using System.Collections.Generic; using System.Diagnostics; @@ -90,11 +91,30 @@ public RestMethodInfo(Type targetInterface, MethodInfo methodInfo, RefitSettings QueryParameterMap = new Dictionary(); for (var i = 0; i < parameterList.Count; i++) { - if (ParameterMap.ContainsKey(i) || HeaderParameterMap.ContainsKey(i) || (BodyParameterInfo != null && BodyParameterInfo.Item3 == i)) + if (ParameterMap.ContainsKey(i) || + HeaderParameterMap.ContainsKey(i) || + (BodyParameterInfo != null && BodyParameterInfo.Item3 == i)) { continue; } + if (parameterList[i].GetCustomAttribute() != null) + { + var complexType = parameterList[i].ParameterType; + if (complexType.IsArray || complexType.GetInterfaces().Contains(typeof(IEnumerable))) + { + QueryParameterMap.Add(QueryParameterMap.Count, GetUrlNameForParameter(parameterList[i])); + } + else + { + foreach (var member in complexType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + QueryParameterMap.Add(QueryParameterMap.Count, GetUrlNameForMember(member)); + } + } + continue; + } + QueryParameterMap[i] = GetUrlNameForParameter(parameterList[i]); } @@ -136,26 +156,25 @@ Dictionary BuildParameterMap(string relativePath, List(); + // This section handles pattern matching in the URL. We also need it to add parameter key/values for any attribute with a [Query] var parameterizedParts = relativePath.Split('/', '?') .SelectMany(x => ParameterRegex.Matches(x).Cast()) .ToList(); - if (parameterizedParts.Count == 0) + if (parameterizedParts.Count > 0) { - return ret; - } - - var paramValidationDict = parameterInfo.ToDictionary(k => GetUrlNameForParameter(k).ToLowerInvariant(), v => v); + var paramValidationDict = parameterInfo.ToDictionary(k => GetUrlNameForParameter(k).ToLowerInvariant(), v => v); - foreach (var match in parameterizedParts) - { - var name = match.Groups[1].Value.ToLowerInvariant(); - if (!paramValidationDict.ContainsKey(name)) + foreach (var match in parameterizedParts) { - throw new ArgumentException($"URL {relativePath} has parameter {name}, but no method parameter matches"); - } + var name = match.Groups[1].Value.ToLowerInvariant(); + if (!paramValidationDict.ContainsKey(name)) + { + throw new ArgumentException($"URL {relativePath} has parameter {name}, but no method parameter matches"); + } - ret.Add(parameterInfo.IndexOf(paramValidationDict[name]), name); + ret.Add(parameterInfo.IndexOf(paramValidationDict[name]), name); + } } return ret; @@ -169,6 +188,14 @@ string GetUrlNameForParameter(ParameterInfo paramInfo) return aliasAttr != null ? aliasAttr.Name : paramInfo.Name; } + string GetUrlNameForMember(MemberInfo memberInfo) + { + var aliasAttr = memberInfo.GetCustomAttributes(true) + .OfType() + .FirstOrDefault(); + return aliasAttr != null ? aliasAttr.Name : memberInfo.Name; + } + string GetAttachmentNameForParameter(ParameterInfo paramInfo) { var nameAttr = paramInfo.GetCustomAttributes(true) @@ -222,7 +249,8 @@ Tuple FindBodyParameter(IList } // see if we're a post/put/patch - var refParams = parameterList.Where(pi => !pi.ParameterType.GetTypeInfo().IsValueType && pi.ParameterType != typeof(string)).ToList(); + // BH: explicitly skip [Query]-denoted params + var refParams = parameterList.Where(pi => !pi.ParameterType.GetTypeInfo().IsValueType && pi.ParameterType != typeof(string) && pi.GetCustomAttribute() == null).ToList(); // Check for rule #3 if (refParams.Count > 1) From ce52764866630fccbb8be6b7c01bc34d2ec2c305 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 5 Jun 2019 13:18:02 +0100 Subject: [PATCH 7/9] Missed a brace in merge... --- Refit/RestMethodInfo.cs | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Refit/RestMethodInfo.cs b/Refit/RestMethodInfo.cs index 83ddae3c1..97e8d12e6 100644 --- a/Refit/RestMethodInfo.cs +++ b/Refit/RestMethodInfo.cs @@ -171,35 +171,35 @@ Dictionary> BuildParameterMap(string relativeP { var paramValidationDict = parameterInfo.ToDictionary(k => GetUrlNameForParameter(k).ToLowerInvariant(), v => v); - foreach (var match in parameterizedParts) - { - var rawName = match.Groups[1].Value.ToLowerInvariant(); - var isRoundTripping = rawName.StartsWith("**"); - string name; - if (isRoundTripping) - { - name = rawName.Substring(2); - } - else + foreach (var match in parameterizedParts) { - name = rawName; - } + var rawName = match.Groups[1].Value.ToLowerInvariant(); + var isRoundTripping = rawName.StartsWith("**"); + string name; + if (isRoundTripping) + { + name = rawName.Substring(2); + } + else + { + name = rawName; + } - if (!paramValidationDict.ContainsKey(name)) - { - throw new ArgumentException($"URL {relativePath} has parameter {rawName}, but no method parameter matches"); - } + if (!paramValidationDict.ContainsKey(name)) + { + throw new ArgumentException($"URL {relativePath} has parameter {rawName}, but no method parameter matches"); + } - var paramType = paramValidationDict[name].ParameterType; - if (isRoundTripping && paramType != typeof(string)) - { - throw new ArgumentException($"URL {relativePath} has round-tripping parameter {rawName}, but the type of matched method parameter is {paramType.FullName}. It must be a string."); - } + var paramType = paramValidationDict[name].ParameterType; + if (isRoundTripping && paramType != typeof(string)) + { + throw new ArgumentException($"URL {relativePath} has round-tripping parameter {rawName}, but the type of matched method parameter is {paramType.FullName}. It must be a string."); + } - var parameterType = isRoundTripping ? ParameterType.RoundTripping : ParameterType.Normal; - ret.Add(parameterInfo.IndexOf(paramValidationDict[name]), Tuple.Create(name, parameterType)); + var parameterType = isRoundTripping ? ParameterType.RoundTripping : ParameterType.Normal; + ret.Add(parameterInfo.IndexOf(paramValidationDict[name]), Tuple.Create(name, parameterType)); + } } - return ret; } From 0eb1f85ded45c10b66cf8e968214790fe74ca068 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 5 Jun 2019 21:08:39 +0100 Subject: [PATCH 8/9] Updated README with fresh documentation on [Query] attributes in non-GET methods --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 675f6d2bd..2d45214da 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 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 From 182ba568bab25fe2fb7c7aec7651a6f209977671 Mon Sep 17 00:00:00 2001 From: Benjamin Howarth Date: Wed, 5 Jun 2019 21:34:56 +0100 Subject: [PATCH 9/9] Added documentation for AuthenticatedParameterizedHttpClientHandler where signature generation is dependent on contents of the HTTP request --- README.md | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2d45214da..0ee7e7f89 100644 --- a/README.md +++ b/README.md @@ -418,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>` parameter, where a signature can be generated without knowing about the request. +The other is `AuthenticatedParameterizedHttpClientHandler`, which takes a `Func>` parameter, where the signature requires information about the request (see earlier notes about Twitter's API) For example: ```csharp @@ -447,6 +449,34 @@ class AuthenticatedHttpClientHandler : HttpClientHandler } ``` +Or: + +```csharp +class AuthenticatedParameterizedHttpClientHandler : DelegatingHandler + { + readonly Func> getToken; + + public AuthenticatedParameterizedHttpClientHandler(Func> getToken, HttpMessageHandler innerHandler = null) + : base(innerHandler ?? new HttpClientHandler()) + { + this.getToken = getToken ?? throw new ArgumentNullException(nameof(getToken)); + } + + protected override async Task 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: