From 6bd891724c364be33b868695609722ffa5ed3c04 Mon Sep 17 00:00:00 2001 From: "ts.Igov" Date: Thu, 12 Jan 2023 14:28:10 +0200 Subject: [PATCH 1/2] .NET 6 migration --- .editorconfig | 35 +- .gitattributes | 8 + .github/workflows/push-action.yml | 2 +- .github/workflows/release-action.yml | 6 +- .gitignore | 5 +- .../Byteology.TypedHttpClients.Tests.csproj | 62 ++- .../JsonHttpClientTests.cs | 482 ++++++++-------- .../Mocks/MockHttpClientFactory.cs | 88 +-- .../TestServices/ITestService.cs | 85 ++- Byteology.TypedHttpClients.sln | 123 ++-- .../Byteology.TypedHttpClients.csproj | 91 +-- .../Clients/JsonHttpClient.cs | 160 +++--- .../Clients/TypedHttpClient.cs | 524 +++++++++--------- .../Dispatching/DispatchProxyDelegator.cs | 96 ++-- .../Dispatching/IDispatchHandler.cs | 46 +- .../HttpEndpointAttribute.cs | 102 ++-- .../HttpUriParameter.cs | 57 +- .../ServiceCollectionExtensions.cs | 96 ++-- Icon.png | Bin 13163 -> 5275 bytes 19 files changed, 1055 insertions(+), 1013 deletions(-) create mode 100644 .gitattributes diff --git a/.editorconfig b/.editorconfig index a23aedf..6cb7ab4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,32 +1,41 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = tab +tab_width = 4 + +[{*.yaml,*.yml}] +indent_style = space +indent_size = 2 + [*.cs] # symbols -dotnet_naming_symbols.private_methods.applicable_kinds = method +dotnet_naming_symbols.private_methods.applicable_kinds = method dotnet_naming_symbols.private_methods.applicable_accessibilities = private -dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private # naming styles dotnet_naming_style.camel_case_style.capitalization = camel_case -dotnet_naming_style.underscore_camel_case.capitalization = camel_case -dotnet_naming_style.underscore_camel_case.required_prefix = _ +dotnet_naming_style.underscore_camel_case.capitalization = camel_case +dotnet_naming_style.underscore_camel_case.required_prefix = _ # naming rules dotnet_naming_rule.private_methods_camel_case.symbols = private_methods -dotnet_naming_rule.private_methods_camel_case.style = camel_case_style +dotnet_naming_rule.private_methods_camel_case.style = camel_case_style dotnet_naming_rule.private_methods_camel_case.severity = suggestion dotnet_naming_rule.private_fields_underscore_camel_case.symbols = private_fields -dotnet_naming_rule.private_fields_underscore_camel_case.style = underscore_camel_case +dotnet_naming_rule.private_fields_underscore_camel_case.style = underscore_camel_case dotnet_naming_rule.private_fields_underscore_camel_case.severity = suggestion +# Sonar - -dotnet_diagnostic.IDE0003.severity = suggestion -dotnet_diagnostic.CA1825.severity = none - -dotnet_diagnostic.S1186.severity = none # does not allow empty methods -dotnet_diagnostic.S3011.severity = none # does not allow reflection of private members -dotnet_diagnostic.S3267.severity = none # Loops should be simplified by LINQ +# Bug, S3903:Types should be defined in named namespaces +# Doesn't take into consideration file scoped namespaces +dotnet_diagnostic.S3903.severity = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f7c7529 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Auto detect text files and perform LF normalization +* text=auto + +*.cs text diff=csharp +*.cshtml text diff=html +*.csx text diff=csharp +*.sln text eol=crlf +*.csproj text eol=crlf diff --git a/.github/workflows/push-action.yml b/.github/workflows/push-action.yml index d810733..d869268 100644 --- a/.github/workflows/push-action.yml +++ b/.github/workflows/push-action.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/release-action.yml b/.github/workflows/release-action.yml index bf9b4b3..79b6e50 100644 --- a/.github/workflows/release-action.yml +++ b/.github/workflows/release-action.yml @@ -12,7 +12,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 6.0.x - name: Checkout uses: actions/checkout@v2 @@ -33,10 +33,10 @@ jobs: run: | dotnet nuget push ./out/*.nupkg \ -s https://nuget.pkg.github.com/Byteology/index.json -k ${{ secrets.GITHUB_TOKEN }} \ - --skip-duplicate --no-symbols true + --skip-duplicate --no-symbols - name: Push package - NuGet.org run: | dotnet nuget push ./out/*.nupkg \ -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} \ - --skip-duplicate --no-symbols true + --skip-duplicate --no-symbols diff --git a/.gitignore b/.gitignore index f677870..3aafe5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ bin obj -.vs \ No newline at end of file +.vs +.idea +**.DotSettings +**.DotSettings.user diff --git a/Byteology.TypedHttpClients.Tests/Byteology.TypedHttpClients.Tests.csproj b/Byteology.TypedHttpClients.Tests/Byteology.TypedHttpClients.Tests.csproj index 0c28ee5..afa3a5e 100644 --- a/Byteology.TypedHttpClients.Tests/Byteology.TypedHttpClients.Tests.csproj +++ b/Byteology.TypedHttpClients.Tests/Byteology.TypedHttpClients.Tests.csproj @@ -1,30 +1,32 @@ - - - - net5.0 - - false - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + + net6.0 + + false + + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Byteology.TypedHttpClients.Tests/JsonHttpClientTests.cs b/Byteology.TypedHttpClients.Tests/JsonHttpClientTests.cs index d7946bf..e3353f1 100644 --- a/Byteology.TypedHttpClients.Tests/JsonHttpClientTests.cs +++ b/Byteology.TypedHttpClients.Tests/JsonHttpClientTests.cs @@ -1,240 +1,242 @@ -using Byteology.TypedHttpClients.Tests.Mocks; -using Byteology.TypedHttpClients.Tests.TestServices; -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Xunit; - -namespace Byteology.TypedHttpClients.Tests -{ - public class JsonHttpClientTests - { - [Fact] - public void Success() - { - // Arrange - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.NoResultActionAsync(); - - // Assert - Assert.True(result.IsCompletedSuccessfully); - } - - [Fact] - public void Success_Generic() - { - // Arrange - TestServiceResult response = new (); - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, new StringContent(response.ToString())); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - TestServiceResult result = service.ActionAsync().Result; - - // Assert - Assert.Equal(response, result); - } - - [Fact] - public void Error() - { - // Arrange - using HttpClient httpClient = getHttpClient(HttpStatusCode.Forbidden, null); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.NoResultActionAsync(); - - // Assert - Assert.ThrowsAny(() => result.Wait()); - } - - [Fact] - public void Error_Generic() - { - // Arrange - using HttpClient httpClient = getHttpClient(HttpStatusCode.Forbidden, null); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.ActionAsync(); - - // Assert - Assert.ThrowsAny(() => result.Wait()); - } - - [Fact] - public void Uri_Simple() - { - // Arrange - string expectedUrl = "https://example.com/simpleuri"; - void assertUri(HttpRequestMessage m) => Assert.Equal(expectedUrl, m.RequestUri?.ToString()); - - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.SimpleUriAsync(); - result.Wait(); - - // Assert - Assert.True(result.IsCompletedSuccessfully); - } - - [Fact] - public void Uri_Simple_NoDash() - { - // Arrange - static void assertUri(HttpRequestMessage m) => Assert.Equal("https://example.com/simpleuri", m.RequestUri?.ToString()); - - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.SimpleUriNoDashAsync(); - result.Wait(); - - // Assert - Assert.True(result.IsCompletedSuccessfully); - } - - [Theory] - [InlineData("test")] - [InlineData(5)] - [InlineData(5.8f)] - [InlineData(true)] - [InlineData(null)] - public void Uri_Param(object param) - { - // Arrange - string expectedUrl = $"https://example.com/paramuri/{param}"; - void assertUri(HttpRequestMessage m) => Assert.Equal(expectedUrl, m.RequestUri?.ToString()); - - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.ParamUriAsync(param); - result.Wait(); - - // Assert - Assert.True(result.IsCompletedSuccessfully); - } - - [Fact] - public void Uri_Query() - { - // Arrange - string expectedUrl = "https://example.com/query?i=5&s=asdf&b=True&f=5.4&n=&a=1&a=2&a=3&a="; - void assertUri(HttpRequestMessage m) => Assert.Equal(expectedUrl, m.RequestUri?.ToString()); - - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.QueryAsync(5, "asdf", true, 5.4f, null, new int?[] { 1, 2, 3, null }); - result.Wait(); - - // Assert - Assert.True(result.IsCompletedSuccessfully); - } - - [Fact] - public void Body() - { - // Arrange - string expectedUrl = "https://example.com/actionbody"; - TestServiceResult body = new(); - void assertUri(HttpRequestMessage m) - { - Assert.Equal(expectedUrl, m.RequestUri?.ToString()); - Assert.Equal(body.ToString(), m.Content.ReadAsStringAsync().Result); - } - - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.BodyActionAsync(body); - result.Wait(); - - // Assert - Assert.True(result.IsCompletedSuccessfully); - } - - [Fact] - public void Verb() - { - // Arrange - static void assertUri(HttpRequestMessage m) => Assert.Equal("VERB", m.Method.Method); - - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act - Task result = service.VerbAsync(); - result.Wait(); - - // Assert - Assert.True(result.IsCompletedSuccessfully); - } - - [Fact] - public void Invalid_OutParam() - { - // Arrange - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act & Assert - Assert.ThrowsAny(() => service.OutParamAsync(out int p).Wait()); - } - - [Fact] - public void Invalid_MultipleBody() - { - // Arrange - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act & Assert - Assert.ThrowsAny(() => service.MultipleBodyAsync(1, 2).Wait()); - } - - [Fact] - public void Invalid_NotDecorated() - { - // Arrange - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act & Assert - Assert.ThrowsAny(() => service.NotDecoratedAsync().Wait()); - } - - [Fact] - public void Invalid_NotAsync() - { - // Arrange - using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); - ITestService service = new JsonHttpClient(httpClient).Endpoints; - - // Act & Assert - Assert.ThrowsAny(() => service.NotAsync()); - } - - private static HttpClient getHttpClient( - HttpStatusCode statusCode, - HttpContent content, - Action onBeforeSend = null) - { - HttpClient httpClient = MockHttpClientFactory.Create(statusCode, content, onBeforeSend); - httpClient.BaseAddress = new Uri("https://example.com"); - return httpClient; - } - } -} +using Byteology.TypedHttpClients.Tests.Mocks; +using Byteology.TypedHttpClients.Tests.TestServices; +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace Byteology.TypedHttpClients.Tests +{ + public class JsonHttpClientTests + { + [Fact] + public void Success() + { + // Arrange + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.NoResultActionAsync(); + + // Assert + Assert.True(result.IsCompletedSuccessfully); + } + + [Fact] + public void Success_Generic() + { + // Arrange + TestServiceResult response = new(); + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, new StringContent(response.ToString())); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + TestServiceResult result = service.ActionAsync().Result; + + // Assert + Assert.Equal(response, result); + } + + [Fact] + public void Error() + { + // Arrange + using HttpClient httpClient = getHttpClient(HttpStatusCode.Forbidden, null); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.NoResultActionAsync(); + + // Assert + Assert.ThrowsAny(() => result.Wait()); + } + + [Fact] + public void Error_Generic() + { + // Arrange + using HttpClient httpClient = getHttpClient(HttpStatusCode.Forbidden, null); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.ActionAsync(); + + // Assert + Assert.ThrowsAny(() => result.Wait()); + } + + [Fact] + public void Uri_Simple() + { + // Arrange + string expectedUrl = "https://example.com/simpleUri"; + void assertUri(HttpRequestMessage m) => Assert.Equal(expectedUrl, m.RequestUri?.ToString()); + + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.SimpleUriAsync(); + result.Wait(); + + // Assert + Assert.True(result.IsCompletedSuccessfully); + } + + [Fact] + public void Uri_Simple_NoDash() + { + // Arrange + static void assertUri(HttpRequestMessage m) => + Assert.Equal("https://example.com/simpleUri", m.RequestUri?.ToString()); + + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.SimpleUriNoDashAsync(); + result.Wait(); + + // Assert + Assert.True(result.IsCompletedSuccessfully); + } + + [Theory] + [InlineData("test")] + [InlineData(5)] + [InlineData(5.8f)] + [InlineData(true)] + [InlineData(null)] + public void Uri_Param(object param) + { + // Arrange + string expectedUrl = $"https://example.com/paramUri/{param}"; + void assertUri(HttpRequestMessage m) => Assert.Equal(expectedUrl, m.RequestUri?.ToString()); + + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.ParamUriAsync(param); + result.Wait(); + + // Assert + Assert.True(result.IsCompletedSuccessfully); + } + + [Fact] + public void Uri_Query() + { + // Arrange + string expectedUrl = "https://example.com/query?i=5&s=a&b=True&f=5.4&n=&a=1&a=2&a=3&a="; + void assertUri(HttpRequestMessage m) => Assert.Equal(expectedUrl, m.RequestUri?.ToString()); + + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.QueryAsync(5, "a", true, 5.4f, null, new int?[] { 1, 2, 3, null }); + result.Wait(); + + // Assert + Assert.True(result.IsCompletedSuccessfully); + } + + [Fact] + public void Body() + { + // Arrange + string expectedUrl = "https://example.com/actionBody"; + TestServiceResult body = new(); + + void assertUri(HttpRequestMessage m) + { + Assert.Equal(expectedUrl, m.RequestUri?.ToString()); + Assert.Equal(body.ToString(), m.Content?.ReadAsStringAsync().Result); + } + + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.BodyActionAsync(body); + result.Wait(); + + // Assert + Assert.True(result.IsCompletedSuccessfully); + } + + [Fact] + public void Verb() + { + // Arrange + static void assertUri(HttpRequestMessage m) => Assert.Equal("VERB", m.Method.Method); + + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null, assertUri); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act + Task result = service.VerbAsync(); + result.Wait(); + + // Assert + Assert.True(result.IsCompletedSuccessfully); + } + + [Fact] + public void Invalid_OutParam() + { + // Arrange + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act & Assert + Assert.ThrowsAny(() => service.OutParamAsync(out int p).Wait()); + } + + [Fact] + public void Invalid_MultipleBody() + { + // Arrange + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act & Assert + Assert.ThrowsAny(() => service.MultipleBodyAsync(1, 2).Wait()); + } + + [Fact] + public void Invalid_NotDecorated() + { + // Arrange + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act & Assert + Assert.ThrowsAny(() => service.NotDecoratedAsync().Wait()); + } + + [Fact] + public void Invalid_NotAsync() + { + // Arrange + using HttpClient httpClient = getHttpClient(HttpStatusCode.OK, null); + ITestService service = new JsonHttpClient(httpClient).Endpoints; + + // Act & Assert + Assert.ThrowsAny(() => service.NotAsync()); + } + + private static HttpClient getHttpClient( + HttpStatusCode statusCode, + HttpContent? content, + Action? onBeforeSend = null) + { + HttpClient httpClient = MockHttpClientFactory.Create(statusCode, content, onBeforeSend); + httpClient.BaseAddress = new Uri("https://example.com"); + return httpClient; + } + } +} diff --git a/Byteology.TypedHttpClients.Tests/Mocks/MockHttpClientFactory.cs b/Byteology.TypedHttpClients.Tests/Mocks/MockHttpClientFactory.cs index b53eea1..993a1e5 100644 --- a/Byteology.TypedHttpClients.Tests/Mocks/MockHttpClientFactory.cs +++ b/Byteology.TypedHttpClients.Tests/Mocks/MockHttpClientFactory.cs @@ -1,43 +1,45 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Byteology.TypedHttpClients.Tests.Mocks -{ - internal static class MockHttpClientFactory - { - public static HttpClient Create(HttpStatusCode statusCode, HttpContent content, Action onBeforeSend = null) - { - MessageHandler handler = new (statusCode, content, onBeforeSend); - return new HttpClient(handler, true); - } - - private class MessageHandler : HttpMessageHandler - { - private readonly HttpStatusCode _statusCode; - private readonly HttpContent _content; - private readonly Action _onBeforeSend; - - public MessageHandler(HttpStatusCode statusCode, HttpContent content, Action onBeforeSend = null) - { - _statusCode = statusCode; - _content = content; - _onBeforeSend = onBeforeSend; - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - _onBeforeSend?.Invoke(request); - - return Task.FromResult( - new HttpResponseMessage() - { - StatusCode = _statusCode, - Content = _content - }); - } - } - } -} +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Byteology.TypedHttpClients.Tests.Mocks +{ + internal static class MockHttpClientFactory + { + public static HttpClient Create(HttpStatusCode statusCode, HttpContent? content, + Action? onBeforeSend = null) + { + MessageHandler handler = new(statusCode, content, onBeforeSend); + return new HttpClient(handler, true); + } + + private class MessageHandler : HttpMessageHandler + { + private readonly HttpContent? _content; + private readonly Action? _onBeforeSend; + private readonly HttpStatusCode _statusCode; + + public MessageHandler(HttpStatusCode statusCode, HttpContent? content, + Action? onBeforeSend = null) + { + _statusCode = statusCode; + _content = content; + _onBeforeSend = onBeforeSend; + } + + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + _onBeforeSend?.Invoke(request); + + return Task.FromResult(new HttpResponseMessage + { + StatusCode = _statusCode, + Content = _content + }); + } + } + } +} diff --git a/Byteology.TypedHttpClients.Tests/TestServices/ITestService.cs b/Byteology.TypedHttpClients.Tests/TestServices/ITestService.cs index 243fd74..b093275 100644 --- a/Byteology.TypedHttpClients.Tests/TestServices/ITestService.cs +++ b/Byteology.TypedHttpClients.Tests/TestServices/ITestService.cs @@ -1,43 +1,42 @@ -using System.Threading.Tasks; - -namespace Byteology.TypedHttpClients.Tests.TestServices -{ - internal interface ITestService - { - [HttpEndpoint("POST", "/noresultaction")] - Task NoResultActionAsync(); - - [HttpEndpoint("POST", "/action")] - Task ActionAsync(); - - [HttpEndpoint("POST", "/actionbody")] - Task BodyActionAsync([HttpBody]TestServiceResult body); - - [HttpEndpoint("POST", "/simpleuri")] - Task SimpleUriAsync(); - - [HttpEndpoint("POST", "simpleuri")] - Task SimpleUriNoDashAsync(); - - [HttpEndpoint("POST", "/paramuri/{param}")] - Task ParamUriAsync(object param); - - [HttpEndpoint("POST", "/query")] - Task QueryAsync(int i, string s, bool b, float f, object n, int?[] a); - - [HttpEndpoint("VERB", "/verb")] - Task VerbAsync(); - - [HttpEndpoint("POST", "/uri")] - Task OutParamAsync(out int param); - - [HttpEndpoint("POST", "/uri")] - Task MultipleBodyAsync([HttpBody] int param, [HttpBody] int param2); - - Task NotDecoratedAsync(); - - [HttpEndpoint("POST", "/uri")] - int NotAsync(); - } - -} +using System.Threading.Tasks; + +namespace Byteology.TypedHttpClients.Tests.TestServices +{ + internal interface ITestService + { + [HttpEndpoint("POST", "/noResultAction")] + Task NoResultActionAsync(); + + [HttpEndpoint("POST", "/action", Tags = new[] { "tag" })] + Task ActionAsync(); + + [HttpEndpoint("POST", "/actionBody")] + Task BodyActionAsync([HttpBody] TestServiceResult body); + + [HttpEndpoint("POST", "/simpleUri")] + Task SimpleUriAsync(); + + [HttpEndpoint("POST", "simpleUri")] + Task SimpleUriNoDashAsync(); + + [HttpEndpoint("POST", "/paramUri/{param}")] + Task ParamUriAsync(object param); + + [HttpEndpoint("POST", "/query")] + Task QueryAsync(int i, string s, bool b, float f, object? n, int?[] a); + + [HttpEndpoint("VERB", "/verb")] + Task VerbAsync(); + + [HttpEndpoint("POST", "/uri")] + Task OutParamAsync(out int param); + + [HttpEndpoint("POST", "/uri")] + Task MultipleBodyAsync([HttpBody] int param, [HttpBody] int param2); + + Task NotDecoratedAsync(); + + [HttpEndpoint("POST", "/uri")] + int NotAsync(); + } +} diff --git a/Byteology.TypedHttpClients.sln b/Byteology.TypedHttpClients.sln index 7b04846..d057ccc 100644 --- a/Byteology.TypedHttpClients.sln +++ b/Byteology.TypedHttpClients.sln @@ -1,61 +1,62 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31808.319 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byteology.TypedHttpClients", "Byteology.TypedHttpClients\Byteology.TypedHttpClients.csproj", "{C212C350-4301-4773-9029-162826EF7CD0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byteology.TypedHttpClients.Tests", "Byteology.TypedHttpClients.Tests\Byteology.TypedHttpClients.Tests.csproj", "{20BB41AD-2786-4550-8D94-182B7D723BA5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{28CC8107-D48A-4EBB-84A4-37D113F3614D}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitignore = .gitignore - Icon.png = Icon.png - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{83E38053-A14F-4FC7-A099-46A611744FCF}" - ProjectSection(SolutionItems) = preProject - .github\workflows\push-action.yml = .github\workflows\push-action.yml - .github\workflows\release-action.yml = .github\workflows\release-action.yml - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E93839AF-ED4E-4074-B642-DCBB4FE18CC9}" - ProjectSection(SolutionItems) = preProject - .github\CODEOWNERS = .github\CODEOWNERS - .github\README.md = .github\README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".sonar", ".sonar", "{78567D61-B59E-4379-A572-4FF8C4F117AA}" - ProjectSection(SolutionItems) = preProject - .sonar\coverlet.runsettings = .sonar\coverlet.runsettings - .sonar\SonarQube.Analysis.xml = .sonar\SonarQube.Analysis.xml - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C212C350-4301-4773-9029-162826EF7CD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C212C350-4301-4773-9029-162826EF7CD0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C212C350-4301-4773-9029-162826EF7CD0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C212C350-4301-4773-9029-162826EF7CD0}.Release|Any CPU.Build.0 = Release|Any CPU - {20BB41AD-2786-4550-8D94-182B7D723BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20BB41AD-2786-4550-8D94-182B7D723BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20BB41AD-2786-4550-8D94-182B7D723BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20BB41AD-2786-4550-8D94-182B7D723BA5}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {83E38053-A14F-4FC7-A099-46A611744FCF} = {E93839AF-ED4E-4074-B642-DCBB4FE18CC9} - {E93839AF-ED4E-4074-B642-DCBB4FE18CC9} = {28CC8107-D48A-4EBB-84A4-37D113F3614D} - {78567D61-B59E-4379-A572-4FF8C4F117AA} = {28CC8107-D48A-4EBB-84A4-37D113F3614D} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {3C5450B1-6E34-4347-9447-4F8814CEECF4} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31808.319 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byteology.TypedHttpClients", "Byteology.TypedHttpClients\Byteology.TypedHttpClients.csproj", "{C212C350-4301-4773-9029-162826EF7CD0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Byteology.TypedHttpClients.Tests", "Byteology.TypedHttpClients.Tests\Byteology.TypedHttpClients.Tests.csproj", "{20BB41AD-2786-4550-8D94-182B7D723BA5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{28CC8107-D48A-4EBB-84A4-37D113F3614D}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + Icon.png = Icon.png + .gitattributes = .gitattributes + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{83E38053-A14F-4FC7-A099-46A611744FCF}" + ProjectSection(SolutionItems) = preProject + .github\workflows\push-action.yml = .github\workflows\push-action.yml + .github\workflows\release-action.yml = .github\workflows\release-action.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E93839AF-ED4E-4074-B642-DCBB4FE18CC9}" + ProjectSection(SolutionItems) = preProject + .github\CODEOWNERS = .github\CODEOWNERS + .github\README.md = .github\README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".sonar", ".sonar", "{78567D61-B59E-4379-A572-4FF8C4F117AA}" + ProjectSection(SolutionItems) = preProject + .sonar\coverlet.runsettings = .sonar\coverlet.runsettings + .sonar\SonarQube.Analysis.xml = .sonar\SonarQube.Analysis.xml + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C212C350-4301-4773-9029-162826EF7CD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C212C350-4301-4773-9029-162826EF7CD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C212C350-4301-4773-9029-162826EF7CD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C212C350-4301-4773-9029-162826EF7CD0}.Release|Any CPU.Build.0 = Release|Any CPU + {20BB41AD-2786-4550-8D94-182B7D723BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20BB41AD-2786-4550-8D94-182B7D723BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20BB41AD-2786-4550-8D94-182B7D723BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20BB41AD-2786-4550-8D94-182B7D723BA5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {83E38053-A14F-4FC7-A099-46A611744FCF} = {E93839AF-ED4E-4074-B642-DCBB4FE18CC9} + {E93839AF-ED4E-4074-B642-DCBB4FE18CC9} = {28CC8107-D48A-4EBB-84A4-37D113F3614D} + {78567D61-B59E-4379-A572-4FF8C4F117AA} = {28CC8107-D48A-4EBB-84A4-37D113F3614D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3C5450B1-6E34-4347-9447-4F8814CEECF4} + EndGlobalSection +EndGlobal diff --git a/Byteology.TypedHttpClients/Byteology.TypedHttpClients.csproj b/Byteology.TypedHttpClients/Byteology.TypedHttpClients.csproj index c9f7bde..166aa20 100644 --- a/Byteology.TypedHttpClients/Byteology.TypedHttpClients.csproj +++ b/Byteology.TypedHttpClients/Byteology.TypedHttpClients.csproj @@ -1,45 +1,46 @@ - - - - net5.0 - True - true - Byteology - Byteology Ltd - https://github.com/Byteology/typed-http-clients.git - Icon.png - Provides classes for creating typed http clients from an interface describing a service. - 1.0.0 - http typed HttpClient HttpClientFactory - MIT - https://github.com/Byteology/typed-http-clients - git - - - - - True - - False - - - - - - True - - False - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + net6.0 + True + true + Byteology + Byteology Ltd + https://github.com/Byteology/typed-http-clients.git + Icon.png + Provides classes for creating typed http clients from an interface describing a service. + 2.0.0 + http typed HttpClient HttpClientFactory + MIT + https://github.com/Byteology/typed-http-clients + git + enable + + + + + True + + False + + + + + + True + + False + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Byteology.TypedHttpClients/Clients/JsonHttpClient.cs b/Byteology.TypedHttpClients/Clients/JsonHttpClient.cs index b891f5f..87ce304 100644 --- a/Byteology.TypedHttpClients/Clients/JsonHttpClient.cs +++ b/Byteology.TypedHttpClients/Clients/JsonHttpClient.cs @@ -1,79 +1,81 @@ -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; - -namespace Byteology.TypedHttpClients -{ - /// - /// An HTTP clients that implement a contract of a service with a JSON content type. - /// - /// - public class JsonHttpClient : TypedHttpClient - where TServiceContract : class - { - /// - /// Gets the JSON serialization options used to create and parse the HTTP requests and responses. - /// - public JsonSerializerOptions JsonSerializerOptions { get; } = new JsonSerializerOptions(JsonSerializerDefaults.Web); - - /// - /// Initializes a new instance of the class using an - /// that will send the HTTP requests. - /// - /// The HTTP client. - public JsonHttpClient(HttpClient httpClient) : base(httpClient) { } - - /// - /// Builds an HTTP request with a JSON content type. - /// - /// - /// - /// - /// - protected override Task BuildRequestAsync(string verb, string uri, object body, string[] tags) - { - HttpRequestMessage httpRequest = new(new HttpMethod(verb), uri); - httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - if (body != null) - httpRequest.Content = new StringContent(JsonSerializer.Serialize(body, JsonSerializerOptions), - Encoding.UTF8, - "application/json"); - - return Task.FromResult(httpRequest); - } - - /// - /// Throws an exception if the - /// property for the HTTP response is false. - /// - /// - /// - /// - protected override Task ProcessResponse(HttpResponseMessage response, string[] tags) - { - response.EnsureSuccessStatusCode(); - return Task.CompletedTask; - } - - /// - /// Throws an exception if the - /// property for the HTTP response is false. Otherwise converts the response content - /// to a object and returns it. - /// - /// - /// - /// - protected override async Task ProcessResponse(HttpResponseMessage response, string[] tags) - { - response.EnsureSuccessStatusCode(); - - string bodyString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - TResult result = JsonSerializer.Deserialize(bodyString, JsonSerializerOptions); - - return result; - } - } -} +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Byteology.TypedHttpClients +{ + /// + /// An HTTP clients that implement a contract of a service with a JSON content type. + /// + /// + public class JsonHttpClient : TypedHttpClient + where TServiceContract : class + { + /// + /// Initializes a new instance of the class using an + /// that will send the HTTP requests. + /// + /// The HTTP client. + public JsonHttpClient(HttpClient httpClient) : base(httpClient) { } + + /// + /// Gets the JSON serialization options used to create and parse the HTTP requests and responses. + /// + public JsonSerializerOptions JsonSerializerOptions { get; } = + new JsonSerializerOptions(JsonSerializerDefaults.Web); + + /// + /// Builds an HTTP request with a JSON content type. + /// + /// + /// + /// + /// + protected override Task BuildRequestAsync(string verb, string uri, object? body, + string[]? tags) + { + HttpRequestMessage httpRequest = new(new HttpMethod(verb), uri); + httpRequest.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + if (body != null) + httpRequest.Content = new StringContent(JsonSerializer.Serialize(body, JsonSerializerOptions), + Encoding.UTF8, + "application/json"); + + return Task.FromResult(httpRequest); + } + + /// + /// Throws an exception if the + /// property for the HTTP response is false. + /// + /// + /// + /// + protected override Task ProcessResponse(HttpResponseMessage response, string[]? tags) + { + response.EnsureSuccessStatusCode(); + return Task.CompletedTask; + } + + /// + /// Throws an exception if the + /// property for the HTTP response is false. Otherwise converts the response content + /// to a object and returns it. + /// + /// + /// + /// + protected override async Task ProcessResponse(HttpResponseMessage response, string[]? tags) + { + response.EnsureSuccessStatusCode(); + + string bodyString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + TResult result = JsonSerializer.Deserialize(bodyString, JsonSerializerOptions)!; + + return result; + } + } +} diff --git a/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs b/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs index 8979bd5..aa0d19e 100644 --- a/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs +++ b/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs @@ -1,257 +1,267 @@ -using Byteology.GuardClauses; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using System.Web; - -namespace Byteology.TypedHttpClients -{ - /// - /// Provides a base class for HTTP clients that implement a service contract. - /// - /// The service contract. It should be an interface containing only - /// async methods decorated with and having no output parameters. - /// A single method parameter is allowed to be decorated with the - /// in order to be used as the request's content. All other parameters will be used either as route or query parameters - /// depending on whether their name has a match in the or not. - #pragma warning disable CS0618 // Type or member is obsolete - public abstract class TypedHttpClient : IDispatchHandler - #pragma warning restore CS0618 // Type or member is obsolete - where TServiceContract : class - { - private readonly HttpClient _httpClient; - - /// - /// Gets the endpoints of the service. - /// - public TServiceContract Endpoints { get; } - - /// - /// Initializes a new instance of the - /// class using an - /// that will send the HTTP requests. - /// - /// The HTTP client. - protected TypedHttpClient(HttpClient httpClient) - { - Guard.Argument(httpClient, nameof(httpClient)).NotNull(); - - _httpClient = httpClient; - #pragma warning disable CS0618 // Type or member is obsolete - Endpoints = DispatchProxyDelegator.Create(this); - #pragma warning restore CS0618 // Type or member is obsolete - } - - /// - /// Builds a query string. - /// - /// The query parameters. - /// The tags of the request. These are provided by the - /// in order for this method to be able to recognize requests that require special treatment. - protected virtual string BuildQueryString(IEnumerable queryParameters, string[] tags) - { - StringBuilder builder = new(); - - if (queryParameters != null) - foreach (HttpUriParameter parameter in queryParameters) - { - if (parameter.Value is ICollection collection) - foreach (object item in collection) - builder.Append($"{HttpUtility.UrlEncode(parameter.Name)}={HttpUtility.UrlEncode(item?.ToString())}&"); - else - builder.Append($"{HttpUtility.UrlEncode(parameter.Name)}={HttpUtility.UrlEncode(parameter.Value?.ToString())}&"); - } - - if (builder.Length > 0) - builder.Remove(builder.Length - 1, 1); - - return builder.ToString(); - } - - /// - /// Builds an HTTP request. - /// - /// The HTTP verb of the request. - /// The URI of the request. - /// The content body of the request or if no body should be provided. - /// The tags of the request. These are provided by the - /// in order for this method to be able to recognize requests that require special treatment. - protected abstract Task BuildRequestAsync(string verb, string uri, object body, string[] tags); - - /// - /// Send an HTTP request as an asynchronous operation. - /// - /// The HTTP client to use. - /// The request to send. - /// The tags of the request. These are specified by the - /// in order for this method to be able to recognize requests that require special treatment. - protected virtual async Task SendRequestAsync(HttpClient httpClient, HttpRequestMessage request, string[] tags) - { - Guard.Argument(httpClient, nameof(httpClient)).NotNull(); - Guard.Argument(request, nameof(request)).NotNull(); - - HttpResponseMessage response = await httpClient.SendAsync(request).ConfigureAwait(false); - response.RequestMessage = request; - return response; - } - - /// - /// Processes the response of an HTTP request. - /// - /// The HTTP response. - /// The tags of the request. These are specified by the - /// in order for this method to be able to recognize requests that require special treatment. - protected abstract Task ProcessResponse(HttpResponseMessage response, string[] tags); - /// - /// Processes the response of an HTTP request and converts its body to . - /// - /// The type of the object the response message should be converted to. - /// The HTTP response. - /// The tags of the request. These are specified by the - /// in order for this method to be able to recognize requests that require special treatment. - protected abstract Task ProcessResponse(HttpResponseMessage response, string[] tags); - - object IDispatchHandler.Dispatch(MethodInfo targetMethod, object[] args) - { - if (targetMethod.GetCustomAttribute(true) == null) - throw new InvalidOperationException($"The method should be decorated with the {typeof(HttpEndpointAttribute)}."); - - ParameterInfo[] parameters = targetMethod.GetParameters(); - - if (parameters.Any(p => p.IsOut)) - throw new InvalidOperationException("Output parameters are not allowed."); - - if (parameters.Count(p => p.GetCustomAttribute() != null) > 1) - throw new InvalidOperationException("Maximum of one body parameter is allowed per method."); - - if (returnsTask(targetMethod)) - return sendRequestAndParseResponseAsync(targetMethod, args); - else if (returnsGenericTask(targetMethod)) - { - MethodInfo sendRequestMethod = typeof(TypedHttpClient) - .GetMethod(nameof(sendRequestAndParseGenericResponseAsync),BindingFlags.NonPublic | BindingFlags.Instance); - sendRequestMethod = sendRequestMethod.MakeGenericMethod(targetMethod.ReturnType.GenericTypeArguments.Single()); - return sendRequestMethod.Invoke(this, new object[] { targetMethod, args }); - } - else - throw new InvalidOperationException("Method should return either Task or Task<>."); - } - - private async Task sendRequestAndParseResponseAsync(MethodInfo targetMethod, object[] args) - { - HttpEndpointAttribute endpointAttribute = targetMethod.GetCustomAttribute(true); - string[] tags = endpointAttribute.Tags; - - HttpRequestMessage request = await createRequestAsync(targetMethod, args).ConfigureAwait(false); - HttpResponseMessage response = await SendRequestAsync(_httpClient, request, tags).ConfigureAwait(false); - - await ProcessResponse(response, tags).ConfigureAwait(false); - } - private async Task sendRequestAndParseGenericResponseAsync(MethodInfo targetMethod, object[] args) - { - HttpEndpointAttribute endpointAttribute = targetMethod.GetCustomAttribute(true); - string[] tags = endpointAttribute.Tags; - HttpRequestMessage request = await createRequestAsync(targetMethod, args).ConfigureAwait(false); - HttpResponseMessage response = await SendRequestAsync(_httpClient, request, tags).ConfigureAwait(false); - - return await ProcessResponse(response, tags).ConfigureAwait(false); - } - - private async Task createRequestAsync(MethodInfo targetMethod, object[] args) - { - HttpEndpointAttribute endpointAttribute = targetMethod.GetCustomAttribute(true); - - string verb = endpointAttribute.Verb; - string routeTemplate = endpointAttribute.RouteTemplate; - string[] tags = endpointAttribute.Tags; - - List uriParameters = getParameters(targetMethod, args, out object body); - string uri = getUri(tags, routeTemplate, ref uriParameters); - - HttpRequestMessage request = await BuildRequestAsync(verb, uri, body, tags).ConfigureAwait(false); - return request; - } - private static List getParameters(MethodInfo targetMethod, object[] args, out object body) - { - List result = new(); - body = null; - - ParameterInfo[] parameters = targetMethod.GetParameters(); - - for (int i = 0; i < parameters.Length; i++) - { - ParameterInfo parameter = parameters[i]; - object value = args[i]; - - if (isBodyParameter(parameter)) - body = value; - else - { - HttpUriParameter uriParameter = new(parameter.Name, value); - result.Add(uriParameter); - } - } - - return result; - - static bool isBodyParameter(ParameterInfo p) - { - HttpBodyAttribute bodyAttribute = p.GetCustomAttribute(); - return bodyAttribute != null; - } - } - private string getUri(string[] tags, string routeTemplate, ref List uriParameters) - { - - if (routeTemplate.StartsWith("/")) - routeTemplate = routeTemplate[1..]; - - for (int i = 0; i < uriParameters.Count; i++) - { - HttpUriParameter parameter = uriParameters[i]; - - if (routeTemplate.Contains($"{{{parameter.Name}}}")) - { - routeTemplate = routeTemplate.Replace( - $"{{{parameter.Name}}}", - HttpUtility.UrlEncode(parameter.Value?.ToString() ?? string.Empty)); - - uriParameters.RemoveAt(i); - i--; - } - } - - routeTemplate = addQueryString(uriParameters, routeTemplate, tags); - - return routeTemplate; - - string addQueryString(List uriParameters, string uri, string[] tags) - { - string queryString = BuildQueryString(uriParameters, tags); - if (!string.IsNullOrEmpty(queryString)) - { - if (!queryString.StartsWith("?")) - queryString = "?" + queryString; - - uri += queryString; - } - - return uri; - } - } - - private static bool returnsTask(MethodInfo method) - { - return method.ReturnType == typeof(Task); - } - private static bool returnsGenericTask(MethodInfo method) - { - return method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>); - } - } -} +using Byteology.GuardClauses; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace Byteology.TypedHttpClients +{ + /// + /// Provides a base class for HTTP clients that implement a service contract. + /// + /// The service contract. It should be an interface containing only + /// async methods decorated with and having no output parameters. + /// A single method parameter is allowed to be decorated with the + /// in order to be used as the request's content. All other parameters will be used either as route or query parameters + /// depending on whether their name has a match in the or not. + #pragma warning disable CS0618 // Type or member is obsolete + public abstract class TypedHttpClient : IDispatchHandler + #pragma warning restore CS0618 // Type or member is obsolete + where TServiceContract : class + { + private readonly HttpClient _httpClient; + + /// + /// Initializes a new instance of the + /// class using an + /// that will send the HTTP requests. + /// + /// The HTTP client. + protected TypedHttpClient(HttpClient httpClient) + { + Guard.Argument(httpClient, nameof(httpClient)).NotNull(); + + _httpClient = httpClient; + #pragma warning disable CS0618 // Type or member is obsolete + Endpoints = DispatchProxyDelegator.Create(this); + #pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Gets the endpoints of the service. + /// + public TServiceContract Endpoints { get; } + + object? IDispatchHandler.Dispatch(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod?.GetCustomAttribute(true) == null) + throw new + InvalidOperationException($"The method should be decorated with the {typeof(HttpEndpointAttribute)}."); + + ParameterInfo[] parameters = targetMethod.GetParameters(); + + if (parameters.Any(p => p.IsOut)) + throw new InvalidOperationException("Output parameters are not allowed."); + + if (parameters.Count(p => p.GetCustomAttribute() != null) > 1) + throw new InvalidOperationException("Maximum of one body parameter is allowed per method."); + + if (returnsTask(targetMethod)) + return sendRequestAndParseResponseAsync(targetMethod, args); + else if (returnsGenericTask(targetMethod)) + { + MethodInfo sendRequestMethod = typeof(TypedHttpClient) + .GetMethod(nameof(sendRequestAndParseGenericResponseAsync), + BindingFlags.NonPublic | BindingFlags.Instance)!; + sendRequestMethod = + sendRequestMethod.MakeGenericMethod(targetMethod.ReturnType.GenericTypeArguments.Single()); + return sendRequestMethod.Invoke(this, new object?[] { targetMethod, args }); + } + else + throw new InvalidOperationException("Method should return either Task or Task<>."); + } + + /// + /// Builds a query string. + /// + /// The query parameters. + /// The tags of the request. These are provided by the + /// in order for this method to be able to recognize requests that require special treatment. + protected virtual string BuildQueryString(IEnumerable queryParameters, string[]? tags) + { + StringBuilder builder = new(); + + if (queryParameters != null) + foreach (HttpUriParameter parameter in queryParameters) + { + if (parameter.Value is ICollection collection) + foreach (object item in collection) + builder.Append($"{HttpUtility.UrlEncode(parameter.Name)}={HttpUtility.UrlEncode(item?.ToString())}&"); + else + builder.Append($"{HttpUtility.UrlEncode(parameter.Name)}={HttpUtility.UrlEncode(parameter.Value?.ToString())}&"); + } + + if (builder.Length > 0) + builder.Remove(builder.Length - 1, 1); + + return builder.ToString(); + } + + /// + /// Builds an HTTP request. + /// + /// The HTTP verb of the request. + /// The URI of the request. + /// The content body of the request or if no body should be provided. + /// The tags of the request. These are provided by the + /// in order for this method to be able to recognize requests that require special treatment. + protected abstract Task BuildRequestAsync(string verb, string uri, object? body, + string[]? tags); + + /// + /// Send an HTTP request as an asynchronous operation. + /// + /// The HTTP client to use. + /// The request to send. + /// The tags of the request. These are specified by the + /// in order for this method to be able to recognize requests that require special treatment. + protected virtual async Task SendRequestAsync( + HttpClient httpClient, HttpRequestMessage request, string[]? tags) + { + Guard.Argument(httpClient, nameof(httpClient)).NotNull(); + Guard.Argument(request, nameof(request)).NotNull(); + + HttpResponseMessage response = await httpClient.SendAsync(request).ConfigureAwait(false); + response.RequestMessage = request; + return response; + } + + /// + /// Processes the response of an HTTP request. + /// + /// The HTTP response. + /// The tags of the request. These are specified by the + /// in order for this method to be able to recognize requests that require special treatment. + protected abstract Task ProcessResponse(HttpResponseMessage response, string[]? tags); + + /// + /// Processes the response of an HTTP request and converts its body to . + /// + /// The type of the object the response message should be converted to. + /// The HTTP response. + /// The tags of the request. These are specified by the + /// in order for this method to be able to recognize requests that require special treatment. + protected abstract Task ProcessResponse(HttpResponseMessage response, string[]? tags); + + private async Task sendRequestAndParseResponseAsync(MethodInfo targetMethod, object?[]? args) + { + HttpEndpointAttribute endpointAttribute = targetMethod.GetCustomAttribute(true)!; + string[]? tags = endpointAttribute.Tags; + + HttpRequestMessage request = await createRequestAsync(targetMethod, args).ConfigureAwait(false); + HttpResponseMessage response = await SendRequestAsync(_httpClient, request, tags).ConfigureAwait(false); + + await ProcessResponse(response, tags).ConfigureAwait(false); + } + + private async Task sendRequestAndParseGenericResponseAsync( + MethodInfo targetMethod, object[] args) + { + HttpEndpointAttribute endpointAttribute = targetMethod.GetCustomAttribute(true)!; + string[]? tags = endpointAttribute.Tags; + HttpRequestMessage request = await createRequestAsync(targetMethod, args).ConfigureAwait(false); + HttpResponseMessage response = await SendRequestAsync(_httpClient, request, tags).ConfigureAwait(false); + + return await ProcessResponse(response, tags).ConfigureAwait(false); + } + + private async Task createRequestAsync(MethodInfo targetMethod, object?[]? args) + { + HttpEndpointAttribute endpointAttribute = targetMethod.GetCustomAttribute(true)!; + + string verb = endpointAttribute.Verb; + string routeTemplate = endpointAttribute.RouteTemplate; + string[]? tags = endpointAttribute.Tags; + + List uriParameters = getParameters(targetMethod, args, out object? body); + string uri = getUri(tags, routeTemplate, ref uriParameters); + + HttpRequestMessage request = await BuildRequestAsync(verb, uri, body, tags).ConfigureAwait(false); + return request; + } + + private static List getParameters(MethodInfo targetMethod, object?[]? args, out object? body) + { + List result = new(); + body = null; + + ParameterInfo[] parameters = targetMethod.GetParameters(); + + for (int i = 0; i < parameters.Length; i++) + { + ParameterInfo parameter = parameters[i]; + object? value = args![i]; + + if (isBodyParameter(parameter)) + body = value; + else + { + HttpUriParameter uriParameter = new(parameter.Name!, value); + result.Add(uriParameter); + } + } + + return result; + + static bool isBodyParameter(ParameterInfo p) + { + HttpBodyAttribute? bodyAttribute = p.GetCustomAttribute(); + return bodyAttribute != null; + } + } + + private string getUri(string[]? tags, string routeTemplate, ref List uriParameters) + { + if (routeTemplate.StartsWith("/")) + routeTemplate = routeTemplate[1..]; + + for (int i = 0; i < uriParameters.Count; i++) + { + HttpUriParameter parameter = uriParameters[i]; + + if (routeTemplate.Contains($"{{{parameter.Name}}}")) + { + routeTemplate = routeTemplate.Replace($"{{{parameter.Name}}}", + HttpUtility.UrlEncode(parameter.Value?.ToString() ?? + string.Empty)); + + uriParameters.RemoveAt(i); + i--; + } + } + + routeTemplate = addQueryString(uriParameters, routeTemplate, tags); + + return routeTemplate; + + string addQueryString(List uriParameters, string uri, string[]? tags) + { + string queryString = BuildQueryString(uriParameters, tags); + if (!string.IsNullOrEmpty(queryString)) + { + if (!queryString.StartsWith("?")) + queryString = "?" + queryString; + + uri += queryString; + } + + return uri; + } + } + + private static bool returnsTask(MethodInfo method) + { + return method.ReturnType == typeof(Task); + } + + private static bool returnsGenericTask(MethodInfo method) + { + return method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>); + } + } +} diff --git a/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs b/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs index d1d9d73..5abd859 100644 --- a/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs +++ b/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs @@ -1,47 +1,49 @@ -using System; -using System.ComponentModel; -using System.Reflection; - -namespace Byteology.TypedHttpClients -{ - /// - /// Used to transfer a calls to an . - /// - [Browsable(false)] // This class shouldn't be used externaly but needs to be seen by calling assemblies so we just hide it from intellisense. - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Meant for internal use only.")] - public class DispatchProxyDelegator : DispatchProxy - { - /// - /// Creates an object instance that implements the provided interface type. - /// - /// The type of the interface to be implemented. - /// The object that will handle the method calls. - public static TInterface Create(IDispatchHandler handler) - where TInterface : class - { - TInterface proxy = DispatchProxy.Create(); - MethodInfo initMethod = typeof(DispatchProxyDelegator).GetMethod(nameof(initialize), BindingFlags.NonPublic | BindingFlags.Instance); - initMethod.Invoke(proxy, new object[] { handler }); - return proxy; - } - - private IDispatchHandler _dispatcher; - - private void initialize(IDispatchHandler dispatcher) - { - _dispatcher = dispatcher; - } - - /// - /// Whenever any method on the generated proxy type is called, this method is invoked to dispatch control. - /// - /// The method the caller invoked. - /// The arguments the caller passed to the method. - /// The object to return to the caller, or for void methods. - protected override object Invoke(MethodInfo targetMethod, object[] args) - { - return _dispatcher.Dispatch(targetMethod, args); - } - } -} +using System; +using System.ComponentModel; +using System.Reflection; + +namespace Byteology.TypedHttpClients +{ + /// + /// Used to transfer a calls to an . + /// + [Browsable(false)] // This class shouldn't be used externally but needs to be seen by calling assemblies so we just hide it from intellisense. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Meant for internal use only.")] + public class DispatchProxyDelegator : DispatchProxy + { + private IDispatchHandler? _dispatcher; + + /// + /// Creates an object instance that implements the provided interface type. + /// + /// The type of the interface to be implemented. + /// The object that will handle the method calls. + public static TInterface Create(IDispatchHandler handler) + where TInterface : class + { + TInterface proxy = DispatchProxy.Create(); + MethodInfo initMethod = + typeof(DispatchProxyDelegator).GetMethod(nameof(initialize), + BindingFlags.NonPublic | BindingFlags.Instance)!; + initMethod.Invoke(proxy, new object[] { handler }); + return proxy; + } + + private void initialize(IDispatchHandler dispatcher) + { + _dispatcher = dispatcher; + } + + /// + /// Whenever any method on the generated proxy type is called, this method is invoked to dispatch control. + /// + /// The method the caller invoked. + /// The arguments the caller passed to the method. + /// The object to return to the caller, or for void methods. + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + return _dispatcher?.Dispatch(targetMethod, args); + } + } +} diff --git a/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs b/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs index 3c00671..7c73920 100644 --- a/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs +++ b/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs @@ -1,23 +1,23 @@ -using System; -using System.ComponentModel; -using System.Reflection; - -namespace Byteology.TypedHttpClients -{ - /// - /// Provides a functionality for handling method calls. - /// - [Browsable(false)] // This interface shouldn't be used externaly but needs to be seen by calling assemblies so we just hide it from intellisense. - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Meant for internal use only.")] - public interface IDispatchHandler - { - /// - /// Dispatches a specified method call. - /// - /// The invoked method. - /// The arguments with which the method was invoked. - /// The result of the method execution. - object Dispatch(MethodInfo targetMethod, object[] args); - } -} +using System; +using System.ComponentModel; +using System.Reflection; + +namespace Byteology.TypedHttpClients +{ + /// + /// Provides a functionality for handling method calls. + /// + [Browsable(false)] // This interface shouldn't be used externally but needs to be seen by calling assemblies so we just hide it from intellisense. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Meant for internal use only.")] + public interface IDispatchHandler + { + /// + /// Dispatches a specified method call. + /// + /// The invoked method. + /// The arguments with which the method was invoked. + /// The result of the method execution. + object? Dispatch(MethodInfo? targetMethod, object?[]? args); + } +} diff --git a/Byteology.TypedHttpClients/HttpEndpointAttribute.cs b/Byteology.TypedHttpClients/HttpEndpointAttribute.cs index 8973868..dc83ced 100644 --- a/Byteology.TypedHttpClients/HttpEndpointAttribute.cs +++ b/Byteology.TypedHttpClients/HttpEndpointAttribute.cs @@ -1,51 +1,51 @@ -using Byteology.GuardClauses; -using System; - -namespace Byteology.TypedHttpClients -{ - /// - /// Used to mark a method's signature as a description of an HTTP endpoint. - /// See for more information. - /// - /// - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] - public class HttpEndpointAttribute : Attribute - { - /// - /// Gets the endpoint route. May contain parameter names surrounded by curly brackets - /// which will be replaced by the passed arguments. - /// - public string RouteTemplate { get; } - - /// - /// Gets the HTTP verb of the endpoint. - /// - public string Verb { get; } - - /// - /// Gets or sets tags for the endpoint. These tags will be - /// passed to all methods connected to building HTTP requests and parsing their responses - /// and can be used to distinguish the endpoints which must be treated differently. - /// - public string[] Tags { get; set; } - - /// - /// Initializes a new instance of the class. - /// It is used to mark a method's signature as a description of an HTTP endpoint. - /// See for more information. - /// - /// The HTTP verb of the endpoint. - /// The endpoint route. - /// May contain parameter names surrounded by curly brackets which will be replaced by the passed argument. - /// - /// - public HttpEndpointAttribute(string verb, string routeTemplate) - { - Guard.Argument(verb, nameof(verb)).NotNullOrWhiteSpace(); - Guard.Argument(routeTemplate, nameof(routeTemplate)).NotNull(); - - Verb = verb; - RouteTemplate = routeTemplate; - } - } -} +using Byteology.GuardClauses; +using System; + +namespace Byteology.TypedHttpClients +{ + /// + /// Used to mark a method's signature as a description of an HTTP endpoint. + /// See for more information. + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class HttpEndpointAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// It is used to mark a method's signature as a description of an HTTP endpoint. + /// See for more information. + /// + /// The HTTP verb of the endpoint. + /// The endpoint route. + /// May contain parameter names surrounded by curly brackets which will be replaced by the passed argument. + /// + /// + public HttpEndpointAttribute(string verb, string routeTemplate) + { + Guard.Argument(verb, nameof(verb)).NotNullOrWhiteSpace(); + Guard.Argument(routeTemplate, nameof(routeTemplate)).NotNull(); + + Verb = verb; + RouteTemplate = routeTemplate; + } + + /// + /// Gets the endpoint route. May contain parameter names surrounded by curly brackets + /// which will be replaced by the passed arguments. + /// + public string RouteTemplate { get; } + + /// + /// Gets the HTTP verb of the endpoint. + /// + public string Verb { get; } + + /// + /// Gets or sets tags for the endpoint. These tags will be + /// passed to all methods connected to building HTTP requests and parsing their responses + /// and can be used to distinguish the endpoints which must be treated differently. + /// + public string[]? Tags { get; set; } + } +} diff --git a/Byteology.TypedHttpClients/HttpUriParameter.cs b/Byteology.TypedHttpClients/HttpUriParameter.cs index 9741a49..ab6a9b3 100644 --- a/Byteology.TypedHttpClients/HttpUriParameter.cs +++ b/Byteology.TypedHttpClients/HttpUriParameter.cs @@ -1,28 +1,29 @@ -namespace Byteology.TypedHttpClients -{ - /// - /// Represents a URI parameter. - /// - public class HttpUriParameter - { - /// - /// Gets the name of the parameter. - /// - public string Name { get; } - /// - /// Gets the value of the parameter. - /// - public object Value { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The name of the parameter. - /// The value of the parameter. - public HttpUriParameter(string name, object value) - { - Name = name; - Value = value; - } - } -} +namespace Byteology.TypedHttpClients +{ + /// + /// Represents a URI parameter. + /// + public class HttpUriParameter + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the parameter. + /// The value of the parameter. + public HttpUriParameter(string name, object? value) + { + Name = name; + Value = value; + } + + /// + /// Gets the name of the parameter. + /// + public string Name { get; } + + /// + /// Gets the value of the parameter. + /// + public object? Value { get; } + } +} diff --git a/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs b/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs index 6b13b59..f2ed1f3 100644 --- a/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs +++ b/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs @@ -1,48 +1,48 @@ -using Byteology.GuardClauses; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Net.Http; - -namespace Byteology.TypedHttpClients -{ - /// - /// Contains extensions methods for injecting instances. - /// - public static class ServiceCollectionExtensions - { - /// - public static IServiceCollection AddTypedHttpClient(this IServiceCollection services) - where TClient : TypedHttpClient - where TServiceContract : class - { - return AddTypedHttpClient(services, x => { }); - } - - /// - /// Configures a binding between and using an - /// underlying named . The name will be set to the - /// type name of . - /// - /// - /// The type of the to use. - /// The . - /// A delegate that is used to configure an . - /// A reference to this instance after the operation has completed. - public static IServiceCollection AddTypedHttpClient( - this IServiceCollection services, - Action configureClient) - where TClient : TypedHttpClient - where TServiceContract : class - { - Guard.Argument(configureClient, nameof(configureClient)).NotNull(); - - if (typeof(TClient).IsAbstract) - throw new ArgumentException(typeof(TClient).Name + " must be a concrete implemntation."); - - services.AddHttpClient(typeof(TServiceContract).FullName, configureClient); - services.AddScoped(sp => sp.GetRequiredService().Endpoints); - - return services; - } - } -} +using Byteology.GuardClauses; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Net.Http; + +namespace Byteology.TypedHttpClients +{ + /// + /// Contains extensions methods for injecting instances. + /// + public static class ServiceCollectionExtensions + { + /// + public static IServiceCollection AddTypedHttpClient(this IServiceCollection services) + where TClient : TypedHttpClient + where TServiceContract : class + { + return AddTypedHttpClient(services, _ => { }); + } + + /// + /// Configures a binding between and using an + /// underlying named . The name will be set to the + /// type name of . + /// + /// + /// The type of the to use. + /// The . + /// A delegate that is used to configure an . + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddTypedHttpClient( + this IServiceCollection services, + Action configureClient) + where TClient : TypedHttpClient + where TServiceContract : class + { + Guard.Argument(configureClient, nameof(configureClient)).NotNull(); + + if (typeof(TClient).IsAbstract) + throw new ArgumentException(typeof(TClient).Name + " must be a concrete implementation."); + + services.AddHttpClient(typeof(TServiceContract).FullName, configureClient); + services.AddScoped(sp => sp.GetRequiredService().Endpoints); + + return services; + } + } +} diff --git a/Icon.png b/Icon.png index b702aaa1c5326a5a949f10ee0ac6413a45ea7ac8..9566f4a5ca5d12b11f98451fdfdbe14c0e623eb6 100644 GIT binary patch literal 5275 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

