From 40eab7ffc3cd1a0c41a0bab39816288deeb92616 Mon Sep 17 00:00:00 2001 From: "codebelt-aicia[bot]" Date: Tue, 26 May 2026 22:22:40 +0000 Subject: [PATCH 1/9] V5.0.8/service update --- .../Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt | 6 ++++++ .nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt | 6 ++++++ .../Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt | 6 ++++++ .nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt | 6 ++++++ CHANGELOG.md | 4 ++++ Directory.Packages.props | 8 ++++---- 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt index 20051f8..bf7fb4a 100644 --- a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 5.0.8 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 5.0.7 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt index 4a47cbb..5dd934b 100644 --- a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 5.0.8 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 5.0.7 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt index 67c7620..171fc19 100644 --- a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 5.0.8 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 5.0.7 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt index 1d99889..66827b3 100644 --- a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 5.0.8 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 5.0.7 Availability: .NET 10 and .NET 9 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4565c7d..248af80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. +## [5.0.8] - 2026-05-26 + +This is a service update that focuses on package dependencies. + ## [5.0.7] - 2026-04-18 This is a service update that focuses on package dependencies. diff --git a/Directory.Packages.props b/Directory.Packages.props index 5b44624..3847281 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,10 +3,10 @@ true - - - - + + + + From 48d12638246f1deab05e0c4b64ac785822befe3c Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Wed, 27 May 2026 23:27:06 +0200 Subject: [PATCH 2/9] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20update=20test=20depend?= =?UTF-8?q?encies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Microsoft.NET.Test.Sdk from 18.4.0 to 18.6.0, coverlet packages from 10.0.0 to 10.0.1, and framework packages (net9 and net10) to latest available versions. --- Directory.Packages.props | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 3847281..0db70ed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,26 +7,26 @@ - + - - + + - - - - - + + + + + - - - - - + + + + + \ No newline at end of file From af891dc76be09923c71c0a99bf15f7efb1763e6d Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Wed, 27 May 2026 23:27:10 +0200 Subject: [PATCH 3/9] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20bump=20docfx=20base=20?= =?UTF-8?q?image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update nginx base image from 1.30.0-alpine to 1.31.0-alpine for documentation publishing. --- .docfx/Dockerfile.docfx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx index ca80886..1719a33 100644 --- a/.docfx/Dockerfile.docfx +++ b/.docfx/Dockerfile.docfx @@ -1,4 +1,4 @@ -ARG NGINX_VERSION=1.30.0-alpine +ARG NGINX_VERSION=1.31.0-alpine FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base RUN rm -rf /usr/share/nginx/html/* From dbd56eff1f89d43f838248477c21f8c58bb26976 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Wed, 27 May 2026 23:27:16 +0200 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=91=B7=20add=20macos=20testing=20to?= =?UTF-8?q?=20CI=20pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable optional macOS test matrix for X64 and ARM64 architectures across Debug and Release configurations. Add test quality gate job to validate all test suites (Linux, Windows, optional macOS) before publishing and security analysis. Decouple test dependencies in sonarcloud, codecov, and codeql jobs. --- .github/workflows/ci-pipeline.yml | 89 ++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 26a3bea..780c9b9 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -12,6 +12,10 @@ on: options: - Debug - Release + run_mac_tests: + type: boolean + description: Run the macOS test matrix despite the additional cost and runtime. + default: false permissions: contents: read @@ -21,6 +25,7 @@ jobs: name: initialize runs-on: ubuntu-24.04 outputs: + run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }} run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }} strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }} build-switches: ${{ steps.vars.outputs.build-switches }} @@ -29,6 +34,12 @@ jobs: name: calculate workflow variables shell: bash run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.run_mac_tests }}" == "true" ]]; then + echo "run-mac-tests=true" >> "$GITHUB_OUTPUT" + else + echo "run-mac-tests=false" >> "$GITHUB_OUTPUT" + fi + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT" echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT" @@ -108,10 +119,74 @@ jobs: build: true download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + test_mac: + if: ${{ needs.init.outputs.run-mac-tests == 'true' }} + name: call-test-mac + needs: [init, build] + strategy: + fail-fast: false + matrix: + arch: [X64, ARM64] + configuration: [Debug, Release] + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }} + projects: test/**/*.csproj + configuration: ${{ matrix.configuration }} + verbosity-level: normal + build-switches: -p:SkipSignAssembly=true + restore: true + build: true + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + test_qualitygate: + if: ${{ always() }} + name: test-qualitygate + needs: [init, test_linux, test_windows, test_mac] + runs-on: ubuntu-24.04 + steps: + - name: Evaluate test results + shell: bash + env: + RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }} + TEST_LINUX_RESULT: ${{ needs.test_linux.result }} + TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }} + TEST_MAC_RESULT: ${{ needs.test_mac.result }} + run: | + require_success() { + local job_name="$1" + local job_result="$2" + + if [[ "$job_result" != "success" ]]; then + echo "::error::$job_name finished with '$job_result'." + exit 1 + fi + } + + require_success_or_skip() { + local job_name="$1" + local job_enabled="$2" + local job_result="$3" + + if [[ "$job_enabled" == "true" ]]; then + require_success "$job_name" "$job_result" + return + fi + + if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then + echo "::error::$job_name finished with '$job_result' while disabled." + exit 1 + fi + } + + require_success "test_linux" "$TEST_LINUX_RESULT" + require_success "test_windows" "$TEST_WINDOWS_RESULT" + require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT" + sonarcloud: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }} name: call-sonarcloud - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: organization: geekle @@ -120,18 +195,18 @@ jobs: secrets: inherit codecov: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }} name: call-codecov - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 with: repository: codebeltnet/bootstrapper secrets: inherit codeql: - if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }} + if: ${{ always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success' }} name: call-codeql - needs: [init, build, test_linux, test_windows] + needs: [init, build, test_qualitygate] uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 permissions: security-events: write @@ -139,7 +214,7 @@ jobs: deploy: if: github.event_name != 'pull_request' name: call-nuget - needs: [build, pack, test_linux, test_windows, sonarcloud, codecov, codeql] + needs: [build, pack, test_qualitygate, sonarcloud, codecov, codeql] uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3 with: version: ${{ needs.build.outputs.version }} From 52a1db1abb6bf9dd8723bb04e5249a3264a59d72 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Wed, 27 May 2026 23:27:22 +0200 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=92=AC=20add=20code=20coverage=20poli?= =?UTF-8?q?cy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the prohibition against using ExcludeFromCodeCoverage attribute on any code path. Establish that all executable lines should be covered by tests or be genuinely unreachable, and provide alternative approaches for handling untestable code paths. --- .github/copilot-instructions.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d014a76..4cd6ee6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -180,6 +180,28 @@ Internal classes and methods must be validated by exercising the public API that - Public entry points provide sufficient coverage of internal code paths. - The internal implementation exists solely as a helper or utility for public-facing functionality. +## 10. ExcludeFromCodeCoverage Prohibition + +**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes: + +- Test classes or test methods +- Production code +- Configuration code +- Any other code path + +### Rationale + +- Excluding code from coverage hides gaps and creates false confidence in test completeness. +- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics. +- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed). + +### Alternative Approaches + +- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path. +- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage. +- **Configuration-only code**: Move to configuration files or extract into testable methods. +- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary. + --- description: 'Writing Performance Tests' applyTo: "tuning/**, **/*Benchmark*.cs" From 95e5b9e872b7669eb8564bbbc379bb7df23d881f Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Wed, 27 May 2026 23:27:29 +0200 Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=85=20increase=20test=20coverage=20to?= =?UTF-8?q?=2095=20percent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive test coverage across bootstrapper modules: console, web, and worker application templates. Introduce new unit and functional tests for program creation, startup configuration, hosted service initialization, and lifetime management. Add test assets and factory helpers (NullStartupFactory, TestConsoleProgram, TestWebProgram, TestWorkerProgram) to support test scenarios. Expand ConsoleHostedServiceTest and BootstrapperLifetimeTest with additional test cases. --- .../Assets/NullStartupFactory.cs | 7 +++ .../Assets/TestConsoleProgram.cs | 9 ++++ .../TestGenericMinimalConsoleProgram.cs | 17 +++++++ .../Assets/TestMinimalConsoleProgram.cs | 3 ++ .../ConsoleHostedServiceTest.cs | 48 +++++++++++++++++++ .../ConsoleProgramTest.cs | 30 ++++++++++++ .../ConsoleStartupTest.cs | 44 +++++++++++++++++ .../MinimalConsoleProgramBuilderTest.cs | 42 ++++++++++++++++ .../BootstrapperLifetimeTest.cs | 17 +++++++ .../Assets/TestMinimalWebProgram.cs | 9 ++++ .../Assets/TestWebProgram.cs | 9 ++++ .../MinimalWebProgramBuilderTest.cs | 40 ++++++++++++++++ .../WebProgramTest.cs | 37 ++++++++++++++ .../Assets/TestMinimalWorkerProgram.cs | 9 ++++ .../Assets/TestWorkerProgram.cs | 9 ++++ .../MinimalWorkerProgramBuilderTest.cs | 25 ++++++++++ .../WorkerProgramTest.cs | 27 +++++++++++ 17 files changed, 382 insertions(+) create mode 100644 test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/NullStartupFactory.cs create mode 100644 test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleProgram.cs create mode 100644 test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestGenericMinimalConsoleProgram.cs create mode 100644 test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleProgramTest.cs create mode 100644 test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleStartupTest.cs create mode 100644 test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleProgramBuilderTest.cs create mode 100644 test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestMinimalWebProgram.cs create mode 100644 test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestWebProgram.cs create mode 100644 test/Codebelt.Bootstrapper.Web.FunctionalTests/MinimalWebProgramBuilderTest.cs create mode 100644 test/Codebelt.Bootstrapper.Web.FunctionalTests/WebProgramTest.cs create mode 100644 test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestMinimalWorkerProgram.cs create mode 100644 test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestWorkerProgram.cs create mode 100644 test/Codebelt.Bootstrapper.Worker.FunctionalTests/MinimalWorkerProgramBuilderTest.cs create mode 100644 test/Codebelt.Bootstrapper.Worker.FunctionalTests/WorkerProgramTest.cs diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/NullStartupFactory.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/NullStartupFactory.cs new file mode 100644 index 0000000..d0b2bbb --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/NullStartupFactory.cs @@ -0,0 +1,7 @@ +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class NullStartupFactory : global::Codebelt.Bootstrapper.IStartupFactory where TStartup : global::Codebelt.Bootstrapper.StartupRoot + { + public TStartup Instance => null; + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleProgram.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleProgram.cs new file mode 100644 index 0000000..2c5e19b --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleProgram.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class TestConsoleProgram : ConsoleProgram + { + public static IHostBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args); + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestGenericMinimalConsoleProgram.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestGenericMinimalConsoleProgram.cs new file mode 100644 index 0000000..3d4f000 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestGenericMinimalConsoleProgram.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class TestGenericMinimalConsoleProgram : MinimalConsoleProgram + { + public static HostApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args); + + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs index 34bdcc6..f1b209a 100644 --- a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs @@ -3,11 +3,14 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; namespace Codebelt.Bootstrapper.Console.Assets { public class TestMinimalConsoleProgram : MinimalConsoleProgram { + public static HostApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args); + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) { var logger = serviceProvider.GetRequiredService>(); diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs index 77572d7..2621c32 100644 --- a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs @@ -223,5 +223,53 @@ public async Task StartAsync_ShouldNotLogFatalError_WhenExceptionOccursAndSuppre var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore(); Assert.DoesNotContain(loggerStore.Query(), entry => entry.Message.Contains("Fatal error") && entry.Message.Contains("activating")); } + + [Fact] + public async Task StartAsync_ShouldLogUnableToActivateInstance_WhenStartupIsNullAndSuppressStatusMessagesIsFalse() + { + await using var test = HostTestFactory.Create(services => + { + services.AddXunitTestLogging(TestOutput); + services.Configure(options => + { + options.SuppressStatusMessages = false; + }); + services.AddSingleton>(new NullStartupFactory()); + }, hb => + { + hb.UseBootstrapperLifetime() + .UseConsoleStartup(); + }); + + await test.Host.StartAsync(); + await test.Host.StopAsync(); + + var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore(); + Assert.Contains(loggerStore.Query(), entry => entry.Message.Contains("Unable to activate")); + } + + [Fact] + public async Task StartAsync_ShouldNotLogUnableToActivateInstance_WhenStartupIsNullAndSuppressStatusMessagesIsTrue() + { + await using var test = HostTestFactory.Create(services => + { + services.AddXunitTestLogging(TestOutput); + services.Configure(options => + { + options.SuppressStatusMessages = true; + }); + services.AddSingleton>(new NullStartupFactory()); + }, hb => + { + hb.UseBootstrapperLifetime() + .UseConsoleStartup(); + }); + + await test.Host.StartAsync(); + await test.Host.StopAsync(); + + var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore(); + Assert.DoesNotContain(loggerStore.Query(), entry => entry.Message.Contains("Unable to activate")); + } } } diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleProgramTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleProgramTest.cs new file mode 100644 index 0000000..448690b --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleProgramTest.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Codebelt.Bootstrapper.Console.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Codebelt.Bootstrapper.Console +{ + public class ConsoleProgramTest : Test + { + public ConsoleProgramTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CreateHostBuilder_ShouldRegisterBootstrapperServices() + { + using var host = TestConsoleProgram.CreateHostBuilderAccessor([]).Build(); + + var lifetime = host.Services.GetRequiredService(); + var startupFactory = host.Services.GetRequiredService>(); + var hostedServices = host.Services.GetServices().ToList(); + + Assert.IsType(lifetime); + Assert.IsType>(startupFactory); + Assert.Contains(hostedServices, hostedService => hostedService is ConsoleHostedService); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleStartupTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleStartupTest.cs new file mode 100644 index 0000000..7233e2e --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleStartupTest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; +using Codebelt.Bootstrapper.Console.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; +using Xunit; + +namespace Codebelt.Bootstrapper.Console +{ + public class ConsoleStartupTest : Test + { + public ConsoleStartupTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ConfigureConsole_ShouldAllowDefaultNoOpImplementation() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "TestKey", "TestValue" } + }) + .Build(); + + var environment = new HostingEnvironment + { + EnvironmentName = Environments.Development, + ApplicationName = "TestApp", + ContentRootPath = Directory.GetCurrentDirectory() + }; + + var startup = new TestConsoleStartup(configuration, environment); + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + + var exception = Record.Exception(() => startup.ConfigureConsole(serviceProvider)); + + Assert.Null(exception); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleProgramBuilderTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleProgramBuilderTest.cs new file mode 100644 index 0000000..eb00750 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleProgramBuilderTest.cs @@ -0,0 +1,42 @@ +using System.Linq; +using Codebelt.Bootstrapper.Console.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Codebelt.Bootstrapper.Console +{ + public class MinimalConsoleProgramBuilderTest : Test + { + public MinimalConsoleProgramBuilderTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CreateHostBuilder_ShouldRegisterMinimalConsoleServices() + { + using var host = TestMinimalConsoleProgram.CreateHostBuilderAccessor([]).Build(); + + var lifetime = host.Services.GetRequiredService(); + var programFactory = host.Services.GetRequiredService(); + var hostedServices = host.Services.GetServices().ToList(); + + Assert.IsType(lifetime); + Assert.NotNull(programFactory); + Assert.Contains(hostedServices, hostedService => hostedService is MinimalConsoleHostedService); + } + + [Fact] + public void CreateHostBuilderOfTProgram_ShouldRegisterMinimalConsoleServices() + { + using var host = TestGenericMinimalConsoleProgram.CreateHostBuilderAccessor([]).Build(); + + var programFactory = host.Services.GetRequiredService(); + var hostedServices = host.Services.GetServices().ToList(); + + Assert.NotNull(programFactory); + Assert.Contains(hostedServices, hostedService => hostedService is MinimalConsoleHostedService); + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs index 529553a..bdfed31 100644 --- a/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs +++ b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs @@ -57,5 +57,22 @@ public void OnApplicationStoppingCallback_OnApplicationStoppedCallback_ShouldBeI Assert.True(stopping && stopped); } + + [Fact] + public void Dispose_ShouldDisposeWrappedHostLifetime() + { + using var test = HostTestFactory.Create(services => + { + services.AddXunitTestLoggingOutputHelperAccessor(); + services.AddXunitTestLogging(TestOutput); + }, hb => + { + hb.UseBootstrapperLifetime(); + }, new TestHostFixture()); + + var bootstrapperLifetime = Assert.IsType(test.Host.Services.GetRequiredService()); + + bootstrapperLifetime.Dispose(); + } } } diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestMinimalWebProgram.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestMinimalWebProgram.cs new file mode 100644 index 0000000..be07474 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestMinimalWebProgram.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Builder; + +namespace Codebelt.Bootstrapper.Web.Assets +{ + internal class TestMinimalWebProgram : MinimalWebProgram + { + public static WebApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args); + } +} diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestWebProgram.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestWebProgram.cs new file mode 100644 index 0000000..491049a --- /dev/null +++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/Assets/TestWebProgram.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Web.Assets +{ + internal class TestWebProgram : WebProgram + { + public static IHostBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args); + } +} diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/MinimalWebProgramBuilderTest.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/MinimalWebProgramBuilderTest.cs new file mode 100644 index 0000000..58f16bd --- /dev/null +++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/MinimalWebProgramBuilderTest.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Web.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Codebelt.Bootstrapper.Web +{ + public class MinimalWebProgramBuilderTest : Test + { + public MinimalWebProgramBuilderTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task CreateHostBuilder_ShouldRegisterBootstrapperLifetime() + { + var builder = TestMinimalWebProgram.CreateHostBuilderAccessor([]); + builder.WebHost.UseTestServer(); + + await using var app = builder.Build(); + app.MapGet("/", () => "Hello World!"); + + await app.StartAsync(); + try + { + var client = app.GetTestClient(); + + Assert.Equal("Hello World!", await client.GetStringAsync("/")); + } + finally + { + await app.StopAsync(); + } + } + } +} diff --git a/test/Codebelt.Bootstrapper.Web.FunctionalTests/WebProgramTest.cs b/test/Codebelt.Bootstrapper.Web.FunctionalTests/WebProgramTest.cs new file mode 100644 index 0000000..0ee69d3 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Web.FunctionalTests/WebProgramTest.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Web.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Codebelt.Bootstrapper.Web +{ + public class WebProgramTest : Test + { + public WebProgramTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task CreateHostBuilder_ShouldRegisterBootstrapperLifetimeAndStartup() + { + var builder = TestWebProgram.CreateHostBuilderAccessor([]); + builder.ConfigureWebHost(webBuilder => webBuilder.UseTestServer()); + + using var host = builder.Build(); + await host.StartAsync(); + try + { + var client = host.GetTestClient(); + + Assert.Equal("Hello World!", await client.GetStringAsync("/")); + } + finally + { + await host.StopAsync(); + } + } + } +} diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestMinimalWorkerProgram.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestMinimalWorkerProgram.cs new file mode 100644 index 0000000..c30f100 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestMinimalWorkerProgram.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Worker.Assets +{ + internal class TestMinimalWorkerProgram : MinimalWorkerProgram + { + public static HostApplicationBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args); + } +} diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestWorkerProgram.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestWorkerProgram.cs new file mode 100644 index 0000000..f4e4d1a --- /dev/null +++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/Assets/TestWorkerProgram.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Worker.Assets +{ + internal class TestWorkerProgram : WorkerProgram + { + public static IHostBuilder CreateHostBuilderAccessor(string[] args) => CreateHostBuilder(args); + } +} diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/MinimalWorkerProgramBuilderTest.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/MinimalWorkerProgramBuilderTest.cs new file mode 100644 index 0000000..2933724 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/MinimalWorkerProgramBuilderTest.cs @@ -0,0 +1,25 @@ +using Codebelt.Bootstrapper.Worker.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Codebelt.Bootstrapper.Worker +{ + public class MinimalWorkerProgramBuilderTest : Test + { + public MinimalWorkerProgramBuilderTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CreateHostBuilder_ShouldRegisterBootstrapperLifetime() + { + using var host = TestMinimalWorkerProgram.CreateHostBuilderAccessor([]).Build(); + + var lifetime = host.Services.GetRequiredService(); + + Assert.IsType(lifetime); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Worker.FunctionalTests/WorkerProgramTest.cs b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/WorkerProgramTest.cs new file mode 100644 index 0000000..a461ffe --- /dev/null +++ b/test/Codebelt.Bootstrapper.Worker.FunctionalTests/WorkerProgramTest.cs @@ -0,0 +1,27 @@ +using Codebelt.Bootstrapper.Worker.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Codebelt.Bootstrapper.Worker +{ + public class WorkerProgramTest : Test + { + public WorkerProgramTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CreateHostBuilder_ShouldRegisterBootstrapperLifetimeAndStartup() + { + using var host = TestWorkerProgram.CreateHostBuilderAccessor([]).Build(); + + var lifetime = host.Services.GetRequiredService(); + var startupFactory = host.Services.GetRequiredService>(); + + Assert.IsType(lifetime); + Assert.IsType>(startupFactory); + } + } +} From c8cf8d20e46abfa66bff29ccc85f23528c9a7f51 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Wed, 27 May 2026 23:30:24 +0200 Subject: [PATCH 7/9] =?UTF-8?q?=E2=9C=85=20add=20bootstrapper=20log=20mess?= =?UTF-8?q?ages=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test coverage for bootstrapper log message validation and startup messaging behavior. --- .../BootstrapperLogMessagesTest.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLogMessagesTest.cs diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLogMessagesTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLogMessagesTest.cs new file mode 100644 index 0000000..8d02feb --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLogMessagesTest.cs @@ -0,0 +1,31 @@ +using System; +using Cuemon; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Codebelt.Bootstrapper +{ + public class BootstrapperLogMessagesTest : Test + { + public BootstrapperLogMessagesTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void FatalErrorActivating_ShouldWriteCriticalEntry() + { + using var services = new ServiceCollection() + .AddXunitTestLogging(TestOutput) + .BuildServiceProvider(); + + var logger = services.GetRequiredService>(); + + Decorator.EncloseToExpose(logger, false).FatalErrorActivating("Test.Type", new InvalidOperationException("Boom")); + + Assert.Contains(logger.GetTestStore().Query(), entry => entry.Message.Contains("Fatal error occurred while activating Test.Type.")); + } + } +} From 9807a0a947dbdd11044b79ed6e8cd3f69c248fb8 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Thu, 28 May 2026 00:30:19 +0200 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=92=AC=20update=20version=20to=205.1.?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release notes and changelog updated to reflect v5.1.0 as a minor release focused on environment configuration defaults for local development, expanded test coverage to 95 percent, dependency updates across all supported target frameworks, CI pipeline improvements for macOS testing, and code coverage policy documentation. --- .../PackageReleaseNotes.txt | 2 +- .../PackageReleaseNotes.txt | 2 +- .../PackageReleaseNotes.txt | 2 +- .../PackageReleaseNotes.txt | 6 ++++- CHANGELOG.md | 22 ++++++++++++++++--- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt index bf7fb4a..184ef75 100644 --- a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt @@ -1,4 +1,4 @@ -Version: 5.0.8 +Version: 5.1.0 Availability: .NET 10 and .NET 9 # ALM diff --git a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt index 5dd934b..4892fbf 100644 --- a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt @@ -1,4 +1,4 @@ -Version: 5.0.8 +Version: 5.1.0 Availability: .NET 10 and .NET 9 # ALM diff --git a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt index 171fc19..c59f6a0 100644 --- a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt @@ -1,4 +1,4 @@ -Version: 5.0.8 +Version: 5.1.0 Availability: .NET 10 and .NET 9 # ALM diff --git a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt index 66827b3..fc098da 100644 --- a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt @@ -1,9 +1,13 @@ -Version: 5.0.8 +Version: 5.1.0 Availability: .NET 10 and .NET 9 # ALM - CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +# New Features +- ADDED UseBootstrapperEnvironmentDefaults extension method on HostApplicationBuilderExtensions to add conventional environment defaults and user secrets for local development, +- ADDED UseBootstrapperEnvironmentDefaults extension method on HostBuilderExtensions to add user secrets for local development. + Version: 5.0.7 Availability: .NET 10 and .NET 9 diff --git a/CHANGELOG.md b/CHANGELOG.md index 248af80..19578f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. -## [5.0.8] - 2026-05-26 +## [5.1.0] - 2026-05-28 -This is a service update that focuses on package dependencies. +This is a minor release focused on environment configuration defaults for local development, expanded test coverage to 95%, dependency updates, and CI/CD improvements for macOS testing. + +### Added + +- UseBootstrapperEnvironmentDefaults extension method on IHostApplicationBuilder to add conventional environment defaults and user secrets for local development, +- UseBootstrapperEnvironmentDefaults extension method on IHostBuilder to add user secrets for local development, +- Comprehensive test coverage across bootstrapper console, web, and worker modules including program creation, startup configuration, hosted service initialization, and lifetime management. + +### Changed + +- Microsoft.NET.Test.Sdk bumped from 18.4.0 to 18.6.0, +- coverlet.collector and coverlet.msbuild bumped from 10.0.0 to 10.0.1, +- Target framework packages updated to latest available versions for net9 and net10, +- DocFX base image updated from nginx 1.30.0-alpine to 1.31.0-alpine, +- CI pipeline enhanced with optional macOS test matrix for X64 and ARM64 architectures across Debug and Release configurations. ## [5.0.7] - 2026-04-18 @@ -230,7 +244,9 @@ Highlighted features included in this release: - WorkerProgram class in the Codebelt.Bootstrapper.Worker namespace that is the base entry point of an application responsible for registering its WorkerStartup partner - WorkerStartup interface in the Codebelt.Bootstrapper.Worker namespace that provides the base class of a conventional based Startup class for a console application -[Unreleased]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.6...HEAD +[Unreleased]: https://github.com/codebeltnet/bootstrapper/compare/v5.1.0...HEAD +[5.1.0]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.7...v5.1.0 +[5.0.7]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.6...v5.0.7 [5.0.6]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.5...v5.0.6 [5.0.5]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.4...v5.0.5 [5.0.4]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.3...v5.0.4 From 40dadb16d73aeff17dea7c4c572c5a2e877b3ea4 Mon Sep 17 00:00:00 2001 From: "aicia[bot]" Date: Thu, 28 May 2026 00:30:33 +0200 Subject: [PATCH 9/9] =?UTF-8?q?=E2=9C=A8=20add=20UseBootstrapperEnvironmen?= =?UTF-8?q?tDefaults=20for=20local=20development?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce new UseBootstrapperEnvironmentDefaults extension methods on HostApplicationBuilderExtensions and HostBuilderExtensions to add conventional environment defaults and user secrets for local development. Add internal HostEnvironmentExtensions helper for environment detection. Integrate the new method into all application templates (console, web, worker) and expand functional test coverage for the new extension methods. --- .../ConsoleProgram.cs | 1 + .../MinimalConsoleProgram.cs | 1 + .../MinimalWebProgram.cs | 1 + src/Codebelt.Bootstrapper.Web/WebProgram.cs | 1 + .../MinimalWorkerProgram.cs | 1 + .../WorkerProgram.cs | 1 + .../HostApplicationBuilderExtensions.cs | 35 +++++- .../HostBuilderExtensions.cs | 25 ++++- .../HostEnvironmentExtensions.cs | 14 +++ ...debelt.Bootstrapper.FunctionalTests.csproj | 1 + .../HostApplicationBuilderExtensionsTest.cs | 102 +++++++++++++++++- .../HostBuilderExtensionsTest.cs | 55 +++++++++- 12 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 src/Codebelt.Bootstrapper/HostEnvironmentExtensions.cs diff --git a/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs b/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs index b7739ac..2883c33 100644 --- a/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs +++ b/src/Codebelt.Bootstrapper.Console/ConsoleProgram.cs @@ -19,6 +19,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args) return Host.CreateDefaultBuilder(args) .UseBootstrapperLifetime() .UseBootstrapperStartup() + .UseBootstrapperEnvironmentDefaults() .UseConsoleStartup(); } } diff --git a/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs b/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs index 2c8903d..5f7c506 100644 --- a/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs +++ b/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs @@ -29,6 +29,7 @@ internal static HostApplicationBuilder CreateHostBuilder(string[] args, Type pro { var hb = Host.CreateApplicationBuilder(args); hb.UseBootstrapperLifetime(); + hb.UseBootstrapperEnvironmentDefaults(); hb.UseBootstrapperProgram(programType); hb.UseMinimalConsoleProgram(); return hb; diff --git a/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs b/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs index 156b897..63a8d68 100644 --- a/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs +++ b/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs @@ -17,6 +17,7 @@ protected static WebApplicationBuilder CreateHostBuilder(string[] args) { var hb = WebApplication.CreateBuilder(args); hb.UseBootstrapperLifetime(); + hb.UseBootstrapperEnvironmentDefaults(); return hb; } } diff --git a/src/Codebelt.Bootstrapper.Web/WebProgram.cs b/src/Codebelt.Bootstrapper.Web/WebProgram.cs index cb2c411..8a17252 100644 --- a/src/Codebelt.Bootstrapper.Web/WebProgram.cs +++ b/src/Codebelt.Bootstrapper.Web/WebProgram.cs @@ -19,6 +19,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .UseBootstrapperLifetime() + .UseBootstrapperEnvironmentDefaults() .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs b/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs index 46c8c23..f5fadd7 100644 --- a/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs +++ b/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs @@ -16,6 +16,7 @@ protected static HostApplicationBuilder CreateHostBuilder(string[] args) { var hb = Host.CreateApplicationBuilder(args); hb.UseBootstrapperLifetime(); + hb.UseBootstrapperEnvironmentDefaults(); return hb; } } diff --git a/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs b/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs index 38542eb..11548e5 100644 --- a/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs +++ b/src/Codebelt.Bootstrapper.Worker/WorkerProgram.cs @@ -18,6 +18,7 @@ protected static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .UseBootstrapperLifetime() + .UseBootstrapperEnvironmentDefaults() .UseBootstrapperStartup(); } } diff --git a/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs b/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs index b22396d..943dab5 100644 --- a/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs +++ b/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Internal; +using System.IO; +using System.Reflection; +using Microsoft.Extensions.Configuration; namespace Codebelt.Bootstrapper { @@ -22,5 +25,33 @@ public static IHostApplicationBuilder UseBootstrapperLifetime(this IHostApplicat hostBuilder.Services.AddSingleton(provider => provider.GetRequiredService() as BootstrapperLifetime); return hostBuilder; } + + /// + /// Adds conventional environment defaults for local development. + /// + /// The to configure. + /// The same instance of the for chaining. + /// + /// When the current environment is local development, this method attempts to resolve the application assembly from + /// and adds user secrets to . If the application assembly cannot be resolved, the operation is ignored. + /// + public static IHostApplicationBuilder UseBootstrapperEnvironmentDefaults(this IHostApplicationBuilder hostBuilder) + { + if (!hostBuilder.Environment.IsLocalDevelopment()) { return hostBuilder; } + if (hostBuilder.Environment.ApplicationName is not { Length: > 0 } applicationName) { return hostBuilder; } + var reloadOnChange = hostBuilder.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true); + + try + { + var appAssembly = Assembly.Load(new AssemblyName(applicationName)); + hostBuilder.Configuration.AddUserSecrets(appAssembly, optional: true, reloadOnChange: reloadOnChange); + } + catch (FileNotFoundException) + { + // The assembly cannot be found, so just skip it. + } + + return hostBuilder; + } } } diff --git a/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs b/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs index b1d2475..4440eea 100644 --- a/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs +++ b/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs @@ -1,7 +1,10 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Internal; +using System.IO; +using System.Reflection; namespace Codebelt.Bootstrapper { @@ -37,5 +40,25 @@ public static IHostBuilder UseBootstrapperStartup(this IHostBuilder ho services.AddSingleton>(new StartupFactory(services, context.Configuration, context.HostingEnvironment)); }); } + + /// + /// Adds conventional environment defaults for local development. + /// + /// The type of the startup class used to resolve user secrets. + /// The to configure. + /// The same instance of the for chaining. + /// + /// When the current environment is local development, this method adds user secrets for to the application configuration. + /// + public static IHostBuilder UseBootstrapperEnvironmentDefaults(this IHostBuilder hostBuilder) where TStartup : StartupRoot + { + return hostBuilder.ConfigureAppConfiguration((context, builder) => + { + var environment = context.HostingEnvironment; + if (!environment.IsLocalDevelopment()) { return; } + var reloadOnChange = context.Configuration.GetValue("hostBuilder:reloadConfigOnChange", defaultValue: true); + builder.AddUserSecrets(optional: true, reloadOnChange: reloadOnChange); + }); + } } } diff --git a/src/Codebelt.Bootstrapper/HostEnvironmentExtensions.cs b/src/Codebelt.Bootstrapper/HostEnvironmentExtensions.cs new file mode 100644 index 0000000..1d0bdf0 --- /dev/null +++ b/src/Codebelt.Bootstrapper/HostEnvironmentExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper +{ + internal static class HostEnvironmentExtensions + { + private const string LocalDevelopment = "LocalDevelopment"; + + internal static bool IsLocalDevelopment(this IHostEnvironment environment) + { + return environment.IsEnvironment(LocalDevelopment); + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj b/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj index 8991216..e91ad88 100644 --- a/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj +++ b/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj @@ -2,6 +2,7 @@ Codebelt.Bootstrapper + dotnet-Codebelt.Bootstrapper.FunctionalTests-279740fe-8d11-4f29-8fa1-972f5a9dc817 diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs index 7cc3245..feae8ea 100644 --- a/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs +++ b/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs @@ -1,20 +1,21 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Codebelt.Bootstrapper.Assets; using Codebelt.Extensions.Xunit; -using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; using Xunit; -using static System.Net.Mime.MediaTypeNames; namespace Codebelt.Bootstrapper { public class HostApplicationBuilderExtensionsTest : Test { + private static readonly string TestAssemblyName = typeof(TestStartup).Assembly.GetName().Name!; + public HostApplicationBuilderExtensionsTest(ITestOutputHelper output) : base(output) { } @@ -31,5 +32,98 @@ public void UseBootstrapperLifetime_ShouldRegisterBootstrapperLifetime() Assert.NotNull(bootstrapperLifetime); Assert.IsType(bootstrapperLifetime); } + + [Fact] + public void UseBootstrapperEnvironmentDefaults_ShouldAddUserSecretsSource_WhenEnvironmentIsLocalDevelopment() + { + var hb = CreateHostApplicationBuilder("LocalDevelopment", TestAssemblyName, new Dictionary + { + ["hostBuilder:reloadConfigOnChange"] = "false" + }); + var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources); + + var result = hb.UseBootstrapperEnvironmentDefaults(); + + Assert.Same(hb, result); + var userSecretsSource = Assert.Single(hb.Configuration.Sources.OfType().Where(IsUserSecretsSource)); + Assert.Equal(initialSecretsSourceCount + 1, CountUserSecretsSources(hb.Configuration.Sources)); + Assert.False(userSecretsSource.ReloadOnChange); + } + + [Fact] + public void UseBootstrapperEnvironmentDefaults_ShouldIgnoreMissingApplicationAssembly_WhenEnvironmentIsLocalDevelopment() + { + var hb = CreateHostApplicationBuilder("LocalDevelopment", "Codebelt.Bootstrapper.Missing.Assembly", null); + var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources); + + var exception = Record.Exception(() => hb.UseBootstrapperEnvironmentDefaults()); + + Assert.Null(exception); + Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources)); + } + + [Fact] + public void UseBootstrapperEnvironmentDefaults_ShouldReturnWithoutAddingUserSecrets_WhenApplicationNameIsEmpty() + { + var hb = CreateHostApplicationBuilder("LocalDevelopment", TestAssemblyName, null); + ((HostingEnvironment)hb.Environment).ApplicationName = string.Empty; + var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources); + + var result = hb.UseBootstrapperEnvironmentDefaults(); + + Assert.Same(hb, result); + Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources)); + } + + [Fact] + public void UseBootstrapperEnvironmentDefaults_ShouldReturnWithoutAddingUserSecrets_WhenApplicationNameIsNull() + { + var hb = CreateHostApplicationBuilder("LocalDevelopment", TestAssemblyName, null); + ((HostingEnvironment)hb.Environment).ApplicationName = null!; + var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources); + + var result = hb.UseBootstrapperEnvironmentDefaults(); + + Assert.Same(hb, result); + Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources)); + } + + [Fact] + public void UseBootstrapperEnvironmentDefaults_ShouldReturnWithoutAddingUserSecrets_WhenEnvironmentIsNotLocalDevelopment() + { + var hb = CreateHostApplicationBuilder(Environments.Development, TestAssemblyName, null); + var initialSecretsSourceCount = CountUserSecretsSources(hb.Configuration.Sources); + + var result = hb.UseBootstrapperEnvironmentDefaults(); + + Assert.Same(hb, result); + Assert.Equal(initialSecretsSourceCount, CountUserSecretsSources(hb.Configuration.Sources)); + } + + private static HostApplicationBuilder CreateHostApplicationBuilder(string environmentName, string applicationName, Dictionary? configuration) + { + var settings = new HostApplicationBuilderSettings + { + EnvironmentName = environmentName, + ApplicationName = applicationName + }; + var hostBuilder = Host.CreateApplicationBuilder(settings); + if (configuration is not null) + { + hostBuilder.Configuration.AddInMemoryCollection(configuration); + } + + return hostBuilder; + } + + private static int CountUserSecretsSources(IEnumerable sources) + { + return sources.OfType().Count(IsUserSecretsSource); + } + + private static bool IsUserSecretsSource(FileConfigurationSource source) + { + return string.Equals(Path.GetFileName(source.Path), "secrets.json", StringComparison.OrdinalIgnoreCase); + } } } diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs index 8bb70d6..b3fcec2 100644 --- a/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs +++ b/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs @@ -1,6 +1,11 @@ -using Codebelt.Bootstrapper.Assets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Codebelt.Bootstrapper.Assets; using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; @@ -44,5 +49,53 @@ public void UseBootstrapperStartup_ShouldRegisterStartupFactory() Assert.NotNull(startupFactory); Assert.IsType>(startupFactory); } + + [Fact] + public void UseBootstrapperEnvironmentDefaults_ShouldAddUserSecretsProvider_WhenEnvironmentIsLocalDevelopment() + { + using var host = CreateHost("LocalDevelopment", configureHostBuilder: hb => hb.UseBootstrapperEnvironmentDefaults(), hostConfiguration: new Dictionary + { + ["hostBuilder:reloadConfigOnChange"] = "false" + }); + + var userSecretsProvider = Assert.Single(GetConfigurationRoot(host).Providers.OfType().Where(IsUserSecretsProvider)); + + Assert.False(userSecretsProvider.Source.ReloadOnChange); + } + + [Fact] + public void UseBootstrapperEnvironmentDefaults_ShouldNotAddUserSecretsProvider_WhenEnvironmentIsNotLocalDevelopment() + { + using var host = CreateHost(Environments.Development, configureHostBuilder: hb => hb.UseBootstrapperEnvironmentDefaults()); + + Assert.Empty(GetConfigurationRoot(host).Providers.OfType().Where(IsUserSecretsProvider)); + } + + private static IHost CreateHost(string environmentName, Action? configureHostBuilder = null, Dictionary? hostConfiguration = null) + { + var hostBuilder = new HostBuilder() + .UseEnvironment(environmentName) + .ConfigureHostConfiguration(builder => + { + if (hostConfiguration is not null) + { + builder.AddInMemoryCollection(hostConfiguration); + } + }); + + configureHostBuilder?.Invoke(hostBuilder); + + return hostBuilder.Build(); + } + + private static IConfigurationRoot GetConfigurationRoot(IHost host) + { + return Assert.IsAssignableFrom(host.Services.GetRequiredService()); + } + + private static bool IsUserSecretsProvider(FileConfigurationProvider provider) + { + return string.Equals(Path.GetFileName(provider.Source.Path), "secrets.json", StringComparison.OrdinalIgnoreCase); + } } }