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"
+ }
+ ]
+}