(EK*8=|qwf&lRXj j|No~Y_rEq7uQO+8wGW=e>b-^jI}GZW&Cx1TuS@?6|8_oa From d89d44e5990c766298e7d5c8e94880971a21862a Mon Sep 17 00:00:00 2001 From: "ts.Igov" Date: Thu, 12 Jan 2023 14:57:49 +0200 Subject: [PATCH 2/2] Fixed some code smells --- .../Clients/TypedHttpClient.cs | 17 ++++++++++------- .../Dispatching/DispatchProxyDelegator.cs | 5 ++++- .../Dispatching/IDispatchHandler.cs | 2 +- .../ServiceCollectionExtensions.cs | 6 +++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs b/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs index aa0d19e..26a6912 100644 --- a/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs +++ b/Byteology.TypedHttpClients/Clients/TypedHttpClient.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net.Http; using System.Reflection; @@ -47,9 +48,11 @@ protected TypedHttpClient(HttpClient httpClient) /// public TServiceContract Endpoints { get; } - object? IDispatchHandler.Dispatch(MethodInfo? targetMethod, object?[]? args) + [SuppressMessage("Major Code Smell", + "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields")] + object? IDispatchHandler.Dispatch(MethodInfo targetMethod, object?[]? args) { - if (targetMethod?.GetCustomAttribute(true) == null) + if (targetMethod.GetCustomAttribute(true) == null) throw new InvalidOperationException($"The method should be decorated with the {typeof(HttpEndpointAttribute)}."); @@ -80,7 +83,7 @@ protected TypedHttpClient(HttpClient httpClient) /// Builds a query string. /// /// The query parameters. - /// The tags of the request. These are provided by the + /// The tags of the request. These are provided by the /// in order for this method to be able to recognize requests that require special treatment. protected virtual string BuildQueryString(IEnumerable queryParameters, string[]? tags) { @@ -108,7 +111,7 @@ protected virtual string BuildQueryString(IEnumerable queryPar /// The HTTP verb of the request. /// The URI of the request. /// The content body of the request or if no body should be provided. - /// The tags of the request. These are provided by the + /// The tags of the request. These are provided by the /// in order for this method to be able to recognize requests that require special treatment. protected abstract Task BuildRequestAsync(string verb, string uri, object? body, string[]? tags); @@ -118,7 +121,7 @@ protected abstract Task BuildRequestAsync(string verb, strin /// /// The HTTP client to use. /// The request to send. - /// The tags of the request. These are specified by the + /// The tags of the request. These are specified by the /// in order for this method to be able to recognize requests that require special treatment. protected virtual async Task SendRequestAsync( HttpClient httpClient, HttpRequestMessage request, string[]? tags) @@ -135,7 +138,7 @@ protected virtual async Task SendRequestAsync( /// Processes the response of an HTTP request. /// /// The HTTP response. - /// The tags of the request. These are specified by the + /// The tags of the request. These are specified by the /// in order for this method to be able to recognize requests that require special treatment. protected abstract Task ProcessResponse(HttpResponseMessage response, string[]? tags); @@ -144,7 +147,7 @@ protected virtual async Task SendRequestAsync( /// /// The type of the object the response message should be converted to. /// The HTTP response. - /// The tags of the request. These are specified by the + /// The tags of the request. These are specified by the /// in order for this method to be able to recognize requests that require special treatment. protected abstract Task ProcessResponse(HttpResponseMessage response, string[]? tags); diff --git a/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs b/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs index 5abd859..aa4a673 100644 --- a/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs +++ b/Byteology.TypedHttpClients/Dispatching/DispatchProxyDelegator.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Byteology.TypedHttpClients @@ -19,6 +20,8 @@ public class DispatchProxyDelegator : DispatchProxy /// /// The type of the interface to be implemented. /// The object that will handle the method calls. + [SuppressMessage("Major Code Smell", + "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields")] public static TInterface Create(IDispatchHandler handler) where TInterface : class { @@ -43,7 +46,7 @@ private void initialize(IDispatchHandler dispatcher) /// The object to return to the caller, or for void methods. protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { - return _dispatcher?.Dispatch(targetMethod, args); + return _dispatcher!.Dispatch(targetMethod!, args); } } } diff --git a/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs b/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs index 7c73920..89b1b50 100644 --- a/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs +++ b/Byteology.TypedHttpClients/Dispatching/IDispatchHandler.cs @@ -18,6 +18,6 @@ public interface IDispatchHandler /// The invoked method. /// The arguments with which the method was invoked. /// The result of the method execution. - object? Dispatch(MethodInfo? targetMethod, object?[]? args); + object? Dispatch(MethodInfo targetMethod, object?[]? args); } } diff --git a/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs b/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs index f2ed1f3..cfe35f5 100644 --- a/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs +++ b/Byteology.TypedHttpClients/ServiceCollectionExtensions.cs @@ -10,7 +10,7 @@ namespace Byteology.TypedHttpClients /// public static class ServiceCollectionExtensions { - /// + /// public static IServiceCollection AddTypedHttpClient(this IServiceCollection services) where TClient : TypedHttpClient where TServiceContract : class @@ -19,8 +19,8 @@ public static IServiceCollection AddTypedHttpClient(t } ///

- /// Configures a binding between and using an - /// underlying named . The name will be set to the + /// Configures a binding between and using an + /// underlying named . The name will be set to the /// type name of . /// ///