From 9c71b37761559eee7db3b15e4f9f943008b9ca38 Mon Sep 17 00:00:00 2001 From: Steve Stanzak Date: Tue, 29 Sep 2020 16:06:48 -0400 Subject: [PATCH 1/3] fix dictionary to convert non-string keys, and serialize when in a complex query object --- Refit.Tests/RequestBuilder.cs | 114 +++++++++++++++++++++++++- Refit/RequestBuilderImplementation.cs | 7 +- 2 files changed, 116 insertions(+), 5 deletions(-) diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index 4233e176d..a39f2885c 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -135,6 +135,11 @@ public class ComplexQueryObject public IEnumerable TestCollection { get; set; } + [AliasAs("test-dictionary-alias")] + public Dictionary TestAliasedDictionary { get; set; } + + public Dictionary TestDictionary { get; set; } + [AliasAs("listOfEnumMulti")] [Query(CollectionFormat.Multi)] public List EnumCollectionMulti { get; set; } @@ -150,7 +155,6 @@ public class ComplexQueryObject public List ObjectCollectionCcv { get; set; } } - public class RestMethodInfoTests { @@ -761,6 +765,18 @@ public interface IDummyHttpApi [Get("/query")] Task QueryWithArrayFormattedAsPipes([Query(CollectionFormat.Pipes)]int[] numbers); + [Get("/foo")] + Task ComplexQueryObjectWithDictionary([Query] ComplexQueryObject query); + + [Get("/foo")] + Task QueryWithDictionaryWithEnumKey([Query] IDictionary query); + + [Get("/foo")] + Task QueryWithDictionaryWithPrefix([Query(".", "dictionary")] IDictionary query); + + [Get("/foo")] + Task QueryWithDictionaryWithNumericKey([Query] IDictionary query); + [Get("/query")] Task QueryWithEnumerableFormattedAsMulti([Query(CollectionFormat.Multi)]IEnumerable lines); @@ -1861,6 +1877,102 @@ public void CachedRequestBuilderCallInternalBuilderForParametersWithSameNamesBut Assert.Equal(4, internalBuilder.CallCount); } + + [Fact] + public void DictionaryQueryWithEnumKeyProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey)); + + var dict = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }; + + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?A=value1&B=value2", uri.PathAndQuery); + } + + [Fact] + public void DictionaryQueryWithCollection() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithPrefix)); + + var dict = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }; + + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?dictionary.A=value1&dictionary.B=value2", uri.PathAndQuery); + } + + [Fact] + public void DictionaryQueryWithNumericKeyProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithNumericKey)); + + var dict = new Dictionary + { + { 1, "value1" }, + { 2, "value2" }, + }; + + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?1=value1&2=value2", uri.PathAndQuery); + } + + [Fact] + public void ComplexQueryObjectWithAliasedDictionaryProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestAliasedDictionary = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }, + }; + + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?test-dictionary-alias.A=value1&test-dictionary-alias.B=value2", uri.PathAndQuery); + } + + [Fact] + public void ComplexQueryObjectWithDictionaryProducesCorrectQueryString() + { + var fixture = new RequestBuilderImplementation(); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestDictionary = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }, + }; + + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal("/foo?TestDictionary.A=value1&TestDictionary.B=value2", uri.PathAndQuery); + } } static class RequestBuilderTestExtensions diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 29bdf9742..3836c45a8 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -361,7 +361,7 @@ List> BuildQueryMap(object @object, string delimite } // If obj is IEnumerable - format it accounting for Query attribute and CollectionFormat - if (!(obj is string) && obj is IEnumerable ienu) + if (!(obj is string) && obj is IEnumerable ienu && !(obj is IDictionary)) { foreach (var value in ParseEnumerableQueryParameterValue(ienu, propertyInfo, propertyInfo.PropertyType, queryAttribute)) { @@ -404,8 +404,7 @@ List> BuildQueryMap(IDictionary dictionary, string { var kvps = new List>(); - var props = dictionary.Keys; - foreach (string key in props) + foreach (var key in dictionary.Keys) { var obj = dictionary[key]; if (obj == null) @@ -413,7 +412,7 @@ List> BuildQueryMap(IDictionary dictionary, string if (DoNotConvertToQueryMap(obj)) { - kvps.Add(new KeyValuePair(key, obj)); + kvps.Add(new KeyValuePair(key.ToString(), obj)); } else { From bb636d27f3dc5529b18784cc6a5baf6262645a09 Mon Sep 17 00:00:00 2001 From: Steve Stanzak Date: Fri, 2 Oct 2020 08:05:15 -0400 Subject: [PATCH 2/3] handle custom url formatters --- Refit.Tests/RequestBuilder.cs | 59 ++++++++++++++++++++++++++- Refit/RequestBuilderImplementation.cs | 10 ++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index a39f2885c..caf0e1e30 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -931,6 +931,20 @@ public string Format(object value, ICustomAttributeProvider attributeProvider, T } } + public class TestEnumUrlParameterFormatter : DefaultUrlParameterFormatter + { + public override string Format(object parameterValue, ICustomAttributeProvider attributeProvider, Type type) + { + if (parameterValue is TestEnum enumValue) + { + var enumBackingValue = (int)enumValue; + return enumBackingValue.ToString(); + } + + return base.Format(parameterValue, attributeProvider, type); + } + } + public class TestEnumerableUrlParameterFormatter : DefaultUrlParameterFormatter { public override string Format(object parameterValue, ICustomAttributeProvider attributeProvider, Type type) @@ -1897,7 +1911,7 @@ public void DictionaryQueryWithEnumKeyProducesCorrectQueryString() } [Fact] - public void DictionaryQueryWithCollection() + public void DictionaryQueryWithPrefix() { var fixture = new RequestBuilderImplementation(); var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithPrefix)); @@ -1932,6 +1946,26 @@ public void DictionaryQueryWithNumericKeyProducesCorrectQueryString() Assert.Equal("/foo?1=value1&2=value2", uri.PathAndQuery); } + [Fact] + public void DictionaryQueryWithCustomFormatterProducesCorrectQueryString() + { + var urlParameterFormatter = new TestEnumUrlParameterFormatter(); + var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter }; + var fixture = new RequestBuilderImplementation(refitSettings); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey)); + + var dict = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }; + + var output = factory(new object[] { dict }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal($"/foo?{(int)TestEnum.A}=value1&{(int)TestEnum.B}=value2", uri.PathAndQuery); + } + [Fact] public void ComplexQueryObjectWithAliasedDictionaryProducesCorrectQueryString() { @@ -1973,6 +2007,29 @@ public void ComplexQueryObjectWithDictionaryProducesCorrectQueryString() Assert.Equal("/foo?TestDictionary.A=value1&TestDictionary.B=value2", uri.PathAndQuery); } + + [Fact] + public void ComplexQueryObjectWithDictionaryAndCustomFormatterProducesCorrectQueryString() + { + var urlParameterFormatter = new TestEnumUrlParameterFormatter(); + var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter }; + var fixture = new RequestBuilderImplementation(refitSettings); + var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary)); + + var complexQuery = new ComplexQueryObject + { + TestDictionary = new Dictionary + { + { TestEnum.A, "value1" }, + { TestEnum.B, "value2" }, + }, + }; + + var output = factory(new object[] { complexQuery }); + var uri = new Uri(new Uri("http://api"), output.RequestUri); + + Assert.Equal($"/foo?TestDictionary.{(int)TestEnum.A}=value1&TestDictionary.{(int)TestEnum.B}=value2", uri.PathAndQuery); + } } static class RequestBuilderTestExtensions diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 3836c45a8..1557b540d 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel.Design; using System.IO; using System.Linq; using System.Net.Http; @@ -410,15 +411,20 @@ List> BuildQueryMap(IDictionary dictionary, string if (obj == null) continue; + var keyType = key.GetType(); + var formattedKey = settings.UrlParameterFormatter.Format(key, keyType, keyType); + if (DoNotConvertToQueryMap(obj)) { - kvps.Add(new KeyValuePair(key.ToString(), obj)); + var objType = obj.GetType(); + var formattedValue = settings.UrlParameterFormatter.Format(obj, objType, objType); + kvps.Add(new KeyValuePair(formattedKey, formattedValue)); } else { foreach (var keyValuePair in BuildQueryMap(obj, delimiter)) { - kvps.Add(new KeyValuePair($"{key}{delimiter}{keyValuePair.Key}", keyValuePair.Value)); + kvps.Add(new KeyValuePair($"{formattedKey}{delimiter}{keyValuePair.Key}", keyValuePair.Value)); } } } From 3221e54b219fafd62236d4050cf6154df8652823 Mon Sep 17 00:00:00 2001 From: Steve Stanzak Date: Fri, 2 Oct 2020 08:54:30 -0400 Subject: [PATCH 3/3] update tests for key and value --- Refit.Tests/RequestBuilder.cs | 13 +++++++++++-- Refit/RequestBuilderImplementation.cs | 4 +--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Refit.Tests/RequestBuilder.cs b/Refit.Tests/RequestBuilder.cs index caf0e1e30..3829a9dba 100644 --- a/Refit.Tests/RequestBuilder.cs +++ b/Refit.Tests/RequestBuilder.cs @@ -931,6 +931,7 @@ public string Format(object value, ICustomAttributeProvider attributeProvider, T } } + // Converts enums to ints and adds a suffix to strings to test that both dictionary keys and values are formatted. public class TestEnumUrlParameterFormatter : DefaultUrlParameterFormatter { public override string Format(object parameterValue, ICustomAttributeProvider attributeProvider, Type type) @@ -941,8 +942,15 @@ public override string Format(object parameterValue, ICustomAttributeProvider at return enumBackingValue.ToString(); } + if (parameterValue is string stringValue) + { + return $"{stringValue}{StringParameterSuffix}"; + } + return base.Format(parameterValue, attributeProvider, type); } + + public string StringParameterSuffix => "suffix"; } public class TestEnumerableUrlParameterFormatter : DefaultUrlParameterFormatter @@ -1950,6 +1958,7 @@ public void DictionaryQueryWithNumericKeyProducesCorrectQueryString() public void DictionaryQueryWithCustomFormatterProducesCorrectQueryString() { var urlParameterFormatter = new TestEnumUrlParameterFormatter(); + var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter }; var fixture = new RequestBuilderImplementation(refitSettings); var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey)); @@ -1963,7 +1972,7 @@ public void DictionaryQueryWithCustomFormatterProducesCorrectQueryString() var output = factory(new object[] { dict }); var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal($"/foo?{(int)TestEnum.A}=value1&{(int)TestEnum.B}=value2", uri.PathAndQuery); + Assert.Equal($"/foo?{(int)TestEnum.A}=value1{urlParameterFormatter.StringParameterSuffix}&{(int)TestEnum.B}=value2{urlParameterFormatter.StringParameterSuffix}", uri.PathAndQuery); } [Fact] @@ -2028,7 +2037,7 @@ public void ComplexQueryObjectWithDictionaryAndCustomFormatterProducesCorrectQue var output = factory(new object[] { complexQuery }); var uri = new Uri(new Uri("http://api"), output.RequestUri); - Assert.Equal($"/foo?TestDictionary.{(int)TestEnum.A}=value1&TestDictionary.{(int)TestEnum.B}=value2", uri.PathAndQuery); + Assert.Equal($"/foo?TestDictionary.{(int)TestEnum.A}=value1{urlParameterFormatter.StringParameterSuffix}&TestDictionary.{(int)TestEnum.B}=value2{urlParameterFormatter.StringParameterSuffix}", uri.PathAndQuery); } } diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index 1557b540d..b7b16b76d 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -416,9 +416,7 @@ List> BuildQueryMap(IDictionary dictionary, string if (DoNotConvertToQueryMap(obj)) { - var objType = obj.GetType(); - var formattedValue = settings.UrlParameterFormatter.Format(obj, objType, objType); - kvps.Add(new KeyValuePair(formattedKey, formattedValue)); + kvps.Add(new KeyValuePair(formattedKey, obj)); } else {