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

(#44) http: udpate http client #53

Merged
merged 2 commits into from
Sep 23, 2024
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
264 changes: 173 additions & 91 deletions src/Paralax.HTTP/src/Paralax.HTTP/ParalaxHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpResponseMessage> GetAsync(string uri)
=> SendAsync(uri, Method.Get);

public Task<HttpResponseMessage> GetAsync(string uri) => SendAsync(uri, HttpMethod.Get);
public Task<HttpResponseMessage> PostAsync(string uri, HttpContent content) => SendAsync(uri, HttpMethod.Post, content);
public Task<HttpResponseMessage> PutAsync(string uri, HttpContent content) => SendAsync(uri, HttpMethod.Put, content);
public Task<HttpResponseMessage> PatchAsync(string uri, HttpContent content) => SendAsync(uri, HttpMethod.Patch, content);
public Task<HttpResponseMessage> DeleteAsync(string uri) => SendAsync(uri, HttpMethod.Delete);
public virtual Task<T> GetAsync<T>(string uri, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Get, null, serializer);

// IHttpClientWithSerialization implementation
public Task<HttpResult<T>> GetResultAsync<T>(string uri, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Get, null, serializer);

public async Task<T> GetAsync<T>(string uri, IHttpClientSerializer serializer = null)
{
var response = await SendAsync(uri, HttpMethod.Get).ConfigureAwait(false);
return await DeserializeResponse<T>(response, serializer).ConfigureAwait(false);
}
public virtual Task<HttpResponseMessage> PostAsync(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendAsync(uri, Method.Post, GetJsonPayload(data, serializer));

public async Task<T> PostAsync<T>(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<T>(response, serializer).ConfigureAwait(false);
}
public Task<HttpResponseMessage> PostAsync(string uri, HttpContent content)
=> SendAsync(uri, Method.Post, content);

public async Task<T> PutAsync<T>(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<T>(response, serializer).ConfigureAwait(false);
}
public virtual Task<T> PostAsync<T>(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Post, GetJsonPayload(data, serializer), serializer);

public async Task<T> PatchAsync<T>(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<T>(response, serializer).ConfigureAwait(false);
}
public Task<T> PostAsync<T>(string uri, HttpContent content, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Post, content, serializer);

public async Task<T> DeleteAsync<T>(string uri, IHttpClientSerializer serializer = null)
{
var response = await SendAsync(uri, HttpMethod.Delete).ConfigureAwait(false);
return await DeserializeResponse<T>(response, serializer).ConfigureAwait(false);
}
public Task<HttpResult<T>> PostResultAsync<T>(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Post, GetJsonPayload(data, serializer), serializer);

// IHttpClientWithResult implementation
public Task<HttpResult<T>> PostResultAsync<T>(string uri, HttpContent content, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Post, content, serializer);

public async Task<HttpResult<T>> GetResultAsync<T>(string uri, IHttpClientSerializer serializer = null)
{
var response = await SendAsync(uri, HttpMethod.Get).ConfigureAwait(false);
return await CreateHttpResult<T>(response, serializer).ConfigureAwait(false);
}
public virtual Task<HttpResponseMessage> PutAsync(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendAsync(uri, Method.Put, GetJsonPayload(data, serializer));

public async Task<HttpResult<T>> PostResultAsync<T>(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<T>(response, serializer).ConfigureAwait(false);
}
public Task<HttpResponseMessage> PutAsync(string uri, HttpContent content)
=> SendAsync(uri, Method.Put, content);

public async Task<HttpResult<T>> PutResultAsync<T>(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<T>(response, serializer).ConfigureAwait(false);
}
public virtual Task<T> PutAsync<T>(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Put, GetJsonPayload(data, serializer), serializer);

public async Task<HttpResult<T>> PatchResultAsync<T>(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<T>(response, serializer).ConfigureAwait(false);
}
public Task<T> PutAsync<T>(string uri, HttpContent content, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Put, content, serializer);

public async Task<HttpResult<T>> DeleteResultAsync<T>(string uri, IHttpClientSerializer serializer = null)
{
var response = await SendAsync(uri, HttpMethod.Delete).ConfigureAwait(false);
return await CreateHttpResult<T>(response, serializer).ConfigureAwait(false);
}
public Task<HttpResult<T>> PutResultAsync<T>(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Put, GetJsonPayload(data, serializer), serializer);

// IHttpClientAdvanced implementation
public Task<HttpResult<T>> PutResultAsync<T>(string uri, HttpContent content, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Put, content, serializer);

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
=> RetryPolicy<HttpResponseMessage>(() => _client.SendAsync(request));
public Task<HttpResponseMessage> PatchAsync(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendAsync(uri, Method.Patch, GetJsonPayload(data, serializer));

public async Task<T> SendAsync<T>(HttpRequestMessage request, IHttpClientSerializer serializer = null)
{
var response = await SendAsync(request).ConfigureAwait(false);
return await DeserializeResponse<T>(response, serializer).ConfigureAwait(false);
}
public Task<HttpResponseMessage> PatchAsync(string uri, HttpContent content)
=> SendAsync(uri, Method.Patch, content);

public async Task<HttpResult<T>> SendResultAsync<T>(HttpRequestMessage request, IHttpClientSerializer serializer = null)
{
var response = await SendAsync(request).ConfigureAwait(false);
return await CreateHttpResult<T>(response, serializer).ConfigureAwait(false);
}
public Task<T> PatchAsync<T>(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Patch, GetJsonPayload(data, serializer), serializer);

public Task<T> PatchAsync<T>(string uri, HttpContent content, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Patch, content, serializer);

public Task<HttpResult<T>> PatchResultAsync<T>(string uri, object data = null, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Patch, GetJsonPayload(data, serializer), serializer);

public Task<HttpResult<T>> PatchResultAsync<T>(string uri, HttpContent content, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Patch, content, serializer);

// IHttpClientHeaders implementation
public virtual Task<HttpResponseMessage> DeleteAsync(string uri)
=> SendAsync(uri, Method.Delete);

public Task<T> DeleteAsync<T>(string uri, IHttpClientSerializer serializer = null)
=> SendAsync<T>(uri, Method.Delete, null, serializer);

public Task<HttpResult<T>> DeleteResultAsync<T>(string uri, IHttpClientSerializer serializer = null)
=> SendResultAsync<T>(uri, Method.Delete, null, serializer);

// Advanced SendAsync methods
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
=> RetryPolicy(() => _client.SendAsync(request));

public Task<T> SendAsync<T>(HttpRequestMessage request, IHttpClientSerializer serializer = null)
=> RetryPolicy(async () =>
{
var response = await _client.SendAsync(request);
return await DeserializeResponse<T>(response, serializer);
});

public Task<HttpResult<T>>SendResultAsync<T>(HttpRequestMessage request, IHttpClientSerializer serializer = null)
=> RetryPolicy(async () =>
{
var response = await _client.SendAsync(request);
return await CreateHttpResult<T>(response, serializer);
});

// Headers Management
public void SetHeaders(IDictionary<string, string> headers)
{
if (headers == null) return;
Expand All @@ -138,22 +147,27 @@ public void SetHeaders(Action<HttpRequestHeaders> headers)
headers?.Invoke(_client.DefaultRequestHeaders);
}

// Private helper methods
// Utility methods for sending HTTP requests
private async Task<HttpResponseMessage> 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<HttpResponseMessage> SendAsync(string uri, HttpMethod method, HttpContent content = null)
private async Task<T> SendAsync<T>(string uri, Method method, HttpContent content = null, IHttpClientSerializer serializer = null)
{
var request = new HttpRequestMessage(method, uri) { Content = content };
return await RetryPolicy<HttpResponseMessage>(() => _client.SendAsync(request));
var response = await SendAsync(uri, method, content);
return await DeserializeResponse<T>(response, serializer);
}

private async Task<T> DeserializeResponse<T>(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<T>(stream).ConfigureAwait(false);
return await serializer.DeserializeAsync<T>(stream);
}

private async Task<HttpResult<T>> CreateHttpResult<T>(HttpResponseMessage response, IHttpClientSerializer serializer = null)
Expand All @@ -163,9 +177,9 @@ private async Task<HttpResult<T>> CreateHttpResult<T>(HttpResponseMessage respon
return new HttpResult<T>(default, response);
}

var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync();
serializer ??= _serializer;
var result = await serializer.DeserializeAsync<T>(stream).ConfigureAwait(false);
var result = await serializer.DeserializeAsync<T>(stream);

return new HttpResult<T>(result, response);
}
Expand All @@ -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<T> RetryPolicy<T>(Func<Task<T>> action)
{
return await Policy
.Handle<Exception>()
.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<HttpResult<T>> SendResultAsync<T>(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<T>(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<T>(stream, serializer);

// Return the deserialized result along with the response
return new HttpResult<T>(result, response);
}

private async Task<T> DeserializeJsonFromStream<T>(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<T>(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}")
};
}
}
}
7 changes: 3 additions & 4 deletions src/Paralax.HTTP/tests/Paralax.HTTP/ParalaxHttpClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -142,10 +142,8 @@ public async Task PostAsync_Generic_ShouldReturnDeserializedResponse()
_serializerMock.Setup(s => s.DeserializeAsync<Dictionary<string, int>>(It.IsAny<Stream>()))
.ReturnsAsync(new Dictionary<string, int> { { "id", 1 } });

var jsonContent = new StringContent("{\"Name\":\"Test\"}", Encoding.UTF8, "application/json");

// Act
var result = await _paralaxHttpClient.PostAsync<Dictionary<string, int>>("/test", jsonContent);
var result = await _paralaxHttpClient.PostAsync<Dictionary<string, int>>("/test", data);

// Assert
result.Should().ContainKey("id");
Expand All @@ -156,6 +154,7 @@ public async Task PostAsync_Generic_ShouldReturnDeserializedResponse()
_serializerMock.Verify(s => s.DeserializeAsync<Dictionary<string, int>>(It.IsAny<Stream>()), Times.Once);
}


[Fact]
public async Task SendAsync_ShouldRetryOnFailure()
{
Expand Down
Loading