From 174f3429ca34bfa745925effbfa946d11d3c9b72 Mon Sep 17 00:00:00 2001 From: Mehdi Hadeli Date: Tue, 17 Dec 2024 17:16:41 +0100 Subject: [PATCH] =?UTF-8?q?ci:=20=F0=9F=91=B7=20using=20.nuspec=20for=20Te?= =?UTF-8?q?mplate=20Nuget=20package=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #23 --- .config/dotnet-tools.json | 2 +- .github/workflows/build-test.yml | 42 +++++----- .github/workflows/publish.yml | 80 ++++++++++++------- .github/workflows/release-drafter.yml | 8 +- .template.config/template.json | 10 +-- Directory.Build.props | 6 -- Vertical.Slice.Template.sln | 6 +- readme-nuget.md | 24 ------ .../Vertical.Slice.Template.Api/Program.cs | 4 +- .../Vertical.Slice.Template.Api.csproj | 14 ++-- .../Shared/Clients/Users/UsersHttpClient.cs | 70 ++++++++++++++-- ...ApplicationBuilderExtensions.HttpClient.cs | 11 ++- src/Directory.Build.props | 39 +++++++++ src/Directory.Packages.props | 3 + .../DependencyInjectionExtensions.cs | 43 +++++++++- .../ServiceCollectionsExtensions.cs | 6 +- .../Resiliency/Options/HttpClientOptions.cs | 1 - .../Shared/UserHttpClientTests.cs | 13 ++- version.json | 2 +- vertical-slice-template.csproj | 55 ------------- vertical-slice-template.nuspec | 49 ++++++++++++ 21 files changed, 309 insertions(+), 179 deletions(-) delete mode 100644 Directory.Build.props delete mode 100644 readme-nuget.md delete mode 100644 vertical-slice-template.csproj create mode 100644 vertical-slice-template.nuspec diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 874e68c..4a50467 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,7 +10,7 @@ "rollForward": false }, "csharpier": { - "version": "0.29.0", + "version": "0.30.2", "commands": [ "dotnet-csharpier" ], diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 874512b..6dd020e 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -19,7 +19,7 @@ jobs: name: Pre-Checks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Conventional Commits Check uses: amannn/action-semantic-pull-request@v5 @@ -30,23 +30,15 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: "18" - name: Setup .NET Core - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Cache NuGet packages - uses: actions/cache@v3 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} - restore-keys: | - nuget-${{ runner.os }}- - # npm install, runs `prepare` script automatically in the initialize step - name: Install NPM Dependencies run: npm install @@ -64,27 +56,39 @@ jobs: runs-on: ubuntu-latest needs: pre-checks steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + with: + # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/cloudbuild.md#github-actions + fetch-depth: 0 # doing deep clone and avoid shallow clone so nbgv can do its work. - name: Setup .NET Core - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Cache NuGet packages - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} - restore-keys: | - nuget-${{ runner.os }}- + key: nuget-cache-${{ runner.os }}-${{ env.DOTNET_VERSION }}-build-test + + # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/nbgv-cli.md + - name: Install Nerdbank.GitVersioning + run: dotnet tool install -g nbgv + + - name: Get PackageVersion + id: get_version + run: | + nugetVersion=$(nbgv get-version | grep "NuGetPackageVersion" | awk -F': ' '{print $2}' | xargs) + echo "NuGetPackageVersion: $nugetVersion" + echo "::set-output name=nuget_version::$nugetVersion" - name: Restore dependencies run: dotnet restore Vertical.Slice.Template.sln - - name: Build Version + - name: Build Version ${{ steps.get_version.outputs.nuget_version }} run: dotnet build Vertical.Slice.Template.sln -c Release --no-restore - - name: Test Version + - name: Test Version ${{ steps.get_version.outputs.nuget_version }} run: | dotnet test Vertical.Slice.Template.sln -c Release --no-restore --no-build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 83cab65..947821e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,12 +9,16 @@ on: - v* # for publish package after each release to nuget branches: - main # for publish package and each commit to github + paths-ignore: + - "tests/**" + env: FEED_SOURCE: https://api.nuget.org/v3/index.json GHC_SOURCE: ${{ vars.GHC_SOURCE }} FEED_API_KEY: ${{ secrets.FEED_API_KEY }} GHC_API_KEY: ${{ secrets.GHC_TOKEN }} NuGetDirectory: ${{ github.workspace}}/nuget + DOTNET_VERSION: "8.0.*" jobs: # https://www.meziantou.net/publishing-a-nuget-package-following-best-practices-using-github.htm @@ -22,42 +26,54 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/cloudbuild.md#github-actions fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Cache NuGet Packages - uses: actions/cache@v3 + - uses: nuget/setup-nuget@v2 + name: Setup NuGet + with: + nuget-version: '6.x' + + - name: Cache NuGet packages + uses: actions/cache@v4 with: - key: vertical-template-nuget path: ~/.nuget/packages - - # https://github.com/joseftw/jos.enumeration/blob/main/.github/workflows/verify.yml - # https://github.com/dotnet/Nerdbank.GitVersioning - - uses: dotnet/nbgv@v0.4.2 - id: nbgv + key: nuget-cache-${{ runner.os }}-${{ env.DOTNET_VERSION }}-publish + + # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/nbgv-cli.md + - name: Install Nerdbank.GitVersioning + run: dotnet tool install -g nbgv + + - name: Get NuGetPackageVersion + id: get_version + run: | + nugetVersion=$(nbgv get-version | grep "NuGetPackageVersion" | awk -F': ' '{print $2}' | xargs) + echo "NuGetPackageVersion: $nugetVersion" + echo "::set-output name=nuget_version::$nugetVersion" - name: Restore dependencies run: dotnet restore Vertical.Slice.Template.sln - - name: Build Version ${{ steps.nbgv.outputs.SemVer2 }} + - name: Build Version ${{ steps.get_version.outputs.nuget_version }} run: dotnet build Vertical.Slice.Template.sln -c Release --no-restore # https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-pack - - name: Pack NuGet Package Version ${{ steps.nbgv.outputs.SemVer2 }} - run: dotnet pack vertical-slice-template.csproj -c Release -o ${{ env.NuGetDirectory }} + - name: Pack NuGet Package Version ${{ steps.get_version.outputs.nuget_version }} + run: nuget pack vertical-slice-template.nuspec -OutputDirectory ${{ env.NuGetDirectory }} -Properties "version=${{ steps.get_version.outputs.nuget_version }}" -NoDefaultExcludes -c Release --no-restore --no-build - # Publish the NuGet package as an artifact, so they can be used in the following jobs - - uses: actions/upload-artifact@v4 + # Publish the NuGet package as an artifact, so they can be used in the following jobs + - name: Upload Package Version ${{ steps.get_version.outputs.nuget_version }} + uses: actions/upload-artifact@v4 with: name: nuget if-no-files-found: error - retention-days: 7 + retention-days: 1 path: ${{ env.NuGetDirectory }}/*.nupkg deploy-nuget: @@ -68,13 +84,15 @@ jobs: # You can update this logic if you want to manage releases differently needs: [create-nuget] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/cloudbuild.md#github-actions fetch-depth: 0 # avoid shallow clone so nbgv can do its work. - # Download the NuGet package created in the previous job + # .nupkg should be in the same folder that we have `.template.config`, so we should put it in the root of source directory - - uses: actions/download-artifact@v4 + # Download the NuGet package created in the previous job and copy in the root + - name: Download Nuget + uses: actions/download-artifact@v4 with: name: nuget ## Optional. Default is $GITHUB_WORKSPACE @@ -82,17 +100,23 @@ jobs: # Install the .NET SDK indicated in the global.json file - name: Setup .NET Core - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: "8.0.x" + dotnet-version: ${{ env.DOTNET_VERSION }} + + # https://github.com/dotnet/Nerdbank.GitVersioning/blob/main/doc/nbgv-cli.md + - name: Install Nerdbank.GitVersioning + run: dotnet tool install -g nbgv - # https://github.com/joseftw/jos.enumeration/blob/main/.github/workflows/verify.yml - # https://github.com/dotnet/Nerdbank.GitVersioning - - uses: dotnet/nbgv@v0.4.2 - id: nbgv + - name: Get NuGetPackageVersion + id: get_version + run: | + nugetVersion=$(nbgv get-version | grep "NuGetPackageVersion" | awk -F': ' '{print $2}' | xargs) + echo "NuGetPackageVersion: $nugetVersion" + echo "::set-output name=nuget_version::$nugetVersion" # for publish package to github for each commit - - name: Publish NuGet Package Version ${{ steps.nbgv.outputs.SemVer2 }} to GitHub + - name: Publish NuGet Package Version ${{ steps.get_version.outputs.nuget_version }} to GitHub run: dotnet nuget push *.nupkg --skip-duplicate --api-key ${{ env.GHC_API_KEY }} --source ${{ env.GHC_SOURCE }} if: github.event_name == 'push' && (startswith(github.ref, 'refs/heads') || startswith(github.ref, 'refs/tags')) @@ -100,6 +124,6 @@ jobs: # Use --skip-duplicate to prevent errors if a package with the same version already exists. # If you retry a failed workflow, already published packages will be skipped without error. # https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-nuget-push - - name: Publish NuGet Package Version ${{ steps.nbgv.outputs.SemVer2 }} to Nuget + - name: Publish NuGet Package Version ${{ steps.get_version.outputs.nuget_version }} to Nuget run: dotnet nuget push *.nupkg --skip-duplicate --source ${{ env.FEED_SOURCE }} --api-key ${{ env.FEED_API_KEY }} if: github.event_name == 'push' && startswith(github.ref, 'refs/tags') diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index b5bce64..8ea4b13 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -22,11 +22,5 @@ jobs: with: config-name: release-drafter.yml disable-autolabeler: true - ## Default versioning just increase the path version as default. but the can use minor, patch and breaking-changes labels to apply semver - # version: 1.29.1 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # - name: Do something when a new release published - # run: | - # echo ${{ $RESOLVED_VERSION steps.semantic.outputs }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.template.config/template.json b/.template.config/template.json index 1a8542d..7aa7f41 100644 --- a/.template.config/template.json +++ b/.template.config/template.json @@ -3,7 +3,7 @@ "$schema": "http://json.schemastore.org/template", "author": "Mehdi Hadeli", "classifications": ["Web", "WebAPI", "C#"], - "name": "Vertical Slice API Template", + "name": "Vertical Slice Template", "identity": "Vertical.Slice.Template", "shortName": "vsa", "sourceName": "Vertical.Slice.Template", @@ -19,13 +19,13 @@ "datatype": "choice", "enableQuotelessLiterals": true, "choices": [ - { - "choice": "net7.0", - "description": "Target net7.0" - }, { "choice": "net8.0", "description": "Target net8.0" + }, + { + "choice": "net9.0", + "description": "Target net9.0" } ], "replaces": "{TargetFramework}", diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 1e80a4a..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/Vertical.Slice.Template.sln b/Vertical.Slice.Template.sln index 1a58d87..c29c4f2 100644 --- a/Vertical.Slice.Template.sln +++ b/Vertical.Slice.Template.sln @@ -35,8 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution-items", "solution- package.json = package.json readme.md = readme.md version.json = version.json - vertical-slice-template.csproj = vertical-slice-template.csproj - Directory.Build.props = Directory.Build.props + vertical-slice-template.nuspec = vertical-slice-template.nuspec EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".config", ".config", "{154A55C1-CE45-463D-B411-816C702D6A25}" @@ -51,6 +50,9 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{AEFEEF63-5831-49E5-A7D9-892AF653C32D}" ProjectSection(SolutionItems) = preProject .github\release-drafter.yml = .github\release-drafter.yml + .github\labeler.yml = .github\labeler.yml + .github\multi-labeler.yml = .github\multi-labeler.yml + .github\release.yml = .github\release.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ED6C6F59-8A39-4D7D-BB93-888AA486AFE9}" diff --git a/readme-nuget.md b/readme-nuget.md deleted file mode 100644 index aee6f0a..0000000 --- a/readme-nuget.md +++ /dev/null @@ -1,24 +0,0 @@ -# Vertical Slice API Template -[![NuGet](https://img.shields.io/nuget/v/Vertical.Slice.Template?style=flat-square)](https://www.nuget.org/packages/Vertical.Slice.Template) -[![CI-CD](https://img.shields.io/github/actions/workflow/status/mehdihadeli/vertical-slice-api-template/ci-cd.yml?style=flat-square)](https://github.com/mehdihadeli/vertical-slice-api-template/actions/workflows/ci-cd.yml) -[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg?&style=flat-square)](http://commitizen.github.io/cz-cli/) - -> This is a An `asp.net core template` based on `Vertical Slice Architecture`, CQRS, Minimal APIs, API Versioning and Swagger. Create a new project based on this template by clicking the above **Use this template** button or by installing and running the associated NuGet package (see Getting Started for full details). - -## Getting Started & Prerequisites -1. This application uses `Https` for hosting apis, to setup a valid certificate on your machine, you can create a [Self-Signed Certificate](https://learn.microsoft.com/en-us/aspnet/core/security/docker-https?view=aspnetcore-7.0#macos-or-linux), see more about enforce certificate [here](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl). -2. Install git - [https://git-scm.com/downloads](https://git-scm.com/downloads). -3. Install .NET Core 7.0 - [https://dotnet.microsoft.com/download/dotnet/7.0](https://dotnet.microsoft.com/download/dotnet/7.0). -4. Install Visual Studio, Rider or VSCode. -5. Run `dotnet new install Vertical.Slice.Template` to install the project templates. -6. Now with running `dotnet new --list`, we should see `Vertical.Slice.Template` in the template list. -7. Create a folder for your solution and cd into it (the template will use it as project name) -8. Run `dotnet new vsa` for short name or `dotnet new Vertical.Slice.Template -n ` to create a new project template. -9. Open [.sln](./Vertical.Slice.Template.sln) solution, make sure that's compiling. -9. Navigate to `src/App/.Api` and run `dotnet run` to launch the back end (ASP.NET Core Web API) -10. Open web browser https://localhost:5158/swagger Swagger UI - -For install package locally you can use this command in the root of your cloned responsitory: -``` bash -dotnet new install . -``` diff --git a/src/App/Vertical.Slice.Template.Api/Program.cs b/src/App/Vertical.Slice.Template.Api/Program.cs index a135af8..250abd2 100644 --- a/src/App/Vertical.Slice.Template.Api/Program.cs +++ b/src/App/Vertical.Slice.Template.Api/Program.cs @@ -92,9 +92,9 @@ app.UseCustomSwagger(); // https://github.com/scalar/scalar/blob/main/packages/scalar.aspnetcore/README.md - app.MapScalarApiReference(x => + app.MapScalarApiReference(redocOptions => { - x.OpenApiRoutePattern = "/swagger/v1/swagger.json"; + redocOptions.WithOpenApiRoutePattern("/swagger/{documentName}/swagger.json"); }); } diff --git a/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj b/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj index 70f267a..094688e 100644 --- a/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj +++ b/src/App/Vertical.Slice.Template.Api/Vertical.Slice.Template.Api.csproj @@ -1,11 +1,11 @@ - - - + + + + + + + - - - - diff --git a/src/App/Vertical.Slice.Template/Shared/Clients/Users/UsersHttpClient.cs b/src/App/Vertical.Slice.Template/Shared/Clients/Users/UsersHttpClient.cs index e57a48f..c34d942 100644 --- a/src/App/Vertical.Slice.Template/Shared/Clients/Users/UsersHttpClient.cs +++ b/src/App/Vertical.Slice.Template/Shared/Clients/Users/UsersHttpClient.cs @@ -2,7 +2,11 @@ using System.Net.Http.Json; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Options; +using Polly; +using Polly.Timeout; +using Polly.Wrap; using Shared.Core.Paging; +using Shared.Resiliency.Options; using Shared.Web.Extensions; using Vertical.Slice.Template.Shared.Clients.Users.Dtos; using Vertical.Slice.Template.Users; @@ -10,10 +14,49 @@ namespace Vertical.Slice.Template.Shared.Clients.Users; -public class UsersHttpClient(HttpClient httpClient, IOptions userHttpClientOptions) - : IUsersHttpClient +public class UsersHttpClient : IUsersHttpClient { - private readonly UsersHttpClientOptions _userHttpClientOptions = userHttpClientOptions.Value; + private readonly HttpClient _client; + private readonly UsersHttpClientOptions _userHttpClientOptions; + private readonly AsyncPolicyWrap _combinedPolicy; + + public UsersHttpClient( + HttpClient client, + IOptions userHttpClientOptions, + IOptions policyOptions + ) + { + _client = client; + _userHttpClientOptions = userHttpClientOptions.Value; + var policyOptionsValue = policyOptions.Value; + + var retryPolicy = Policy + .Handle() + .OrResult(r => !r.IsSuccessStatusCode) + .RetryAsync(policyOptionsValue.RetryPolicyOptions.Count); + + // HttpClient itself will still enforce its own timeout, which is 100 seconds by default. To fix this issue, you need to set the HttpClient.Timeout property to match or exceed the timeout configured in Polly's policy. + var timeoutPolicy = Policy.TimeoutAsync( + policyOptionsValue.TimeoutPolicyOptions.TimeoutInSeconds, + TimeoutStrategy.Pessimistic + ); + + // at any given time there will 3 parallel requests execution for specific service call and another 6 requests for other services can be in the queue. So that if the response from customer service is delayed or blocked then we don’t use too many resources + var bulkheadPolicy = Policy.BulkheadAsync(3, 6); + + // https://github.com/App-vNext/Polly#handing-return-values-and-policytresult + var circuitBreakerPolicy = Policy + .Handle() + .OrResult(r => !r.IsSuccessStatusCode) + .CircuitBreakerAsync( + policyOptionsValue.RetryPolicyOptions.Count + 1, + TimeSpan.FromSeconds(policyOptionsValue.CircuitBreakerPolicyOptions.DurationOfBreak) + ); + + var combinedPolicy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy, bulkheadPolicy); + + _combinedPolicy = combinedPolicy.WrapAsync(timeoutPolicy); + } public async Task> GetAllUsersAsync( PageRequest pageRequest, @@ -28,10 +71,23 @@ public async Task> GetAllUsersAsync( }; // https://github.com/App-vNext/Polly#handing-return-values-and-policytresult - var httpResponse = await httpClient.GetAsync( - $"{_userHttpClientOptions.UsersEndpoint}?{qb.ToQueryString().Value}", - cancellationToken - ); + var httpResponse = await _combinedPolicy.ExecuteAsync(async () => + { + // https://ollama.com/blog/openai-compatibility + // https://www.youtube.com/watch?v=38jlvmBdBrU + // https://platform.openai.com/docs/api-reference/chat/create + // https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-chat-completion + var response = await _client.GetAsync( + $"{ + _userHttpClientOptions.UsersEndpoint + }?{ + qb.ToQueryString().Value + }", + cancellationToken + ); + + return response; + }); // https://stackoverflow.com/questions/21097730/usage-of-ensuresuccessstatuscode-and-handling-of-httprequestexception-it-throws // throw HttpResponseException instead of HttpRequestException (because we want detail response exception) with corresponding status code diff --git a/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.HttpClient.cs b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.HttpClient.cs index 08f3979..43937c5 100644 --- a/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.HttpClient.cs +++ b/src/App/Vertical.Slice.Template/Shared/Extensions/WebApplicationBuilderExtensions/WebApplicationBuilderExtensions.HttpClient.cs @@ -32,15 +32,14 @@ private static void AddCatalogsApiClient(this WebApplicationBuilder builder) { builder.Services.AddValidatedOptions(); builder.Services.AddHttpClient( - (client, sp) => + (sp, client) => { - var catalogApiOptions = sp.GetRequiredService>(); - var policyOptions = sp.GetRequiredService>(); - catalogApiOptions.Value.NotBeNull(); + var catalogApiOptions = sp.GetRequiredService>().Value.NotBeNull(); + var policyOptions = sp.GetRequiredService>().Value.NotBeNull(); - var baseAddress = catalogApiOptions.Value.BaseAddress; + var baseAddress = catalogApiOptions.BaseAddress; + client.Timeout = TimeSpan.FromSeconds(policyOptions.TimeoutPolicyOptions.TimeoutInSeconds); client.BaseAddress = new Uri(baseAddress); - return new CatalogsApiClient(client); } ); diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 04c0a78..f90a468 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -41,6 +41,45 @@ TRACE;$(DefineConstants) + + + + + + + + + + + + + Template + $(AssemblyName) + Vertical.Slice.Template + Vertical Slice Template + Mehdi Hadeli + An asp.net core template based on Vertical Slice Architecture, CQRS, Minimal APIs, API Versioning and Swagger. + dotnet dotnet-core templates csharp vertical-slices vertical-slices-architecture clean-architecture cqrs minimal-api + $(SolutionDir)nugets + readme.md + icon.png + MIT + https://github.com/mehdihadeli/vertical-slice-api-template + https://github.com/mehdihadeli/vertical-slice-api-template + git + true + false + content + main + true + true + Copyright (c) 2024 Mehdi Hadeli + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 3a2f05b..b66bbf2 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -207,4 +207,7 @@ + + + \ No newline at end of file diff --git a/src/Shared/Core/Extensions/DependencyInjectionExtensions.cs b/src/Shared/Core/Extensions/DependencyInjectionExtensions.cs index 13b23c0..fa4892b 100644 --- a/src/Shared/Core/Extensions/DependencyInjectionExtensions.cs +++ b/src/Shared/Core/Extensions/DependencyInjectionExtensions.cs @@ -1,10 +1,15 @@ using System.Reflection; +using Microsoft.Extensions.Options; using Polly; +using Polly.Timeout; +using Polly.Wrap; using Shared.Abstractions.Core.Domain.Events; using Shared.Core.Domain.Events; +using Shared.Core.Extensions.ServiceCollectionsExtensions; using Shared.Core.Paging; using Shared.Core.Persistence.Extensions; using Shared.Core.Reflection; +using Shared.Resiliency.Options; using Sieve.Services; namespace Shared.Core.Extensions; @@ -27,8 +32,42 @@ public static IServiceCollection AddCore(this IServiceCollection services, param services.AddPersistenceCore(assemblies); - var policy = Policy.Handle().RetryAsync(2); - services.AddSingleton(policy); + services.AddValidatedOptions(nameof(PolicyOptions)); + + // `AsyncPolicyWrap` can be injected in clients and can be reused. + services.AddSingleton>(sp => + { + var policyOptions = sp.GetRequiredService>().Value.NotBeNull(); + + var retryPolicy = Policy + .Handle() + .OrResult(r => !r.IsSuccessStatusCode) + .RetryAsync(policyOptions.RetryPolicyOptions.Count); + + // HttpClient itself will still enforce its own timeout, which is 100 seconds by default. To fix this issue, you need to set the HttpClient.Timeout property to match or exceed the timeout configured in Polly's policy. + var timeoutPolicy = Policy.TimeoutAsync( + policyOptions.TimeoutPolicyOptions.TimeoutInSeconds, + TimeoutStrategy.Pessimistic + ); + + // at any given time there will 3 parallel requests execution for specific service call and another 6 requests for other services can be in the queue. So that if the response from customer service is delayed or blocked then we don’t use too many resources + var bulkheadPolicy = Policy.BulkheadAsync(3, 6); + + // https://github.com/App-vNext/Polly#handing-return-values-and-policytresult + var circuitBreakerPolicy = Policy + .Handle() + .OrResult(r => !r.IsSuccessStatusCode) + .CircuitBreakerAsync( + policyOptions.RetryPolicyOptions.Count + 1, + TimeSpan.FromSeconds(policyOptions.CircuitBreakerPolicyOptions.DurationOfBreak) + ); + + var combinedPolicy = Policy.WrapAsync(retryPolicy, circuitBreakerPolicy, bulkheadPolicy); + + var finalPolicy = combinedPolicy.WrapAsync(timeoutPolicy); + + return finalPolicy; + }); return services; } diff --git a/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs b/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs index ff67162..90fed8c 100644 --- a/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs +++ b/src/Shared/Resiliency/Extensions/ServiceCollectionsExtensions.cs @@ -121,8 +121,9 @@ public static IServiceCollection AddCustomHttpClient { var httpClientOptions = serviceProvider.GetRequiredService>().Value; + var policyOptions = serviceProvider.GetRequiredService>().Value; httpClient.BaseAddress = new Uri(httpClientOptions.BaseAddress); - httpClient.Timeout = TimeSpan.FromSeconds(httpClientOptions.Timeout); + httpClient.Timeout = TimeSpan.FromSeconds(policyOptions.TimeoutPolicyOptions.TimeoutInSeconds); configureClient?.Invoke(serviceProvider, httpClient); } @@ -174,8 +175,9 @@ public static IServiceCollection AddCustomHttpClient( (sp, httpClient) => { var httpClientOptions = sp.GetRequiredService>().Value; + var policyOptions = sp.GetRequiredService>().Value; httpClient.BaseAddress = new Uri(httpClientOptions.BaseAddress); - httpClient.Timeout = TimeSpan.FromSeconds(httpClientOptions.Timeout); + httpClient.Timeout = TimeSpan.FromSeconds(policyOptions.TimeoutPolicyOptions.TimeoutInSeconds); configureClient?.Invoke(sp, httpClient); } diff --git a/src/Shared/Resiliency/Options/HttpClientOptions.cs b/src/Shared/Resiliency/Options/HttpClientOptions.cs index 6ab7d9d..c850b08 100644 --- a/src/Shared/Resiliency/Options/HttpClientOptions.cs +++ b/src/Shared/Resiliency/Options/HttpClientOptions.cs @@ -3,5 +3,4 @@ namespace Shared.Resiliency.Options; public class HttpClientOptions { public virtual string BaseAddress { get; set; } = default!; - public virtual int Timeout { get; set; } = 60; } diff --git a/tests/Vertical.Slice.Template.UnitTests/Shared/UserHttpClientTests.cs b/tests/Vertical.Slice.Template.UnitTests/Shared/UserHttpClientTests.cs index 38ae557..1c40cf5 100644 --- a/tests/Vertical.Slice.Template.UnitTests/Shared/UserHttpClientTests.cs +++ b/tests/Vertical.Slice.Template.UnitTests/Shared/UserHttpClientTests.cs @@ -7,6 +7,7 @@ using RichardSzalay.MockHttp; using Shared.Core.Exceptions; using Shared.Core.Paging; +using Shared.Resiliency.Options; using Vertical.Slice.Template.Shared.Clients.Users; using Vertical.Slice.Template.Shared.Clients.Users.Dtos; using Vertical.Slice.Template.Users; @@ -25,6 +26,7 @@ public async Task get_all_users_should_call_http_client_with_valid_parameters_on var total = 20; var options = Substitute.For>(); + var policyOptions = Options.Create(new PolicyOptions()); var usersHttpClientOptions = new UsersHttpClientOptions { UsersEndpoint = "users", @@ -56,7 +58,7 @@ public async Task get_all_users_should_call_http_client_with_valid_parameters_on var pageRequest = new PageRequest { PageNumber = page, PageSize = pageSize }; - var usersHttpClient = new UsersHttpClient(client, options); + var usersHttpClient = new UsersHttpClient(client, options, policyOptions); // Act await usersHttpClient.GetAllUsersAsync(pageRequest); @@ -74,6 +76,7 @@ public async Task get_all_users_should_return_users_list() var total = 20; var options = Substitute.For>(); + var policyOptions = Options.Create(new PolicyOptions()); var usersHttpClientOptions = new UsersHttpClientOptions { UsersEndpoint = "users", @@ -109,7 +112,7 @@ public async Task get_all_users_should_return_users_list() var expectedPageList = new PageList(users.ToList(), page, pageSize, total); - var usersHttpClient = new UsersHttpClient(client, options); + var usersHttpClient = new UsersHttpClient(client, options, policyOptions); // Act var result = await usersHttpClient.GetAllUsersAsync(pageRequest); @@ -126,6 +129,7 @@ public async Task get_all_users_with_http_response_exception_should_throw_http_r var page = 1; var options = Substitute.For>(); + var policyOptions = Options.Create(new PolicyOptions()); var usersHttpClientOptions = new UsersHttpClientOptions { UsersEndpoint = "users", @@ -146,7 +150,7 @@ public async Task get_all_users_with_http_response_exception_should_throw_http_r var pageRequest = new PageRequest { PageNumber = page, PageSize = pageSize }; - var usersHttpClient = new UsersHttpClient(client, options); + var usersHttpClient = new UsersHttpClient(client, options, policyOptions); // Act Func act = () => usersHttpClient.GetAllUsersAsync(pageRequest); @@ -163,6 +167,7 @@ public async Task get_all_users_with_exception_should_throw_exception() var page = 1; var options = Substitute.For>(); + var policyOptions = Options.Create(new PolicyOptions()); var usersHttpClientOptions = new UsersHttpClientOptions { UsersEndpoint = "users", @@ -184,7 +189,7 @@ public async Task get_all_users_with_exception_should_throw_exception() var pageRequest = new PageRequest { PageNumber = page, PageSize = pageSize }; - var usersHttpClient = new UsersHttpClient(client, options); + var usersHttpClient = new UsersHttpClient(client, options, policyOptions); // Act Func act = () => usersHttpClient.GetAllUsersAsync(pageRequest); diff --git a/version.json b/version.json index 1e03bcd..5ed1010 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.3.5", + "version": "1.3.6-preview", "gitCommitIdShortAutoMinimum": 7, "nugetPackageVersion": { "semVer": 2 diff --git a/vertical-slice-template.csproj b/vertical-slice-template.csproj deleted file mode 100644 index cf5f830..0000000 --- a/vertical-slice-template.csproj +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - Template - Vertical.Slice.Template - Vertical Slice API Template - Mehdi Hadeli - An asp.net core template based on Vertical Slice Architecture, CQRS, Minimal APIs, API Versioning and Swagger. - dotnet;dotnet-core;templates;csharp;vertical-slices;vertical-slices-architecture;clean-architecture;cqrs;minimal-api - true - false - content - readme-nuget.md - MIT - https://github.com/mehdihadeli/vertical-slice-api-template - https://github.com/mehdihadeli/vertical-slice-api-template - git - main - true - icon.png - true - - - - net8.0 - - - - - - - - - - - - - - - - - - - diff --git a/vertical-slice-template.nuspec b/vertical-slice-template.nuspec new file mode 100644 index 0000000..aa09787 --- /dev/null +++ b/vertical-slice-template.nuspec @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + Vertical.Slice.Template + $version$ + Vertical Slice Template + Mehdi Hadeli + Mehdi Hadeli + An asp.net core template based on Vertical Slice Architecture, CQRS, Minimal APIs, API Versioning + and Swagger. + + An ASP.NET Core template. + dotnet dotnet-core templates csharp vertical-slices vertical-slices-architecture clean-architecture cqrs + minimal-api + + MIT + https://github.com/mehdihadeli/vertical-slice-api-template + icon.png + readme.md + + false + Copyright (c) 2024 Mehdi Hadeli + + + + + + + + + + + +