diff --git a/.idea/.idea.Ploch.CommandLine/.idea/.name b/.idea/.idea.Ploch.CommandLine/.idea/.name deleted file mode 100644 index 22bd15d..0000000 --- a/.idea/.idea.Ploch.CommandLine/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Ploch.CommandLine \ No newline at end of file diff --git a/.idea/.idea.Ploch.CommandLine/.idea/git_toolbox_blame.xml b/.idea/.idea.Ploch.CommandLine/.idea/git_toolbox_blame.xml deleted file mode 100644 index 7dc1249..0000000 --- a/.idea/.idea.Ploch.CommandLine/.idea/git_toolbox_blame.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.Ploch.CommandLine/.idea/indexLayout.xml b/.idea/.idea.Ploch.CommandLine/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/.idea/.idea.Ploch.CommandLine/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/qodana.yaml b/qodana.yaml index ce2a708..dd3433f 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -1,6 +1,6 @@ version: "1.0" -linter: jetbrains/qodana-cdnet:2024.1 +linter: jetbrains/qodana-cdnet:latest profile: name: qodana.recommended include: - - name: CheckDependencyLicenses \ No newline at end of file + - name: CheckDependencyLicenses diff --git a/samples/Directory.Packages.props b/samples/Directory.Packages.props new file mode 100644 index 0000000..55086e4 --- /dev/null +++ b/samples/Directory.Packages.props @@ -0,0 +1,5 @@ + + + false + + \ No newline at end of file diff --git a/samples/HelloWorldConsoleApp/HelloWorldConsoleApp.csproj b/samples/HelloWorldConsoleApp/HelloWorldConsoleApp.csproj index 0780d59..7f9123a 100644 --- a/samples/HelloWorldConsoleApp/HelloWorldConsoleApp.csproj +++ b/samples/HelloWorldConsoleApp/HelloWorldConsoleApp.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net9.0 enable enable Linux diff --git a/samples/HelloWorldConsoleApp/HellowWorldCommand.cs b/samples/HelloWorldConsoleApp/HellowWorldCommand.cs new file mode 100644 index 0000000..ae76530 --- /dev/null +++ b/samples/HelloWorldConsoleApp/HellowWorldCommand.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using McMaster.Extensions.CommandLineUtils; +using Ploch.Common.CommandLine; +#pragma warning disable Ex0100 + +namespace HelloWorldConsoleApp; + +[Command(Name = "HelloWorld", Description = "Prints Hello World")] +#pragma warning disable ClassDocumentationHeader +public class HellowWorldCommand : IAsyncCommand +#pragma warning restore ClassDocumentationHeader +#pragma warning disable MethodDocumentationHeader +#pragma warning disable PropertyDocumentationHeader +{ + [Option(Description = "Person First Name", ShortName = "f")] + [Required] + + public required string FirstName { get; set; } + + + [Option(Description = "Person Last Name", ShortName = "l")] + [Required] + public required string LastName { get; set; } + + [Option(Description = "Person Age", ShortName = "a")] + [Required] + public required int Age { get; set; } + + + public Task OnExecuteAsync(CancellationToken cancellationToken = default) + { + Console.WriteLine($"{FirstName} {LastName} ({Age}) - Hello World!"); + + return Task.CompletedTask; + } +} +#pragma warning restore PropertyDocumentationHeader \ No newline at end of file diff --git a/samples/HelloWorldConsoleApp/Program.cs b/samples/HelloWorldConsoleApp/Program.cs index 47c8c91..734aa46 100644 --- a/samples/HelloWorldConsoleApp/Program.cs +++ b/samples/HelloWorldConsoleApp/Program.cs @@ -1,31 +1,5 @@ -// See https://aka.ms/new-console-template for more information - -using System.ComponentModel.DataAnnotations; -using McMaster.Extensions.CommandLineUtils; +using HelloWorldConsoleApp; using Ploch.Common.CommandLine; -var app = AppBuilder.CreateDefault().Build(); -app.Command(); - -[Command(Name = "HelloWorld", Description = "Prints Hello World")] -public class HellowWorldCommand : IAsyncCommand -{ - [Option(Description = "Person First Name", ShortName = "f")] - [Required] - public required string FirstName { get; set; } - - [Option(Description = "Person Last Name", ShortName = "l")] - [Required] - public required string LastName { get; set; } - - [Option(Description = "Person Age", ShortName = "a")] - [Required] - public required int Age { get; set; } - - public Task OnExecuteAsync(CancellationToken cancellationToken = default) - { - Console.WriteLine($"{FirstName} {LastName} ({Age}) - Hello World!"); - - return Task.CompletedTask; - } -} \ No newline at end of file +var app = AppBuilder.CreateDefault("Hello World App", "A sample app").Build(); +app.Command(); \ No newline at end of file diff --git a/samples/HostingSample/Dockerfile b/samples/HostingSample/Dockerfile new file mode 100644 index 0000000..a7c5778 --- /dev/null +++ b/samples/HostingSample/Dockerfile @@ -0,0 +1,21 @@ +FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base +USER $APP_UID +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["HostingSample/HostingSample.csproj", "HostingSample/"] +RUN dotnet restore "HostingSample/HostingSample.csproj" +COPY . . +WORKDIR "/src/HostingSample" +RUN dotnet build "HostingSample.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "HostingSample.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "HostingSample.dll"] diff --git a/samples/HostingSample/HelloRootCommand.cs b/samples/HostingSample/HelloRootCommand.cs new file mode 100644 index 0000000..3970065 --- /dev/null +++ b/samples/HostingSample/HelloRootCommand.cs @@ -0,0 +1,14 @@ +using Ploch.Common.CommandLine; +using McMaster.Extensions.CommandLineUtils; +using System.ComponentModel.DataAnnotations; + +namespace HostingSample; +public class HelloRootCommand(CommandLineApplication app) : HelpOnlyCommand(app) +{ + [Option(Inherited = true)] + public bool Verbose { get; set; } + + [Option(Inherited = true)] + [Required] + public string HelloText { get; set; } = null!; +} \ No newline at end of file diff --git a/samples/HostingSample/HostingSample.csproj b/samples/HostingSample/HostingSample.csproj new file mode 100644 index 0000000..f36f915 --- /dev/null +++ b/samples/HostingSample/HostingSample.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + dotnet-HostingSample-ba67250b-2688-481f-ae6e-990048d3df9a + Linux + + + + + + + + + + .dockerignore + + + + + + + diff --git a/samples/HostingSample/Program.cs b/samples/HostingSample/Program.cs new file mode 100644 index 0000000..dabbad0 --- /dev/null +++ b/samples/HostingSample/Program.cs @@ -0,0 +1,23 @@ +using Autofac.Core; +using Autofac.Core.Registration; +using HostingSample; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHostedService(); +//builder.ConfigureContainer(new AutofacServiceProviderFactory(), collection => collection.RegisterModule); + +var host = builder.Build(); +host.Run(); + +public class AutofacBuilder : IModule +{ + public AutofacBuilder() + { + throw new NotImplementedException(); + } + + public void Configure(IComponentRegistryBuilder componentRegistry) + { + // componentRegistry.AddRegistrationSource(new Se); + } +} \ No newline at end of file diff --git a/samples/HostingSample/Properties/launchSettings.json b/samples/HostingSample/Properties/launchSettings.json new file mode 100644 index 0000000..f436e37 --- /dev/null +++ b/samples/HostingSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "HostingSample": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/HostingSample/Worker.cs b/samples/HostingSample/Worker.cs new file mode 100644 index 0000000..7ee7c66 --- /dev/null +++ b/samples/HostingSample/Worker.cs @@ -0,0 +1,24 @@ +namespace HostingSample; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + + public Worker(ILogger logger) + { + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (_logger.IsEnabled(LogLevel.Information)) + { + _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); + } + + await Task.Delay(1000, stoppingToken); + } + } +} \ No newline at end of file diff --git a/samples/HostingSample/appsettings.Development.json b/samples/HostingSample/appsettings.Development.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/samples/HostingSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/HostingSample/appsettings.json b/samples/HostingSample/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/samples/HostingSample/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/CommandLine.Autofac/Ploch.Common.CommandLine.Autofac.csproj b/src/CommandLine.Autofac/Ploch.Common.CommandLine.Autofac.csproj index 1618548..be9c3d4 100644 --- a/src/CommandLine.Autofac/Ploch.Common.CommandLine.Autofac.csproj +++ b/src/CommandLine.Autofac/Ploch.Common.CommandLine.Autofac.csproj @@ -1,18 +1,22 @@ - + - net8.0 + net9.0 enable enable Nullable + + False + + - - + + all - runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers all @@ -20,12 +24,12 @@ all - runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers - + diff --git a/src/CommandLine.Hosting/IUnhandledExceptionHandler.cs b/src/CommandLine.Hosting/IUnhandledExceptionHandler.cs new file mode 100644 index 0000000..583951c --- /dev/null +++ b/src/CommandLine.Hosting/IUnhandledExceptionHandler.cs @@ -0,0 +1,20 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using McMaster.Extensions.CommandLineUtils; +using Ploch.CommandLine.Hosting.Internal; + +namespace Ploch.CommandLine.Hosting; + +/// +/// Used by to handle exceptions that are emitted from the +/// e.g. during parsing or execution +/// +public interface IUnhandledExceptionHandler +{ + /// + /// Handle otherwise uncaught exception. You are free to log, rethrow, … the exception + /// + /// An otherwise uncaught exception + void HandleException(Exception e); +} \ No newline at end of file diff --git a/src/CommandLine.Hosting/Internal/CommandLineLifetime.cs b/src/CommandLine.Hosting/Internal/CommandLineLifetime.cs new file mode 100644 index 0000000..481e03a --- /dev/null +++ b/src/CommandLine.Hosting/Internal/CommandLineLifetime.cs @@ -0,0 +1,92 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.ExceptionServices; +using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Hosting; + +namespace Ploch.CommandLine.Hosting.Internal; + +/// +/// Waits from completion of the and +/// initiates shutdown. +/// +internal class CommandLineLifetime : IHostLifetime, IDisposable +{ + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly ICommandLineService _cliService; + private readonly IConsole _console; + private readonly IUnhandledExceptionHandler? _unhandledExceptionHandler; + + /// + /// Creates a new instance. + /// + public CommandLineLifetime(IHostApplicationLifetime applicationLifetime, + ICommandLineService cliService, + IConsole console, + IUnhandledExceptionHandler? unhandledExceptionHandler = null) + { + _applicationLifetime = applicationLifetime; + _cliService = cliService; + _console = console; + _unhandledExceptionHandler = unhandledExceptionHandler; + } + + public void Dispose() + { } + + /// + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + /// + /// Registers an ApplicationStarted hook that runs the + /// . This ensures the container and all + /// hosted services are started before the + /// is run. After the + /// ICliService completes, the ExitCode is + /// recorded and the application is stopped. + /// + /// Used to indicate when stop should no longer be graceful. + /// + /// + public Task WaitForStartAsync(CancellationToken cancellationToken) + { + _applicationLifetime.ApplicationStarted.Register(async () => + { + try + { + ExitCode = await _cliService.RunAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + if (_unhandledExceptionHandler != null) + { + _unhandledExceptionHandler.HandleException(e); + } + else + { + ExceptionDispatchInfo.Capture(e).Throw(); + } + } + finally + { + _applicationLifetime.StopApplication(); + } + }); + + // Capture CTRL+C and prevent it from immediately force killing the app. + _console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + _applicationLifetime.StopApplication(); + }; + + return Task.CompletedTask; + } + + /// The exit code returned by the command line application + public int ExitCode { get; private set; } +} \ No newline at end of file diff --git a/src/CommandLine.Hosting/Internal/CommandLineService.cs b/src/CommandLine.Hosting/Internal/CommandLineService.cs new file mode 100644 index 0000000..b364463 --- /dev/null +++ b/src/CommandLine.Hosting/Internal/CommandLineService.cs @@ -0,0 +1,115 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using McMaster.Extensions.CommandLineUtils; +using McMaster.Extensions.CommandLineUtils.Conventions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Ploch.CommandLine.Hosting.Internal; + +/// +/// A service to be run as part of the when using builder API. +/// +internal class CommandLineService : IDisposable, ICommandLineService +{ + private readonly CommandLineApplication _application; + private readonly ILogger? _logger; + private readonly CommandLineState _state; + + /// + /// Creates a new instance. + /// + /// A logger + /// The command line state + /// The DI service provider + /// The delegate to configure the app + public CommandLineService(CommandLineState state, + IServiceProvider serviceProvider, + Action configure, + ILogger? logger = null) + { + _logger = logger; + _state = state; + + logger?.LogDebug("Constructing CommandLineApplication with args [{args}]", string.Join(",", state.Arguments)); + _application = new CommandLineApplication(state.Console, state.WorkingDirectory); + + _application.Conventions + .UseDefaultConventions() + .UseConstructorInjection(serviceProvider); + foreach (var convention in serviceProvider.GetServices()) + { + _application.Conventions.AddConvention(convention); + } + + configure(_application); + } + + /// + public async Task RunAsync(CancellationToken cancellationToken) + { + _logger?.LogDebug("Running"); + _state.ExitCode = await _application.ExecuteAsync(_state.Arguments, cancellationToken); + return _state.ExitCode; + } + + public void Dispose() + { + _application.Dispose(); + } +} + +/// +/// A service to be run as part of the when using attribute API. +/// +internal class CommandLineService : IDisposable, ICommandLineService + where T : class +{ + private readonly CommandLineApplication _application; + private readonly ILogger _logger; + private readonly CommandLineState _state; + + /// + /// Creates a new instance. + /// + /// A logger + /// The command line state + /// The DI service provider + /// The delegate to configure the app + public CommandLineService(ILogger> logger, + CommandLineState state, + IServiceProvider serviceProvider, + Action> configure) + { + _logger = logger; + _state = state; + + logger.LogDebug("Constructing CommandLineApplication<{type}> with args [{args}]", + typeof(T).FullName, string.Join(",", state.Arguments)); + _application = new CommandLineApplication(state.Console, state.WorkingDirectory); + _application.Conventions + .UseDefaultConventions() + .UseConstructorInjection(serviceProvider); + + foreach (var convention in serviceProvider.GetServices()) + { + _application.Conventions.AddConvention(convention); + } + + configure(_application); + } + + /// + public async Task RunAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("Running"); + _state.ExitCode = await _application.ExecuteAsync(_state.Arguments, cancellationToken); + return _state.ExitCode; + } + + public void Dispose() + { + _application.Dispose(); + } +} \ No newline at end of file diff --git a/src/CommandLine.Hosting/Internal/CommandLineState.cs b/src/CommandLine.Hosting/Internal/CommandLineState.cs new file mode 100644 index 0000000..290ca8d --- /dev/null +++ b/src/CommandLine.Hosting/Internal/CommandLineState.cs @@ -0,0 +1,25 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using McMaster.Extensions.CommandLineUtils; +using McMaster.Extensions.CommandLineUtils.Abstractions; + +namespace Ploch.CommandLine.Hosting.Internal; + +/// +/// A DI container for storing command line arguments. +/// +internal class CommandLineState : CommandLineContext +{ + public CommandLineState(IEnumerable args) + { + Arguments = args.ToArray(); + } + + public int ExitCode { get; set; } + + internal void SetConsole(IConsole console) + { + Console = console; + } +} \ No newline at end of file diff --git a/src/CommandLine.Hosting/Internal/ICommandLineService.cs b/src/CommandLine.Hosting/Internal/ICommandLineService.cs new file mode 100644 index 0000000..b2840f9 --- /dev/null +++ b/src/CommandLine.Hosting/Internal/ICommandLineService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Ploch.CommandLine.Hosting.Internal; + +/// +/// A service to be run as part of the . +/// +internal interface ICommandLineService +{ + /// + /// Runs the application asynchronously and returns the exit code. + /// + /// Used to indicate when stop should no longer be graceful. + /// The exit code + Task RunAsync(CancellationToken cancellationToken); +} \ No newline at end of file diff --git a/src/CommandLine.Hosting/Internal/StoreExceptionHandler.cs b/src/CommandLine.Hosting/Internal/StoreExceptionHandler.cs new file mode 100644 index 0000000..2feafbd --- /dev/null +++ b/src/CommandLine.Hosting/Internal/StoreExceptionHandler.cs @@ -0,0 +1,35 @@ +// Copyright (c) Nate McMaster. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Ploch.CommandLine.Hosting.Internal; + +/// +/// Implementation of that stores an unhandled exception so it can later be +/// rethrown by . +/// +internal class StoreExceptionHandler : IUnhandledExceptionHandler +{ + /// + /// The captured exception, if any + /// + public Exception? StoredException { get; private set; } + + /// + /// This will store the first unhandled exception and throw an if called a + /// second time. + /// + /// The unhandled exception to store + /// + /// If called a second time an containing + /// both exceptions is raised + /// + public void HandleException(Exception e) + { + if (StoredException != null) + { + throw new AggregateException("Second exception received!", StoredException, e); + } + + StoredException = e; + } +} \ No newline at end of file diff --git a/src/CommandLine.Hosting/Ploch.CommandLine.Hosting.csproj b/src/CommandLine.Hosting/Ploch.CommandLine.Hosting.csproj new file mode 100644 index 0000000..40ead38 --- /dev/null +++ b/src/CommandLine.Hosting/Ploch.CommandLine.Hosting.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + False + + + + + + + + + diff --git a/src/CommandLine.Hosting/ServiceCollectionRegistrations.cs b/src/CommandLine.Hosting/ServiceCollectionRegistrations.cs new file mode 100644 index 0000000..1e20688 --- /dev/null +++ b/src/CommandLine.Hosting/ServiceCollectionRegistrations.cs @@ -0,0 +1,61 @@ +using McMaster.Extensions.CommandLineUtils; +using McMaster.Extensions.CommandLineUtils.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Ploch.CommandLine.Hosting.Internal; + +namespace Ploch.CommandLine.Hosting; + +public static class ServiceCollectionRegistrations +{ + /*public static IHostBuilder UseCommandLineApplication( + this IHostBuilder hostBuilder, + string[] args, + Action> configure) + where TApp : class + { + configure ??= _ => { }; + var state = new CommandLineState(args); + hostBuilder.Properties[typeof(CommandLineState)] = state; + hostBuilder.ConfigureServices((context, services) => + services + .AddCommonServices(state) + .AddSingleton>() + .AddSingleton(configure)); + + return hostBuilder; + }* + */ + public static IServiceCollection AddCommandLineAppServices(IServiceCollection services, + IEnumerable args, + Action> configure) + where TApp : class + { + // ReSharper disable PossibleMultipleEnumeration + var state = new CommandLineState(args); + + return services.AddCommonServices(state) + .AddSingleton>() + .AddSingleton(configure); + + // ReSharper restore PossibleMultipleEnumeration + } + + private static IServiceCollection AddCommonServices(this IServiceCollection services, CommandLineState state) + { + services.TryAddSingleton(); + services.TryAddSingleton(provider => provider.GetRequiredService()); + services.TryAddSingleton(PhysicalConsole.Singleton); + services + .AddSingleton() + .AddSingleton(provider => + { + state.SetConsole(provider.GetRequiredService()); + return state; + }) + .AddSingleton(state); + + return services; + } +} \ No newline at end of file diff --git a/src/CommandLine.Serilog/LoggingSetup.cs b/src/CommandLine.Serilog/LoggingSetup.cs index bb2d6c0..8869a05 100644 --- a/src/CommandLine.Serilog/LoggingSetup.cs +++ b/src/CommandLine.Serilog/LoggingSetup.cs @@ -28,26 +28,26 @@ public static AppBuilder UseSerilog(this AppBuilder appBuilder, string? logName private static void ConfigureServices(IServiceCollection serviceCollection, string? logName = null, string? logPath = null) { var loggerConfiguration = new LoggerConfiguration().Enrich.FromLogContext() - .Enrich.WithThreadId() - .Enrich.WithThreadName() - .Enrich.FromLogContext() - .WriteTo.File(BuildFullLogPath(logName, logPath), - rollOnFileSizeLimit: true, - fileSizeLimitBytes: 2 * 1024 * 1024, - // outputTemplate: template, - retainedFileCountLimit: 10, - formatProvider: CultureInfo.CurrentCulture) - .WriteTo.Logger(l => l.Filter.ByIncludingOnly(logEvent => - logEvent.Level is LogEventLevel.Error - or LogEventLevel.Warning - or LogEventLevel.Fatal)) - .WriteTo.File(BuildFullLogPath(logName, logPath, "errors"), - rollOnFileSizeLimit: true, - fileSizeLimitBytes: 2 * 1024 * 1024, - // outputTemplate: template, - retainedFileCountLimit: 10, - formatProvider: CultureInfo.CurrentCulture) - .WriteTo.Console(formatProvider: CultureInfo.CurrentCulture); + .Enrich.WithThreadId() + .Enrich.WithThreadName() + .Enrich.FromLogContext() + .WriteTo.File(BuildFullLogPath(logName, logPath), + rollOnFileSizeLimit: true, + fileSizeLimitBytes: 2 * 1024 * 1024, + // outputTemplate: template, + retainedFileCountLimit: 10, + formatProvider: CultureInfo.CurrentCulture) + .WriteTo.Logger(l => l.Filter.ByIncludingOnly(logEvent => + logEvent.Level is LogEventLevel.Error + or LogEventLevel.Warning + or LogEventLevel.Fatal)) + .WriteTo.File(BuildFullLogPath(logName, logPath, "errors"), + rollOnFileSizeLimit: true, + fileSizeLimitBytes: 2 * 1024 * 1024, + // outputTemplate: template, + retainedFileCountLimit: 10, + formatProvider: CultureInfo.CurrentCulture) + .WriteTo.Console(formatProvider: CultureInfo.CurrentCulture).MinimumLevel.Error(); serviceCollection.AddLogging(builder => builder.AddSerilog(loggerConfiguration.CreateLogger())); } diff --git a/src/CommandLine.Serilog/Ploch.Common.CommandLine.Serilog.csproj b/src/CommandLine.Serilog/Ploch.Common.CommandLine.Serilog.csproj index 6875f99..4627918 100644 --- a/src/CommandLine.Serilog/Ploch.Common.CommandLine.Serilog.csproj +++ b/src/CommandLine.Serilog/Ploch.Common.CommandLine.Serilog.csproj @@ -1,35 +1,27 @@ - + - net8.0 + net9.0 enable enable Nullable + + False + + - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + - + diff --git a/src/CommandLine/AppBuilder.cs b/src/CommandLine/AppBuilder.cs index e42367d..026246f 100644 --- a/src/CommandLine/AppBuilder.cs +++ b/src/CommandLine/AppBuilder.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using Microsoft.Extensions.Configuration; using McMaster.Extensions.CommandLineUtils; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace Ploch.Common.CommandLine; @@ -32,6 +32,26 @@ private AppBuilder(Func? appBuildFunc, } } + /// + /// Creates a default instance of the with optional configuration + /// and command-line arguments. + /// + /// The application name (or title) shown during execution and in the help screens. + /// The application description displayed during execution and in the help screens. + /// + /// An optional action to apply additional configuration settings to the . + /// + /// + /// An array of command-line arguments to be added to the configuration. + /// + /// + /// Returns an instance of with the default setup and the supplied configuration. + /// + public static AppBuilder CreateDefault(string name, string description, Action? configurationAction = null, params string[] args) + { + return CreateDefault(new CommandAppProperties(name, description), configurationAction, args); + } + /// /// Creates a default instance of the with optional configuration /// and command-line arguments. @@ -82,7 +102,7 @@ public AppBuilder Configure(Action configurationAction /// /// Returns a fully constructed and configured instance of . /// - public CommandLineApplication Build() + public CommandLineApplication BuildConsole() { var app = _appBuildFunc(); var serviceCollections = new ServiceCollection(); diff --git a/src/CommandLine/CommandAppProperties.cs b/src/CommandLine/CommandAppProperties.cs index ac6f965..18bc2af 100644 --- a/src/CommandLine/CommandAppProperties.cs +++ b/src/CommandLine/CommandAppProperties.cs @@ -1,3 +1,26 @@ namespace Ploch.Common.CommandLine; -public record CommandAppProperties(string Name, string Description); \ No newline at end of file +public record CommandAppProperties(string Name, string Description); + +public class CommandAppPropertiesBuilder +{ + private string _description = string.Empty; + private string _name = string.Empty; + + public CommandAppPropertiesBuilder WithName(string name) + { + _name = name; + return this; + } + + public CommandAppPropertiesBuilder WithDescription(string description) + { + _description = description; + return this; + } + + public CommandAppProperties Build() + { + return new CommandAppProperties(_name, _description); + } +} \ No newline at end of file diff --git a/src/CommandLine/CommandLineApplicationConfigurationExtensions.cs b/src/CommandLine/CommandLineApplicationConfigurationExtensions.cs index 031c599..e4414ba 100644 --- a/src/CommandLine/CommandLineApplicationConfigurationExtensions.cs +++ b/src/CommandLine/CommandLineApplicationConfigurationExtensions.cs @@ -4,8 +4,49 @@ namespace Ploch.Common.CommandLine; +/// +/// Provides extension methods for configuring and enhancing the behavior of +/// instances. +/// +/// +/// This class includes methods to add custom validators and configure commands for command-line applications. +/// public static class CommandLineApplicationConfigurationExtensions { + /// + /// Adds a custom validator to the specified + /// instance. + /// + /// + /// The type of the model associated with the + /// . + /// + /// + /// The instance to which the + /// validator will be added. + /// + /// + /// A delegate representing the custom validation logic to be executed before the command is executed. + /// + /// + /// The instance with the added + /// validator. + /// + /// + /// This method allows you to add a custom pre-execution validator to a command-line application. + /// The validator is executed before the command is executed, ensuring that the application meets + /// specific validation criteria. + /// + /// + /// + /// var app = new CommandLineApplication<MyModel>(); + /// app.AddValidator((command, context) => + /// { + /// // Custom validation logic + /// return ValidationResult.Success; + /// }); + /// + /// public static CommandLineApplication AddValidator(this CommandLineApplication application, PreExecuteCommandValidator validator) where TModel : class { diff --git a/src/CommandLine/ICommand.cs b/src/CommandLine/ICommand.cs index 8322891..b519539 100644 --- a/src/CommandLine/ICommand.cs +++ b/src/CommandLine/ICommand.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; namespace Ploch.Common.CommandLine; @@ -11,4 +10,4 @@ public interface ICommand /// Executed status code integer [SuppressMessage("ReSharper", "UnusedMemberInSuper.Global", Justification = "Called dynamically by the CommandLineUtils library")] void OnExecute(); -} +} \ No newline at end of file diff --git a/src/CommandLine/ICommandAppPropertiesConfigurator.cs b/src/CommandLine/ICommandAppPropertiesConfigurator.cs new file mode 100644 index 0000000..8ef3c6a --- /dev/null +++ b/src/CommandLine/ICommandAppPropertiesConfigurator.cs @@ -0,0 +1,13 @@ +namespace Ploch.Common.CommandLine; + +public interface ICommandAppPropertiesConfigurator +{ + ICommandAppPropertiesConfigurator WithName(string name); + + ICommandAppPropertiesConfigurator WithDescription(string description); +} + +public interface ICommandAppPropertiesBuilder : ICommandAppPropertiesConfigurator +{ + CommandAppProperties Build(); +} \ No newline at end of file diff --git a/src/CommandLine/Ploch.Common.CommandLine.csproj b/src/CommandLine/Ploch.Common.CommandLine.csproj index 9ee6eae..0972614 100644 --- a/src/CommandLine/Ploch.Common.CommandLine.csproj +++ b/src/CommandLine/Ploch.Common.CommandLine.csproj @@ -1,25 +1,29 @@ - + - - net8.0 - enable - Nullable - + + net9.0 + enable + Nullable + - - - - - - - - - - - - - - - + + False + + + + + + + + + + + + + + + + + diff --git a/src/DemoApp/AdvancedFeaturesSample/AdvancedFeaturesSample.csproj b/src/DemoApp/AdvancedFeaturesSample/AdvancedFeaturesSample.csproj new file mode 100644 index 0000000..1bdbc04 --- /dev/null +++ b/src/DemoApp/AdvancedFeaturesSample/AdvancedFeaturesSample.csproj @@ -0,0 +1,55 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + PreserveNewest + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + diff --git a/src/DemoApp/AdvancedFeaturesSample/ISampleService.cs b/src/DemoApp/AdvancedFeaturesSample/ISampleService.cs new file mode 100644 index 0000000..a8e6bd8 --- /dev/null +++ b/src/DemoApp/AdvancedFeaturesSample/ISampleService.cs @@ -0,0 +1,4 @@ +public interface ISampleService +{ + void ExecuteSomeAction(); +} \ No newline at end of file diff --git a/src/DemoApp/ConsoleApp1/Program.cs b/src/DemoApp/AdvancedFeaturesSample/Program.cs similarity index 62% rename from src/DemoApp/ConsoleApp1/Program.cs rename to src/DemoApp/AdvancedFeaturesSample/Program.cs index 197fee8..3b504a0 100644 --- a/src/DemoApp/ConsoleApp1/Program.cs +++ b/src/DemoApp/AdvancedFeaturesSample/Program.cs @@ -1,4 +1,5 @@ -using ConsoleApp1; +using BasicConsoleApp; +using AdvancedFeaturesSample; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ploch.Common.CommandLine; @@ -9,12 +10,12 @@ await AppBuilder.CreateDefault(new CommandAppProperties("MyTestApp", "My App Des .UseAutofac() .Configure(container => { - container.Services.AddSingleton() - .AddSingleton() - .AddSingleton(); - container.Application.Command(app => + container.Services.AddSingleton() + .AddSingleton() + .AddSingleton(); + container.Application.Command(app => { - app.Command(); + app.Command(); }); }) .Build() diff --git a/src/DemoApp/AdvancedFeaturesSample/README.md b/src/DemoApp/AdvancedFeaturesSample/README.md new file mode 100644 index 0000000..b52bc01 --- /dev/null +++ b/src/DemoApp/AdvancedFeaturesSample/README.md @@ -0,0 +1,13 @@ +# Ploch CommandLine Applications Advanced Features Sample + +## Overview + +This sample demonstrates some of the advanced features of the Ploch CommandLine Applications library. + +This includes: + +- Using Verbs and Multi-Level Composite Commands +- Application Configuration +- Dependency Injection +- Logging + diff --git a/src/DemoApp/AdvancedFeaturesSample/SampleChildCommand.cs b/src/DemoApp/AdvancedFeaturesSample/SampleChildCommand.cs new file mode 100644 index 0000000..9de0c7f --- /dev/null +++ b/src/DemoApp/AdvancedFeaturesSample/SampleChildCommand.cs @@ -0,0 +1,29 @@ +using BasicConsoleApp; +using McMaster.Extensions.CommandLineUtils; +using Ploch.Common.CommandLine; + +namespace AdvancedFeaturesSample; + +/// +/// This is a sub-command for the command. +/// +/// The instance. +/// An interface that is injected here using the dependency injection. +/// Parent command allowing access to its options. +[Command(Name = "inner1")] +public class SampleChildCommand(CommandLineApplication app, ISampleService sampleService, SampleRootCommand parentCommand) : ICommand +{ + [Option] + public string? SomeOption { get; set; } + + public void OnExecute() + { + sampleService.ExecuteSomeAction(); + Console.WriteLine($"{SomeOption}{parentCommand.Verbose}-{parentCommand.Colour}"); + + Console.WriteLine("ChildCommand1 executed"); + + Console.WriteLine(); + app.WriteHelpText(); + } +} \ No newline at end of file diff --git a/src/DemoApp/AdvancedFeaturesSample/SampleHelloWorldService.cs b/src/DemoApp/AdvancedFeaturesSample/SampleHelloWorldService.cs new file mode 100644 index 0000000..957d390 --- /dev/null +++ b/src/DemoApp/AdvancedFeaturesSample/SampleHelloWorldService.cs @@ -0,0 +1,10 @@ +using McMaster.Extensions.CommandLineUtils; + +public class SampleHelloWorldService(IConsole console) : ISampleService +{ + public void ExecuteSomeAction() + { + ; + console.WriteLine("Hello World from SampleHelloWorldService"); + } +} \ No newline at end of file diff --git a/src/DemoApp/ConsoleApp1/RootCommand1.cs b/src/DemoApp/AdvancedFeaturesSample/SampleRootCommand.cs similarity index 69% rename from src/DemoApp/ConsoleApp1/RootCommand1.cs rename to src/DemoApp/AdvancedFeaturesSample/SampleRootCommand.cs index c691666..344d0cb 100644 --- a/src/DemoApp/ConsoleApp1/RootCommand1.cs +++ b/src/DemoApp/AdvancedFeaturesSample/SampleRootCommand.cs @@ -1,10 +1,10 @@ using McMaster.Extensions.CommandLineUtils; using Ploch.Common.CommandLine; -namespace ConsoleApp1; +namespace BasicConsoleApp; [Command(Name = "command1")] -public class RootCommand1(CommandLineApplication app) : HelpOnlyCommand(app) +public class SampleRootCommand(CommandLineApplication app) : HelpOnlyCommand(app) { [Option(Inherited = true)] public bool Verbose { get; set; } diff --git a/src/DemoApp/ConsoleApp1/appsettings.json b/src/DemoApp/AdvancedFeaturesSample/appsettings.json similarity index 100% rename from src/DemoApp/ConsoleApp1/appsettings.json rename to src/DemoApp/AdvancedFeaturesSample/appsettings.json diff --git a/src/DemoApp/ConsoleApp1/ChildCommand1.cs b/src/DemoApp/ConsoleApp1/ChildCommand1.cs deleted file mode 100644 index b164ab1..0000000 --- a/src/DemoApp/ConsoleApp1/ChildCommand1.cs +++ /dev/null @@ -1,22 +0,0 @@ -using McMaster.Extensions.CommandLineUtils; -using Ploch.Common.CommandLine; - -namespace ConsoleApp1; - -[Command(Name = "inner1")] -public class ChildCommand1(CommandLineApplication app, ISomeInterface someInterface, RootCommand1 parentCommand) : ICommand -{ - [Option] - public string? SomeOption { get; set; } - - public void OnExecute() - { - someInterface.SomeMethod(); - Console.WriteLine($"{SomeOption}{parentCommand.Verbose}-{parentCommand.Colour}"); - - Console.WriteLine("ChildCommand1 executed"); - - Console.WriteLine(); - app.WriteHelpText(); - } -} \ No newline at end of file diff --git a/src/DemoApp/ConsoleApp1/ConsoleApp1.csproj b/src/DemoApp/ConsoleApp1/ConsoleApp1.csproj deleted file mode 100644 index cef42f2..0000000 --- a/src/DemoApp/ConsoleApp1/ConsoleApp1.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - - - - - - - PreserveNewest - - - - diff --git a/src/DemoApp/ConsoleApp1/ISomeInterface.cs b/src/DemoApp/ConsoleApp1/ISomeInterface.cs deleted file mode 100644 index e5afe76..0000000 --- a/src/DemoApp/ConsoleApp1/ISomeInterface.cs +++ /dev/null @@ -1,4 +0,0 @@ -public interface ISomeInterface -{ - void SomeMethod(); -} \ No newline at end of file diff --git a/src/DemoApp/ConsoleApp1/SomeClass.cs b/src/DemoApp/ConsoleApp1/SomeClass.cs deleted file mode 100644 index 35b14b1..0000000 --- a/src/DemoApp/ConsoleApp1/SomeClass.cs +++ /dev/null @@ -1,7 +0,0 @@ -public class SomeClass : ISomeInterface -{ - public void SomeMethod() - { - Console.WriteLine("SomeMethod"); - } -} \ No newline at end of file diff --git a/tests/CommandLine.IntegrationTests/Ploch.Common.CommandLine.IntegrationTests.csproj b/tests/CommandLine.IntegrationTests/Ploch.Common.CommandLine.IntegrationTests.csproj index deab123..a1470c0 100644 --- a/tests/CommandLine.IntegrationTests/Ploch.Common.CommandLine.IntegrationTests.csproj +++ b/tests/CommandLine.IntegrationTests/Ploch.Common.CommandLine.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable @@ -38,10 +38,6 @@ - - - - diff --git a/tests/CommandLine.IntegrationTests/TestCommand.cs b/tests/CommandLine.IntegrationTests/TestCommand.cs index 3b2e6d4..cf4db64 100644 --- a/tests/CommandLine.IntegrationTests/TestCommand.cs +++ b/tests/CommandLine.IntegrationTests/TestCommand.cs @@ -5,7 +5,8 @@ namespace Ploch.Common.CommandLine.Tests; [Command(Name = "testCommand")] public class TestCommand(TestCallback testCallback) : IAsyncCommand { - [Option] public string? TestArg { get; set; } + [Option] + public string? TestArg { get; set; } public Task OnExecuteAsync(CancellationToken cancellationToken = default) { diff --git a/tests/CommandLine.IntegrationTests/TestCommandLineApp.cs b/tests/CommandLine.IntegrationTests/TestCommandLineApp.cs index af93ab1..9d92faa 100644 --- a/tests/CommandLine.IntegrationTests/TestCommandLineApp.cs +++ b/tests/CommandLine.IntegrationTests/TestCommandLineApp.cs @@ -18,7 +18,7 @@ public static int AppMain(string[] args, TestCallback testCallback) appContainer.Application.Command(); }) - .Build() + .BuildConsole() .Execute(args); } } \ No newline at end of file