Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dictionary query request mapping #976

Merged
merged 5 commits into from
Oct 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 179 additions & 1 deletion Refit.Tests/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ public class ComplexQueryObject

public IEnumerable<int> TestCollection { get; set; }

[AliasAs("test-dictionary-alias")]
public Dictionary<TestEnum, string> TestAliasedDictionary { get; set; }

public Dictionary<TestEnum, string> TestDictionary { get; set; }

[AliasAs("listOfEnumMulti")]
[Query(CollectionFormat.Multi)]
public List<TestEnum> EnumCollectionMulti { get; set; }
Expand All @@ -153,7 +158,6 @@ public class ComplexQueryObject
public List<object> ObjectCollectionCcv { get; set; }
}


public class RestMethodInfoTests
{

Expand Down Expand Up @@ -777,6 +781,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<TestEnum, string> query);

[Get("/foo")]
Task QueryWithDictionaryWithPrefix([Query(".", "dictionary")] IDictionary<TestEnum, string> query);

[Get("/foo")]
Task QueryWithDictionaryWithNumericKey([Query] IDictionary<int, string> query);

[Get("/query")]
Task QueryWithEnumerableFormattedAsMulti([Query(CollectionFormat.Multi)]IEnumerable<string> lines);

Expand Down Expand Up @@ -931,6 +947,28 @@ 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)
{
if (parameterValue is TestEnum enumValue)
{
var enumBackingValue = (int)enumValue;
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
{
public override string Format(object parameterValue, ICustomAttributeProvider attributeProvider, Type type)
Expand Down Expand Up @@ -1877,6 +1915,146 @@ public void CachedRequestBuilderCallInternalBuilderForParametersWithSameNamesBut

Assert.Equal(4, internalBuilder.CallCount);
}

[Fact]
public void DictionaryQueryWithEnumKeyProducesCorrectQueryString()
{
var fixture = new RequestBuilderImplementation<IDummyHttpApi>();
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey));

var dict = new Dictionary<TestEnum, string>
{
{ 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 DictionaryQueryWithPrefix()
{
var fixture = new RequestBuilderImplementation<IDummyHttpApi>();
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithPrefix));

var dict = new Dictionary<TestEnum, string>
{
{ 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<IDummyHttpApi>();
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithNumericKey));

var dict = new Dictionary<int, string>
{
{ 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 DictionaryQueryWithCustomFormatterProducesCorrectQueryString()
{
var urlParameterFormatter = new TestEnumUrlParameterFormatter();

var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter };
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.QueryWithDictionaryWithEnumKey));

var dict = new Dictionary<TestEnum, string>
{
{ 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{urlParameterFormatter.StringParameterSuffix}&{(int)TestEnum.B}=value2{urlParameterFormatter.StringParameterSuffix}", uri.PathAndQuery);
}

[Fact]
public void ComplexQueryObjectWithAliasedDictionaryProducesCorrectQueryString()
{
var fixture = new RequestBuilderImplementation<IDummyHttpApi>();
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary));

var complexQuery = new ComplexQueryObject
{
TestAliasedDictionary = new Dictionary<TestEnum, string>
{
{ 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<IDummyHttpApi>();
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary));

var complexQuery = new ComplexQueryObject
{
TestDictionary = new Dictionary<TestEnum, string>
{
{ 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);
}

[Fact]
public void ComplexQueryObjectWithDictionaryAndCustomFormatterProducesCorrectQueryString()
{
var urlParameterFormatter = new TestEnumUrlParameterFormatter();
var refitSettings = new RefitSettings { UrlParameterFormatter = urlParameterFormatter };
var fixture = new RequestBuilderImplementation<IDummyHttpApi>(refitSettings);
var factory = fixture.BuildRequestFactoryForMethod(nameof(IDummyHttpApi.ComplexQueryObjectWithDictionary));

var complexQuery = new ComplexQueryObject
{
TestDictionary = new Dictionary<TestEnum, string>
{
{ 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{urlParameterFormatter.StringParameterSuffix}&TestDictionary.{(int)TestEnum.B}=value2{urlParameterFormatter.StringParameterSuffix}", uri.PathAndQuery);
}
}

static class RequestBuilderTestExtensions
Expand Down
13 changes: 8 additions & 5 deletions Refit/RequestBuilderImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -361,7 +362,7 @@ List<KeyValuePair<string, object>> 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))
{
Expand Down Expand Up @@ -404,22 +405,24 @@ List<KeyValuePair<string, object>> BuildQueryMap(IDictionary dictionary, string
{
var kvps = new List<KeyValuePair<string, object>>();

var props = dictionary.Keys;
foreach (string key in props)
foreach (var key in dictionary.Keys)
{
var obj = dictionary[key];
if (obj == null)
continue;

var keyType = key.GetType();
var formattedKey = settings.UrlParameterFormatter.Format(key, keyType, keyType);

if (DoNotConvertToQueryMap(obj))
{
kvps.Add(new KeyValuePair<string, object>(key, obj));
kvps.Add(new KeyValuePair<string, object>(formattedKey, obj));
}
else
{
foreach (var keyValuePair in BuildQueryMap(obj, delimiter))
{
kvps.Add(new KeyValuePair<string, object>($"{key}{delimiter}{keyValuePair.Key}", keyValuePair.Value));
kvps.Add(new KeyValuePair<string, object>($"{formattedKey}{delimiter}{keyValuePair.Key}", keyValuePair.Value));
}
}
}
Expand Down