diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6210625..f87933f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,5 +28,8 @@ jobs: - name: Build run: dotnet build --no-restore --configuration '${{ env.BUILD_CONFIGURATION }}' + - name: Run check-deps in Demo Application + run: dotnet run --project Neolution.DotNet.Console.Demo --no-build --configuration '${{ env.BUILD_CONFIGURATION }}' -- check-deps + - name: Test run: dotnet test --no-build --verbosity normal --configuration '${{ env.BUILD_CONFIGURATION }}' diff --git a/CHANGELOG.md b/CHANGELOG.md index b570625..96c89fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added `DotNetConsoleLogger.cs` static class to provide initialization, access, and shutdown for the logger instance used in console applications. + +### Changed + +- Updated Scrutor to v6.1.0. +- Updated Microsoft.Extensions packages to latest patch versions. + +### Removed + +- Removed obsolete serialization constructor and `[Serializable]` attribute from `DotNetConsoleException` as formatter-based serialization is no longer supported or recommended in modern .NET. + ## [5.0.0] - 2025-01-27 ### Added diff --git a/Neolution.DotNet.Console.SampleAsync/Commands/Echo/EchoCommand.cs b/Neolution.DotNet.Console.Demo/Commands/Echo/EchoCommand.cs similarity index 96% rename from Neolution.DotNet.Console.SampleAsync/Commands/Echo/EchoCommand.cs rename to Neolution.DotNet.Console.Demo/Commands/Echo/EchoCommand.cs index 6f68bb9..3b52e0c 100644 --- a/Neolution.DotNet.Console.SampleAsync/Commands/Echo/EchoCommand.cs +++ b/Neolution.DotNet.Console.Demo/Commands/Echo/EchoCommand.cs @@ -1,4 +1,4 @@ -namespace Neolution.DotNet.Console.SampleAsync.Commands.Echo +namespace Neolution.DotNet.Console.Demo.Commands.Echo { using System; using Microsoft.Extensions.Configuration; diff --git a/Neolution.DotNet.Console.SampleAsync/Commands/Echo/EchoOptions.cs b/Neolution.DotNet.Console.Demo/Commands/Echo/EchoOptions.cs similarity index 85% rename from Neolution.DotNet.Console.SampleAsync/Commands/Echo/EchoOptions.cs rename to Neolution.DotNet.Console.Demo/Commands/Echo/EchoOptions.cs index 16f41f0..582aeab 100644 --- a/Neolution.DotNet.Console.SampleAsync/Commands/Echo/EchoOptions.cs +++ b/Neolution.DotNet.Console.Demo/Commands/Echo/EchoOptions.cs @@ -1,4 +1,4 @@ -namespace Neolution.DotNet.Console.SampleAsync.Commands.Echo +namespace Neolution.DotNet.Console.Demo.Commands.Echo { using CommandLine; diff --git a/Neolution.DotNet.Console.SampleAsync/Commands/GuidGen/GuidGenCommand.cs b/Neolution.DotNet.Console.Demo/Commands/GuidGen/GuidGenCommand.cs similarity index 92% rename from Neolution.DotNet.Console.SampleAsync/Commands/GuidGen/GuidGenCommand.cs rename to Neolution.DotNet.Console.Demo/Commands/GuidGen/GuidGenCommand.cs index a866aa4..f3eca57 100644 --- a/Neolution.DotNet.Console.SampleAsync/Commands/GuidGen/GuidGenCommand.cs +++ b/Neolution.DotNet.Console.Demo/Commands/GuidGen/GuidGenCommand.cs @@ -1,4 +1,4 @@ -namespace Neolution.DotNet.Console.SampleAsync.Commands.GuidGen +namespace Neolution.DotNet.Console.Demo.Commands.GuidGen { using System; using System.Globalization; @@ -41,7 +41,7 @@ public Task RunAsync(GuidGenOptions options, CancellationToken cancellationToken result = result.ToUpperInvariant(); } - System.Console.WriteLine(result); + Console.WriteLine(result); this.logger.LogTrace("Wrote result to console"); return Task.CompletedTask; diff --git a/Neolution.DotNet.Console.SampleAsync/Commands/GuidGen/GuidGenOptions.cs b/Neolution.DotNet.Console.Demo/Commands/GuidGen/GuidGenOptions.cs similarity index 87% rename from Neolution.DotNet.Console.SampleAsync/Commands/GuidGen/GuidGenOptions.cs rename to Neolution.DotNet.Console.Demo/Commands/GuidGen/GuidGenOptions.cs index 20ef17a..494873a 100644 --- a/Neolution.DotNet.Console.SampleAsync/Commands/GuidGen/GuidGenOptions.cs +++ b/Neolution.DotNet.Console.Demo/Commands/GuidGen/GuidGenOptions.cs @@ -1,7 +1,7 @@ -namespace Neolution.DotNet.Console.SampleAsync.Commands.GuidGen +namespace Neolution.DotNet.Console.Demo.Commands.GuidGen { using CommandLine; - using Neolution.DotNet.Console.SampleAsync.Commands.Echo; + using Neolution.DotNet.Console.Demo.Commands.Echo; /// /// The options for the . diff --git a/Neolution.DotNet.Console.SampleAsync/Commands/Start/StartCommand.cs b/Neolution.DotNet.Console.Demo/Commands/Start/StartCommand.cs similarity index 97% rename from Neolution.DotNet.Console.SampleAsync/Commands/Start/StartCommand.cs rename to Neolution.DotNet.Console.Demo/Commands/Start/StartCommand.cs index d12d104..47ca39b 100644 --- a/Neolution.DotNet.Console.SampleAsync/Commands/Start/StartCommand.cs +++ b/Neolution.DotNet.Console.Demo/Commands/Start/StartCommand.cs @@ -1,4 +1,4 @@ -namespace Neolution.DotNet.Console.SampleAsync.Commands.Start +namespace Neolution.DotNet.Console.Demo.Commands.Start { using System; using Microsoft.Extensions.Logging; diff --git a/Neolution.DotNet.Console.SampleAsync/Commands/Start/StartOptions.cs b/Neolution.DotNet.Console.Demo/Commands/Start/StartOptions.cs similarity index 85% rename from Neolution.DotNet.Console.SampleAsync/Commands/Start/StartOptions.cs rename to Neolution.DotNet.Console.Demo/Commands/Start/StartOptions.cs index 4b12266..4861bba 100644 --- a/Neolution.DotNet.Console.SampleAsync/Commands/Start/StartOptions.cs +++ b/Neolution.DotNet.Console.Demo/Commands/Start/StartOptions.cs @@ -1,4 +1,4 @@ -namespace Neolution.DotNet.Console.SampleAsync.Commands.Start +namespace Neolution.DotNet.Console.Demo.Commands.Start { using CommandLine; diff --git a/Neolution.DotNet.Console.SampleAsync/Dockerfile b/Neolution.DotNet.Console.Demo/Dockerfile similarity index 82% rename from Neolution.DotNet.Console.SampleAsync/Dockerfile rename to Neolution.DotNet.Console.Demo/Dockerfile index 51a39b4..fe0faaa 100644 --- a/Neolution.DotNet.Console.SampleAsync/Dockerfile +++ b/Neolution.DotNet.Console.Demo/Dockerfile @@ -12,4 +12,4 @@ RUN dotnet publish -c Release -o out FROM mcr.microsoft.com/dotnet/runtime:6.0 WORKDIR /app COPY --from=build-env /app/out . -ENTRYPOINT ["dotnet", "Neolution.DotNet.Console.SampleAsync.dll"] +ENTRYPOINT ["dotnet", "Neolution.DotNet.Console.Demo.dll"] diff --git a/Neolution.DotNet.Console.SampleAsync/Neolution.DotNet.Console.SampleAsync.csproj b/Neolution.DotNet.Console.Demo/Neolution.DotNet.Console.Demo.csproj similarity index 95% rename from Neolution.DotNet.Console.SampleAsync/Neolution.DotNet.Console.SampleAsync.csproj rename to Neolution.DotNet.Console.Demo/Neolution.DotNet.Console.Demo.csproj index 13a4e78..37757ff 100644 --- a/Neolution.DotNet.Console.SampleAsync/Neolution.DotNet.Console.SampleAsync.csproj +++ b/Neolution.DotNet.Console.Demo/Neolution.DotNet.Console.Demo.csproj @@ -1,4 +1,4 @@ - + Exe @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Neolution.DotNet.Console.Demo/Program.cs b/Neolution.DotNet.Console.Demo/Program.cs new file mode 100644 index 0000000..305c94d --- /dev/null +++ b/Neolution.DotNet.Console.Demo/Program.cs @@ -0,0 +1,37 @@ +namespace Neolution.DotNet.Console.Demo +{ + /// + /// The program + /// + public static class Program + { + /// + /// Defines the entry point of the application. + /// + /// The arguments. + /// The . + public static async Task Main(string[] args) + { + try + { + var builder = DotNetConsole.CreateDefaultBuilder(args); + DotNetConsoleLogger.Initialize(builder.Configuration); + + var startup = new Startup(builder.Environment, builder.Configuration); + startup.ConfigureServices(builder.Services); + var console = builder.Build(); + await console.RunAsync(); + } + catch (Exception ex) + { + DotNetConsoleLogger.Log.Error(ex, "Stopped program because of an unexpected exception"); + throw; + } + finally + { + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + DotNetConsoleLogger.Shutdown(); + } + } + } +} diff --git a/Neolution.DotNet.Console.SampleAsync/Properties/launchSettings.json b/Neolution.DotNet.Console.Demo/Properties/launchSettings.json similarity index 81% rename from Neolution.DotNet.Console.SampleAsync/Properties/launchSettings.json rename to Neolution.DotNet.Console.Demo/Properties/launchSettings.json index b3628c5..cfae5ad 100644 --- a/Neolution.DotNet.Console.SampleAsync/Properties/launchSettings.json +++ b/Neolution.DotNet.Console.Demo/Properties/launchSettings.json @@ -1,12 +1,12 @@ { "profiles": { - "Sample Console App": { + "Demo Console App": { "commandName": "Project", "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } }, - "Sample Console App: check-deps": { + "Demo Console App: check-deps": { "commandName": "Project", "commandLineArgs": "check-deps", "environmentVariables": { diff --git a/Neolution.DotNet.Console.SampleAsync/Startup.cs b/Neolution.DotNet.Console.Demo/Startup.cs similarity index 97% rename from Neolution.DotNet.Console.SampleAsync/Startup.cs rename to Neolution.DotNet.Console.Demo/Startup.cs index 672b630..0756364 100644 --- a/Neolution.DotNet.Console.SampleAsync/Startup.cs +++ b/Neolution.DotNet.Console.Demo/Startup.cs @@ -1,4 +1,4 @@ -namespace Neolution.DotNet.Console.SampleAsync +namespace Neolution.DotNet.Console.Demo { using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/Neolution.DotNet.Console.SampleAsync/appsettings.Development.json b/Neolution.DotNet.Console.Demo/appsettings.Development.json similarity index 100% rename from Neolution.DotNet.Console.SampleAsync/appsettings.Development.json rename to Neolution.DotNet.Console.Demo/appsettings.Development.json diff --git a/Neolution.DotNet.Console.SampleAsync/appsettings.Production.json b/Neolution.DotNet.Console.Demo/appsettings.Production.json similarity index 100% rename from Neolution.DotNet.Console.SampleAsync/appsettings.Production.json rename to Neolution.DotNet.Console.Demo/appsettings.Production.json diff --git a/Neolution.DotNet.Console.SampleAsync/appsettings.json b/Neolution.DotNet.Console.Demo/appsettings.json similarity index 100% rename from Neolution.DotNet.Console.SampleAsync/appsettings.json rename to Neolution.DotNet.Console.Demo/appsettings.json diff --git a/Neolution.DotNet.Console.SampleAsync/packages.lock.json b/Neolution.DotNet.Console.Demo/packages.lock.json similarity index 94% rename from Neolution.DotNet.Console.SampleAsync/packages.lock.json rename to Neolution.DotNet.Console.Demo/packages.lock.json index 12a7087..046052e 100644 --- a/Neolution.DotNet.Console.SampleAsync/packages.lock.json +++ b/Neolution.DotNet.Console.Demo/packages.lock.json @@ -24,11 +24,11 @@ }, "Neolution.CodeAnalysis": { "type": "Direct", - "requested": "[3.2.1, )", - "resolved": "3.2.1", - "contentHash": "AQDkBJ9e6TrnhpwTtf4KrwlAPBLy77I5XkNey586nN2IFfjk1LVwBwvOcjORD1dSNtEhOf69JQjdmiMf54C90w==", + "requested": "[3.3.0-beta.2, )", + "resolved": "3.3.0-beta.2", + "contentHash": "88mG4f4P16K4Ql3uTqeaoeXGJLNMReFh9z3ZKmKZ0bY4RTk4z4syjuF/TefouGM2hR+mcBSdWbAtsKT/+FowlA==", "dependencies": { - "SonarAnalyzer.CSharp": "9.20.0.85982", + "SonarAnalyzer.CSharp": "9.32.0.97167", "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, @@ -332,8 +332,8 @@ }, "Scrutor": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "5xKT6ND5GqnFzwSaYozHCJe75GFL8sPy4yw/iRFqeBFGlmqPNFpOg1T9Q0Gl2h76Cklt0ZTg6Ypkri5iUBKXsA==", + "resolved": "6.1.0", + "contentHash": "m4+0RdgnX+jeiaqteq9x5SwEtuCjWG0KTw1jBjCzn7V8mCanXKoeF8+59E0fcoRbAjdEq6YqHFCmxZ49Kvqp3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", "Microsoft.Extensions.DependencyModel": "8.0.2" @@ -341,8 +341,8 @@ }, "SonarAnalyzer.CSharp": { "type": "Transitive", - "resolved": "9.20.0.85982", - "contentHash": "c0IYtFg4mYusTafTy0Bs5wev45vRKNShkuoWyQyPMbC6imEFHL7tY/7xbbUJd6JNLYx5vP8wyi2LgiBsMHqb2Q==" + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", @@ -358,18 +358,18 @@ "type": "Project", "dependencies": { "CommandLineParser": "[2.9.1, )", - "Microsoft.Extensions.Configuration.Abstractions": "[8.0.0, )", - "Microsoft.Extensions.Configuration.CommandLine": "[8.0.0, )", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "[8.0.0, )", - "Microsoft.Extensions.Configuration.Json": "[8.0.1, )", - "Microsoft.Extensions.Configuration.UserSecrets": "[8.0.1, )", - "Microsoft.Extensions.DependencyInjection": "[8.0.1, )", - "Microsoft.Extensions.Hosting": "[8.0.1, )", - "Microsoft.Extensions.Logging": "[8.0.1, )", - "Microsoft.Extensions.Logging.Configuration": "[8.0.1, )", + "Microsoft.Extensions.Configuration.Abstractions": "[8.0.*, )", + "Microsoft.Extensions.Configuration.CommandLine": "[8.0.*, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[8.0.*, )", + "Microsoft.Extensions.Configuration.Json": "[8.0.*, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[8.0.*, )", + "Microsoft.Extensions.DependencyInjection": "[8.0.*, )", + "Microsoft.Extensions.Hosting": "[8.0.*, )", + "Microsoft.Extensions.Logging": "[8.0.*, )", + "Microsoft.Extensions.Logging.Configuration": "[8.0.*, )", "NLog": "[5.3.4, )", "NLog.Extensions.Logging": "[5.3.15, )", - "Scrutor": "[6.0.1, )" + "Scrutor": "[6.1.0, )" } } } diff --git a/Neolution.DotNet.Console.SampleAsync/Program.cs b/Neolution.DotNet.Console.SampleAsync/Program.cs deleted file mode 100644 index e47d3a7..0000000 --- a/Neolution.DotNet.Console.SampleAsync/Program.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace Neolution.DotNet.Console.SampleAsync -{ - using System; - using System.Globalization; - using NLog; - using NLog.Extensions.Logging; - - /// - /// The program - /// - public static class Program - { - /// - /// Defines the entry point of the application. - /// - /// The arguments. - /// The . - public static async Task Main(string[] args) - { - var builder = DotNetConsole.CreateDefaultBuilder(args); - var logger = LogManager.Setup().LoadConfigurationFromSection(builder.Configuration).GetCurrentClassLogger(); - - try - { - // Use startup class as composition root - var startup = new Startup(builder.Environment, builder.Configuration); - startup.ConfigureServices(builder.Services); - - // Check if IHostEnvironment and IConfiguration are available before building the app - logger.Debug(CultureInfo.InvariantCulture, message: $"Environment: {builder.Environment.EnvironmentName}"); - logger.Debug(CultureInfo.InvariantCulture, message: $"Setting Value: {builder.Configuration["NLog:throwConfigExceptions"]}"); - - var console = builder.Build(); - await console.RunAsync(); - } - catch (Exception ex) - { - // NLog: catch any exception and log it. - logger.Error(ex, "Stopped program because of an unexpected exception"); - throw; - } - finally - { - // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) - LogManager.Shutdown(); - } - } - } -} diff --git a/Neolution.DotNet.Console.UnitTests/Neolution.DotNet.Console.UnitTests.csproj b/Neolution.DotNet.Console.UnitTests/Neolution.DotNet.Console.UnitTests.csproj index 49dc81d..825ac42 100644 --- a/Neolution.DotNet.Console.UnitTests/Neolution.DotNet.Console.UnitTests.csproj +++ b/Neolution.DotNet.Console.UnitTests/Neolution.DotNet.Console.UnitTests.csproj @@ -14,15 +14,15 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Neolution.DotNet.Console.UnitTests/packages.lock.json b/Neolution.DotNet.Console.UnitTests/packages.lock.json index e281163..6e69ab8 100644 --- a/Neolution.DotNet.Console.UnitTests/packages.lock.json +++ b/Neolution.DotNet.Console.UnitTests/packages.lock.json @@ -16,40 +16,40 @@ }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.12.0, )", - "resolved": "17.12.0", - "contentHash": "kt/PKBZ91rFCWxVIJZSgVLk+YR+4KxTuHf799ho8WNiK5ZQpJNAEZCAWX86vcKrs+DiYjiibpYKdGZP6+/N17w==", + "requested": "[17.14.1, )", + "resolved": "17.14.1", + "contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==", "dependencies": { - "Microsoft.CodeCoverage": "17.12.0", - "Microsoft.TestPlatform.TestHost": "17.12.0" + "Microsoft.CodeCoverage": "17.14.1", + "Microsoft.TestPlatform.TestHost": "17.14.1" } }, "Neolution.CodeAnalysis.TestsRuleset": { "type": "Direct", - "requested": "[3.2.1, )", - "resolved": "3.2.1", - "contentHash": "tjiu8+Zg8rUddWrv5Ltp5PVWEEA3R1hP0zt+ecAgUgZ5Zd3LioArbu+Ut/SzJWY4DH1T9+eUoP3IKz6uDg+F4Q==", + "requested": "[3.3.0-beta.2, )", + "resolved": "3.3.0-beta.2", + "contentHash": "GQXIkpfRlgKFZw+vsQM2wTqRVq10Sq/zcEniQgFGTNZqyBCzogBLzi8FabptRO38yblVPN7YS35IqQ3bJtfp1Q==", "dependencies": { - "SonarAnalyzer.CSharp": "9.20.0.85982", + "SonarAnalyzer.CSharp": "9.32.0.97167", "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, "Objectivity.AutoFixture.XUnit2.AutoNSubstitute": { "type": "Direct", - "requested": "[3.6.2, )", - "resolved": "3.6.2", - "contentHash": "Uo6qw1xkBIT2VNoeME+QfPqmqlOKMgOTWKzaPE4bg9mFcge/ZvrlN95gssYJSXzybx2yWnEqvRxjYXYHeFL49g==", + "requested": "[3.6.3, )", + "resolved": "3.6.3", + "contentHash": "YXjVWh9OumYK0lO9NHLPrNHaYiEvAKeTQ2AI5z8zRJKscbzENjbP66S81xThSN+guODDqc6TdliReGAZ2m4Cfw==", "dependencies": { "AutoFixture": "4.18.1", "AutoFixture.AutoNSubstitute": "4.18.1", "AutoFixture.Xunit2": "4.18.1", - "Castle.Core": "5.1.1", - "JetBrains.Annotations": "2024.2.0", - "NSubstitute": "5.1.0", + "Castle.Core": "5.2.1", + "JetBrains.Annotations": "2024.3.0", + "NSubstitute": "5.3.0", "xunit.abstractions": "2.0.3", - "xunit.core": "2.9.2", - "xunit.extensibility.core": "2.9.2", - "xunit.extensibility.execution": "2.9.2" + "xunit.core": "2.9.3", + "xunit.extensibility.core": "2.9.3", + "xunit.extensibility.execution": "2.9.3" } }, "Shouldly": { @@ -75,9 +75,9 @@ }, "xunit.runner.visualstudio": { "type": "Direct", - "requested": "[3.0.1, )", - "resolved": "3.0.1", - "contentHash": "lbyYtsBxA8Pz8kaf5Xn/Mj1mL9z2nlBWdZhqFaj66nxXBa4JwiTDm4eGcpSMet6du9TOWI6bfha+gQR6+IHawg==" + "requested": "[3.1.1, )", + "resolved": "3.1.1", + "contentHash": "gNu2zhnuwjq5vQlU4S7yK/lfaKZDLmtcu+vTjnhfTlMAUYn+Hmgu8IIX0UCwWepYkk+Szx03DHx1bDnc9Fd+9w==" }, "AutoFixture": { "type": "Transitive", @@ -108,8 +108,8 @@ }, "Castle.Core": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "resolved": "5.2.1", + "contentHash": "wHARzQA695jwwKreOzNsq54KiGqKP38tv8hi8e2FXDEC/sA6BtrX90tVPDkOfVu13PbEzr00TCV8coikl+D1Iw==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } @@ -143,13 +143,13 @@ }, "JetBrains.Annotations": { "type": "Transitive", - "resolved": "2024.2.0", - "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.12.0", - "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA==" + "resolved": "17.14.1", + "contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg==" }, "Microsoft.Extensions.Configuration": { "type": "Transitive", @@ -446,19 +446,19 @@ }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "17.12.0", - "contentHash": "TDqkTKLfQuAaPcEb3pDDWnh7b3SyZF+/W9OZvWFp6eJCIiiYFdSB6taE2I6tWrFw5ywhzOb6sreoGJTI6m3rSQ==", + "resolved": "17.14.1", + "contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ==", "dependencies": { - "System.Reflection.Metadata": "1.6.0" + "System.Reflection.Metadata": "8.0.0" } }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "17.12.0", - "contentHash": "MiPEJQNyADfwZ4pJNpQex+t9/jOClBGMiCiVVFuELCMSX2nmNfvUor3uFVxNNCg30uxDP8JDYfPnMXQzsfzYyg==", + "resolved": "17.14.1", + "contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.12.0", - "Newtonsoft.Json": "13.0.1" + "Microsoft.TestPlatform.ObjectModel": "17.14.1", + "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Win32.Primitives": { @@ -524,8 +524,8 @@ }, "Newtonsoft.Json": { "type": "Transitive", - "resolved": "13.0.1", - "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "NLog": { "type": "Transitive", @@ -544,8 +544,8 @@ }, "NSubstitute": { "type": "Transitive", - "resolved": "5.1.0", - "contentHash": "ZCqOP3Kpp2ea7QcLyjMU4wzE+0wmrMN35PQMsdPOHYc2IrvjmusG9hICOiqiOTPKN0gJon6wyCn6ZuGHdNs9hQ==", + "resolved": "5.3.0", + "contentHash": "lJ47Cps5Qzr86N99lcwd+OUvQma7+fBgr8+Mn+aOC0WrlqMNkdivaYD9IvnZ5Mqo6Ky3LS7ZI+tUq1/s9ERd0Q==", "dependencies": { "Castle.Core": "5.1.1" } @@ -659,8 +659,8 @@ }, "Scrutor": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "5xKT6ND5GqnFzwSaYozHCJe75GFL8sPy4yw/iRFqeBFGlmqPNFpOg1T9Q0Gl2h76Cklt0ZTg6Ypkri5iUBKXsA==", + "resolved": "6.1.0", + "contentHash": "m4+0RdgnX+jeiaqteq9x5SwEtuCjWG0KTw1jBjCzn7V8mCanXKoeF8+59E0fcoRbAjdEq6YqHFCmxZ49Kvqp3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", "Microsoft.Extensions.DependencyModel": "8.0.2" @@ -668,8 +668,8 @@ }, "SonarAnalyzer.CSharp": { "type": "Transitive", - "resolved": "9.20.0.85982", - "contentHash": "c0IYtFg4mYusTafTy0Bs5wev45vRKNShkuoWyQyPMbC6imEFHL7tY/7xbbUJd6JNLYx5vP8wyi2LgiBsMHqb2Q==" + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", @@ -728,6 +728,11 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + }, "System.ComponentModel": { "type": "Transitive", "resolved": "4.3.0", @@ -1091,8 +1096,11 @@ }, "System.Reflection.Metadata": { "type": "Transitive", - "resolved": "1.6.0", - "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + "resolved": "8.0.0", + "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "dependencies": { + "System.Collections.Immutable": "8.0.0" + } }, "System.Reflection.Primitives": { "type": "Transitive", @@ -1489,18 +1497,18 @@ "type": "Project", "dependencies": { "CommandLineParser": "[2.9.1, )", - "Microsoft.Extensions.Configuration.Abstractions": "[8.0.0, )", - "Microsoft.Extensions.Configuration.CommandLine": "[8.0.0, )", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "[8.0.0, )", - "Microsoft.Extensions.Configuration.Json": "[8.0.1, )", - "Microsoft.Extensions.Configuration.UserSecrets": "[8.0.1, )", - "Microsoft.Extensions.DependencyInjection": "[8.0.1, )", - "Microsoft.Extensions.Hosting": "[8.0.1, )", - "Microsoft.Extensions.Logging": "[8.0.1, )", - "Microsoft.Extensions.Logging.Configuration": "[8.0.1, )", + "Microsoft.Extensions.Configuration.Abstractions": "[8.0.*, )", + "Microsoft.Extensions.Configuration.CommandLine": "[8.0.*, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[8.0.*, )", + "Microsoft.Extensions.Configuration.Json": "[8.0.*, )", + "Microsoft.Extensions.Configuration.UserSecrets": "[8.0.*, )", + "Microsoft.Extensions.DependencyInjection": "[8.0.*, )", + "Microsoft.Extensions.Hosting": "[8.0.*, )", + "Microsoft.Extensions.Logging": "[8.0.*, )", + "Microsoft.Extensions.Logging.Configuration": "[8.0.*, )", "NLog": "[5.3.4, )", "NLog.Extensions.Logging": "[5.3.15, )", - "Scrutor": "[6.0.1, )" + "Scrutor": "[6.1.0, )" } } } diff --git a/Neolution.DotNet.Console.sln b/Neolution.DotNet.Console.sln index dc1e47b..67bf04b 100644 --- a/Neolution.DotNet.Console.sln +++ b/Neolution.DotNet.Console.sln @@ -18,10 +18,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neolution.DotNet.Console.SampleAsync", "Neolution.DotNet.Console.SampleAsync\Neolution.DotNet.Console.SampleAsync.csproj", "{C31218C2-00A1-4B5C-A4B3-AF83E2CBA3A4}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neolution.DotNet.Console.UnitTests", "Neolution.DotNet.Console.UnitTests\Neolution.DotNet.Console.UnitTests.csproj", "{66E5F184-CCCC-47A2-8B9E-25AE0036CE1B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neolution.DotNet.Console.Demo", "Neolution.DotNet.Console.Demo\Neolution.DotNet.Console.Demo.csproj", "{3F17699A-2864-0EEC-AC50-93648D6E5BDE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,14 +32,14 @@ Global {0206E537-E125-4E03-BAE8-E009211412DF}.Debug|Any CPU.Build.0 = Debug|Any CPU {0206E537-E125-4E03-BAE8-E009211412DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {0206E537-E125-4E03-BAE8-E009211412DF}.Release|Any CPU.Build.0 = Release|Any CPU - {C31218C2-00A1-4B5C-A4B3-AF83E2CBA3A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C31218C2-00A1-4B5C-A4B3-AF83E2CBA3A4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C31218C2-00A1-4B5C-A4B3-AF83E2CBA3A4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C31218C2-00A1-4B5C-A4B3-AF83E2CBA3A4}.Release|Any CPU.Build.0 = Release|Any CPU {66E5F184-CCCC-47A2-8B9E-25AE0036CE1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {66E5F184-CCCC-47A2-8B9E-25AE0036CE1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {66E5F184-CCCC-47A2-8B9E-25AE0036CE1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {66E5F184-CCCC-47A2-8B9E-25AE0036CE1B}.Release|Any CPU.Build.0 = Release|Any CPU + {3F17699A-2864-0EEC-AC50-93648D6E5BDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F17699A-2864-0EEC-AC50-93648D6E5BDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F17699A-2864-0EEC-AC50-93648D6E5BDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F17699A-2864-0EEC-AC50-93648D6E5BDE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Neolution.DotNet.Console/DotNetConsole.cs b/Neolution.DotNet.Console/DotNetConsole.cs index 8758041..9ebe36b 100644 --- a/Neolution.DotNet.Console/DotNetConsole.cs +++ b/Neolution.DotNet.Console/DotNetConsole.cs @@ -6,12 +6,9 @@ using System.Threading; using System.Threading.Tasks; using CommandLine; - using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Neolution.DotNet.Console.Abstractions; - using NLog; - using NLog.Extensions.Logging; /// /// The console application. @@ -111,12 +108,11 @@ public async Task RunAsync(CancellationToken cancellationToken) cancellationToken.ThrowIfCancellationRequested(); await this.commandLineParserResult.WithParsedAsync(async options => await this.RunWithOptionsAsync(options, cancellationToken)); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { try { - var logger = LogManager.Setup().LoadConfigurationFromSection(this.Services.GetRequiredService()).GetCurrentClassLogger(); - logger.Log(LogLevel.Info, CultureInfo.InvariantCulture, message: "Operation was canceled by the user."); + DotNetConsoleLogger.Log.Info(ex, CultureInfo.InvariantCulture, "Operation was canceled by the user."); } catch (Exception) { diff --git a/Neolution.DotNet.Console/DotNetConsoleException.cs b/Neolution.DotNet.Console/DotNetConsoleException.cs index 35d3822..cd0d0e8 100644 --- a/Neolution.DotNet.Console/DotNetConsoleException.cs +++ b/Neolution.DotNet.Console/DotNetConsoleException.cs @@ -3,7 +3,6 @@ using System; /// - [Serializable] public class DotNetConsoleException : Exception { /// @@ -31,16 +30,5 @@ public DotNetConsoleException(string message, Exception innerException) public DotNetConsoleException() { } - - /// - /// Initializes a new instance of the class. - /// - /// The serialization information. - /// The streaming context. - [Obsolete("This API supports obsolete formatter-based serialization. It should not be called or extended by application code.", DiagnosticId = "SYSLIB0051")] - protected DotNetConsoleException(System.Runtime.Serialization.SerializationInfo serializationInfo, System.Runtime.Serialization.StreamingContext streamingContext) - : base(serializationInfo, streamingContext) - { - } } } diff --git a/Neolution.DotNet.Console/DotNetConsoleLogger.cs b/Neolution.DotNet.Console/DotNetConsoleLogger.cs new file mode 100644 index 0000000..5692252 --- /dev/null +++ b/Neolution.DotNet.Console/DotNetConsoleLogger.cs @@ -0,0 +1,72 @@ +namespace Neolution.DotNet.Console +{ + using System; + using Microsoft.Extensions.Configuration; + using NLog; + using NLog.Extensions.Logging; + using NLog.Targets; + + /// + /// Provides static methods to initialize and manage a logger instance. + /// + public static class DotNetConsoleLogger + { + /// + /// The internal logger instance used for logging. + /// + private static Logger? logger; + + /// + /// Gets the currently initialized logger. + /// + /// Thrown when the logger has not been initialized. + public static Logger Log + { + get + { + if (logger == null) + { + throw new InvalidOperationException("Logger has not been initialized. Call Initialize(configuration) first."); + } + + return logger; + } + } + + /// + /// Initializes the logger based on the provided configuration. + /// + /// The configuration used to initialize the logger. + public static void Initialize(IConfiguration configuration) + { + ConsoleTarget? consoleTarget = null; + try + { + logger = LogManager.Setup().LoadConfigurationFromSection(configuration).GetCurrentClassLogger(); + } + catch (Exception ex) + { + // Create a simple NLog configuration that logs to the console + var config = new NLog.Config.LoggingConfiguration(); + consoleTarget = new ConsoleTarget("console"); + config.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget); + + LogManager.Configuration = config; + logger = LogManager.GetCurrentClassLogger(); + logger.Error(ex, "Logger initialization failed"); + } + finally + { + consoleTarget?.Dispose(); + } + } + + /// + /// Ensures the logger flushes messages and shuts down internal timers. + /// + public static void Shutdown() + { + LogManager.Shutdown(); + } + } +} diff --git a/Neolution.DotNet.Console/Logging/NlnHelperLogLevelLayoutRenderer.cs b/Neolution.DotNet.Console/Logging/NlnHelperLogLevelLayoutRenderer.cs index 7b4450b..35780e7 100644 --- a/Neolution.DotNet.Console/Logging/NlnHelperLogLevelLayoutRenderer.cs +++ b/Neolution.DotNet.Console/Logging/NlnHelperLogLevelLayoutRenderer.cs @@ -10,7 +10,7 @@ /// Custom NLog layout renderer to render log level like NLNHelper did. /// /// - [NLog.LayoutRenderers.LayoutRenderer("NLNHelperLogLevel")] + [LayoutRenderer("NLNHelperLogLevel")] public class NlnHelperLogLevelLayoutRenderer : LayoutRenderer { /// diff --git a/Neolution.DotNet.Console/Neolution.DotNet.Console.csproj b/Neolution.DotNet.Console/Neolution.DotNet.Console.csproj index ff4eb43..190463a 100644 --- a/Neolution.DotNet.Console/Neolution.DotNet.Console.csproj +++ b/Neolution.DotNet.Console/Neolution.DotNet.Console.csproj @@ -2,34 +2,49 @@ net6.0;net8.0 + Neolution.DotNet.Console + Neolution AG + Library for building .NET console applications with dependency injection, async command execution, flexible logging via NLog, and configuration support. + https://github.com/neolution-ch/Neolution.DotNet.Console + https://github.com/neolution-ch/Neolution.DotNet.Console + git + dotnet;console;cli;dependency-injection;async;commands;logging;nlog;configuration;hosting;commandlineparser;dotnet-cli;hostbuilder;microsoft-extensions-hosting;command-line MIT + icon.png README.md enable true + true + snupkg + See CHANGELOG.md for release notes. - - - - - - - - - - + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + diff --git a/Neolution.DotNet.Console/icon.png b/Neolution.DotNet.Console/icon.png new file mode 100644 index 0000000..68121cc Binary files /dev/null and b/Neolution.DotNet.Console/icon.png differ diff --git a/Neolution.DotNet.Console/packages.lock.json b/Neolution.DotNet.Console/packages.lock.json index dcaa52d..cbe3d0d 100644 --- a/Neolution.DotNet.Console/packages.lock.json +++ b/Neolution.DotNet.Console/packages.lock.json @@ -10,7 +10,7 @@ }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Direct", - "requested": "[8.0.0, )", + "requested": "[8.0.*, )", "resolved": "8.0.0", "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { @@ -19,7 +19,7 @@ }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Direct", - "requested": "[8.0.0, )", + "requested": "[8.0.*, )", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { @@ -29,7 +29,7 @@ }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", - "requested": "[8.0.0, )", + "requested": "[8.0.*, )", "resolved": "8.0.0", "contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==", "dependencies": { @@ -39,7 +39,7 @@ }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "L89DLNuimOghjV3tLx0ArFDwVEJD6+uGB3BMCMX01kaLzXkaXHb2021xOMl2QOxUxbdePKUZsUY7n2UUkycjRg==", "dependencies": { @@ -52,7 +52,7 @@ }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "7tYqdPPpAK+3jO9d5LTuCK2VxrEdf85Ol4trUr6ds4jclBecadWZ/RyPCbNjfbN5iGTfUnD/h65TOQuqQv2c+A==", "dependencies": { @@ -64,7 +64,7 @@ }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==", "dependencies": { @@ -73,7 +73,7 @@ }, "Microsoft.Extensions.Hosting": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "bP9EEkHBEfjgYiG8nUaXqMk/ujwJrffOkNPP7onpRMO8R+OUSESSP4xHkCAXgYZ1COP2Q9lXlU5gkMFh20gRuw==", "dependencies": { @@ -103,7 +103,7 @@ }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==", "dependencies": { @@ -114,7 +114,7 @@ }, "Microsoft.Extensions.Logging.Configuration": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "QWwTrsgOnJMmn+XUslm8D2H1n3PkP/u/v52FODtyBc/k4W9r3i2vcXXeeX/upnzllJYRRbrzVzT0OclfNJtBJA==", "dependencies": { @@ -130,11 +130,11 @@ }, "Neolution.CodeAnalysis": { "type": "Direct", - "requested": "[3.2.1, )", - "resolved": "3.2.1", - "contentHash": "AQDkBJ9e6TrnhpwTtf4KrwlAPBLy77I5XkNey586nN2IFfjk1LVwBwvOcjORD1dSNtEhOf69JQjdmiMf54C90w==", + "requested": "[3.3.0-beta.2, )", + "resolved": "3.3.0-beta.2", + "contentHash": "88mG4f4P16K4Ql3uTqeaoeXGJLNMReFh9z3ZKmKZ0bY4RTk4z4syjuF/TefouGM2hR+mcBSdWbAtsKT/+FowlA==", "dependencies": { - "SonarAnalyzer.CSharp": "9.20.0.85982", + "SonarAnalyzer.CSharp": "9.32.0.97167", "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, @@ -157,9 +157,9 @@ }, "Scrutor": { "type": "Direct", - "requested": "[6.0.1, )", - "resolved": "6.0.1", - "contentHash": "5xKT6ND5GqnFzwSaYozHCJe75GFL8sPy4yw/iRFqeBFGlmqPNFpOg1T9Q0Gl2h76Cklt0ZTg6Ypkri5iUBKXsA==", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "m4+0RdgnX+jeiaqteq9x5SwEtuCjWG0KTw1jBjCzn7V8mCanXKoeF8+59E0fcoRbAjdEq6YqHFCmxZ49Kvqp3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", "Microsoft.Extensions.DependencyModel": "8.0.2" @@ -353,8 +353,8 @@ }, "SonarAnalyzer.CSharp": { "type": "Transitive", - "resolved": "9.20.0.85982", - "contentHash": "c0IYtFg4mYusTafTy0Bs5wev45vRKNShkuoWyQyPMbC6imEFHL7tY/7xbbUJd6JNLYx5vP8wyi2LgiBsMHqb2Q==" + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", @@ -406,7 +406,7 @@ }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Direct", - "requested": "[8.0.0, )", + "requested": "[8.0.*, )", "resolved": "8.0.0", "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { @@ -415,7 +415,7 @@ }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Direct", - "requested": "[8.0.0, )", + "requested": "[8.0.*, )", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { @@ -425,7 +425,7 @@ }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", - "requested": "[8.0.0, )", + "requested": "[8.0.*, )", "resolved": "8.0.0", "contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==", "dependencies": { @@ -435,7 +435,7 @@ }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "L89DLNuimOghjV3tLx0ArFDwVEJD6+uGB3BMCMX01kaLzXkaXHb2021xOMl2QOxUxbdePKUZsUY7n2UUkycjRg==", "dependencies": { @@ -447,7 +447,7 @@ }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "7tYqdPPpAK+3jO9d5LTuCK2VxrEdf85Ol4trUr6ds4jclBecadWZ/RyPCbNjfbN5iGTfUnD/h65TOQuqQv2c+A==", "dependencies": { @@ -459,7 +459,7 @@ }, "Microsoft.Extensions.DependencyInjection": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==", "dependencies": { @@ -468,7 +468,7 @@ }, "Microsoft.Extensions.Hosting": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "bP9EEkHBEfjgYiG8nUaXqMk/ujwJrffOkNPP7onpRMO8R+OUSESSP4xHkCAXgYZ1COP2Q9lXlU5gkMFh20gRuw==", "dependencies": { @@ -498,7 +498,7 @@ }, "Microsoft.Extensions.Logging": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "4x+pzsQEbqxhNf1QYRr5TDkLP9UsLT3A6MdRKDDEgrW7h1ljiEPgTNhKYUhNCCAaVpQECVQ+onA91PTPnIp6Lw==", "dependencies": { @@ -509,7 +509,7 @@ }, "Microsoft.Extensions.Logging.Configuration": { "type": "Direct", - "requested": "[8.0.1, )", + "requested": "[8.0.*, )", "resolved": "8.0.1", "contentHash": "QWwTrsgOnJMmn+XUslm8D2H1n3PkP/u/v52FODtyBc/k4W9r3i2vcXXeeX/upnzllJYRRbrzVzT0OclfNJtBJA==", "dependencies": { @@ -525,11 +525,11 @@ }, "Neolution.CodeAnalysis": { "type": "Direct", - "requested": "[3.2.1, )", - "resolved": "3.2.1", - "contentHash": "AQDkBJ9e6TrnhpwTtf4KrwlAPBLy77I5XkNey586nN2IFfjk1LVwBwvOcjORD1dSNtEhOf69JQjdmiMf54C90w==", + "requested": "[3.3.0-beta.2, )", + "resolved": "3.3.0-beta.2", + "contentHash": "88mG4f4P16K4Ql3uTqeaoeXGJLNMReFh9z3ZKmKZ0bY4RTk4z4syjuF/TefouGM2hR+mcBSdWbAtsKT/+FowlA==", "dependencies": { - "SonarAnalyzer.CSharp": "9.20.0.85982", + "SonarAnalyzer.CSharp": "9.32.0.97167", "StyleCop.Analyzers.Unstable": "1.2.0.556" } }, @@ -552,9 +552,9 @@ }, "Scrutor": { "type": "Direct", - "requested": "[6.0.1, )", - "resolved": "6.0.1", - "contentHash": "5xKT6ND5GqnFzwSaYozHCJe75GFL8sPy4yw/iRFqeBFGlmqPNFpOg1T9Q0Gl2h76Cklt0ZTg6Ypkri5iUBKXsA==", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "m4+0RdgnX+jeiaqteq9x5SwEtuCjWG0KTw1jBjCzn7V8mCanXKoeF8+59E0fcoRbAjdEq6YqHFCmxZ49Kvqp3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", "Microsoft.Extensions.DependencyModel": "8.0.2" @@ -735,8 +735,8 @@ }, "SonarAnalyzer.CSharp": { "type": "Transitive", - "resolved": "9.20.0.85982", - "contentHash": "c0IYtFg4mYusTafTy0Bs5wev45vRKNShkuoWyQyPMbC6imEFHL7tY/7xbbUJd6JNLYx5vP8wyi2LgiBsMHqb2Q==" + "resolved": "9.32.0.97167", + "contentHash": "Yxk86RV+8ynJpUhku1Yw2hITFmnmXKkXJ73cIFSy85ol5SnWREQg9RuTyV8nI7V7+pyLKpCfRmD7P0widsgjkg==" }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", diff --git a/README.md b/README.md index f34e1a8..f14b07c 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,262 @@ -# Introduction +# Neolution.DotNet.Console -Neolution.DotNet.Console is a versatile package designed as a launchpad for .NET console applications. It comes equipped with a built-in dependency injection setup, mirroring the functionality found in ASP.NET Core applications, thus providing a seamless and intuitive user experience. +[![NuGet](https://img.shields.io/nuget/v/Neolution.DotNet.Console.svg)](https://www.nuget.org/packages/Neolution.DotNet.Console) +[![Build Status](https://github.com/neolution-ch/Neolution.DotNet.Console/actions/workflows/cd-production.yml/badge.svg)](https://github.com/neolution-ch/Neolution.DotNet.Console/actions) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -# Getting Started +Neolution.DotNet.Console is a library for building .NET console applications. It uses the Microsoft.Extensions.Hosting model to provide a familiar setup for dependency injection, logging, and command-line parsing. -To help you kickstart your console application, we've provided a a [sample application](/Neolution.DotNet.Console.SampleAsync/Program.cs) that should demonstrate the basic usage of this package. +This project works for simple CLI tools as well as longer-running console services, and supports cancellation tokens, configuration from JSON, environment variables, or command-line arguments. -# Guides +## Features -## Migrate from V3 to V5 +- Async commands with cancellation support +- Dependency injection setup matching ASP.NET Core patterns +- Command-line parsing with verb and option support +- Logging integration using NLog +- Configuration from appsettings.json, environment variables, user secrets, and command line +- Strict verb matching to prevent unintended command execution +- A built-in `check-deps` command to verify all services are registered correctly +- Support for multiple environments (Development, Production, etc.) +- Automatic scope creation for each command execution to support scoped services -*Note: V4 was intentionally skipped, there is no V4 release.* +## Installation -To support cancellation tokens, the `IDotNetConsoleCommand` interface had to be changed: The `RunAsync` method now requires also a `CancellationToken` as a parameter. This change is breaking, so you will need to update your commands to reflect this change. +Install the package via NuGet: -## Migrate from V2 to V3 +### .NET CLI -In .NET 6 the hosting model for ASP.NET Core applications was changed, we adjusted to that to fulfill the primary goal of this package: to provide a seamless and intuitive user experience. This introduces breaking changes that are explained below. +```sh +dotnet add package Neolution.DotNet.Console +``` -### Removed `ICompositionRoot` interface and `UseCompositionRoot` extension method +### Package Manager -The builder returned from calling `DotNetConsole.CreateDefaultBuilder(args)` now has a `Services` property that can be used to register services. Like in ASP.NET, this can now be done directly in `Program.cs`. +```powershell +Install-Package Neolution.DotNet.Console +``` + +## Getting Started + +To help you kickstart your console application, we've provided a [demo application](/Neolution.DotNet.Console.Demo/Program.cs) that demonstrates the basic usage of this package. + +### Example Main Method with Startup Class (Recommended) + +Below is the recommended pattern for your `Program.cs` entry point, using a `Startup` class for service registration and configuration: - public static async Task Main(string[] args) +```csharp +// Program.cs +namespace Neolution.DotNet.Console.Demo +{ + public static class Program { - var builder = DotNetConsole.CreateDefaultBuilder(args); + public static async Task Main(string[] args) + { + try + { + var builder = DotNetConsole.CreateDefaultBuilder(args); + DotNetConsoleLogger.Initialize(builder.Configuration); + + var startup = new Startup(builder.Environment, builder.Configuration); + startup.ConfigureServices(builder.Services); - // Register your services here... - builder.Services.AddHttpClient(); - builder.Services.AddScoped(); - builder.Services.AddSingleton(); + var console = builder.Build(); + await console.RunAsync(); + } + catch (Exception ex) + { + DotNetConsoleLogger.Log.Error(ex, "Stopped program because of an unexpected exception"); + throw; + } + finally + { + // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) + DotNetConsoleLogger.Shutdown(); + } + } + } +} +``` + +```csharp +// Startup.cs +namespace Neolution.DotNet.Console.Demo +{ + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + + /// + /// The startup class, composition root for the application. + /// + internal class Startup + { + public Startup(IHostEnvironment environment, IConfiguration configuration) + { + this.Environment = environment; + this.Configuration = configuration; + } - await builder.Build().RunAsync(); + public IHostEnvironment Environment { get; } + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + // Register your services here. This is the main place for service registrations. + // Example: services.AddHttpClient(); + + // You have full access to IHostEnvironment and IConfiguration here. + // Use 'this.Environment' to register services based on the environment (e.g., Development, Production). + // Use 'this.Configuration' to register/configure services based on configuration values. + } } +} +``` + +This pattern keeps your `Program.cs` clean and delegates service registration and configuration to a dedicated `Startup` class, similar to ASP.NET Core applications. + +## Usage + +### Defining a Command + +To add a command, implement the `IDotNetConsoleCommand` interface. For example: + +```csharp +using CommandLine; +using Neolution.DotNet.Console.Abstractions; + +[Verb("echo", HelpText = "Write a string into the console.")] +public class EchoOptions +{ + [Value(0)] + public string? Message { get; set; } +} + +public class EchoCommand : IDotNetConsoleCommand +{ + public async Task RunAsync(EchoOptions options, CancellationToken cancellationToken) + { + Console.WriteLine($"Echo: {options.Message}"); + await Task.CompletedTask; + } +} +``` + +The library will automatically discover and register all commands (classes implementing `IDotNetConsoleCommand`). + +### Running the Application + +Run your application and specify the command verb as an argument. The library uses [CommandLineParser](https://github.com/commandlineparser/commandline) verbs and options internally, so you can invoke your command like this: + +```sh +dotnet run -- [verb] [options] +``` + +Replace `[verb]` with the name of your command (e.g., `echo`). Any additional options or parameters defined in your command will be parsed and passed automatically. -### Async by default +## Dependency Validation with check-deps + +The framework includes a built-in `check-deps` command that validates your application's dependency injection setup without running any business logic. This is particularly useful for catching DI configuration issues early. + +### Why use check-deps? + +- **Early Detection**: Catch dependency injection issues during build/CI rather than at runtime +- **Fast Feedback**: Validates your entire DI container setup in seconds without executing business logic +- **Confidence**: Ensures all your services can be properly constructed by the DI container +- **CI Integration**: Perfect for automated validation in your deployment pipeline + +The command validates: + +- All registered services can be instantiated +- No circular dependencies exist +- Service lifetimes are properly configured +- All required dependencies are registered + +If validation fails, the command will exit with a non-zero code and provide detailed error information about what's missing or misconfigured. + +### Running check-deps locally + +```sh +# Validate your DI setup +dotnet run -- check-deps + +# Or if you have a published/built application +myapp.exe check-deps +``` + +### Using check-deps in CI/CD + +The `check-deps` command is especially valuable in CI/CD pipelines to catch dependency injection issues before deployment: + +```yaml +# Example GitHub Actions workflow +name: CI +on: [push, pull_request] + +jobs: + validate-dependencies: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.x' + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Validate DI setup + run: dotnet run --project YourApp -- check-deps + - name: Run tests + run: dotnet test --no-build +``` + +## Migration Guides + +### Migrate from V3 to V5 + +> ⚠️ *Note: V4 was intentionally skipped, there is no V4 release.* + +To support cancellation tokens, the `IDotNetConsoleCommand` interface had to be changed: The `RunAsync` method now requires also a `CancellationToken` as a parameter. This change is breaking, so you will need to update your commands to reflect this change. + +### Migrate from V2 to V3 + +In .NET 6 the hosting model for ASP.NET Core applications was changed, we adjusted to that to fulfill the primary goal of this package: to provide a seamless and intuitive user experience. This introduces breaking changes that are explained below. + +#### Removed `ICompositionRoot` interface and `UseCompositionRoot` extension method + +The builder returned from calling `DotNetConsole.CreateDefaultBuilder(args)` now has a `Services` property that can be used to register services. Like in ASP.NET, this can now be done directly in `Program.cs`. + +Refer to the example above for the preferred structure of your Main method. + +#### Async by default All commands are now async by default. Use the new `IDotNetConsoleCommand` interface instead of `IConsoleAppCommand` and `IAsyncConsoleAppCommand`. -### Service registration validation +#### Service registration validation The service registrations are now validated when `app.Build()` is called. This means that the application will not start if not all services that are registered (even if not used during runtime) can be created by the DI container or when there are scope/lifetime issues with the services. -### Strict verb matching when default verb is defined +#### Strict verb matching when default verb is defined If one verb is defined as default, but the verb passed as argument is not matching any of the available verbs, the builder will no longer build a console application and instead throw an exception. This is done to avoid running the default verb command if the user accidentally uses a verb that is not available or simply made a typo. E.g. if the app uses `start` as the default verb and the user intention is to start the command assigned to the verb `echo` but mistypes it as `eccho` or similar, the builder will fail with an exception. Previous versions would have run the `start` command, because it's defined as default verb, but it was not the intention. -## Migrate from V1 to V2 +### Migrate from V1 to V2 V2 **introduces breaking changes** from V1, primarily because it upgrades NLog to Version 5. For a detailed review of these changes, please refer to the [official NLog release notes](https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready.html). -### Removed `Logging` section in appsettings.json +#### Removed `Logging` section in appsettings.json -NLog decided to deprecate Microsoft's `Logging` section in the appsettings.json starting with V5. In accordance of that decision we've also transitioned to only using NLog rules for filtering logging output. Although these rules might be a bit more complex, they offer greater flexibility and control. +NLog decided to deprecate Microsoft's `Logging` section in the appsettings.json starting with V5. In accordance of that decision we've also transitioned to only using NLog rules for filtering logging output. Although these rules might be a bit more complex, they offer greater flexibility and control. To get acquainted with these rules and learn how to migrate your current configuration, check out the [NLog documentation on logging rules](https://github.com/NLog/NLog/wiki/Configuration-file#rules). There's also a guide on the [new finalMinLevel attribute](https://github.com/NLog/NLog/wiki/Logging-Rules-FinalMinLevel) that you might find helpful. -### Removed default logging targets +#### Removed default logging targets - **AWS Logger:** The AWS Logger, previously used for logging to CloudWatch, has been removed due to decreased usage. If you still need it, don't worry, you can easily [download it separately](https://www.nuget.org/packages/AWS.Logger.NLog). The configuration remains the same. - - **DatabaseTarget:** The DatabaseTarget is no longer part of the NLog NuGet package. If you use this target, you'll need to [download it separately](https://www.nuget.org/packages/NLog.Database) to ensure its continued functionality. There are additional targets that have been transitioned into separate packages. Please review [this list](https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready.html#nlog-targets-extracted-into-their-own-nuget-packages) to determine if any targets you use are affected.