From 97c2c0b56843a1d5fcc2e48b10c1de833313a40e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:47:01 +0000 Subject: [PATCH 1/3] Initial plan From f59b6bb61d8dd16050b548f6c85d743e4df8126f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:56:55 +0000 Subject: [PATCH 2/3] Implement IParamConverter support for IDictionary values Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- .../DictionaryParameterTests.cs | 145 ++++++++++++++++++ .../Implementation/RequestHandler.cs | 15 +- .../ParamConverter/IParamConverterManager.cs | 4 +- .../Implementation/ParamConverterManager.cs | 35 ++++- 4 files changed, 194 insertions(+), 5 deletions(-) diff --git a/Activout.RestClient.Test/DictionaryParameterTests.cs b/Activout.RestClient.Test/DictionaryParameterTests.cs index c5568b6..6edf1bb 100644 --- a/Activout.RestClient.Test/DictionaryParameterTests.cs +++ b/Activout.RestClient.Test/DictionaryParameterTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; @@ -176,6 +178,134 @@ public async Task TestBackwardCompatibilityWithNonDictionaryParams() // assert _mockHttp.VerifyNoOutstandingExpectation(); } + + [Fact] + public async Task TestQueryParamDictionaryWithDateTime() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var testDate = new DateTime(2023, 12, 25, 14, 30, 45, DateTimeKind.Utc); + var expectedDateString = testDate.ToString("o"); // ISO 8601 format + var queryParams = new Dictionary + { + ["stringParam"] = "value1", + ["dateParam"] = testDate + }; + + _mockHttp + .When("https://example.com/api/test") + .WithExactQueryString($"stringParam=value1&dateParam={Uri.EscapeDataString(expectedDateString)}") + .Respond("application/json", "{}"); + + // act + await service.TestQueryParamObjectDictionary(queryParams); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestFormParamDictionaryWithDateTime() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var testDate = new DateTime(2023, 12, 25, 14, 30, 45, DateTimeKind.Utc); + var expectedDateString = testDate.ToString("o"); // ISO 8601 format + var formParams = new Dictionary + { + ["stringField"] = "value1", + ["dateField"] = testDate + }; + + _mockHttp + .When(HttpMethod.Post, "https://example.com/api/test") + .WithFormData("stringField", "value1") + .WithFormData("dateField", expectedDateString) + .Respond("application/json", "{}"); + + // act + await service.TestFormParamObjectDictionary(formParams); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestHeaderParamDictionaryWithDateTime() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var testDate = new DateTime(2023, 12, 25, 14, 30, 45, DateTimeKind.Utc); + var expectedDateString = testDate.ToString("o"); // ISO 8601 format + var headers = new Dictionary + { + ["X-String-Header"] = "value1", + ["X-Date-Header"] = testDate + }; + + _mockHttp + .When("https://example.com/api/test") + .WithHeaders("X-String-Header", "value1") + .WithHeaders("X-Date-Header", expectedDateString) + .Respond("application/json", "{}"); + + // act + await service.TestHeaderParamObjectDictionary(headers); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestGenericDictionaryWithDateTime() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var testDate = new DateTime(2023, 12, 25, 14, 30, 45, DateTimeKind.Utc); + var expectedDateString = testDate.ToString("o"); // ISO 8601 format + var queryParams = new Dictionary + { + ["startDate"] = testDate, + ["endDate"] = testDate.AddDays(1) + }; + + _mockHttp + .When("https://example.com/api/test") + .WithExactQueryString($"startDate={Uri.EscapeDataString(expectedDateString)}&endDate={Uri.EscapeDataString(testDate.AddDays(1).ToString("o"))}") + .Respond("application/json", "{}"); + + // act + await service.TestQueryParamDateTimeDictionary(queryParams); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task TestNonGenericDictionaryWithDateTime() + { + // arrange + var service = CreateRestClientBuilder().Build(); + var testDate = new DateTime(2023, 12, 25, 14, 30, 45, DateTimeKind.Utc); + var expectedDateString = testDate.ToString("o"); // ISO 8601 format + + var queryParams = new Hashtable + { + ["stringParam"] = "value1", + ["dateParam"] = testDate + }; + + _mockHttp + .When("https://example.com/api/test") + .WithExactQueryString($"stringParam=value1&dateParam={Uri.EscapeDataString(expectedDateString)}") + .Respond("application/json", "{}"); + + // act + await service.TestQueryParamNonGenericDictionary(queryParams); + + // assert + _mockHttp.VerifyNoOutstandingExpectation(); + } } public interface ITestService @@ -194,4 +324,19 @@ public interface ITestService [Get("test")] Task TestRegularParam([QueryParam("regularParam")] string regularParam); + + [Get("test")] + Task TestQueryParamObjectDictionary([QueryParam] Dictionary queryParams); + + [Post("test")] + Task TestFormParamObjectDictionary([FormParam] Dictionary formParams); + + [Get("test")] + Task TestHeaderParamObjectDictionary([HeaderParam] Dictionary headers); + + [Get("test")] + Task TestQueryParamDateTimeDictionary([QueryParam] Dictionary queryParams); + + [Get("test")] + Task TestQueryParamNonGenericDictionary([QueryParam] IDictionary queryParams); } \ No newline at end of file diff --git a/Activout.RestClient/Implementation/RequestHandler.cs b/Activout.RestClient/Implementation/RequestHandler.cs index 58b48ac..9d1971b 100644 --- a/Activout.RestClient/Implementation/RequestHandler.cs +++ b/Activout.RestClient/Implementation/RequestHandler.cs @@ -247,6 +247,15 @@ private void SetHeaders(HttpRequestMessage request, List request.Headers.Add(p.Key, p.Value.ToString())); } + private string ConvertValueToString(object value) + { + if (value == null) + return null; + + var converter = _context.ParamConverterManager.GetConverter(value.GetType()); + return converter?.ToString(value) ?? value.ToString(); + } + private CancellationToken GetParams( object[] args, Dictionary pathParams, @@ -300,7 +309,7 @@ private CancellationToken GetParams( foreach (DictionaryEntry entry in dictionary) { var key = entry.Key?.ToString(); - var value = entry.Value?.ToString(); + var value = ConvertValueToString(entry.Value); if (key != null && value != null) { queryParams.Add(Uri.EscapeDataString(key) + "=" + Uri.EscapeDataString(value)); @@ -321,7 +330,7 @@ private CancellationToken GetParams( foreach (DictionaryEntry entry in dictionary) { var key = entry.Key?.ToString(); - var value = entry.Value?.ToString(); + var value = ConvertValueToString(entry.Value); if (key != null && value != null) { formParams.Add(new KeyValuePair(key, value)); @@ -342,7 +351,7 @@ private CancellationToken GetParams( foreach (DictionaryEntry entry in dictionary) { var key = entry.Key?.ToString(); - var value = entry.Value?.ToString(); + var value = ConvertValueToString(entry.Value); if (key != null && value != null) { headers.AddOrReplaceHeader(key, value, headerParamAttribute.Replace); diff --git a/Activout.RestClient/ParamConverter/IParamConverterManager.cs b/Activout.RestClient/ParamConverter/IParamConverterManager.cs index 334b7b8..6da3814 100644 --- a/Activout.RestClient/ParamConverter/IParamConverterManager.cs +++ b/Activout.RestClient/ParamConverter/IParamConverterManager.cs @@ -1,9 +1,11 @@ -using System.Reflection; +using System; +using System.Reflection; namespace Activout.RestClient.ParamConverter { public interface IParamConverterManager { IParamConverter GetConverter(ParameterInfo parameterInfo); + IParamConverter GetConverter(Type type); } } \ No newline at end of file diff --git a/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs b/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs index 0ce7d9f..cad35d0 100644 --- a/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs +++ b/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Reflection; namespace Activout.RestClient.ParamConverter.Implementation @@ -24,5 +25,37 @@ public IParamConverter GetConverter(ParameterInfo parameterInfo) return null; } + + public IParamConverter GetConverter(Type type) + { + foreach (var paramConverter in ParamConverters) + { + if (paramConverter.CanConvert(CreateParameterInfoForType(type))) + { + return paramConverter; + } + } + + return null; + } + + private static ParameterInfo CreateParameterInfoForType(Type type) + { + // Create a minimal ParameterInfo-like object that only provides the Type + // This is used internally to reuse existing CanConvert logic + return new TypeOnlyParameterInfo(type); + } + + private class TypeOnlyParameterInfo : ParameterInfo + { + private readonly Type _parameterType; + + public TypeOnlyParameterInfo(Type parameterType) + { + _parameterType = parameterType; + } + + public override Type ParameterType => _parameterType; + } } } \ No newline at end of file From 1bff5004aeb8ca1abc0d09b4fe5bce26a0c93726 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:11:43 +0000 Subject: [PATCH 3/3] Change IParamConverter.CanConvert() to take Type instead of ParameterInfo Co-authored-by: twogood <189982+twogood@users.noreply.github.com> --- .../ParamConverter/IParamConverter.cs | 4 +-- .../DateTimeEpochParamConverter.cs | 5 ++- .../DateTimeIso8601ParamConverter.cs | 5 ++- .../Implementation/ParamConverterManager.cs | 31 ++----------------- .../Implementation/ToStringParamConverter.cs | 4 +-- 5 files changed, 10 insertions(+), 39 deletions(-) diff --git a/Activout.RestClient/ParamConverter/IParamConverter.cs b/Activout.RestClient/ParamConverter/IParamConverter.cs index d8fb4a4..44e39b1 100644 --- a/Activout.RestClient/ParamConverter/IParamConverter.cs +++ b/Activout.RestClient/ParamConverter/IParamConverter.cs @@ -1,10 +1,10 @@ -using System.Reflection; +using System; namespace Activout.RestClient.ParamConverter { public interface IParamConverter { - bool CanConvert(ParameterInfo parameterInfo); + bool CanConvert(Type type); string ToString(object value); } } \ No newline at end of file diff --git a/Activout.RestClient/ParamConverter/Implementation/DateTimeEpochParamConverter.cs b/Activout.RestClient/ParamConverter/Implementation/DateTimeEpochParamConverter.cs index 7f255d3..924be88 100644 --- a/Activout.RestClient/ParamConverter/Implementation/DateTimeEpochParamConverter.cs +++ b/Activout.RestClient/ParamConverter/Implementation/DateTimeEpochParamConverter.cs @@ -1,5 +1,4 @@ using System; -using System.Reflection; namespace Activout.RestClient.ParamConverter.Implementation { @@ -14,9 +13,9 @@ public static long ToUnixTime(this DateTime date) public class DateTimeEpochParamConverter : IParamConverter { - public bool CanConvert(ParameterInfo parameterInfo) + public bool CanConvert(Type type) { - return parameterInfo.ParameterType == typeof(DateTime); + return type == typeof(DateTime); } public string ToString(object value) diff --git a/Activout.RestClient/ParamConverter/Implementation/DateTimeIso8601ParamConverter.cs b/Activout.RestClient/ParamConverter/Implementation/DateTimeIso8601ParamConverter.cs index 528e643..6827615 100644 --- a/Activout.RestClient/ParamConverter/Implementation/DateTimeIso8601ParamConverter.cs +++ b/Activout.RestClient/ParamConverter/Implementation/DateTimeIso8601ParamConverter.cs @@ -1,13 +1,12 @@ using System; -using System.Reflection; namespace Activout.RestClient.ParamConverter.Implementation { public class DateTimeIso8601ParamConverter : IParamConverter { - public bool CanConvert(ParameterInfo parameterInfo) + public bool CanConvert(Type type) { - return parameterInfo.ParameterType == typeof(DateTime); + return type == typeof(DateTime); } public string ToString(object value) diff --git a/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs b/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs index cad35d0..807b4f9 100644 --- a/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs +++ b/Activout.RestClient/ParamConverter/Implementation/ParamConverterManager.cs @@ -15,22 +15,14 @@ public ParamConverterManager() public IParamConverter GetConverter(ParameterInfo parameterInfo) { - foreach (var paramConverter in ParamConverters) - { - if (paramConverter.CanConvert(parameterInfo)) - { - return paramConverter; - } - } - - return null; + return GetConverter(parameterInfo.ParameterType); } public IParamConverter GetConverter(Type type) { foreach (var paramConverter in ParamConverters) { - if (paramConverter.CanConvert(CreateParameterInfoForType(type))) + if (paramConverter.CanConvert(type)) { return paramConverter; } @@ -38,24 +30,5 @@ public IParamConverter GetConverter(Type type) return null; } - - private static ParameterInfo CreateParameterInfoForType(Type type) - { - // Create a minimal ParameterInfo-like object that only provides the Type - // This is used internally to reuse existing CanConvert logic - return new TypeOnlyParameterInfo(type); - } - - private class TypeOnlyParameterInfo : ParameterInfo - { - private readonly Type _parameterType; - - public TypeOnlyParameterInfo(Type parameterType) - { - _parameterType = parameterType; - } - - public override Type ParameterType => _parameterType; - } } } \ No newline at end of file diff --git a/Activout.RestClient/ParamConverter/Implementation/ToStringParamConverter.cs b/Activout.RestClient/ParamConverter/Implementation/ToStringParamConverter.cs index a2b0b32..e9e5af1 100644 --- a/Activout.RestClient/ParamConverter/Implementation/ToStringParamConverter.cs +++ b/Activout.RestClient/ParamConverter/Implementation/ToStringParamConverter.cs @@ -1,10 +1,10 @@ -using System.Reflection; +using System; namespace Activout.RestClient.ParamConverter.Implementation { public class ToStringParamConverter : IParamConverter { - public bool CanConvert(ParameterInfo parameterInfo) + public bool CanConvert(Type type) { return true; }