Skip to content

Commit d7d5957

Browse files
authored
Add solution with initial setup (#2)
Created a solution that contains a console project and implemented dependency injection via HostBuilder. Furthermore, some startup logging was setup, whereby the Microsoft Console logger was configured for usage via dependency injection, based on appsettings.json file configuration. In addition, the console app got some dirty pseudo worker code added for simulating that something happens and to check if the cancellation implementation works.
1 parent c00f76a commit d7d5957

11 files changed

+281
-1
lines changed

README.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
11
# ConnyConsole
2-
ConnyConsole is a console CLI playground for testing different ways of console implementations incl. argument parsing. Maybe, somewhen it becomes a CLI sample collection.
2+
ConnyConsole is a console CLI project that uses `System.CommandLine` from Microsoft for argument parsing to collect some experience with this library.
3+
4+
Following some references/documentation:
5+
- [Parse the Command Line with System.CommandLine][1]
6+
- [System.CommandLine overview][2]
7+
- [System.CommandLine on GitHub][3]
8+
- [Tutorial: Get started with System.CommandLine][4]
9+
10+
<!--# references --->
11+
[1]: https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/march/net-parse-the-command-line-with-system-commandline "Parse the Command Line with System.CommandLine"
12+
[2]: https://learn.microsoft.com/en-us/dotnet/standard/commandline/ "System.CommandLine overview"
13+
[3]: https://github.com/dotnet/command-line-api "GitHub: dotnet > command-line-api"
14+
[4]: https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial "Tutorial: Get started with System.CommandLine"

src/ConnyConsole.sln

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConnyConsole", "ConnyConsole/ConnyConsole.csproj", "{88969CC9-34FC-4668-902F-9D5354CE92E2}"
4+
EndProject
5+
Global
6+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7+
Debug|Any CPU = Debug|Any CPU
8+
Release|Any CPU = Release|Any CPU
9+
EndGlobalSection
10+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
11+
{88969CC9-34FC-4668-902F-9D5354CE92E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12+
{88969CC9-34FC-4668-902F-9D5354CE92E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
13+
{88969CC9-34FC-4668-902F-9D5354CE92E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
14+
{88969CC9-34FC-4668-902F-9D5354CE92E2}.Release|Any CPU.Build.0 = Release|Any CPU
15+
EndGlobalSection
16+
EndGlobal

src/ConnyConsole.sln.DotSettings

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
2+
xmlns:s="clr-namespace:System;assembly=mscorlib"
3+
xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml"
4+
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
5+
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
6+
</wpf:ResourceDictionary>

src/ConnyConsole/App.cs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using ConnyConsole.Infrastructure;
2+
using ConnyConsole.Settings;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Options;
5+
6+
namespace ConnyConsole;
7+
8+
public class App
9+
{
10+
private readonly AppSettings _appSettings;
11+
private readonly CancellationTokenFactory _cancellationTokenFactory;
12+
private readonly ILogger<App> _logger;
13+
14+
public App(IOptions<AppSettings> appSettings, CancellationTokenFactory cancellationTokenFactory, ILogger<App> logger)
15+
{
16+
_appSettings = appSettings.Value;
17+
_cancellationTokenFactory = cancellationTokenFactory;
18+
_logger = logger;
19+
20+
RegisterCancellation();
21+
}
22+
23+
public Task<int> RunAsync()
24+
{
25+
while (!_cancellationTokenFactory.CancellationToken.IsCancellationRequested)
26+
{
27+
_logger.LogInformation("I'm working every {LoopOutputInterval} seconds...",
28+
_appSettings.LoopOutputInterval.TotalSeconds);
29+
30+
// just some dirty pseudo work
31+
Thread.Sleep(_appSettings.LoopOutputInterval);
32+
}
33+
34+
_logger.LogInformation("Bye bye!");
35+
36+
return Task.FromResult(0);
37+
}
38+
39+
private void RegisterCancellation()
40+
{
41+
Console.CancelKeyPress +=
42+
_cancellationTokenFactory.CreateHandler(_appSettings.CancellationTimeout);
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Debug",
5+
"System": "Information",
6+
"Microsoft": "Information"
7+
},
8+
"Console": {
9+
"LogLevel": {
10+
"Default": "Debug",
11+
"Microsoft": "Information"
12+
},
13+
"FormatterOptions": {
14+
"IncludeScopes": true
15+
}
16+
}
17+
}
18+
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"System": "Warning",
6+
"Microsoft": "Warning"
7+
},
8+
"Console": {
9+
"LogLevel": {
10+
"Default": "Information",
11+
"Microsoft": "Warning",
12+
"Microsoft.Hosting.Lifetime": "Information"
13+
},
14+
"FormatterName": "console",
15+
"FormatterOptions": {
16+
"SingleLine": true,
17+
"IncludeScopes": false,
18+
"TimestampFormat": "HH:mm:ss.fff "
19+
}
20+
}
21+
},
22+
"AppSettings": {
23+
"LoopOutputInterval": "00:00:02",
24+
"CancellationTimeout": "00:00:03"
25+
}
26+
}

src/ConnyConsole/ConnyConsole.csproj

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.1.25080.5" />
12+
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<None Update="Config\appsettings.json">
17+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
18+
</None>
19+
<None Update="Config\appsettings.Development.json">
20+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
21+
</None>
22+
</ItemGroup>
23+
24+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using ConnyConsole.Infrastructure;
2+
using ConnyConsole.Settings;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace ConnyConsole.Extensions;
8+
9+
public static class ServiceCollectionExtensions
10+
{
11+
// ReSharper disable once UnusedMethodReturnValue.Global
12+
public static IServiceCollection AddConfiguration(this IServiceCollection services, HostBuilderContext hostContext)
13+
{
14+
services.Configure<AppSettings>(hostContext.Configuration.GetSection(AppSettings.SectionName));
15+
16+
services.AddLogging(builder => builder.AddConsole());
17+
18+
services.AddTransient<CancellationTokenFactory>();
19+
services.AddTransient<App>();
20+
21+
return services;
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// This class function is based on https://medium.com/@sawyer.watts/a-beginners-guide-to-net-s-hostbuilder-part-2-cancellation-857ae3e6ff02
2+
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace ConnyConsole.Infrastructure;
6+
7+
public sealed class CancellationTokenFactory(ILogger<CancellationTokenFactory> logger)
8+
{
9+
private bool _gracefulCancel = true;
10+
private readonly CancellationTokenSource _cancellationTokenSource = new();
11+
12+
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
13+
14+
/// <summary>
15+
/// Creates a <see cref="ConsoleCancelEventHandler"/> for a gracefully (first Ctrl+C) or forced (second Ctrl+C) application exit.
16+
/// It can be registered on the <see cref="Console.CancelKeyPress"/> event.
17+
/// </summary>
18+
/// <param name="timeout">The timeout after which the app is forcibly terminated.</param>
19+
/// <returns>The configured <see cref="ConsoleCancelEventHandler"/> event.</returns>
20+
public ConsoleCancelEventHandler CreateHandler(TimeSpan timeout)
21+
{
22+
return (_, cancelEvent) =>
23+
{
24+
if (_gracefulCancel)
25+
{
26+
logger.LogInformation(
27+
$"Received interrupt signal, attempting to shut down gracefully but will force-close in {timeout.TotalSeconds} seconds. Send again to immediately force-close.");
28+
29+
_cancellationTokenSource.Cancel();
30+
cancelEvent.Cancel = true;
31+
_gracefulCancel = false;
32+
33+
ForceExitAfterTimeout((int)timeout.TotalMilliseconds);
34+
}
35+
else
36+
{
37+
logger.LogInformation("Second interrupt received, force-closing the app");
38+
}
39+
};
40+
}
41+
42+
/// <summary>
43+
/// Waits a defined timeout in milliseconds, afterward enforces the application exit.
44+
/// </summary>
45+
private void ForceExitAfterTimeout(int timeoutInMilliseconds)
46+
{
47+
_ = new Timer(
48+
_ =>
49+
{
50+
logger.LogInformation("Timeout reached, force-closing app.");
51+
Environment.Exit(0);
52+
},
53+
state: null,
54+
dueTime: timeoutInMilliseconds,
55+
period: 0);
56+
}
57+
}

src/ConnyConsole/Program.cs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using ConnyConsole;
2+
using ConnyConsole.Extensions;
3+
using Microsoft.Extensions.Configuration;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Hosting;
6+
using Microsoft.Extensions.Logging;
7+
8+
var logger = LoggerFactory.Create(config =>
9+
config.AddSimpleConsole(options =>
10+
{
11+
options.SingleLine = true;
12+
options.TimestampFormat = "HH:mm:ss.fff ";
13+
}))
14+
.CreateLogger<Program>();
15+
16+
logger.LogInformation("Starting application...");
17+
18+
int exitCode;
19+
try
20+
{
21+
var host = Host.CreateDefaultBuilder(args)
22+
.ConfigureAppConfiguration((hostContext, hostConfig) =>
23+
{
24+
var env = hostContext.HostingEnvironment;
25+
26+
hostConfig.AddJsonFile("Config/appsettings.json", optional: false, reloadOnChange: true);
27+
hostConfig.AddJsonFile($"Config/appsettings.{env.EnvironmentName}.json", optional: true,
28+
reloadOnChange: true);
29+
})
30+
.ConfigureServices((hostContext, services) => services.AddConfiguration(hostContext))
31+
.Build();
32+
33+
var app = host.Services.GetRequiredService<App>();
34+
exitCode = await app.RunAsync().ConfigureAwait(false);
35+
}
36+
catch (Exception e)
37+
{
38+
logger.LogError(e, "Failed to start application");
39+
exitCode = -1;
40+
}
41+
42+
logger.LogInformation("Application shutdown.");
43+
44+
return exitCode;
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace ConnyConsole.Settings;
2+
3+
public class AppSettings
4+
{
5+
public const string SectionName = "AppSettings";
6+
7+
public TimeSpan LoopOutputInterval { get; init; }
8+
9+
public TimeSpan CancellationTimeout { get; init; }
10+
}

0 commit comments

Comments
 (0)