Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .docfx/Dockerfile.docfx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG NGINX_VERSION=1.31.2-alpine
ARG NGINX_VERSION=1.31-alpine

FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base
RUN rm -rf /usr/share/nginx/html/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ example:
---
The static `BootstrapperLogMessages` class is the host's source of truth for the four lifecycle events that `ConsoleHostedService<TStartup>` and `MinimalConsoleHostedService` log through `Decorator.EncloseToExpose(logger, false)`: `RunAsyncStarted` when the run loop begins, `RunAsyncPrematureEnd` when the host stops before the run finishes, `RunAsyncCompleted` when the run finishes cleanly, and `FatalErrorActivating` when the run loop's `catch` block sees an exception. The companion `UnableToActivateInstance` extension surfaces a warning when the bootstrapper could not resolve a startup or program type. To use them from your own `ConsoleStartup` or `MinimalConsoleProgram`, build an `ILogger<T>` through your normal `ServiceCollection` configuration and wrap it with `Decorator.EncloseToExpose(logger, false)`; the resulting `IDecorator<ILogger>` exposes all five extensions through receiver-style calls. The example below wires a console logger, demonstrates the run-loop happy path, exercises the failure path, and shows the startup-resolution warning so every extension in this class is invoked in a single coherent workflow.

> [!NOTE]
> The `BootstrapperLogMessages` class is not intended to be used directly. That is why it's hidden behind the `Decorator.EncloseToExpose(logger, false)` call. The extensions are intended to be used through the `IDecorator<ILogger>` interface and only internal to the `ConsoleHostedService<TStartup>` and `MinimalConsoleHostedService` classes.

```csharp
using System;
using Codebelt.Bootstrapper;
Expand Down
28 changes: 28 additions & 0 deletions .docfx/api/types/Codebelt.Bootstrapper.CommandLineContext.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
uid: Codebelt.Bootstrapper.CommandLineContext
example:
- *content
---
The following example shows how to wrap the `args` array once at the start of `Main`, then let the rest of the bootstrapper flow inspect `CommandLineContext.Args` for switches without passing the raw array around. The outcome changes when the `--debug` switch is present, so the captured arguments directly control the startup mode.

```csharp
using System;
using System.Linq;
using Codebelt.Bootstrapper;

namespace CommandLineContextDemo;

