diff --git a/Poolz.Finance.CSharp.Http.sln b/Poolz.Finance.CSharp.Http.sln new file mode 100644 index 0000000..d4946c7 --- /dev/null +++ b/Poolz.Finance.CSharp.Http.sln @@ -0,0 +1,39 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35828.75 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C43445A2-69BB-48F5-8D8B-45F82D29028D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poolz.Finance.CSharp.Http", "src\Poolz.Finance.CSharp.Http\Poolz.Finance.CSharp.Http.csproj", "{6EAF7CF1-0D92-218F-D1AC-37BD1E926245}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Poolz.Finance.CSharp.Http.Tests", "tests\Poolz.Finance.CSharp.Http.Tests\Poolz.Finance.CSharp.Http.Tests.csproj", "{7B8DC796-734F-C107-EB88-001D0D01E09F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6EAF7CF1-0D92-218F-D1AC-37BD1E926245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EAF7CF1-0D92-218F-D1AC-37BD1E926245}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EAF7CF1-0D92-218F-D1AC-37BD1E926245}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EAF7CF1-0D92-218F-D1AC-37BD1E926245}.Release|Any CPU.Build.0 = Release|Any CPU + {7B8DC796-734F-C107-EB88-001D0D01E09F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B8DC796-734F-C107-EB88-001D0D01E09F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B8DC796-734F-C107-EB88-001D0D01E09F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B8DC796-734F-C107-EB88-001D0D01E09F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6EAF7CF1-0D92-218F-D1AC-37BD1E926245} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {7B8DC796-734F-C107-EB88-001D0D01E09F} = {C43445A2-69BB-48F5-8D8B-45F82D29028D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AF2F8BF3-0CF6-467C-944A-792A7D687E3C} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 2ba2321..f65029c 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# Poolz.Finance.CSharp.LibTemplate +# Poolz.Finance.CSharp.Http + +Poolz.Finance.CSharp.Http is a lightweight .NET 8 library that centralizes the creation of `HttpClient` instances for Poolz Finance services. +It wraps the default `HttpClientHandler` with a custom `FailureOnlyLoggingHandler` that surfaces detailed information when requests fail while keeping successful responses silent. + +## Getting started + +```csharp +using Poolz.Finance.CSharp.Http; + +var factory = new HttpClientFactory(); +var client = factory.Create( + url: "https://api.poolz.finance", + configureHeaders: headers => + { + headers.Add("Authorization", "Bearer "); + headers.Add("User-Agent", "Poolz-SDK-Demo/1.0"); + }); + +var response = await client.GetAsync("/v1/pools"); +var content = await response.Content.ReadAsStringAsync(); +``` + +If the request fails, `FailureOnlyLoggingHandler` rethrows an exception that includes the HTTP method and URL so that you immediately know which call needs attention. + diff --git a/src/Poolz.Finance.CSharp.Http/FailureOnlyLoggingHandler.cs b/src/Poolz.Finance.CSharp.Http/FailureOnlyLoggingHandler.cs new file mode 100644 index 0000000..65c8f17 --- /dev/null +++ b/src/Poolz.Finance.CSharp.Http/FailureOnlyLoggingHandler.cs @@ -0,0 +1,22 @@ +namespace Poolz.Finance.CSharp.Http; + +public class FailureOnlyLoggingHandler(HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler) +{ + protected override async Task SendAsync(HttpRequestMessage req, CancellationToken ct) + { + try + { + var response = await base.SendAsync(req, ct); + response.EnsureSuccessStatusCode(); + return response; + } + catch (HttpRequestException exception) + { + throw new HttpRequestException( + $"HTTP request failed. METHOD: {req.Method}. URL: {req.RequestUri}", + exception, + exception.StatusCode + ); + } + } +} \ No newline at end of file diff --git a/src/Poolz.Finance.CSharp.Http/HttpClientFactory.cs b/src/Poolz.Finance.CSharp.Http/HttpClientFactory.cs new file mode 100644 index 0000000..a62840b --- /dev/null +++ b/src/Poolz.Finance.CSharp.Http/HttpClientFactory.cs @@ -0,0 +1,17 @@ +using System.Net.Http.Headers; + +namespace Poolz.Finance.CSharp.Http; + +public class HttpClientFactory : IHttpClientFactory +{ + public HttpClient Create(string url, Action? configureHeaders = null) + { + var client = new HttpClient(new FailureOnlyLoggingHandler(new HttpClientHandler())) + { + BaseAddress = new Uri(url) + }; + + configureHeaders?.Invoke(client.DefaultRequestHeaders); + return client; + } +} \ No newline at end of file diff --git a/src/Poolz.Finance.CSharp.Http/IHttpClientFactory.cs b/src/Poolz.Finance.CSharp.Http/IHttpClientFactory.cs new file mode 100644 index 0000000..bb4ded9 --- /dev/null +++ b/src/Poolz.Finance.CSharp.Http/IHttpClientFactory.cs @@ -0,0 +1,8 @@ +using System.Net.Http.Headers; + +namespace Poolz.Finance.CSharp.Http; + +public interface IHttpClientFactory +{ + public HttpClient Create(string url, Action? configure = null); +} \ No newline at end of file diff --git a/src/Poolz.Finance.CSharp.Http/Poolz.Finance.CSharp.Http.csproj b/src/Poolz.Finance.CSharp.Http/Poolz.Finance.CSharp.Http.csproj new file mode 100644 index 0000000..20ebbe3 --- /dev/null +++ b/src/Poolz.Finance.CSharp.Http/Poolz.Finance.CSharp.Http.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/tests/Poolz.Finance.CSharp.Http.Tests/FailureOnlyLoggingHandlerTests.cs b/tests/Poolz.Finance.CSharp.Http.Tests/FailureOnlyLoggingHandlerTests.cs new file mode 100644 index 0000000..e0b0240 --- /dev/null +++ b/tests/Poolz.Finance.CSharp.Http.Tests/FailureOnlyLoggingHandlerTests.cs @@ -0,0 +1,46 @@ +using Xunit; +using System.Net; + +namespace Poolz.Finance.CSharp.Http.Tests; + +public class FailureOnlyLoggingHandlerTests +{ + [Fact] + public async Task SendAsync_ReturnsResponse_WhenStatusIsSuccessful() + { + using var handler = new FailureOnlyLoggingHandler(new StubHandler((_, _) => + Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)))); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(HttpMethod.Get, "https://poolz.finance/success"); + + var response = await invoker.SendAsync(request, CancellationToken.None); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task SendAsync_ThrowsHttpRequestExceptionWithContext_WhenStatusIsFailure() + { + using var handler = new FailureOnlyLoggingHandler(new StubHandler((_, _) => + Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)))); + using var invoker = new HttpMessageInvoker(handler); + using var request = new HttpRequestMessage(HttpMethod.Post, "https://poolz.finance/failure"); + + var exception = await Assert.ThrowsAsync(() => invoker.SendAsync(request, CancellationToken.None)); + + Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); + Assert.NotNull(exception.InnerException); + Assert.Contains("METHOD: POST", exception.Message); + Assert.Contains("URL: https://poolz.finance/failure", exception.Message); + } + + private sealed class StubHandler( + Func> responseFactory) + : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return responseFactory(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/tests/Poolz.Finance.CSharp.Http.Tests/HttpClientFactoryTests.cs b/tests/Poolz.Finance.CSharp.Http.Tests/HttpClientFactoryTests.cs new file mode 100644 index 0000000..bd65906 --- /dev/null +++ b/tests/Poolz.Finance.CSharp.Http.Tests/HttpClientFactoryTests.cs @@ -0,0 +1,22 @@ +using Xunit; + +namespace Poolz.Finance.CSharp.Http.Tests; + +public class HttpClientFactoryTests +{ + [Fact] + public void Create_ConfiguresBaseAddressAndHeaders() + { + var factory = new HttpClientFactory(); + var expectedBaseAddress = new Uri("https://api.poolz.finance/"); + + using var client = factory.Create(expectedBaseAddress.ToString(), headers => + { + headers.Add("X-Test", "42"); + }); + + Assert.Equal(expectedBaseAddress, client.BaseAddress); + Assert.True(client.DefaultRequestHeaders.Contains("X-Test")); + Assert.Equal("42", client.DefaultRequestHeaders.GetValues("X-Test").Single()); + } +} \ No newline at end of file diff --git a/tests/Poolz.Finance.CSharp.Http.Tests/Poolz.Finance.CSharp.Http.Tests.csproj b/tests/Poolz.Finance.CSharp.Http.Tests/Poolz.Finance.CSharp.Http.Tests.csproj new file mode 100644 index 0000000..a349d7f --- /dev/null +++ b/tests/Poolz.Finance.CSharp.Http.Tests/Poolz.Finance.CSharp.Http.Tests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + Poolz.Finance.CSharp.Http.Tests + Poolz.Finance.CSharp.Http.Tests + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +