diff --git a/Activout.RestClient.Test.Json/Activout.RestClient.Test.Json.csproj b/Activout.RestClient.Test.Json/Activout.RestClient.Test.Json.csproj
new file mode 100644
index 0000000..7b053ca
--- /dev/null
+++ b/Activout.RestClient.Test.Json/Activout.RestClient.Test.Json.csproj
@@ -0,0 +1,31 @@
+
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/LoggerFactoryHelpers.cs b/Activout.RestClient.Test.Json/LoggerFactoryHelpers.cs
new file mode 100644
index 0000000..d4ff212
--- /dev/null
+++ b/Activout.RestClient.Test.Json/LoggerFactoryHelpers.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace Activout.RestClient.Test.Json;
+
+public static class LoggerFactoryHelpers
+{
+ public static ILoggerFactory CreateLoggerFactory(ITestOutputHelper outputHelper)
+ {
+ return LoggerFactory.Create(builder =>
+ {
+ builder
+ .AddFilter("Microsoft", LogLevel.Warning)
+ .AddFilter("System", LogLevel.Warning)
+ .AddFilter("Activout.RestClient", LogLevel.Debug)
+ .AddXUnit(outputHelper);
+ });
+ }
+}
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/MovieReviews/ErrorResponse.cs b/Activout.RestClient.Test.Json/MovieReviews/ErrorResponse.cs
new file mode 100644
index 0000000..7a22081
--- /dev/null
+++ b/Activout.RestClient.Test.Json/MovieReviews/ErrorResponse.cs
@@ -0,0 +1,12 @@
+namespace Activout.RestClient.Test.Json.MovieReviews;
+
+public class ErrorResponse
+{
+ public List Errors { get; set; } = [];
+
+ public class Error
+ {
+ public string Message { get; set; } = string.Empty;
+ public int Code { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/MovieReviews/IMovieReviewService.cs b/Activout.RestClient.Test.Json/MovieReviews/IMovieReviewService.cs
new file mode 100644
index 0000000..5ed2a89
--- /dev/null
+++ b/Activout.RestClient.Test.Json/MovieReviews/IMovieReviewService.cs
@@ -0,0 +1,36 @@
+namespace Activout.RestClient.Test.Json.MovieReviews;
+
+[Path("movies")]
+[ErrorResponse(typeof(ErrorResponse))]
+public interface IMovieReviewService
+{
+ [Get]
+ Task> GetAllMovies();
+
+ [Get]
+ Task> GetAllMoviesCancellable(CancellationToken cancellationToken);
+
+ [Get]
+ [Path("/{movieId}/reviews")]
+ Task> GetAllReviews(string movieId);
+
+ [Get("/{movieId}/reviews/{reviewId}")]
+ Review GetReview(string movieId, string reviewId);
+
+ [Post]
+ [Path("/{movieId}/reviews")]
+ Task SubmitReview([PathParam("movieId")] string movieId, Review review);
+
+ [Put]
+ [Path("/{movieId}/reviews/{reviewId}")]
+ Review UpdateReview(string movieId, [PathParam] string reviewId, Review review);
+
+ [Patch]
+ [Path("/{movieId}/reviews/{reviewId}")]
+ Review PartialUpdateReview(string movieId, [PathParam] string reviewId, Review review);
+
+ [Get]
+ Task> QueryMoviesByDate(
+ [QueryParam] DateTime begin,
+ [QueryParam] DateTime end);
+}
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/MovieReviews/Movie.cs b/Activout.RestClient.Test.Json/MovieReviews/Movie.cs
new file mode 100644
index 0000000..c14b95f
--- /dev/null
+++ b/Activout.RestClient.Test.Json/MovieReviews/Movie.cs
@@ -0,0 +1,9 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Activout.RestClient.Test.Json.MovieReviews;
+
+[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
+public class Movie
+{
+ public string Title { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/MovieReviews/Review.cs b/Activout.RestClient.Test.Json/MovieReviews/Review.cs
new file mode 100644
index 0000000..85b9568
--- /dev/null
+++ b/Activout.RestClient.Test.Json/MovieReviews/Review.cs
@@ -0,0 +1,15 @@
+namespace Activout.RestClient.Test.Json.MovieReviews;
+
+public class Review
+{
+ public Review(int stars, string text)
+ {
+ Stars = stars;
+ Text = text;
+ }
+
+ public string? MovieId { get; set; }
+ public string? ReviewId { get; set; }
+ public int Stars { get; set; }
+ public string Text { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/RestClientTests.cs b/Activout.RestClient.Test.Json/RestClientTests.cs
new file mode 100644
index 0000000..efab20e
--- /dev/null
+++ b/Activout.RestClient.Test.Json/RestClientTests.cs
@@ -0,0 +1,310 @@
+using Activout.RestClient.Helpers.Implementation;
+using Activout.RestClient.Json;
+using Activout.RestClient.Newtonsoft.Json;
+using Activout.RestClient.Test.Json.MovieReviews;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using RichardSzalay.MockHttp;
+using System.Net;
+using System.Text;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Activout.RestClient.Test.Json;
+
+public enum JsonImplementation
+{
+ SystemTextJson,
+ NewtonsoftJson
+}
+
+public class RestClientTests(ITestOutputHelper outputHelper)
+{
+ private const string BaseUri = "https://example.com/movieReviewService";
+ private const string MovieId = "*MOVIE_ID*";
+ private const string ReviewId = "*REVIEW_ID*";
+
+ private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory();
+ private readonly MockHttpMessageHandler _mockHttp = new();
+ private readonly ILoggerFactory _loggerFactory = LoggerFactoryHelpers.CreateLoggerFactory(outputHelper);
+
+ private IRestClientBuilder CreateRestClientBuilder(JsonImplementation jsonImplementation)
+ {
+ var builder = _restClientFactory.CreateBuilder();
+
+ return jsonImplementation switch
+ {
+ JsonImplementation.SystemTextJson => builder.WithSystemTextJson(),
+ JsonImplementation.NewtonsoftJson => builder.WithNewtonsoftJson(),
+ _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation))
+ };
+ }
+
+ private IMovieReviewService CreateMovieReviewService(JsonImplementation jsonImplementation)
+ {
+ return CreateRestClientBuilder(jsonImplementation)
+ .Accept("application/json")
+ .ContentType("application/json")
+ .With(_loggerFactory.CreateLogger())
+ .With(_mockHttp.ToHttpClient())
+ .BaseUri(BaseUri)
+ .Build();
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestErrorAsyncWithOldTaskConverter(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ ExpectGetAllReviewsAndReturnError();
+
+ var reviewSvc = CreateRestClientBuilder(jsonImplementation)
+ .Accept("application/json")
+ .ContentType("application/json")
+ .With(new TaskConverterFactory())
+ .With(_loggerFactory.CreateLogger())
+ .With(_mockHttp.ToHttpClient())
+ .BaseUri(BaseUri)
+ .Build();
+
+ // act
+ var aggregateException =
+ await Assert.ThrowsAsync(() => reviewSvc.GetAllReviews(MovieId));
+
+ // assert
+ _mockHttp.VerifyNoOutstandingExpectation();
+
+ Assert.IsType(aggregateException.InnerException);
+ var exception = (RestClientException)aggregateException.InnerException!;
+
+ Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode);
+ var error = exception.GetErrorResponse();
+ Assert.Equal(34, error.Errors[0].Code);
+ Assert.Equal("Sorry, that page does not exist", error.Errors[0].Message);
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestErrorAsync(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ ExpectGetAllReviewsAndReturnError();
+
+ var reviewSvc = CreateMovieReviewService(jsonImplementation);
+
+ // act
+ var exception =
+ await Assert.ThrowsAsync(() => reviewSvc.GetAllReviews(MovieId));
+
+ // assert
+ _mockHttp.VerifyNoOutstandingExpectation();
+
+ Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode);
+ var error = exception.GetErrorResponse();
+ Assert.Equal(34, error.Errors[0].Code);
+ Assert.Equal("Sorry, that page does not exist", error.Errors[0].Message);
+ }
+
+ private void ExpectGetAllReviewsAndReturnError(string movieId = MovieId)
+ {
+ _mockHttp
+ .Expect(HttpMethod.Get, $"{BaseUri}/movies/{movieId}/reviews")
+ .Respond(HttpStatusCode.NotFound, request => new StringContent(JsonConvert.SerializeObject(new
+ {
+ Errors = new object[]
+ {
+ new {Message = "Sorry, that page does not exist", Code = 34}
+ }
+ }),
+ Encoding.UTF8,
+ "application/json"));
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public void TestErrorSync(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ _mockHttp
+ .When(HttpMethod.Get, $"{BaseUri}/movies/{MovieId}/reviews/{ReviewId}")
+ .Respond(HttpStatusCode.NotFound, request => new StringContent(JsonConvert.SerializeObject(new
+ {
+ Errors = new object[]
+ {
+ new {Message = "Sorry, that page does not exist", Code = 34}
+ }
+ }),
+ Encoding.UTF8,
+ "application/json"));
+
+ var reviewSvc = CreateMovieReviewService(jsonImplementation);
+
+ // act
+ var aggregateException = Assert.Throws(() => reviewSvc.GetReview(MovieId, ReviewId));
+
+ // assert
+ var exception = (RestClientException)aggregateException.GetBaseException();
+ Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode);
+
+ dynamic dynamicError = exception.ErrorResponse!;
+ string message = dynamicError.Errors[0].Message;
+ int code = dynamicError.Errors[0].Code;
+ Assert.Equal(34, code);
+ Assert.Equal("Sorry, that page does not exist", message);
+
+ var error = exception.GetErrorResponse();
+ Assert.Equal(34, error.Errors[0].Code);
+ Assert.Equal("Sorry, that page does not exist", error.Errors[0].Message);
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestGetEmptyIEnumerableAsync(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ _mockHttp
+ .When($"{BaseUri}/movies")
+ .WithHeaders("Accept", "application/json")
+ .Respond("application/json", "[]");
+
+ var reviewSvc = CreateMovieReviewService(jsonImplementation);
+
+ // act
+ var movies = await reviewSvc.GetAllMovies();
+
+ // assert
+ Assert.Empty(movies);
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestQueryParamAsync(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ _mockHttp
+ .When($"{BaseUri}/movies?begin=2017-01-01T00%3A00%3A00.0000000Z&end=2018-01-01T00%3A00%3A00.0000000Z")
+ .Respond("application/json", "[{\"Title\":\"Blade Runner 2049\"}]");
+
+ var reviewSvc = CreateMovieReviewService(jsonImplementation);
+
+ // act
+ var movies = await reviewSvc.QueryMoviesByDate(
+ new DateTime(2017, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ new DateTime(2018, 1, 1, 0, 0, 0, DateTimeKind.Utc));
+
+ // assert
+ var list = movies.ToList();
+ Assert.Single(list);
+ var movie = list.First();
+ Assert.Equal("Blade Runner 2049", movie.Title);
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestPostJsonAsync(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ var movieId = "FOOBAR";
+
+ // The replacement logic needs to handle different JSON formats
+ _mockHttp
+ .When(HttpMethod.Post, $"{BaseUri}/movies/{movieId}/reviews")
+ .WithHeaders("Content-Type", "application/json; charset=utf-8")
+ .Respond(request =>
+ {
+ var content = request.Content!.ReadAsStringAsync().Result;
+
+ // Handle different serialization formats
+ content = jsonImplementation switch
+ {
+ JsonImplementation.SystemTextJson => content.Replace("\"reviewId\":null", "\"reviewId\":\"*REVIEW_ID*\"")
+ .Replace("}", ",\"reviewId\":\"*REVIEW_ID*\"}"), // Add reviewId if not present
+ JsonImplementation.NewtonsoftJson => content.Replace("\"ReviewId\":null", "\"ReviewId\":\"*REVIEW_ID*\""),
+ _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation))
+ };
+
+ return new StringContent(content, Encoding.UTF8, "application/json");
+ });
+
+ var reviewSvc = CreateMovieReviewService(jsonImplementation);
+
+ // act
+ var text = "This was a delightful comedy, but not terribly realistic.";
+ var stars = 3;
+ var review = new Review(stars, text);
+ var result = await reviewSvc.SubmitReview(movieId, review);
+
+ // assert
+ Assert.Equal("*REVIEW_ID*", result.ReviewId);
+ Assert.Equal(stars, result.Stars);
+ Assert.Equal(text, result.Text);
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public void TestPutSync(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ var movieId = "*MOVIE_ID*";
+ var reviewId = "*REVIEW_ID*";
+ _mockHttp
+ .When(HttpMethod.Put, $"{BaseUri}/movies/{movieId}/reviews/{reviewId}")
+ .Respond(request => request.Content!);
+
+ var reviewSvc = CreateMovieReviewService(jsonImplementation);
+
+ // act
+ var text = "This was actally really good!";
+ var stars = 5;
+ var review = new Review(stars, text)
+ {
+ MovieId = movieId,
+ ReviewId = reviewId
+ };
+ var result = reviewSvc.UpdateReview(movieId, reviewId, review);
+
+ // assert
+ Assert.Equal(movieId, result.MovieId);
+ Assert.Equal(reviewId, result.ReviewId);
+ Assert.Equal(stars, result.Stars);
+ Assert.Equal(text, result.Text);
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public void TestPatchSync(JsonImplementation jsonImplementation)
+ {
+ // arrange
+ var movieId = "*MOVIE_ID*";
+ var reviewId = "*REVIEW_ID*";
+ _mockHttp
+ .When(HttpMethod.Patch, $"{BaseUri}/movies/{movieId}/reviews/{reviewId}")
+ .Respond(request => request.Content!);
+
+ var reviewSvc = CreateMovieReviewService(jsonImplementation);
+
+ // act
+ var text = "This was actally really good!";
+ var stars = 5;
+ var review = new Review(stars, text)
+ {
+ MovieId = movieId,
+ ReviewId = reviewId
+ };
+ var result = reviewSvc.PartialUpdateReview(movieId, reviewId, review);
+
+ // assert
+ Assert.Equal(movieId, result.MovieId);
+ Assert.Equal(reviewId, result.ReviewId);
+ Assert.Equal(stars, result.Stars);
+ Assert.Equal(text, result.Text);
+ }
+}
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/SerializationOrderTest.cs b/Activout.RestClient.Test.Json/SerializationOrderTest.cs
new file mode 100644
index 0000000..402f7f5
--- /dev/null
+++ b/Activout.RestClient.Test.Json/SerializationOrderTest.cs
@@ -0,0 +1,114 @@
+using Activout.RestClient.Json;
+using Activout.RestClient.Newtonsoft.Json;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using RichardSzalay.MockHttp;
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using Xunit;
+
+namespace Activout.RestClient.Test.Json;
+
+public class SerializationOrderTest
+{
+ private const string BaseUri = "https://example.com/";
+ private const int OrderFirst = -1000;
+ private const int OrderLast = 1000;
+
+ private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory();
+ private readonly MockHttpMessageHandler _mockHttp = new();
+
+ [Theory]
+ [InlineData(OrderFirst, "snake")]
+ [InlineData(OrderLast, "camel")]
+ public async Task TestSerializationOrderNewtonsoft(int order, string expectedValue)
+ {
+ // Arrange
+ var client = CreateNewtonsoftClient(order);
+
+ _mockHttp
+ .Expect(HttpMethod.Get, BaseUri)
+ .Respond(new StringContent(JsonConvert.SerializeObject(new
+ {
+ my_value = "snake",
+ MyValue = "camel"
+ }),
+ Encoding.UTF8,
+ "application/json"));
+
+ // Act
+ var model = await client.GetValue();
+
+ // Assert
+ Assert.Equal(expectedValue, model.MyValue);
+ }
+
+ [Theory]
+ [InlineData("camel")] // System.Text.Json uses camelCase by default and matches MyValue
+ public async Task TestSerializationOrderSystemTextJson(string expectedValue)
+ {
+ // Arrange
+ var client = CreateSystemTextJsonClient();
+
+ _mockHttp
+ .Expect(HttpMethod.Get, BaseUri)
+ .Respond(new StringContent(System.Text.Json.JsonSerializer.Serialize(new
+ {
+ myValue = "camel", // System.Text.Json maps MyValue to myValue (camelCase)
+ my_value = "snake"
+ }),
+ Encoding.UTF8,
+ "application/json"));
+
+ // Act
+ var model = await client.GetValue();
+
+ // Assert
+ Assert.Equal(expectedValue, model.MyValue);
+ }
+
+ public class SerializationOrderModel
+ {
+ public string MyValue { get; set; } = string.Empty;
+ }
+
+ public interface ISerializationOrderClient
+ {
+ Task GetValue();
+ }
+
+ private ISerializationOrderClient CreateNewtonsoftClient(int orderOfJsonDeserializer)
+ {
+ return CreateRestClientBuilder()
+ .WithNewtonsoftJson()
+ .With(new NewtonsoftJsonDeserializer(new JsonSerializerSettings()
+ {
+ ContractResolver = new DefaultContractResolver()
+ {
+ NamingStrategy = new SnakeCaseNamingStrategy()
+ }
+ })
+ { Order = orderOfJsonDeserializer })
+ .Build();
+ }
+
+ private ISerializationOrderClient CreateSystemTextJsonClient()
+ {
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+
+ return CreateRestClientBuilder()
+ .WithSystemTextJson(options)
+ .Build();
+ }
+
+ private IRestClientBuilder CreateRestClientBuilder()
+ {
+ return _restClientFactory.CreateBuilder()
+ .With(_mockHttp.ToHttpClient())
+ .BaseUri(new Uri(BaseUri));
+ }
+}
\ No newline at end of file
diff --git a/Activout.RestClient.Test.Json/SimpleValueObjectTest.cs b/Activout.RestClient.Test.Json/SimpleValueObjectTest.cs
new file mode 100644
index 0000000..8f51eed
--- /dev/null
+++ b/Activout.RestClient.Test.Json/SimpleValueObjectTest.cs
@@ -0,0 +1,160 @@
+using Activout.RestClient.Json;
+using Activout.RestClient.Newtonsoft.Json;
+using Newtonsoft.Json;
+using RichardSzalay.MockHttp;
+using System.Net;
+using System.Text;
+using Xunit;
+
+namespace Activout.RestClient.Test.Json;
+
+public record MySimpleValueObject(string Value);
+
+public class ApiData
+{
+ public MySimpleValueObject? FooBar { get; set; }
+ public int? NullableInteger { get; set; }
+}
+
+public interface IValueObjectClient
+{
+ Task GetData();
+
+ [Post]
+ Task SetData(ApiData wrapper);
+}
+
+public class SimpleValueObjectTest
+{
+ private const string BaseUri = "https://example.com/api/";
+
+ private readonly IRestClientFactory _restClientFactory = Services.CreateRestClientFactory();
+ private readonly MockHttpMessageHandler _mockHttp = new();
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestSimpleValueObjectSerialization(JsonImplementation jsonImplementation)
+ {
+ // Arrange
+ var expectedContent = jsonImplementation switch
+ {
+ JsonImplementation.SystemTextJson => System.Text.Json.JsonSerializer.Serialize(new
+ {
+ fooBar = "foobar",
+ nullableInteger = 42
+ }),
+ JsonImplementation.NewtonsoftJson => JsonConvert.SerializeObject(new
+ {
+ FooBar = "foobar",
+ NullableInteger = 42
+ }),
+ _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation))
+ };
+
+ _mockHttp
+ .Expect(HttpMethod.Post, BaseUri)
+ .WithContent(expectedContent)
+ .Respond(HttpStatusCode.OK);
+
+ var client = CreateClient(jsonImplementation);
+
+ var wrapper = new ApiData
+ {
+ FooBar = new MySimpleValueObject("foobar"),
+ NullableInteger = 42
+ };
+
+ // Act
+ await client.SetData(wrapper);
+
+ // Assert
+ _mockHttp.VerifyNoOutstandingExpectation();
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestSimpleValueObjectDeserialization(JsonImplementation jsonImplementation)
+ {
+ // Arrange
+ var responseContent = jsonImplementation switch
+ {
+ JsonImplementation.SystemTextJson => System.Text.Json.JsonSerializer.Serialize(new
+ {
+ FooBar = "foobar",
+ NullableInteger = 42
+ }),
+ JsonImplementation.NewtonsoftJson => JsonConvert.SerializeObject(new
+ {
+ FooBar = "foobar",
+ NullableInteger = 42
+ }),
+ _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation))
+ };
+
+ _mockHttp
+ .Expect(BaseUri)
+ .Respond(new StringContent(responseContent, Encoding.UTF8, "application/json"));
+
+ var client = CreateClient(jsonImplementation);
+
+ // Act
+ var result = await client.GetData();
+
+ // Assert
+ _mockHttp.VerifyNoOutstandingExpectation();
+ Assert.Equal("foobar", result.FooBar?.Value);
+ Assert.Equal(42, result.NullableInteger);
+ }
+
+ [Theory]
+ [InlineData(JsonImplementation.SystemTextJson)]
+ [InlineData(JsonImplementation.NewtonsoftJson)]
+ public async Task TestSimpleValueObjectDeserializationWithNulls(JsonImplementation jsonImplementation)
+ {
+ // Arrange
+ var responseContent = jsonImplementation switch
+ {
+ JsonImplementation.SystemTextJson => System.Text.Json.JsonSerializer.Serialize(new
+ {
+ FooBar = (string?)null,
+ NullableInteger = (int?)null
+ }),
+ JsonImplementation.NewtonsoftJson => JsonConvert.SerializeObject(new
+ {
+ FooBar = (string?)null,
+ NullableInteger = (int?)null
+ }),
+ _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation))
+ };
+
+ _mockHttp
+ .Expect(BaseUri)
+ .Respond(new StringContent(responseContent, Encoding.UTF8, "application/json"));
+
+ var client = CreateClient(jsonImplementation);
+
+ // Act
+ var result = await client.GetData();
+
+ // Assert
+ _mockHttp.VerifyNoOutstandingExpectation();
+ Assert.Null(result.FooBar);
+ Assert.Null(result.NullableInteger);
+ }
+
+ private IValueObjectClient CreateClient(JsonImplementation jsonImplementation)
+ {
+ var builder = _restClientFactory.CreateBuilder()
+ .With(_mockHttp.ToHttpClient())
+ .BaseUri(new Uri(BaseUri));
+
+ return jsonImplementation switch
+ {
+ JsonImplementation.SystemTextJson => builder.WithSystemTextJson().Build(),
+ JsonImplementation.NewtonsoftJson => builder.WithNewtonsoftJson().Build(),
+ _ => throw new ArgumentOutOfRangeException(nameof(jsonImplementation))
+ };
+ }
+}
\ No newline at end of file
diff --git a/ActivoutRestClient.sln b/ActivoutRestClient.sln
index 56767a2..d2b072c 100644
--- a/ActivoutRestClient.sln
+++ b/ActivoutRestClient.sln
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Activout.RestClient.Json",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Activout.RestClient.Json.Test", "Activout.RestClient.Json.Test\Activout.RestClient.Json.Test.csproj", "{690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Activout.RestClient.Test.Json", "Activout.RestClient.Test.Json\Activout.RestClient.Test.Json.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -57,6 +59,10 @@ Global
{690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{690BAF36-9404-44EE-ABA8-E16F5FEAFDFA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1B2C3D4-E5F6-7890-ABCD-EFABCDEF1234}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE