diff --git a/.github/workflows/pipelines.yml b/.github/workflows/pipelines.yml index 2f4dfa2..608db0c 100644 --- a/.github/workflows/pipelines.yml +++ b/.github/workflows/pipelines.yml @@ -21,7 +21,7 @@ on: jobs: build: name: 🛠️ Build - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: configuration: [Debug, Release] @@ -62,7 +62,7 @@ jobs: pack: name: 📦 Pack - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: configuration: [Debug, Release] @@ -80,10 +80,16 @@ jobs: uploadPackedArtifact: true version: ${{ needs.build.outputs.version }} - sonarcloud: - name: 🔬 Code Quality Analysis + test: + name: 🧪 Test needs: [build] - runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, windows-2022] + configuration: [Debug, Release] + runs-on: ${{ matrix.os }} + timeout-minutes: 15 steps: - name: Checkout uses: codebeltnet/git-checkout@v1 @@ -93,64 +99,43 @@ jobs: with: includePreview: true - - name: Install .NET Tool - Sonar Scanner - uses: codebeltnet/dotnet-tool-install-sonarscanner@v1 - - - name: Restore Dependencies - uses: codebeltnet/dotnet-restore@v2 - - - name: Run SonarCloud Analysis - uses: codebeltnet/sonarcloud-scan@v1 - with: - token: ${{ secrets.SONAR_TOKEN }} - organization: geekle - projectKey: bootstrapper - version: ${{ needs.build.outputs.version }} + - name: Install .NET Tool - Report Generator + uses: codebeltnet/dotnet-tool-install-reportgenerator@v1 - - name: Build - uses: codebeltnet/dotnet-build@v2 + - name: Test with ${{ matrix.configuration }} build + uses: codebeltnet/dotnet-test@v3 with: + configuration: ${{ matrix.configuration }} buildSwitches: -p:SkipSignAssembly=true - uploadBuildArtifact: false - - name: Finalize SonarCloud Analysis - uses: codebeltnet/sonarcloud-scan-finalize@v1 - with: - token: ${{ secrets.SONAR_TOKEN }} + sonarcloud: + name: call-sonarcloud + needs: [build,test] + uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v1 + with: + organization: geekle + projectKey: bootstrapper + version: ${{ needs.build.outputs.version }} + secrets: inherit + + codecov: + name: call-codecov + needs: [build,test] + uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 + with: + repository: codebeltnet/bootstrapper + secrets: inherit codeql: - name: 🛡️ Security Analysis - needs: [build] - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: codebeltnet/git-checkout@v1 - - - name: Install .NET - uses: codebeltnet/install-dotnet@v1 - with: - includePreview: true - - - name: Restore Dependencies - uses: codebeltnet/dotnet-restore@v2 - - - name: Prepare CodeQL SAST Analysis - uses: codebeltnet/codeql-scan@v1 - - - name: Build - uses: codebeltnet/dotnet-build@v2 - with: - buildSwitches: -p:SkipSignAssembly=true - uploadBuildArtifact: false - - - name: Finalize CodeQL SAST Analysis - uses: codebeltnet/codeql-scan-finalize@v1 + name: call-codeql + needs: [build,test] + uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v1 deploy: if: github.event_name != 'pull_request' name: 🚀 Deploy v${{ needs.build.outputs.version }} - runs-on: ubuntu-22.04 - needs: [build,pack,sonarcloud,codeql] + runs-on: ubuntu-24.04 + needs: [build, pack, test, sonarcloud, codecov, codeql] environment: Production steps: - uses: codebeltnet/nuget-push@v1 diff --git a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt index e102d9c..8d82edb 100644 --- a/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt @@ -1,4 +1,14 @@ -Version 3.0.1 +Version 4.0.0 +Availability: .NET 9 and .NET 8 +  +# ALM +- CHANGED Dependencies to latest and greatest with respect to TFMs +  +# Breaking Changes +- CHANGED UseBootstrapperProgram method on the HostApplicationBuilderExtensions class in the Codebelt.Bootstrapper.Console namespace to extend IHostApplicationBuilder instead of HostApplicationBuilder +- CHANGED UseMinimalConsoleProgram method on the HostApplicationBuilderExtensions class in the Codebelt.Bootstrapper.Console namespace to extend IHostApplicationBuilder instead of HostApplicationBuilder +  +Version 3.0.1 Availability: .NET 9 and .NET 8   # ALM diff --git a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt index 10dcafe..3c78178 100644 --- a/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt @@ -1,4 +1,10 @@ -Version 3.0.1 +Version 4.0.0 +Availability: .NET 9 and .NET 8 +  +# ALM +- CHANGED Dependencies to latest and greatest with respect to TFMs +  +Version 3.0.1 Availability: .NET 9 and .NET 8   # ALM diff --git a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt index d84a2fc..b63b882 100644 --- a/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt @@ -1,4 +1,10 @@ -Version 3.0.1 +Version 4.0.0 +Availability: .NET 9 and .NET 8 +  +# ALM +- CHANGED Dependencies to latest and greatest with respect to TFMs +  +Version 3.0.1 Availability: .NET 9 and .NET 8   # ALM diff --git a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt index 944a7e1..266b80e 100644 --- a/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt @@ -1,4 +1,21 @@ -Version 3.0.1 +Version 4.0.0 +Availability: .NET 9 and .NET 8 +  +# ALM +- CHANGED Dependencies to latest and greatest with respect to TFMs +  +# Breaking Changes +- REMOVED HostedServiceExtensions class in the Codebelt.Bootstrapper namespace (WaitForApplicationStartedAnnouncementAsync extension method) +- CHANGED BootstrapperLifetime class in the Codebelt.Bootstrapper namespace to implement IHostLifetimeEvents (hereby removing static equivalents) +- CHANGED UseBootstrapperStartup method on the HostApplicationBuilderExtensions class in the Codebelt.Bootstrapper namespace to extend IHostApplicationBuilder instead of HostApplicationBuilder +  +# New Features +- ADDED IHostLifetimeEvents interface in the Codebelt.Bootstrapper namespace that provides a convenient way to be notified of host lifetime events +  +# Bug Fixes +- FIXED BootstrapperLifetime class in the Codebelt.Bootstrapper namespace to disregard SuppressStatusMessages and always assign callbacks to members of IHostLifetimeEvents +  +Version 3.0.1 Availability: .NET 9 and .NET 8   # ALM diff --git a/CHANGELOG.md b/CHANGELOG.md index 8de4e17..958302b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ 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. +## [4.0.0] - TBD + +This major release revisits and refines some of the earlier design decisions to offer a more consistent and flexible API. It also brings forward improvements to reliability and maintainability. + +### Added + +- IHostLifetimeEvents interface in the Codebelt.Bootstrapper namespace that provides a convenient way to be notified of host lifetime events + +### Changed + +- BootstrapperLifetime class in the Codebelt.Bootstrapper namespace to implement IHostLifetimeEvents and hereby removing static equivalents (breaking change) +- UseBootstrapperStartup method on the HostApplicationBuilderExtensions class in the Codebelt.Bootstrapper namespace to extend IHostApplicationBuilder instead of HostApplicationBuilder (breaking change) +- UseBootstrapperProgram method on the HostApplicationBuilderExtensions class in the Codebelt.Bootstrapper.Console namespace to extend IHostApplicationBuilder instead of HostApplicationBuilder (breaking change) +- UseMinimalConsoleProgram method on the HostApplicationBuilderExtensions class in the Codebelt.Bootstrapper.Console namespace to extend IHostApplicationBuilder instead of HostApplicationBuilder (breaking change) + +### Removed + +- HostedServiceExtensions class in the Codebelt.Bootstrapper namespace (breaking change) + +### Fixed + +- BootstrapperLifetime class in the Codebelt.Bootstrapper namespace to disregard SuppressStatusMessages and always assign callbacks to members of IHostLifetimeEvents + ## [3.0.1] - 2025-01-31 This is a service update that primarily focuses on package dependencies and minor improvements. diff --git a/Codebelt.Bootstrapper.sln b/Codebelt.Bootstrapper.sln index 9922c94..23dde0f 100644 --- a/Codebelt.Bootstrapper.sln +++ b/Codebelt.Bootstrapper.sln @@ -39,6 +39,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Bootstrapper.Minim EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Bootstrapper.MinimalWorker.App", "app\Codebelt.Bootstrapper.MinimalWorker.App\Codebelt.Bootstrapper.MinimalWorker.App.csproj", "{E87D127B-4CD1-4009-962E-EEF70A522C13}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Bootstrapper.Tests", "test\Codebelt.Bootstrapper.Tests\Codebelt.Bootstrapper.Tests.csproj", "{CC656409-7479-4553-8E9B-0854801E1590}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Bootstrapper.FunctionalTests", "test\Codebelt.Bootstrapper.FunctionalTests\Codebelt.Bootstrapper.FunctionalTests.csproj", "{8970491F-E0BD-489D-AA7E-617A67C2E39F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codebelt.Bootstrapper.Console.FunctionalTests", "test\Codebelt.Bootstrapper.Console.FunctionalTests\Codebelt.Bootstrapper.Console.FunctionalTests.csproj", "{585A199C-4675-4A8A-89B7-EFF824377456}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -109,6 +117,18 @@ Global {E87D127B-4CD1-4009-962E-EEF70A522C13}.Debug|Any CPU.Build.0 = Debug|Any CPU {E87D127B-4CD1-4009-962E-EEF70A522C13}.Release|Any CPU.ActiveCfg = Release|Any CPU {E87D127B-4CD1-4009-962E-EEF70A522C13}.Release|Any CPU.Build.0 = Release|Any CPU + {CC656409-7479-4553-8E9B-0854801E1590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC656409-7479-4553-8E9B-0854801E1590}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC656409-7479-4553-8E9B-0854801E1590}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC656409-7479-4553-8E9B-0854801E1590}.Release|Any CPU.Build.0 = Release|Any CPU + {8970491F-E0BD-489D-AA7E-617A67C2E39F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8970491F-E0BD-489D-AA7E-617A67C2E39F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8970491F-E0BD-489D-AA7E-617A67C2E39F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8970491F-E0BD-489D-AA7E-617A67C2E39F}.Release|Any CPU.Build.0 = Release|Any CPU + {585A199C-4675-4A8A-89B7-EFF824377456}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {585A199C-4675-4A8A-89B7-EFF824377456}.Debug|Any CPU.Build.0 = Debug|Any CPU + {585A199C-4675-4A8A-89B7-EFF824377456}.Release|Any CPU.ActiveCfg = Release|Any CPU + {585A199C-4675-4A8A-89B7-EFF824377456}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -130,6 +150,9 @@ Global {3C43A01F-51E7-4166-91D5-E2088E68F4E5} = {28409021-5670-4008-9061-DBAAA0FC4DA6} {497AD6CD-1E7E-4B4D-ACE5-CDD005A14F96} = {28409021-5670-4008-9061-DBAAA0FC4DA6} {E87D127B-4CD1-4009-962E-EEF70A522C13} = {28409021-5670-4008-9061-DBAAA0FC4DA6} + {CC656409-7479-4553-8E9B-0854801E1590} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {8970491F-E0BD-489D-AA7E-617A67C2E39F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {585A199C-4675-4A8A-89B7-EFF824377456} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {72FB037E-3629-4CDB-812E-D577A3D4FD26} diff --git a/Directory.Build.props b/Directory.Build.props index c107324..6e78383 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -73,6 +73,6 @@ - + diff --git a/Directory.Packages.props b/Directory.Packages.props index c60caa3..6de61c3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,26 +4,26 @@ - - - - + + + + - + - - - - - + + + + + - - + + diff --git a/app/Codebelt.Bootstrapper.Console.App/Startup.cs b/app/Codebelt.Bootstrapper.Console.App/Startup.cs index 959ef3b..a727e2d 100644 --- a/app/Codebelt.Bootstrapper.Console.App/Startup.cs +++ b/app/Codebelt.Bootstrapper.Console.App/Startup.cs @@ -24,14 +24,21 @@ public override void ConfigureServices(IServiceCollection services) public override void ConfigureConsole(IServiceProvider serviceProvider) { var logger = serviceProvider.GetRequiredService>(); - BootstrapperLifetime.OnApplicationStartedCallback = () => logger.LogWarning("Console started."); - BootstrapperLifetime.OnApplicationStoppingCallback = () => + var events = serviceProvider.GetRequiredService(); + + events.OnApplicationStartedCallback = () => + { + logger.LogWarning("Console started."); + }; + + events.OnApplicationStoppingCallback = () => { logger.LogWarning("Stopping and cleaning .."); Thread.Sleep(TimeSpan.FromSeconds(5)); // simulate graceful shutdown logger.LogWarning(".. done!"); }; - BootstrapperLifetime.OnApplicationStoppedCallback = () => logger.LogCritical("Console stopped."); + + events.OnApplicationStoppedCallback = () => logger.LogCritical("Console stopped."); } public async override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) diff --git a/app/Codebelt.Bootstrapper.MinimalConsole.App/Program.cs b/app/Codebelt.Bootstrapper.MinimalConsole.App/Program.cs index 38c33dc..b37566a 100644 --- a/app/Codebelt.Bootstrapper.MinimalConsole.App/Program.cs +++ b/app/Codebelt.Bootstrapper.MinimalConsole.App/Program.cs @@ -18,14 +18,16 @@ static Task Main(string[] args) var host = builder.Build(); var logger = host.Services.GetRequiredService>(); - BootstrapperLifetime.OnApplicationStartedCallback = () => logger.LogWarning("Console started."); - BootstrapperLifetime.OnApplicationStoppingCallback = () => + var events = host.Services.GetRequiredService(); + + events.OnApplicationStartedCallback = () => logger.LogWarning("Console started."); + events.OnApplicationStoppingCallback = () => { logger.LogWarning("Stopping and cleaning .."); Thread.Sleep(TimeSpan.FromSeconds(5)); // simulate graceful shutdown logger.LogWarning(".. done!"); }; - BootstrapperLifetime.OnApplicationStoppedCallback = () => logger.LogCritical("Console stopped."); + events.OnApplicationStoppedCallback = () => logger.LogCritical("Console stopped."); return host.RunAsync(); } diff --git a/app/Codebelt.Bootstrapper.MinimalWorker.App/FakeHostedService.cs b/app/Codebelt.Bootstrapper.MinimalWorker.App/FakeHostedService.cs index 96e46e1..9ff8335 100644 --- a/app/Codebelt.Bootstrapper.MinimalWorker.App/FakeHostedService.cs +++ b/app/Codebelt.Bootstrapper.MinimalWorker.App/FakeHostedService.cs @@ -5,24 +5,24 @@ public class FakeHostedService : BackgroundService private readonly ILogger _logger; private bool _gracefulShutdown; - public FakeHostedService(ILogger logger) + public FakeHostedService(ILogger logger, IHostLifetimeEvents events) { _logger = logger; - BootstrapperLifetime.OnApplicationStartedCallback = () => logger.LogInformation("Started"); - BootstrapperLifetime.OnApplicationStoppingCallback = () => + + events.OnApplicationStartedCallback = () => logger.LogInformation("Started"); + events.OnApplicationStoppingCallback = () => { _gracefulShutdown = true; logger.LogWarning("Stopping and cleaning .."); Thread.Sleep(TimeSpan.FromSeconds(5)); // simulate graceful shutdown logger.LogWarning(".. done!"); }; - BootstrapperLifetime.OnApplicationStoppedCallback = () => logger.LogCritical("Stopped"); + events.OnApplicationStoppedCallback = () => logger.LogCritical("Stopped"); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await this.WaitForApplicationStartedAnnouncementAsync(stoppingToken).ConfigureAwait(false); while (!stoppingToken.IsCancellationRequested) { if (_gracefulShutdown) { return; } diff --git a/app/Codebelt.Bootstrapper.Worker.App/FakeHostedService.cs b/app/Codebelt.Bootstrapper.Worker.App/FakeHostedService.cs index 2d6d0f1..7dd9c9c 100644 --- a/app/Codebelt.Bootstrapper.Worker.App/FakeHostedService.cs +++ b/app/Codebelt.Bootstrapper.Worker.App/FakeHostedService.cs @@ -11,24 +11,23 @@ public class FakeHostedService : BackgroundService private readonly ILogger _logger; private bool _gracefulShutdown; - public FakeHostedService(ILogger logger) + public FakeHostedService(ILogger logger, IHostLifetimeEvents events) { _logger = logger; - BootstrapperLifetime.OnApplicationStartedCallback = () => logger.LogInformation("Started"); - BootstrapperLifetime.OnApplicationStoppingCallback = () => + events.OnApplicationStartedCallback = () => logger.LogInformation("Started"); + events.OnApplicationStoppingCallback = () => { _gracefulShutdown = true; logger.LogWarning("Stopping and cleaning .."); Thread.Sleep(TimeSpan.FromSeconds(5)); // simulate graceful shutdown logger.LogWarning(".. done!"); }; - BootstrapperLifetime.OnApplicationStoppedCallback = () => logger.LogCritical("Stopped"); + events.OnApplicationStoppedCallback = () => logger.LogCritical("Stopped"); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - await this.WaitForApplicationStartedAnnouncementAsync(stoppingToken).ConfigureAwait(false); while (!stoppingToken.IsCancellationRequested) { if (_gracefulShutdown) { return; } diff --git a/src/Codebelt.Bootstrapper.Console/ConsoleHostedService.cs b/src/Codebelt.Bootstrapper.Console/ConsoleHostedService.cs index 39d92c2..02f3396 100644 --- a/src/Codebelt.Bootstrapper.Console/ConsoleHostedService.cs +++ b/src/Codebelt.Bootstrapper.Console/ConsoleHostedService.cs @@ -20,18 +20,21 @@ public class ConsoleHostedService : IHostedService where TStartup : Co private ILogger _logger; private bool _ranToCompletion; private Task _runAsyncTask; + private readonly IHostLifetimeEvents _events; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The dependency injected . - /// The dependency injected . - /// The dependency injected . - public ConsoleHostedService(IStartupFactory factory, IHostApplicationLifetime applicationLifetime, IServiceProvider provider) + /// The dependency injected . + /// The dependency injected . + /// The dependency injected . + /// The dependency injected . + public ConsoleHostedService(IStartupFactory factory, IHostApplicationLifetime applicationLifetime, IServiceProvider provider, IHostLifetimeEvents events) { _factory = factory; _applicationLifetime = applicationLifetime; _provider = provider; + _events = events; } /// @@ -41,36 +44,34 @@ public ConsoleHostedService(IStartupFactory factory, IHostApplicationL /// A that represents the asynchronous operation. public Task StartAsync(CancellationToken cancellationToken) { - var startup = _factory.Instance; - _logger = _provider.GetRequiredService>(); - - _runAsyncTask = Task.Run(async () => + var startup = _factory.Instance; + if (startup != null) { - try + startup.ConfigureConsole(_provider); + _events.OnApplicationStartedCallback += () => { - if (startup != null) - { - startup.ConfigureConsole(_provider); - - await this.WaitForApplicationStartedAnnouncementAsync(cancellationToken).ConfigureAwait(false); - - _logger.LogInformation("RunAsync started."); - await startup.RunAsync(_provider, cancellationToken).ConfigureAwait(false); - _ranToCompletion = true; - } - else + _runAsyncTask = Task.Run(async () => { - _logger.LogWarning("Unable to activate an instance of {TypeFullName}.", typeof(TStartup).FullName); - } - } - catch (Exception e) - { - _logger.LogCritical(e, "Fatal error occurred while activating {TypeFullName}.", typeof(TStartup).FullName); - } - }, cancellationToken); + try + { + _logger.LogInformation("RunAsync started."); + await startup.RunAsync(_provider, cancellationToken).ConfigureAwait(false); + _ranToCompletion = true; + } + catch (Exception e) + { + _logger.LogCritical(e, "Fatal error occurred while activating {TypeFullName}.", typeof(TStartup).FullName); + } + }, cancellationToken); - StartWaitForCompletionOfRunAsync().ConfigureAwait(false); + StartWaitForCompletionOfRunAsync().ConfigureAwait(false); + }; + } + else + { + _logger.LogWarning("Unable to activate an instance of {TypeFullName}.", typeof(TStartup).FullName); + } return Task.CompletedTask; } diff --git a/src/Codebelt.Bootstrapper.Console/HostApplicationBuilderExtensions.cs b/src/Codebelt.Bootstrapper.Console/HostApplicationBuilderExtensions.cs index cc12581..d3ce6b8 100644 --- a/src/Codebelt.Bootstrapper.Console/HostApplicationBuilderExtensions.cs +++ b/src/Codebelt.Bootstrapper.Console/HostApplicationBuilderExtensions.cs @@ -5,17 +5,17 @@ namespace Codebelt.Bootstrapper.Console { /// - /// Extension methods for the . + /// Extension methods for the . /// public static class HostApplicationBuilderExtensions { /// /// Provides an implementation of a conventional based . /// - /// The to configure. + /// The to configure. /// The that must be assignable from . - /// The same instance of the for chaining. - public static HostApplicationBuilder UseBootstrapperProgram(this HostApplicationBuilder hostBuilder, Type minimalConsoleProgramType) + /// The same instance of the for chaining. + public static IHostApplicationBuilder UseBootstrapperProgram(this IHostApplicationBuilder hostBuilder, Type minimalConsoleProgramType) { hostBuilder.Services.AddSingleton(new ProgramFactory(minimalConsoleProgramType)); return hostBuilder; @@ -26,7 +26,7 @@ public static HostApplicationBuilder UseBootstrapperProgram(this HostApplication /// /// The to configure. /// The same instance of the for chaining. - public static HostApplicationBuilder UseMinimalConsoleProgram(this HostApplicationBuilder hostBuilder) + public static IHostApplicationBuilder UseMinimalConsoleProgram(this IHostApplicationBuilder hostBuilder) { hostBuilder.Services.AddHostedService(); return hostBuilder; diff --git a/src/Codebelt.Bootstrapper.Console/MinimalConsoleHostedService.cs b/src/Codebelt.Bootstrapper.Console/MinimalConsoleHostedService.cs index 1f4c3e0..67e68c9 100644 --- a/src/Codebelt.Bootstrapper.Console/MinimalConsoleHostedService.cs +++ b/src/Codebelt.Bootstrapper.Console/MinimalConsoleHostedService.cs @@ -16,9 +16,10 @@ public class MinimalConsoleHostedService : IHostedService private readonly IProgramFactory _factory; private readonly IHostApplicationLifetime _applicationLifetime; private readonly IServiceProvider _provider; - private ILogger _logger; + private ILogger _logger; private bool _ranToCompletion; private Task _runAsyncTask; + private readonly IHostLifetimeEvents _events; /// /// Initializes a new instance of the class. @@ -26,11 +27,13 @@ public class MinimalConsoleHostedService : IHostedService /// The dependency injected . /// The dependency injected . /// The dependency injected . - public MinimalConsoleHostedService(IProgramFactory factory, IHostApplicationLifetime applicationLifetime, IServiceProvider provider) + /// The dependency injected . + public MinimalConsoleHostedService(IProgramFactory factory, IHostApplicationLifetime applicationLifetime, IServiceProvider provider, IHostLifetimeEvents events) { _factory = factory; _applicationLifetime = applicationLifetime; _provider = provider; + _events = events; } /// @@ -40,35 +43,37 @@ public MinimalConsoleHostedService(IProgramFactory factory, IHostApplicationLife /// A that represents the asynchronous operation. public Task StartAsync(CancellationToken cancellationToken) { - var program = _factory.Instance; - var programType = program?.GetType() ?? typeof(MinimalConsoleProgram); + _events.OnApplicationStartedCallback += () => + { + var program = _factory.Instance; + var programType = program?.GetType() ?? typeof(MinimalConsoleProgram); + var loggerType = typeof(ILogger<>).MakeGenericType(programType); - _logger = _provider.GetRequiredService>(); + _logger = _provider.GetRequiredService(loggerType) as ILogger; - _runAsyncTask = Task.Run(async () => - { - try + _runAsyncTask = Task.Run(async () => { - if (program != null) + try { - await this.WaitForApplicationStartedAnnouncementAsync(cancellationToken).ConfigureAwait(false); - - _logger.LogInformation("RunAsync started."); - await program.RunAsync(_provider, cancellationToken).ConfigureAwait(false); - _ranToCompletion = true; + if (program != null) + { + _logger.LogInformation("RunAsync started."); + await program.RunAsync(_provider, cancellationToken).ConfigureAwait(false); + _ranToCompletion = true; + } + else + { + _logger.LogWarning("Unable to activate an instance of {TypeFullName}.", programType.FullName); + } } - else + catch (Exception e) { - _logger.LogWarning("Unable to activate an instance of {TypeFullName}.", programType.FullName); + _logger.LogCritical(e, "Fatal error occurred while activating {TypeFullName}.", programType.FullName); } - } - catch (Exception e) - { - _logger.LogCritical(e, "Fatal error occurred while activating {TypeFullName}.", programType.FullName); - } - }, cancellationToken); + }, cancellationToken); - StartWaitForCompletionOfRunAsync().ConfigureAwait(false); + StartWaitForCompletionOfRunAsync().ConfigureAwait(false); + }; return Task.CompletedTask; } diff --git a/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs b/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs index 68c54e5..3849930 100644 --- a/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs +++ b/src/Codebelt.Bootstrapper.Console/MinimalConsoleProgram.cs @@ -17,10 +17,11 @@ public abstract class MinimalConsoleProgram : ProgramRoot /// The initialized . protected static HostApplicationBuilder CreateHostBuilder(string[] args) { - return Host.CreateApplicationBuilder(args) - .UseBootstrapperLifetime() - .UseBootstrapperProgram(typeof(MinimalConsoleProgram)) - .UseMinimalConsoleProgram(); + var hb = Host.CreateApplicationBuilder(args); + hb.UseBootstrapperLifetime(); + hb.UseBootstrapperProgram(typeof(MinimalConsoleProgram)); + hb.UseMinimalConsoleProgram(); + return hb; } /// diff --git a/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs b/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs index fdbf564..ae550d7 100644 --- a/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs +++ b/src/Codebelt.Bootstrapper.Web/MinimalWebProgram.cs @@ -9,7 +9,7 @@ namespace Codebelt.Bootstrapper.Web public abstract class MinimalWebProgram : ProgramRoot { /// - /// Creates an used to set up the host. + /// Creates an used to set up the host. /// /// The command line arguments. /// The initialized . diff --git a/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs b/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs index 37108d4..46c8c23 100644 --- a/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs +++ b/src/Codebelt.Bootstrapper.Worker/MinimalWorkerProgram.cs @@ -14,8 +14,9 @@ public abstract class MinimalWorkerProgram : ProgramRoot /// The initialized . protected static HostApplicationBuilder CreateHostBuilder(string[] args) { - return Host.CreateApplicationBuilder(args) - .UseBootstrapperLifetime(); + var hb = Host.CreateApplicationBuilder(args); + hb.UseBootstrapperLifetime(); + return hb; } } } diff --git a/src/Codebelt.Bootstrapper/BootstrapperLifetime.cs b/src/Codebelt.Bootstrapper/BootstrapperLifetime.cs index 375c039..24a5160 100644 --- a/src/Codebelt.Bootstrapper/BootstrapperLifetime.cs +++ b/src/Codebelt.Bootstrapper/BootstrapperLifetime.cs @@ -14,28 +14,24 @@ namespace Codebelt.Bootstrapper /// /// /// - public class BootstrapperLifetime : Disposable, IHostLifetime + public class BootstrapperLifetime : Disposable, IHostLifetime, IHostLifetimeEvents { - private readonly ConsoleLifetimeOptions _options; private readonly ConsoleLifetime _hostLifetime; private readonly IHostApplicationLifetime _applicationLifetime; /// /// Triggered when the application host has fully started. /// - public static Action OnApplicationStartedCallback { get; set; } + public Action OnApplicationStartedCallback { get; set; } /// /// Triggered when the application host is starting a graceful shutdown. - /// Shutdown will block until all callbacks registered on this token have completed. /// - public static Action OnApplicationStoppingCallback { get; set; } + public Action OnApplicationStoppingCallback { get; set; } /// - /// Triggered when the application host has completed a graceful shutdown. - /// The application will not exit until all callbacks registered on this token have completed. - /// - public static Action OnApplicationStoppedCallback { get; set; } + /// Triggered when the application host has completed a graceful shutdown./// + public Action OnApplicationStoppedCallback { get; set; } /// /// Initializes a new instance of the class. @@ -49,7 +45,6 @@ public BootstrapperLifetime(IOptions options, IHostEnvir { _hostLifetime = new ConsoleLifetime(options, environment, applicationLifetime, hostOptions, loggerFactory); _applicationLifetime = applicationLifetime; - _options = options.Value; } /// @@ -70,26 +65,23 @@ public Task StopAsync(CancellationToken cancellationToken) /// A that represents the asynchronous operation. public Task WaitForStartAsync(CancellationToken cancellationToken) { - if (!_options.SuppressStatusMessages) - { - _applicationLifetime.ApplicationStarted.Register(OnApplicationStarted); - _applicationLifetime.ApplicationStopped.Register(OnApplicationStopped); - _applicationLifetime.ApplicationStopping.Register(OnApplicationStopping); - } + _applicationLifetime.ApplicationStarted.Register(OnApplicationStarted); + _applicationLifetime.ApplicationStopped.Register(OnApplicationStopped); + _applicationLifetime.ApplicationStopping.Register(OnApplicationStopping); return _hostLifetime.WaitForStartAsync(cancellationToken); } - private static void OnApplicationStarted() + private void OnApplicationStarted() { OnApplicationStartedCallback?.Invoke(); } - private static void OnApplicationStopped() + private void OnApplicationStopped() { OnApplicationStoppedCallback?.Invoke(); } - private static void OnApplicationStopping() + private void OnApplicationStopping() { OnApplicationStoppingCallback?.Invoke(); } diff --git a/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs b/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs index 1314f1d..b22396d 100644 --- a/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs +++ b/src/Codebelt.Bootstrapper/HostApplicationBuilderExtensions.cs @@ -6,19 +6,20 @@ namespace Codebelt.Bootstrapper { /// - /// Extension methods for the . + /// Extension methods for the . /// public static class HostApplicationBuilderExtensions { /// /// Listens for Ctrl+C or SIGTERM and calls to start the shutdown process. /// - /// The to configure. - /// The same instance of the for chaining. + /// The to configure. + /// The same instance of the for chaining. /// Complements the default implementation of . - public static HostApplicationBuilder UseBootstrapperLifetime(this HostApplicationBuilder hostBuilder) + public static IHostApplicationBuilder UseBootstrapperLifetime(this IHostApplicationBuilder hostBuilder) { hostBuilder.Services.Replace(ServiceDescriptor.Singleton()); + hostBuilder.Services.AddSingleton(provider => provider.GetRequiredService() as BootstrapperLifetime); return hostBuilder; } } diff --git a/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs b/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs index d449814..b1d2475 100644 --- a/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs +++ b/src/Codebelt.Bootstrapper/HostBuilderExtensions.cs @@ -21,6 +21,7 @@ public static IHostBuilder UseBootstrapperLifetime(this IHostBuilder hostBuilder return hostBuilder.ConfigureServices(services => { services.Replace(ServiceDescriptor.Singleton()); + services.AddSingleton(provider => provider.GetRequiredService() as BootstrapperLifetime); }); } diff --git a/src/Codebelt.Bootstrapper/HostedServiceExtensions.cs b/src/Codebelt.Bootstrapper/HostedServiceExtensions.cs deleted file mode 100644 index 2184e69..0000000 --- a/src/Codebelt.Bootstrapper/HostedServiceExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - -namespace Codebelt.Bootstrapper -{ - /// - /// Extension methods for the . - /// - public static class HostedServiceExtensions - { - /// - /// Waits for the application to start and present an informational message. - /// - /// The to extend. - /// The cancellation token. - /// A task that represents the asynchronous operation. - public static async Task WaitForApplicationStartedAnnouncementAsync(this IHostedService hostedService, CancellationToken cancellationToken = default) - { - var tsc = new TaskCompletionSource(); - BootstrapperLifetime.OnApplicationStartedCallback += () => tsc.SetResult(); - await tsc.Task.ConfigureAwait(false); // give time for the host to start and present informational message - } - } -} diff --git a/src/Codebelt.Bootstrapper/IHostLifetimeEvents.cs b/src/Codebelt.Bootstrapper/IHostLifetimeEvents.cs new file mode 100644 index 0000000..e79ace0 --- /dev/null +++ b/src/Codebelt.Bootstrapper/IHostLifetimeEvents.cs @@ -0,0 +1,25 @@ +using System; + +namespace Codebelt.Bootstrapper +{ + /// + /// Provides a convenient way to be notified of host lifetime events. + /// + public interface IHostLifetimeEvents + { + /// + /// Triggered when the application host has fully started. + /// + Action OnApplicationStartedCallback { get; set; } + + /// + /// Triggered when the application host is starting a graceful shutdown. + /// + Action OnApplicationStoppingCallback { get; set; } + + /// + /// Triggered when the application host has completed a graceful shutdown. + /// + Action OnApplicationStoppedCallback { get; set; } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/ManualGenericHostFixture.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/ManualGenericHostFixture.cs new file mode 100644 index 0000000..3ff99f5 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/ManualGenericHostFixture.cs @@ -0,0 +1,12 @@ +using Codebelt.Extensions.Xunit.Hosting; + +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class ManualGenericHostFixture : GenericHostFixture + { + public ManualGenericHostFixture() + { + HostRunnerCallback = host => { }; + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/ManualMinimalHostFixture.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/ManualMinimalHostFixture.cs new file mode 100644 index 0000000..6bcb555 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/ManualMinimalHostFixture.cs @@ -0,0 +1,12 @@ +using Codebelt.Extensions.Xunit.Hosting; + +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class ManualMinimalHostFixture : MinimalHostFixture + { + public ManualMinimalHostFixture() + { + HostRunnerCallback = host => { }; + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleStartup.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleStartup.cs new file mode 100644 index 0000000..df3c4df --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestConsoleStartup.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class TestConsoleStartup : ConsoleStartup + { + public TestConsoleStartup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + } + + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var logger = serviceProvider.GetRequiredService>(); + logger.LogTrace($"Inside {nameof(RunAsync)} of {nameof(TestConsoleStartup)}."); + return Task.CompletedTask; + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestHostFixture.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestHostFixture.cs new file mode 100644 index 0000000..6dac83b --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestHostFixture.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class TestHostFixture : GenericHostFixture + { + public override void ConfigureHost(Test hostTest) + { + var hb = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + { + ConfigureCallback(config.Build(), context.HostingEnvironment); + }) + .ConfigureServices((context, services) => + { + Configuration = context.Configuration; + Environment = context.HostingEnvironment; + ConfigureServicesCallback(services); + }) + .ConfigureHostConfiguration(builder => + { + builder.AddInMemoryCollection(new Dictionary + { + { HostDefaults.ApplicationKey, hostTest.CallerType.Assembly.GetName().Name } + }); + }); + ConfigureHostCallback(hb); + Host = hb.Build(); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs new file mode 100644 index 0000000..34bdcc6 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Assets/TestMinimalConsoleProgram.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Codebelt.Bootstrapper.Console.Assets +{ + public class TestMinimalConsoleProgram : MinimalConsoleProgram + { + public override Task RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) + { + var logger = serviceProvider.GetRequiredService>(); + logger.LogTrace($"Inside {nameof(RunAsync)} of {nameof(TestMinimalConsoleProgram)}."); + return Task.CompletedTask; + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/Codebelt.Bootstrapper.Console.FunctionalTests.csproj b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Codebelt.Bootstrapper.Console.FunctionalTests.csproj new file mode 100644 index 0000000..680c1d8 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/Codebelt.Bootstrapper.Console.FunctionalTests.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Bootstrapper.Console + + + + + + + diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs new file mode 100644 index 0000000..33aae0c --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/ConsoleHostedServiceTest.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Console.Assets; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Codebelt.Bootstrapper.Console +{ + public class ConsoleHostedServiceTest : Test + { + public ConsoleHostedServiceTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task StartAsync_ShouldInvokeRunAsyncInTestConsoleStartup() + { + await using var test = HostTestFactory.Create(services => + { + services.AddXunitTestLogging(TestOutput); + }, hb => + { + hb.UseBootstrapperLifetime() + .UseBootstrapperStartup() + .UseConsoleStartup(); + }); + + await test.Host.WaitForShutdownAsync(); + + var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore(); + Assert.Collection(loggerStore.Query(), + entry => Assert.Equal("Information: RunAsync started.", entry.Message), + entry => Assert.Equal("Trace: Inside RunAsync of TestConsoleStartup.", entry.Message), + entry => Assert.Equal("Information: RunAsync completed successfully.", entry.Message) + ); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/LoggerExtensions.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/LoggerExtensions.cs new file mode 100644 index 0000000..a1295f9 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/LoggerExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.Logging; + +namespace Codebelt.Bootstrapper.Console +{ + public static class LoggerExtensions + { + /// + /// Returns the associated that is provided when settings up services from . + /// + /// The from which to retrieve the . + /// Returns an implementation of with all logged entries expressed as . + /// + /// cannot be null. + /// + /// + /// does not contain a test store. + /// + public static ITestStore GetTestStore(this ILogger logger, Type loggerType) + { + return logger.GetTestStore(loggerType.FullName); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleHostedServiceTest.cs b/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleHostedServiceTest.cs new file mode 100644 index 0000000..456b5c7 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Console.FunctionalTests/MinimalConsoleHostedServiceTest.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Console.Assets; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Codebelt.Bootstrapper.Console +{ + public class MinimalConsoleHostedServiceTest : Test + { + public MinimalConsoleHostedServiceTest(ITestOutputHelper output) : base(output) + { + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + var exception = e.ExceptionObject as Exception; + Debug.WriteLine($"Unhandled exception: {exception?.Message}"); + }; + + TaskScheduler.UnobservedTaskException += (sender, e) => + { + Debug.WriteLine($"Unobserved task exception: {e.Exception.Message}"); + e.SetObserved(); + }; + } + + [Fact] + public async Task StartAsync_ShouldInvokeRunAsyncInTestConsoleStartup() + { + await using var test = MinimalHostTestFactory.Create(services => + { + services.AddXunitTestLogging(TestOutput); + }, hb => + { + hb.UseBootstrapperLifetime() + .UseBootstrapperProgram(typeof(TestMinimalConsoleProgram)) + .UseMinimalConsoleProgram(); + }); + + //var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + + //if (SynchronizationContext.Current == null) + //{ + // // normal ASP.Net Core environment does not have a synchronization context, + // // no problem with await here, it will be executed on the thread pool + // await test.Host.StartAsync(); + // await test.Host.WaitForShutdownAsync(); + //} + //else + //{ + // // xunit uses it's own SynchronizationContext that allows a maximum thread count + // // equal to the logical cpu count (that is 1 on our single cpu build agents). So + // // when we're trying to await something here, the task get's scheduled to xunit's + // // synchronization context, which is already at it's limit running the test thread + // // so we end up in a deadlock here. + // // solution is to run the await explicitly on the thread pool by using Task.Run + // Task.Run(async () => + // { + // await test.Host.StartAsync(); + // await test.Host.WaitForShutdownAsync(); + // }).Wait(); + //} + + await test.Host.WaitForShutdownAsync(); + + var loggerStore = test.Host.Services.GetRequiredService>().GetTestStore(); + Assert.Collection(loggerStore.Query(), + entry => Assert.Equal("Information: RunAsync started.", entry.Message), + entry => Assert.Equal("Trace: Inside RunAsync of TestMinimalConsoleProgram.", entry.Message), + entry => Assert.Equal("Information: RunAsync completed successfully.", entry.Message) + ); + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestBackgroundService.cs b/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestBackgroundService.cs new file mode 100644 index 0000000..c040a1b --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestBackgroundService.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Codebelt.Bootstrapper.Assets +{ + public class TestBackgroundService : BackgroundService + { + private readonly ILogger _logger; + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly IHostLifetime _hostLifetime; + private IHostLifetimeEvents _events; + + public TestBackgroundService(ILogger logger, IHostApplicationLifetime applicationLifetime, IHostLifetimeEvents events) + { + _logger = logger; + _applicationLifetime = applicationLifetime; + _events = events; + } + + public TimeSpan Elapsed { get; private set; } = TimeSpan.Zero; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var sw = Stopwatch.StartNew(); + _events.OnApplicationStartedCallback += () => + { + sw.Stop(); + Elapsed = sw.Elapsed; + _logger.LogInformation("TestBackgroundService started after {Elapsed}", Elapsed); + }; + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestHostFixture.cs b/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestHostFixture.cs new file mode 100644 index 0000000..82901e6 --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestHostFixture.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Codebelt.Extensions.Xunit.Hosting; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Assets +{ + public class TestHostFixture : GenericHostFixture + { + public override void ConfigureHost(Test hostTest) + { + var hb = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + { + ConfigureCallback(config.Build(), context.HostingEnvironment); + }) + .ConfigureServices((context, services) => + { + Configuration = context.Configuration; + Environment = context.HostingEnvironment; + ConfigureServicesCallback(services); + }) + .ConfigureHostConfiguration(builder => + { + builder.AddInMemoryCollection(new Dictionary + { + { HostDefaults.ApplicationKey, hostTest.CallerType.Assembly.GetName().Name } + }); + }); + ConfigureHostCallback(hb); + Host = hb.Build(); + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestStartup.cs b/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestStartup.cs new file mode 100644 index 0000000..d8461ab --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/Assets/TestStartup.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Assets +{ + public class TestStartup : StartupRoot + { + public TestStartup(IConfiguration configuration, IHostEnvironment environment) : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs new file mode 100644 index 0000000..991b907 --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/BootstrapperLifetimeTest.cs @@ -0,0 +1,62 @@ +using Codebelt.Bootstrapper.Assets; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Xunit.Abstractions; + +namespace Codebelt.Bootstrapper +{ + public class BootstrapperLifetimeTest : Test + { + public BootstrapperLifetimeTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void OnApplicationStartedCallback_ShouldBeInvokedWhenStartingHost() + { + var started = false; + using var test = HostTestFactory.Create(services => + { + services.AddXunitTestLoggingOutputHelperAccessor(); + services.AddXunitTestLogging(TestOutput); + }, hb => + { + hb.UseBootstrapperLifetime(); + hb.UseBootstrapperStartup(); + }, new TestHostFixture()); + + test.Host.Services.GetRequiredService().OnApplicationStartedCallback = () => { started = true; }; + + test.Host.Start(); + + Assert.True(started); + } + + [Fact] + public void OnApplicationStoppingCallback_OnApplicationStoppedCallback_ShouldBeInvokedWhenStoppingHost() + { + var stopping = false; + var stopped = false; + using var test = HostTestFactory.Create(services => + { + services.AddXunitTestLoggingOutputHelperAccessor(); + services.AddXunitTestLogging(TestOutput); + }, hb => + { + hb.UseBootstrapperLifetime(); + hb.UseBootstrapperStartup(); + }, new TestHostFixture()); + + test.Host.Services.GetRequiredService().OnApplicationStoppingCallback = () => { stopping = true; }; + test.Host.Services.GetRequiredService().OnApplicationStoppedCallback = () => { stopped = true; }; + + test.Host.Start(); + test.Host.StopAsync().GetAwaiter().GetResult(); + + Assert.True(stopping && stopped); + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj b/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj new file mode 100644 index 0000000..dc95b98 --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Bootstrapper + + + + + + + diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs new file mode 100644 index 0000000..59d0f3a --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/HostApplicationBuilderExtensionsTest.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +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.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Xunit.Abstractions; +using static System.Net.Mime.MediaTypeNames; + +namespace Codebelt.Bootstrapper +{ + public class HostApplicationBuilderExtensionsTest : Test + { + public HostApplicationBuilderExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void UseBootstrapperLifetime_ShouldRegisterBootstrapperLifetime() + { + var hb = Host.CreateApplicationBuilder(); + hb.UseBootstrapperLifetime(); + var host = hb.Build(); + + var bootstrapperLifetime = host.Services.GetService(); + + Assert.NotNull(bootstrapperLifetime); + Assert.IsType(bootstrapperLifetime); + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs new file mode 100644 index 0000000..abda99d --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/HostBuilderExtensionsTest.cs @@ -0,0 +1,49 @@ +using Codebelt.Bootstrapper.Assets; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Xunit.Abstractions; + +namespace Codebelt.Bootstrapper +{ + public class HostBuilderExtensionsTest : Test + { + public HostBuilderExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void UseBootstrapperLifetime_ShouldRegisterBootstrapperLifetime() + { + using var test = HostTestFactory.Create(services => + { + }, hb => + { + hb.UseBootstrapperLifetime(); + }); + + var bootstrapperLifetime = test.Host.Services.GetService(); + + Assert.NotNull(bootstrapperLifetime); + Assert.IsType(bootstrapperLifetime); + } + + [Fact] + public void UseBootstrapperStartup_ShouldRegisterStartupFactory() + { + using var test = HostTestFactory.Create(services => + { + }, hb => + { + hb.UseBootstrapperStartup(); + }); + + var startupFactory = test.Host.Services.GetService>(); + + Assert.NotNull(startupFactory); + Assert.IsType>(startupFactory); + } + } +} diff --git a/test/Codebelt.Bootstrapper.FunctionalTests/HostedServiceExtensionsTest.cs b/test/Codebelt.Bootstrapper.FunctionalTests/HostedServiceExtensionsTest.cs new file mode 100644 index 0000000..37d4a96 --- /dev/null +++ b/test/Codebelt.Bootstrapper.FunctionalTests/HostedServiceExtensionsTest.cs @@ -0,0 +1,51 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Codebelt.Bootstrapper.Assets; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Xunit.Abstractions; + +namespace Codebelt.Bootstrapper +{ + public class HostedServiceExtensionsTest : Test + { + public HostedServiceExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task WaitForApplicationStartedAnnouncementAsync_MustWaitForApplicationStarted() + { + var timeToWait = TimeSpan.FromMilliseconds(50); + var started = false; + + using var test = HostTestFactory.Create(services => + { + services.AddXunitTestLoggingOutputHelperAccessor(); + services.AddXunitTestLogging(TestOutput); + services.AddHostedService(); + }, hb => + { + hb.UseBootstrapperLifetime(); + }, new TestHostFixture()); + + test.Host.Services.GetRequiredService().OnApplicationStartedCallback += () => + { + Thread.Sleep(timeToWait); + }; + + + var bgs = test.Host.Services.GetRequiredService() as TestBackgroundService; + + await test.Host.StartAsync().ConfigureAwait(false); + + await Task.Delay(timeToWait).ConfigureAwait(false); + + Assert.True(bgs.Elapsed >= timeToWait, $"{bgs.Elapsed} >= {timeToWait}"); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Tests/Assets/StartupRootUnsafeAccessor.cs b/test/Codebelt.Bootstrapper.Tests/Assets/StartupRootUnsafeAccessor.cs new file mode 100644 index 0000000..fca1272 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Tests/Assets/StartupRootUnsafeAccessor.cs @@ -0,0 +1,15 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Assets +{ + public static class StartupRootUnsafeAccessor + { + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Configuration")] + public static extern IConfiguration GetConfiguration(StartupRoot startup); + + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Environment")] + public static extern IHostEnvironment GetEnvironment(StartupRoot startup); + } +} diff --git a/test/Codebelt.Bootstrapper.Tests/Assets/TestStartupRoot.cs b/test/Codebelt.Bootstrapper.Tests/Assets/TestStartupRoot.cs new file mode 100644 index 0000000..34df4c4 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Tests/Assets/TestStartupRoot.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Codebelt.Bootstrapper.Assets +{ + public class TestStartupRoot : StartupRoot + { + public TestStartupRoot(IConfiguration configuration, IHostEnvironment environment) + : base(configuration, environment) + { + } + + public override void ConfigureServices(IServiceCollection services) + { + // Add test services here + services.AddSingleton("TestService"); + } + } +} diff --git a/test/Codebelt.Bootstrapper.Tests/Codebelt.Bootstrapper.Tests.csproj b/test/Codebelt.Bootstrapper.Tests/Codebelt.Bootstrapper.Tests.csproj new file mode 100644 index 0000000..dc95b98 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Tests/Codebelt.Bootstrapper.Tests.csproj @@ -0,0 +1,11 @@ + + + + Codebelt.Bootstrapper + + + + + + + diff --git a/test/Codebelt.Bootstrapper.Tests/StartupRootTest.cs b/test/Codebelt.Bootstrapper.Tests/StartupRootTest.cs new file mode 100644 index 0000000..65d5471 --- /dev/null +++ b/test/Codebelt.Bootstrapper.Tests/StartupRootTest.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.IO; +using Codebelt.Bootstrapper.Assets; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; +using Xunit; +using Xunit.Abstractions; + +namespace Codebelt.Bootstrapper +{ + public class StartupRootTest : Test + { + private readonly IConfiguration _configuration; + private readonly IHostEnvironment _environment; + + public StartupRootTest(ITestOutputHelper output) : base(output) + { + var inMemorySettings = new Dictionary { + {"TestKey", "TestValue"} + }; + + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(inMemorySettings) + .Build(); + + _environment = new HostingEnvironment() + { + EnvironmentName = Environments.Development, + ApplicationName = "TestApp", + ContentRootPath = Directory.GetCurrentDirectory() + }; + } + + [Fact] + public void ConfigurationProperty_ShouldReturnInjectedConfiguration() + { + // Arrange + var startup = new TestStartupRoot(_configuration, _environment); + + // Act + var configuration = StartupRootUnsafeAccessor.GetConfiguration(startup); + + // Assert + Assert.Equal(_configuration, configuration); + } + + + + [Fact] + public void EnvironmentProperty_ShouldReturnInjectedEnvironment() + { + // Arrange + var startup = new TestStartupRoot(_configuration, _environment); + + // Act + var environment = StartupRootUnsafeAccessor.GetEnvironment(startup); + + // Assert + Assert.Equal(_environment, environment); + } + + [Fact] + public void ConfigureServices_ShouldAddServicesToServiceCollection() + { + // Arrange + var services = new ServiceCollection(); + var startup = new TestStartupRoot(_configuration, _environment); + + // Act + startup.ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + var testService = serviceProvider.GetService(); + + // Assert + Assert.Equal("TestService", testService); + } + } +} diff --git a/testenvironments.json b/testenvironments.json new file mode 100644 index 0000000..095d438 --- /dev/null +++ b/testenvironments.json @@ -0,0 +1,15 @@ +{ + "version": "1", + "environments": [ + { + "name": "WSL-Ubuntu", + "type": "wsl", + "wslDistribution": "Ubuntu-24.04" + }, + { + "name": "Docker-Ubuntu", + "type": "docker", + "dockerImage": "gimlichael/ubuntu-testrunner:net8.0.408-9.0.203" + } + ] +}