diff --git a/Activout.RestClient.Test/DictionaryParameterTests.cs b/Activout.RestClient.Test/DictionaryParameterTests.cs new file mode 100644 index 0000000..e215f58 --- /dev/null +++ b/Activout.RestClient.Test/DictionaryParameterTests.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using RichardSzalay.MockHttp; +using Xunit; +using Xunit.Abstractions; + +namespace Activout.RestClient.Test; + +public class DictionaryParameterTests(ITestOutputHelper outputHelper) +{ + private const string BaseUri = "https://example.com/api"; + + private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory(); + private readonly MockHttpMessageHandler _mockHttp = new MockHttpMessageHandler(); + private readonly ILoggerFactory _loggerFactory = LoggerFactoryHelpers.CreateLoggerFactory(outputHelper); + + private IRestClientBuilder CreateRestClientBuilder() + { + return _restClientFactory.CreateBuilder() + .With(_loggerFactory.CreateLogger()) + .With(_mockHttp.ToHttpClient()) + .BaseUri(BaseUri); + } + + [Fact] + public async Task TestQueryParamDictionary() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var queryParams = new Dictionary + { + ["param1"] = "value1", + ["param2"] = "value2" + }; + + _mockHttp + .When("https://example.com/api/test") + .WithQueryString("param1=value1¶m2=value2") + .Respond("application/json", "{}"); + + // act + await service.TestQueryParamDictionary(queryParams); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestFormParamDictionary() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var formParams = new Dictionary + { + ["field1"] = "value1", + ["field2"] = "value2" + }; + + _mockHttp + .When(HttpMethod.Post, "https://example.com/api/test") + .WithFormData("field1", "value1") + .WithFormData("field2", "value2") + .Respond("application/json", "{}"); + + // act + await service.TestFormParamDictionary(formParams); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestHeaderParamDictionary() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var headers = new Dictionary + { + ["X-Custom-Header1"] = "value1", + ["X-Custom-Header2"] = "value2" + }; + + _mockHttp + .When("https://example.com/api/test") + .WithHeaders("X-Custom-Header1", "value1") + .WithHeaders("X-Custom-Header2", "value2") + .Respond("application/json", "{}"); + + // act + await service.TestHeaderParamDictionary(headers); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestMixedDictionaryAndRegularParams() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var queryParams = new Dictionary + { + ["param1"] = "value1", + ["param2"] = "value2" + }; + + _mockHttp + .When("https://example.com/api/test") + .WithQueryString("param1=value1¶m2=value2&singleParam=singleValue") + .Respond("application/json", "{}"); + + // act + await service.TestMixedParams(queryParams, "singleValue"); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestEmptyDictionary() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var emptyParams = new Dictionary(); + + _mockHttp + .When("https://example.com/api/test") + .Respond("application/json", "{}"); + + // act + await service.TestQueryParamDictionary(emptyParams); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestNullDictionaryValues() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var paramsWithNull = new Dictionary + { + ["param1"] = "value1", + ["param2"] = null + }; + + _mockHttp + .When("https://example.com/api/test") + .WithQueryString("param1=value1¶m2=") + .Respond("application/json", "{}"); + + // act + await service.TestQueryParamDictionary(paramsWithNull); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestBackwardCompatibilityWithNonDictionaryParams() + { + // arrange + var service = CreateRestClientBuilder().Build(); + + _mockHttp + .When("https://example.com/api/test") + .WithQueryString("regularParam=regularValue") + .Respond("application/json", "{}"); + + // act + await service.TestRegularParam("regularValue"); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } +} + +public interface ITestService +{ + [Get("test")] + Task TestQueryParamDictionary([QueryParam] Dictionary queryParams); + + [Post("test")] + Task TestFormParamDictionary([FormParam] Dictionary formParams); + + [Get("test")] + Task TestHeaderParamDictionary([HeaderParam] Dictionary headers); + + [Get("test")] + Task TestMixedParams([QueryParam] Dictionary queryParams, [QueryParam("singleParam")] string singleParam); + + [Get("test")] + Task TestRegularParam([QueryParam("regularParam")] string regularParam); +} \ No newline at end of file diff --git a/Activout.RestClient/Implementation/RequestHandler.cs b/Activout.RestClient/Implementation/RequestHandler.cs index 87ba7d3..1928913 100644 --- a/Activout.RestClient/Implementation/RequestHandler.cs +++ b/Activout.RestClient/Implementation/RequestHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net; @@ -295,20 +296,56 @@ private CancellationToken GetParams( } else if (attribute is QueryParamAttribute queryParamAttribute) { - queryParams.Add(Uri.EscapeDataString(queryParamAttribute.Name ?? parameterName) + "=" + - Uri.EscapeDataString(stringValue)); + if (rawValue is IDictionary dictionary) + { + foreach (DictionaryEntry entry in dictionary) + { + var key = entry.Key?.ToString() ?? string.Empty; + var value = entry.Value?.ToString() ?? string.Empty; + queryParams.Add(Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(value)); + } + } + else + { + queryParams.Add(Uri.EscapeDataString(queryParamAttribute.Name ?? parameterName) + "=" + + Uri.EscapeDataString(stringValue)); + } handled = true; } else if (attribute is FormParamAttribute formParamAttribute) { - formParams.Add(new KeyValuePair(formParamAttribute.Name ?? parameterName, - stringValue)); + if (rawValue is IDictionary dictionary) + { + foreach (DictionaryEntry entry in dictionary) + { + var key = entry.Key?.ToString() ?? string.Empty; + var value = entry.Value?.ToString() ?? string.Empty; + formParams.Add(new KeyValuePair(key, value)); + } + } + else + { + formParams.Add(new KeyValuePair(formParamAttribute.Name ?? parameterName, + stringValue)); + } handled = true; } else if (attribute is HeaderParamAttribute headerParamAttribute) { - headers.AddOrReplaceHeader(headerParamAttribute.Name ?? parameterName, stringValue, - headerParamAttribute.Replace); + if (rawValue is IDictionary dictionary) + { + foreach (DictionaryEntry entry in dictionary) + { + var key = entry.Key?.ToString() ?? string.Empty; + var value = entry.Value?.ToString() ?? string.Empty; + headers.AddOrReplaceHeader(key, value, headerParamAttribute.Replace); + } + } + else + { + headers.AddOrReplaceHeader(headerParamAttribute.Name ?? parameterName, stringValue, + headerParamAttribute.Replace); + } handled = true; } }