public static class Program
{
public static void Main(string[] args)
{
var context = new CommandLineContext(args);

var mode = context.Args.Contains("--debug", StringComparer.OrdinalIgnoreCase)
? "Debug bootstrap enabled."
: "Standard bootstrap enabled.";

Console.WriteLine(mode);
}
}
```
9 changes: 9 additions & 0 deletions .nuget/Codebelt.Bootstrapper.Console/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
Version: 5.2.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

# Improvements
- REFACTORED ConsoleHostedService and MinimalConsoleHostedService to extract nested callback logic into focused private methods for improved code clarity, testability, and maintainability

Version: 5.1.2
Availability: .NET 10 and .NET 9

Expand Down
6 changes: 6 additions & 0 deletions .nuget/Codebelt.Bootstrapper.Web/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version: 5.2.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

Version: 5.1.2
Availability: .NET 10 and .NET 9

Expand Down
6 changes: 6 additions & 0 deletions .nuget/Codebelt.Bootstrapper.Worker/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Version: 5.2.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

Version: 5.1.2
Availability: .NET 10 and .NET 9

Expand Down
9 changes: 9 additions & 0 deletions .nuget/Codebelt.Bootstrapper/PackageReleaseNotes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
Version: 5.2.0
Availability: .NET 10 and .NET 9

# ALM
- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)

# New Features
- ADDED CommandLineContext class in the Codebelt.Bootstrapper namespace to encapsulate command-line arguments for convenient access

Version: 5.1.2
Availability: .NET 10 and .NET 9

Expand Down
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder.

## [5.2.0] - 2026-07-27

This is a minor release introducing command-line context infrastructure, comprehensive test coverage for console hosted services, dependency updates, and build configuration hardening.

### Added

- CommandLineContext class in the Codebelt.Bootstrapper namespace to encapsulate command-line arguments for convenient access,
- Comprehensive unit test coverage for CommandLineContext, ConsoleHostedService, and MinimalConsoleHostedService to ensure robust argument handling and service lifecycle management.

### Changed

- All NuGet dependencies upgraded to latest stable releases: Codebelt.Extensions.Swashbuckle.AspNetCore to 10.2.4, Codebelt.Extensions.Xunit.App to 11.1.2, Cuemon.Core and Cuemon.Extensions.Hosting to 10.5.5, Microsoft.NET.Test.Sdk to 18.8.1, and all Microsoft.AspNetCore and Microsoft.Extensions packages for net9 (9.0.18) and net10 (10.0.10) to latest patch releases,
- Build analyzer configuration enhanced with warning suppressions (7035, CA2260, S6618), code style enforcement enabled in build process, and MinVer tag prefix configured to 'v',
- ConsoleHostedService and MinimalConsoleHostedService refactored to extract nested callback logic into focused private methods (StartRunAsync, RunAsync, ResolveLogger, LogRunAsyncStarted, LogUnableToActivate, LogFatalError) for improved code clarity, testability, and maintainability,
- DocFX nginx base image adjusted to 1.31-alpine to use stable release channel for documentation build infrastructure.

## [5.1.2] - 2026-07-01

This is a patch release focused on dependency upgrades for security and stability, comprehensive API documentation enhancements, and CI pipeline robustness improvements.
Expand Down Expand Up @@ -263,7 +279,7 @@ Highlighted features included in this release:
- WorkerProgram class in the Codebelt.Bootstrapper.Worker namespace that is the base entry point of an application responsible for registering its WorkerStartup partner
- WorkerStartup interface in the Codebelt.Bootstrapper.Worker namespace that provides the base class of a conventional based Startup class for a console application

[Unreleased]: https://github.com/codebeltnet/bootstrapper/compare/v5.1.2...HEAD
[5.2.0]: https://github.com/codebeltnet/bootstrapper/compare/v5.1.2...v5.2.0
[5.1.2]: https://github.com/codebeltnet/bootstrapper/compare/v5.1.1...v5.1.2
[5.1.1]: https://github.com/codebeltnet/bootstrapper/compare/v5.1.0...v5.1.1
[5.1.0]: https://github.com/codebeltnet/bootstrapper/compare/v5.0.7...v5.1.0
Expand Down
1 change: 1 addition & 0 deletions Codebelt.Bootstrapper.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Project Path="src/Codebelt.Bootstrapper/Codebelt.Bootstrapper.csproj" />
</Folder>
<Folder Name="/test/">
<Project Path="test/Codebelt.Bootstrapper.Console.Tests/Codebelt.Bootstrapper.Console.Tests.csproj" />
<Project Path="test/Codebelt.Bootstrapper.Console.FunctionalTests/Codebelt.Bootstrapper.Console.FunctionalTests.csproj" />
<Project Path="test/Codebelt.Bootstrapper.FunctionalTests/Codebelt.Bootstrapper.FunctionalTests.csproj" />
<Project Path="test/Codebelt.Bootstrapper.Tests/Codebelt.Bootstrapper.Tests.csproj" />
Expand Down
7 changes: 5 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>Recommended</AnalysisMode>
<NoWarn>7035,CA2260,S6618</NoWarn>
<MinVerTagPrefix>v</MinVerTagPrefix>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

<ItemGroup Condition="'$(NuGetPackageRoot)' != ''">
Expand All @@ -57,7 +60,7 @@
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
<SonarQubeExclude>true</SonarQubeExclude>
<WarningLevel>0</WarningLevel>
<AnalysisLevel>none</AnalysisLevel>
<AnalysisMode>none</AnalysisMode>
<NoWarn>NU1701,NETSDK1206</NoWarn>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<UseMicrosoftTestingPlatformRunner>true</UseMicrosoftTestingPlatformRunner>
Expand All @@ -71,7 +74,7 @@
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
<SonarQubeExclude>true</SonarQubeExclude>
<WarningLevel>0</WarningLevel>
<AnalysisLevel>none</AnalysisLevel>
<AnalysisMode>none</AnalysisMode>
<NoWarn>NU1701,NETSDK1206</NoWarn>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
</PropertyGroup>
Expand Down
30 changes: 15 additions & 15 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Codebelt.Extensions.Swashbuckle.AspNetCore" Version="10.2.3" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.1.1" />
<PackageVersion Include="Cuemon.Core" Version="10.5.4" />
<PackageVersion Include="Cuemon.Extensions.Hosting" Version="10.5.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Codebelt.Extensions.Swashbuckle.AspNetCore" Version="10.2.4" />
<PackageVersion Include="Codebelt.Extensions.Xunit.App" Version="11.1.2" />
<PackageVersion Include="Cuemon.Core" Version="10.5.5" />
<PackageVersion Include="Cuemon.Extensions.Hosting" Version="10.5.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="MinVer" Version="7.0.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="coverlet.msbuild" Version="10.0.1" />
Expand All @@ -16,17 +16,17 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net9'))">
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.17" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.17" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.17" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.17" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.17" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.18" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.18" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.18" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.18" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.18" />
</ItemGroup>
<ItemGroup Condition="$(TargetFramework.StartsWith('net10'))">
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
</ItemGroup>
</Project>
58 changes: 38 additions & 20 deletions src/Codebelt.Bootstrapper.Console/ConsoleHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,37 +49,55 @@ public ConsoleHostedService(IStartupFactory<TStartup> factory, IHostApplicationL
public Task StartAsync(CancellationToken cancellationToken)
{
_logger = _provider.GetRequiredService<ILogger<TStartup>>();
var startup = _factory.Instance;
TStartup startup = _factory.Instance;
if (startup != null)
{
startup.ConfigureConsole(_provider);
_events.OnApplicationStartedCallback += () =>
{
_runAsyncTask = Task.Run(async () =>
{
try
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).RunAsyncStarted(); }
await startup.RunAsync(_provider, cancellationToken).ConfigureAwait(false);
_ranToCompletion = true;
}
catch (Exception e)
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).FatalErrorActivating(typeof(TStartup).FullName, e); }
}
}, cancellationToken);

StartWaitForCompletionOfRunAsync().ConfigureAwait(false);
};
_events.OnApplicationStartedCallback += () => StartRunAsync(startup, cancellationToken);
}
else
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).UnableToActivateInstance(typeof(TStartup).FullName); }
LogUnableToActivate(typeof(TStartup).FullName);
}

return Task.CompletedTask;
}

private void StartRunAsync(TStartup startup, CancellationToken cancellationToken)
{
_runAsyncTask = Task.Run(() => RunAsync(startup, cancellationToken), cancellationToken);
_ = StartWaitForCompletionOfRunAsync();
}

private async Task RunAsync(TStartup startup, CancellationToken cancellationToken)
{
try
{
LogRunAsyncStarted();
await startup.RunAsync(_provider, cancellationToken).ConfigureAwait(false);
_ranToCompletion = true;
}
catch (Exception e)
{
LogFatalError(typeof(TStartup).FullName, e);
}
}

private void LogRunAsyncStarted()
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).RunAsyncStarted(); }
}

private void LogUnableToActivate(string typeFullName)
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).UnableToActivateInstance(typeFullName); }
}

private void LogFatalError(string typeFullName, Exception exception)
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).FatalErrorActivating(typeFullName, exception); }
}

private async Task StartWaitForCompletionOfRunAsync()
{
await _runAsyncTask.ConfigureAwait(false);
Expand Down
77 changes: 49 additions & 28 deletions src/Codebelt.Bootstrapper.Console/MinimalConsoleHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,39 +47,60 @@ public MinimalConsoleHostedService(IProgramFactory factory, IHostApplicationLife
/// <returns>A <see cref="Task" /> that represents the asynchronous operation.</returns>
public Task StartAsync(CancellationToken cancellationToken)
{
_events.OnApplicationStartedCallback += () =>
{
var program = _factory.Instance;
var programType = program?.GetType() ?? typeof(MinimalConsoleProgram);
var loggerType = typeof(ILogger<>).MakeGenericType(programType);
_events.OnApplicationStartedCallback += () => StartRunAsync(cancellationToken);

return Task.CompletedTask;
}

private void StartRunAsync(CancellationToken cancellationToken)
{
MinimalConsoleProgram program = _factory.Instance;
Type programType = program?.GetType() ?? typeof(MinimalConsoleProgram);

_logger = _provider.GetRequiredService(loggerType) as ILogger;
_logger = ResolveLogger(programType);
_runAsyncTask = Task.Run(() => RunAsync(program, programType, cancellationToken), cancellationToken);
_ = StartWaitForCompletionOfRunAsync();
}

private ILogger ResolveLogger(Type programType)
{
Type loggerType = typeof(ILogger<>).MakeGenericType(programType);
return _provider.GetRequiredService(loggerType) as ILogger;
}

_runAsyncTask = Task.Run(async () =>
private async Task RunAsync(MinimalConsoleProgram program, Type programType, CancellationToken cancellationToken)
{
try
{
if (program == null)
{
try
{
if (program != null)
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).RunAsyncStarted(); }
await program.RunAsync(_provider, cancellationToken).ConfigureAwait(false);
_ranToCompletion = true;
}
else
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).UnableToActivateInstance(programType.FullName); }
}
}
catch (Exception e)
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).FatalErrorActivating(programType.FullName, e); }
}
}, cancellationToken);
LogUnableToActivate(programType.FullName);
return;
}

StartWaitForCompletionOfRunAsync().ConfigureAwait(false);
};
LogRunAsyncStarted();
await program.RunAsync(_provider, cancellationToken).ConfigureAwait(false);
_ranToCompletion = true;
}
catch (Exception e)
{
LogFatalError(programType.FullName, e);
}
}

return Task.CompletedTask;
private void LogRunAsyncStarted()
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).RunAsyncStarted(); }
}

private void LogUnableToActivate(string typeFullName)
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).UnableToActivateInstance(typeFullName); }
}

private void LogFatalError(string typeFullName, Exception exception)
{
if (!_suppressStatusMessages) { Decorator.EncloseToExpose(_logger, false).FatalErrorActivating(typeFullName, exception); }
}

private async Task StartWaitForCompletionOfRunAsync()
Expand Down
29 changes: 29 additions & 0 deletions src/Codebelt.Bootstrapper/CommandLineContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace Codebelt.Bootstrapper;

/// <summary>
/// Represents the command-line arguments available to a bootstrapper.
/// </summary>
public class CommandLineContext
{
/// <summary>
/// Initializes a new instance of the <see cref="CommandLineContext"/> class
/// with the specified command-line arguments.
/// </summary>
/// <param name="args">
/// The command-line arguments to expose through <see cref="Args"/>.
/// If <see langword="null"/>, <see cref="Args"/> is initialized to an empty array.
/// </param>
public CommandLineContext(string[] args)
{
Args = args ?? [];
}

/// <summary>
/// Gets the command-line arguments associated with this context.
/// </summary>
/// <value>
/// The array supplied to the constructor, or an empty array when the supplied value was
/// <see langword="null"/>. The supplied array is retained without creating a copy.
/// </value>
public string[] Args { get; }
}
Loading
Loading