diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..dc62df9 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,41 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - 'hotfix' + - 'invalid' + - title: '🧰 Maintenance' + labels: + - 'edits' + - 'chore' + - 'refactoring' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + - 'feature' + patch: + labels: + - 'patch' + - 'bug' + - 'bugfix' + - 'hotfix' + - 'fix' + default: patch +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..2b54af6 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,24 @@ +name: Release Drafter + +on: + push: + branches: + - master + pull_request: + types: [opened, reopened, synchronize] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v5 + with: + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/HttpClient.sln b/HttpClient.sln new file mode 100644 index 0000000..60bc72f --- /dev/null +++ b/HttpClient.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35027.167 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{43F47EAD-C0B5-4EB7-8B05-B722629743DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpClient.Core", "src\HttpClient.Core\HttpClient.Core.csproj", "{536A2A51-FBBA-4240-B7EE-2986787453FE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {536A2A51-FBBA-4240-B7EE-2986787453FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {536A2A51-FBBA-4240-B7EE-2986787453FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {536A2A51-FBBA-4240-B7EE-2986787453FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {536A2A51-FBBA-4240-B7EE-2986787453FE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {536A2A51-FBBA-4240-B7EE-2986787453FE} = {43F47EAD-C0B5-4EB7-8B05-B722629743DC} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B53CA9C3-ADD2-43D1-BFE6-9D1C7B898CA9} + EndGlobalSection +EndGlobal diff --git a/src/HttpClient.Core/ApiResponse.cs b/src/HttpClient.Core/ApiResponse.cs new file mode 100644 index 0000000..0dc5a1b --- /dev/null +++ b/src/HttpClient.Core/ApiResponse.cs @@ -0,0 +1,48 @@ +using HttpClient.Core.Web.Http.Extensions; +using Newtonsoft.Json; +using System.Net; + +namespace HttpClient.Core.Web.Http +{ + public class ApiResponse + { + [JsonProperty("isError")] + public bool IsError => Code.IsErrorStatusCode() || Exception != null; + + [JsonProperty("message")] + public string? Message { get; } + + [JsonProperty("result")] + public TResult? Result { get; } + + [JsonProperty("code")] + public HttpStatusCode? Code { get; } + + [JsonProperty("exception")] + public Exception? Exception { get; } + + [JsonConstructor] + public ApiResponse() + { + Message = null; + Result = default(TResult); + Code = null; + Exception = null; + } + + public ApiResponse(TResult? result, HttpStatusCode? code, string? message, + Exception? exception = null) + { + Message = message; + Code = code; + Result = result; + Exception = exception; + } + + public ApiResponse(TResult? result, HttpStatusCode? code, Exception? exception = null) : + this(result, code, code?.ToString(), exception) + { + + } + } +} diff --git a/src/HttpClient.Core/BaseHttpClient.cs b/src/HttpClient.Core/BaseHttpClient.cs new file mode 100644 index 0000000..bb1f04a --- /dev/null +++ b/src/HttpClient.Core/BaseHttpClient.cs @@ -0,0 +1,106 @@ +using HttpClient.Core.Web.Http.Builders; +using HttpClient.Core.Web.Http.Extensions; +using HttpClient.Core.Web.Http.RequestHandlers; + +namespace HttpClient.Core.Web.Http +{ + public class BaseHttpClient + { + private readonly System.Net.Http.HttpClient _client; + + protected virtual IRequestHandler RequestHandler => new RequestHandler(); + + public BaseHttpClient(Uri? uri = null, HttpClientHandlerBuilder? builder = null) + { + var httpClientHanlder = builder?.Build() + ?? new HttpClientHandlerBuilder().WithAllowAutoRedirect().WithAutomaticDecompression() + .UseCertificateCustomValidation().UseSslProtocols().Build(); + + _client = new System.Net.Http.HttpClient(httpClientHanlder); + _client.BaseAddress = uri; + } + + public virtual async Task> GetAsync(string uri, + CancellationToken cancellationToken = default) + { + if (uri == null) throw new ArgumentNullException(nameof(uri)); + + return await GetAsync(new Uri(uri, UriKind.Relative), cancellationToken); + } + + public virtual async Task> GetAsync(Uri relativeUri, + CancellationToken cancellationToken = default) + { + return await RequestHandler.HandleAsync(() => + _client.GetAsync(relativeUri, cancellationToken)); + } + + public virtual async Task> PostAsync(string uri, object content, + CancellationToken cancellationToken = default) + { + if (uri == null) throw new ArgumentNullException(nameof(uri)); + + return await PostAsync(new Uri(uri, UriKind.Relative), content, cancellationToken); + } + + public virtual async Task> PostAsync(Uri relativeUri, object content, + CancellationToken cancellationToken = default) + { + return await RequestHandler.HandleAsync(() => + _client.PostAsync(relativeUri, content.ToStringContent(), cancellationToken)); + } + + public virtual async Task> PutAsync(string uri, object content, + CancellationToken cancellationToken = default) + { + if (uri == null) throw new ArgumentNullException(nameof(uri)); + + return await PutAsync(new Uri(uri, UriKind.Relative), content, cancellationToken); + } + + public virtual async Task> PutAsync(Uri relativeUri, object content, + CancellationToken cancellationToken = default) + { + return await RequestHandler.HandleAsync(() => + _client.PutAsync(relativeUri, content.ToStringContent(), cancellationToken)); + } + + public virtual async Task> DeleteAsync(string uri, + CancellationToken cancellationToken = default) + { + if (uri == null) throw new ArgumentNullException(nameof(uri)); + + return await DeleteAsync(uri, cancellationToken); + } + + public virtual async Task> DeleteAsync(Uri relativeUri, + CancellationToken cancellationToken = default) + { + return await RequestHandler.HandleAsync(() => + _client.DeleteAsync(relativeUri, cancellationToken)); + } + + public void UseHeaders(IDictionary headers) + { + if (headers == null) throw new ArgumentNullException(nameof(headers)); + + _client.DefaultRequestHeaders.Clear(); + + foreach (var header in headers) + { + _client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + + public void OverrideHeaders(Dictionary headers) + { + if (headers == null) throw new ArgumentNullException(nameof(headers)); + + foreach (var header in headers) + { + _client.DefaultRequestHeaders.Remove(header.Key); + _client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + } +} diff --git a/src/HttpClient.Core/Builders/HttpClientHandlerBuilder.cs b/src/HttpClient.Core/Builders/HttpClientHandlerBuilder.cs new file mode 100644 index 0000000..0f8f2d2 --- /dev/null +++ b/src/HttpClient.Core/Builders/HttpClientHandlerBuilder.cs @@ -0,0 +1,58 @@ +using System.Net; + +namespace HttpClient.Core.Web.Http.Builders +{ + public class HttpClientHandlerBuilder + { + private readonly HttpClientHandler _httpClientHandler; + + public HttpClientHandlerBuilder() + { + _httpClientHandler = new HttpClientHandler(); + } + + public HttpClientHandlerBuilder UseSslProtocols() + { + ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault | SecurityProtocolType.Tls + | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13; + + return this; + } + + public HttpClientHandlerBuilder UseCertificateCustomValidation() + { + _httpClientHandler.ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true; + + return this; + } + + public HttpClientHandlerBuilder WithAllowAutoRedirect() + { + _httpClientHandler.AllowAutoRedirect = true; + + return this; + } + + public HttpClientHandlerBuilder WithAutomaticDecompression() + { + _httpClientHandler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | + DecompressionMethods.Brotli; + + return this; + } + + public HttpClientHandlerBuilder WithProxy(string address) + { + _httpClientHandler.Proxy = new WebProxy(address); + + _httpClientHandler.UseProxy = true; + + return this; + } + + public HttpClientHandler Build() + { + return _httpClientHandler; + } + } +} diff --git a/src/HttpClient.Core/Extensions/HttpStatusCodeExtensions.cs b/src/HttpClient.Core/Extensions/HttpStatusCodeExtensions.cs new file mode 100644 index 0000000..ba5f605 --- /dev/null +++ b/src/HttpClient.Core/Extensions/HttpStatusCodeExtensions.cs @@ -0,0 +1,12 @@ +using System.Net; + +namespace HttpClient.Core.Web.Http.Extensions +{ + public static class HttpStatusCodeExtensions + { + public static bool IsErrorStatusCode(this HttpStatusCode? code) + { + return code >= HttpStatusCode.BadRequest; + } + } +} diff --git a/src/HttpClient.Core/Extensions/ObjectExtensions.cs b/src/HttpClient.Core/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..dce60a7 --- /dev/null +++ b/src/HttpClient.Core/Extensions/ObjectExtensions.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; +using System.Text; + +namespace HttpClient.Core.Web.Http.Extensions +{ + internal static class ObjectExtensions + { + public static StringContent ToStringContent(this object obj) + { + return new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json"); + } + } +} diff --git a/src/HttpClient.Core/HttpClient.Core.csproj b/src/HttpClient.Core/HttpClient.Core.csproj new file mode 100644 index 0000000..3b5b170 --- /dev/null +++ b/src/HttpClient.Core/HttpClient.Core.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/HttpClient.Core/RequestHandlers/IRequestHandler.cs b/src/HttpClient.Core/RequestHandlers/IRequestHandler.cs new file mode 100644 index 0000000..2afbddc --- /dev/null +++ b/src/HttpClient.Core/RequestHandlers/IRequestHandler.cs @@ -0,0 +1,14 @@ +using HttpClient.Core.Web.Http.ResponseMapers; +using HttpClient.Core.Web.Http.ResponseReaders; + +namespace HttpClient.Core.Web.Http.RequestHandlers +{ + public interface IRequestHandler + { + IResponseMaper Maper { get; } + + IResponseReader Reader { get; } + + Task> HandleAsync(Func> request); + } +} diff --git a/src/HttpClient.Core/RequestHandlers/RequestHandler.cs b/src/HttpClient.Core/RequestHandlers/RequestHandler.cs new file mode 100644 index 0000000..2a4bcc4 --- /dev/null +++ b/src/HttpClient.Core/RequestHandlers/RequestHandler.cs @@ -0,0 +1,30 @@ +using HttpClient.Core.Web.Http.ResponseMapers; +using HttpClient.Core.Web.Http.ResponseReaders; + +namespace HttpClient.Core.Web.Http.RequestHandlers +{ + public class RequestHandler : IRequestHandler + { + public virtual IResponseMaper Maper => new JsonResponseMaper(); + + public virtual IResponseReader Reader => new ResponseReader(); + + public async Task> HandleAsync(Func> request) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + + try + { + var response = await request.Invoke(); + + var responseContent = await Reader.ReadAsync(response); + + return Maper.Map(responseContent, response.StatusCode); + } + catch (Exception ex) + { + return new ApiResponse(default, null, ex.Message, ex); + } + } + } +} diff --git a/src/HttpClient.Core/ResponseMapers/IResponseMaper.cs b/src/HttpClient.Core/ResponseMapers/IResponseMaper.cs new file mode 100644 index 0000000..f2f5106 --- /dev/null +++ b/src/HttpClient.Core/ResponseMapers/IResponseMaper.cs @@ -0,0 +1,10 @@ +using System.Net; + +namespace HttpClient.Core.Web.Http.ResponseMapers +{ + public interface IResponseMaper + { + ApiResponse Map(string? responseContent, + HttpStatusCode statusCode = HttpStatusCode.OK); + } +} diff --git a/src/HttpClient.Core/ResponseMapers/JsonResponseMaper.cs b/src/HttpClient.Core/ResponseMapers/JsonResponseMaper.cs new file mode 100644 index 0000000..4a77b81 --- /dev/null +++ b/src/HttpClient.Core/ResponseMapers/JsonResponseMaper.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; +using System.Net; + +namespace HttpClient.Core.Web.Http.ResponseMapers +{ + public class JsonResponseMaper : IResponseMaper + { + public ApiResponse Map(string? responseContent, + HttpStatusCode statusCode = HttpStatusCode.OK) + { + if (responseContent == null) throw new ArgumentNullException(nameof(responseContent)); + + return new ApiResponse(JsonConvert.DeserializeObject(responseContent), + statusCode); + } + } +} diff --git a/src/HttpClient.Core/ResponseMapers/WrappedJsonResponseMaper.cs b/src/HttpClient.Core/ResponseMapers/WrappedJsonResponseMaper.cs new file mode 100644 index 0000000..959aeca --- /dev/null +++ b/src/HttpClient.Core/ResponseMapers/WrappedJsonResponseMaper.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using System.Net; + +namespace HttpClient.Core.Web.Http.ResponseMapers +{ + public class WrappedJsonResponseMaper : IResponseMaper + { + public ApiResponse Map(string? responseContent, + HttpStatusCode statusCode = HttpStatusCode.OK) + { + if (responseContent == null) throw new ArgumentNullException(nameof(responseContent)); + + return JsonConvert.DeserializeObject>(responseContent); + } + } +} diff --git a/src/HttpClient.Core/ResponseReaders/IResponseReader.cs b/src/HttpClient.Core/ResponseReaders/IResponseReader.cs new file mode 100644 index 0000000..e75b960 --- /dev/null +++ b/src/HttpClient.Core/ResponseReaders/IResponseReader.cs @@ -0,0 +1,7 @@ +namespace HttpClient.Core.Web.Http.ResponseReaders +{ + public interface IResponseReader + { + Task ReadAsync(HttpResponseMessage response); + } +} diff --git a/src/HttpClient.Core/ResponseReaders/ResponseReader.cs b/src/HttpClient.Core/ResponseReaders/ResponseReader.cs new file mode 100644 index 0000000..24993da --- /dev/null +++ b/src/HttpClient.Core/ResponseReaders/ResponseReader.cs @@ -0,0 +1,12 @@ +namespace HttpClient.Core.Web.Http.ResponseReaders +{ + public class ResponseReader : IResponseReader + { + public async Task ReadAsync(HttpResponseMessage response) + { + if (response == null) throw new ArgumentNullException(nameof(response)); + + return await response.Content.ReadAsStringAsync(); + } + } +}