diff --git a/src/Paralax.HTTP/src/Paralax.HTTP/ParalaxHttpClient.cs b/src/Paralax.HTTP/src/Paralax.HTTP/ParalaxHttpClient.cs index b994d70..ad3e309 100644 --- a/src/Paralax.HTTP/src/Paralax.HTTP/ParalaxHttpClient.cs +++ b/src/Paralax.HTTP/src/Paralax.HTTP/ParalaxHttpClient.cs @@ -13,113 +13,122 @@ public class ParalaxHttpClient : IHttpClient { private const string JsonContentType = "application/json"; private readonly HttpClient _client; + private readonly HttpClientOptions _options; private readonly IHttpClientSerializer _serializer; - private readonly int _retries; - public ParalaxHttpClient(HttpClient client, IHttpClientSerializer serializer, int retries = 3) + public ParalaxHttpClient(HttpClient client, HttpClientOptions options, IHttpClientSerializer serializer, + ICorrelationContextFactory correlationContextFactory, ICorrelationIdFactory correlationIdFactory) { _client = client ?? throw new ArgumentNullException(nameof(client)); + _options = options ?? throw new ArgumentNullException(nameof(options)); _serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); - _retries = retries; + + // Set default headers for correlation context and correlation ID + if (!string.IsNullOrWhiteSpace(_options.CorrelationContextHeader)) + { + var correlationContext = correlationContextFactory.Create(); + _client.DefaultRequestHeaders.TryAddWithoutValidation(_options.CorrelationContextHeader, correlationContext); + } + + if (!string.IsNullOrWhiteSpace(_options.CorrelationIdHeader)) + { + var correlationId = correlationIdFactory.Create(); + _client.DefaultRequestHeaders.TryAddWithoutValidation(_options.CorrelationIdHeader, correlationId); + } } - // IHttpClientBase implementation + // Overloaded methods for GET, POST, PUT, PATCH, DELETE with proper serializer and content handling. + public virtual Task GetAsync(string uri) + => SendAsync(uri, Method.Get); - public Task GetAsync(string uri) => SendAsync(uri, HttpMethod.Get); - public Task PostAsync(string uri, HttpContent content) => SendAsync(uri, HttpMethod.Post, content); - public Task PutAsync(string uri, HttpContent content) => SendAsync(uri, HttpMethod.Put, content); - public Task PatchAsync(string uri, HttpContent content) => SendAsync(uri, HttpMethod.Patch, content); - public Task DeleteAsync(string uri) => SendAsync(uri, HttpMethod.Delete); + public virtual Task GetAsync(string uri, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Get, null, serializer); - // IHttpClientWithSerialization implementation + public Task> GetResultAsync(string uri, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Get, null, serializer); - public async Task GetAsync(string uri, IHttpClientSerializer serializer = null) - { - var response = await SendAsync(uri, HttpMethod.Get).ConfigureAwait(false); - return await DeserializeResponse(response, serializer).ConfigureAwait(false); - } + public virtual Task PostAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Post, GetJsonPayload(data, serializer)); - public async Task PostAsync(string uri, object data = null, IHttpClientSerializer serializer = null) - { - var content = GetJsonPayload(data, serializer); - var response = await SendAsync(uri, HttpMethod.Post, content).ConfigureAwait(false); - return await DeserializeResponse(response, serializer).ConfigureAwait(false); - } + public Task PostAsync(string uri, HttpContent content) + => SendAsync(uri, Method.Post, content); - public async Task PutAsync(string uri, object data = null, IHttpClientSerializer serializer = null) - { - var content = GetJsonPayload(data, serializer); - var response = await SendAsync(uri, HttpMethod.Put, content).ConfigureAwait(false); - return await DeserializeResponse(response, serializer).ConfigureAwait(false); - } + public virtual Task PostAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Post, GetJsonPayload(data, serializer), serializer); - public async Task PatchAsync(string uri, object data = null, IHttpClientSerializer serializer = null) - { - var content = GetJsonPayload(data, serializer); - var response = await SendAsync(uri, HttpMethod.Patch, content).ConfigureAwait(false); - return await DeserializeResponse(response, serializer).ConfigureAwait(false); - } + public Task PostAsync(string uri, HttpContent content, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Post, content, serializer); - public async Task DeleteAsync(string uri, IHttpClientSerializer serializer = null) - { - var response = await SendAsync(uri, HttpMethod.Delete).ConfigureAwait(false); - return await DeserializeResponse(response, serializer).ConfigureAwait(false); - } + public Task> PostResultAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Post, GetJsonPayload(data, serializer), serializer); - // IHttpClientWithResult implementation + public Task> PostResultAsync(string uri, HttpContent content, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Post, content, serializer); - public async Task> GetResultAsync(string uri, IHttpClientSerializer serializer = null) - { - var response = await SendAsync(uri, HttpMethod.Get).ConfigureAwait(false); - return await CreateHttpResult(response, serializer).ConfigureAwait(false); - } + public virtual Task PutAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Put, GetJsonPayload(data, serializer)); - public async Task> PostResultAsync(string uri, object data = null, IHttpClientSerializer serializer = null) - { - var content = GetJsonPayload(data, serializer); - var response = await SendAsync(uri, HttpMethod.Post, content).ConfigureAwait(false); - return await CreateHttpResult(response, serializer).ConfigureAwait(false); - } + public Task PutAsync(string uri, HttpContent content) + => SendAsync(uri, Method.Put, content); - public async Task> PutResultAsync(string uri, object data = null, IHttpClientSerializer serializer = null) - { - var content = GetJsonPayload(data, serializer); - var response = await SendAsync(uri, HttpMethod.Put, content).ConfigureAwait(false); - return await CreateHttpResult(response, serializer).ConfigureAwait(false); - } + public virtual Task PutAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Put, GetJsonPayload(data, serializer), serializer); - public async Task> PatchResultAsync(string uri, object data = null, IHttpClientSerializer serializer = null) - { - var content = GetJsonPayload(data, serializer); - var response = await SendAsync(uri, HttpMethod.Patch, content).ConfigureAwait(false); - return await CreateHttpResult(response, serializer).ConfigureAwait(false); - } + public Task PutAsync(string uri, HttpContent content, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Put, content, serializer); - public async Task> DeleteResultAsync(string uri, IHttpClientSerializer serializer = null) - { - var response = await SendAsync(uri, HttpMethod.Delete).ConfigureAwait(false); - return await CreateHttpResult(response, serializer).ConfigureAwait(false); - } + public Task> PutResultAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Put, GetJsonPayload(data, serializer), serializer); - // IHttpClientAdvanced implementation + public Task> PutResultAsync(string uri, HttpContent content, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Put, content, serializer); - public Task SendAsync(HttpRequestMessage request) - => RetryPolicy(() => _client.SendAsync(request)); + public Task PatchAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Patch, GetJsonPayload(data, serializer)); - public async Task SendAsync(HttpRequestMessage request, IHttpClientSerializer serializer = null) - { - var response = await SendAsync(request).ConfigureAwait(false); - return await DeserializeResponse(response, serializer).ConfigureAwait(false); - } + public Task PatchAsync(string uri, HttpContent content) + => SendAsync(uri, Method.Patch, content); - public async Task> SendResultAsync(HttpRequestMessage request, IHttpClientSerializer serializer = null) - { - var response = await SendAsync(request).ConfigureAwait(false); - return await CreateHttpResult(response, serializer).ConfigureAwait(false); - } + public Task PatchAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Patch, GetJsonPayload(data, serializer), serializer); + + public Task PatchAsync(string uri, HttpContent content, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Patch, content, serializer); + + public Task> PatchResultAsync(string uri, object data = null, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Patch, GetJsonPayload(data, serializer), serializer); + + public Task> PatchResultAsync(string uri, HttpContent content, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Patch, content, serializer); - // IHttpClientHeaders implementation + public virtual Task DeleteAsync(string uri) + => SendAsync(uri, Method.Delete); + + public Task DeleteAsync(string uri, IHttpClientSerializer serializer = null) + => SendAsync(uri, Method.Delete, null, serializer); + + public Task> DeleteResultAsync(string uri, IHttpClientSerializer serializer = null) + => SendResultAsync(uri, Method.Delete, null, serializer); + + // Advanced SendAsync methods + public Task SendAsync(HttpRequestMessage request) + => RetryPolicy(() => _client.SendAsync(request)); + public Task SendAsync(HttpRequestMessage request, IHttpClientSerializer serializer = null) + => RetryPolicy(async () => + { + var response = await _client.SendAsync(request); + return await DeserializeResponse(response, serializer); + }); + + public Task>SendResultAsync(HttpRequestMessage request, IHttpClientSerializer serializer = null) + => RetryPolicy(async () => + { + var response = await _client.SendAsync(request); + return await CreateHttpResult(response, serializer); + }); + + // Headers Management public void SetHeaders(IDictionary headers) { if (headers == null) return; @@ -138,22 +147,27 @@ public void SetHeaders(Action headers) headers?.Invoke(_client.DefaultRequestHeaders); } - // Private helper methods + // Utility methods for sending HTTP requests + private async Task SendAsync(string uri, Method method, HttpContent content = null) + { + var request = new HttpRequestMessage(method.ToHttpMethod(), uri) { Content = content }; + return await RetryPolicy(() => _client.SendAsync(request)); + } - private async Task SendAsync(string uri, HttpMethod method, HttpContent content = null) + private async Task SendAsync(string uri, Method method, HttpContent content = null, IHttpClientSerializer serializer = null) { - var request = new HttpRequestMessage(method, uri) { Content = content }; - return await RetryPolicy(() => _client.SendAsync(request)); + var response = await SendAsync(uri, method, content); + return await DeserializeResponse(response, serializer); } private async Task DeserializeResponse(HttpResponseMessage response, IHttpClientSerializer serializer = null) { if (!response.IsSuccessStatusCode) return default; - var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(); serializer ??= _serializer; - return await serializer.DeserializeAsync(stream).ConfigureAwait(false); + return await serializer.DeserializeAsync(stream); } private async Task> CreateHttpResult(HttpResponseMessage response, IHttpClientSerializer serializer = null) @@ -163,9 +177,9 @@ private async Task> CreateHttpResult(HttpResponseMessage respon return new HttpResult(default, response); } - var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(); serializer ??= _serializer; - var result = await serializer.DeserializeAsync(stream).ConfigureAwait(false); + var result = await serializer.DeserializeAsync(stream); return new HttpResult(result, response); } @@ -176,15 +190,83 @@ private StringContent GetJsonPayload(object data, IHttpClientSerializer serializ serializer ??= _serializer; var content = new StringContent(serializer.Serialize(data), Encoding.UTF8, JsonContentType); + if (_options.RemoveCharsetFromContentType && content.Headers.ContentType != null) + { + content.Headers.ContentType.CharSet = null; + } + return content; } + // Retry policy using Polly private async Task RetryPolicy(Func> action) { return await Policy .Handle() - .WaitAndRetryAsync(_retries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))) - .ExecuteAsync(action).ConfigureAwait(false); + .WaitAndRetryAsync(_options.Retries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))) + .ExecuteAsync(action); + } + + protected virtual async Task> SendResultAsync(string uri, Method method, HttpContent content = null, IHttpClientSerializer serializer = null) + { + // Sending the request using the method and content + var response = await SendAsync(uri, method, content); + + // If the response status code is not successful, return a default result + if (!response.IsSuccessStatusCode) + { + return new HttpResult(default, response); + } + + // Read the response content stream + var stream = await response.Content.ReadAsStreamAsync(); + + // Deserialize the stream using the provided serializer (or default to _serializer) + var result = await DeserializeJsonFromStream(stream, serializer); + + // Return the deserialized result along with the response + return new HttpResult(result, response); + } + + private async Task DeserializeJsonFromStream(Stream stream, IHttpClientSerializer serializer = null) + { + if (stream == null || !stream.CanRead) + { + return default; + } + + // Use the provided serializer or default to _serializer + serializer ??= _serializer; + + return await serializer.DeserializeAsync(stream); + } + + + + public enum Method + { + Get, + Post, + Put, + Patch, + Delete + } + } + + // Extension method for converting Method enum to HttpMethod + internal static class MethodExtensions + { + public static HttpMethod ToHttpMethod(this ParalaxHttpClient.Method method) + { + return method switch + { + ParalaxHttpClient.Method.Get => HttpMethod.Get, + ParalaxHttpClient.Method.Post => HttpMethod.Post, + ParalaxHttpClient.Method.Put => HttpMethod.Put, + ParalaxHttpClient.Method.Patch => HttpMethod.Patch, + ParalaxHttpClient.Method.Delete => HttpMethod.Delete, + _ => throw new InvalidOperationException($"Unsupported HTTP method: {method}") + }; } } } diff --git a/src/Paralax.HTTP/tests/Paralax.HTTP/ParalaxHttpClientTests.cs b/src/Paralax.HTTP/tests/Paralax.HTTP/ParalaxHttpClientTests.cs index da4616b..108ba06 100644 --- a/src/Paralax.HTTP/tests/Paralax.HTTP/ParalaxHttpClientTests.cs +++ b/src/Paralax.HTTP/tests/Paralax.HTTP/ParalaxHttpClientTests.cs @@ -30,7 +30,7 @@ public ParalaxHttpClientTests() BaseAddress = new Uri("https://api.example.com") }; - _paralaxHttpClient = new ParalaxHttpClient(_httpClient, _serializerMock.Object); + _paralaxHttpClient = new ParalaxHttpClient(_httpClient, new HttpClientOptions(), _serializerMock.Object, null, null); } [Fact] @@ -142,10 +142,8 @@ public async Task PostAsync_Generic_ShouldReturnDeserializedResponse() _serializerMock.Setup(s => s.DeserializeAsync>(It.IsAny())) .ReturnsAsync(new Dictionary { { "id", 1 } }); - var jsonContent = new StringContent("{\"Name\":\"Test\"}", Encoding.UTF8, "application/json"); - // Act - var result = await _paralaxHttpClient.PostAsync>("/test", jsonContent); + var result = await _paralaxHttpClient.PostAsync>("/test", data); // Assert result.Should().ContainKey("id"); @@ -156,6 +154,7 @@ public async Task PostAsync_Generic_ShouldReturnDeserializedResponse() _serializerMock.Verify(s => s.DeserializeAsync>(It.IsAny()), Times.Once); } + [Fact] public async Task SendAsync_ShouldRetryOnFailure() {