├── logs └── .gitingore ├── .gitignore ├── Models ├── Output │ ├── _IOutput.cs │ ├── CalculateOutput.cs │ └── StatisticsOutput.cs └── Arguments │ ├── OutputFormat.cs │ ├── CalculateOptions.cs │ └── StatisticsOptions.cs ├── appsettings.json ├── Services ├── Tasks │ ├── _ITaskFactory.cs │ ├── CalculateTaskFactory.cs │ └── StatisticsTaskFactory.cs └── ConsoleOutput.cs ├── sample.console.sln.DotSettings ├── sample.console.sln ├── sample.console.csproj ├── Program.cs └── Readme.md /logs/.gitingore: -------------------------------------------------------------------------------- 1 | *.txt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | logs/*.txt -------------------------------------------------------------------------------- /Models/Output/_IOutput.cs: -------------------------------------------------------------------------------- 1 | namespace sample.console.Models.Output 2 | { 3 | public interface IOutput 4 | { 5 | string ToJson(); 6 | string ToString(); 7 | } 8 | } -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": "Debug", 4 | "WriteTo": [ 5 | { "Name": "File", "Args": { "path": "logs/log.txt" } } 6 | ], 7 | "Enrich": [ "FromLogContext" ] 8 | } 9 | } -------------------------------------------------------------------------------- /Models/Arguments/OutputFormat.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace sample.console.Models.Arguments 4 | { 5 | [SuppressMessage("ReSharper", "InconsistentNaming")] 6 | public enum OutputFormat 7 | { 8 | text, 9 | json 10 | } 11 | } -------------------------------------------------------------------------------- /Services/Tasks/_ITaskFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace sample.console.Services.Tasks 4 | { 5 | /// 6 | /// Interface for launching an executable task 7 | /// 8 | public interface ITaskFactory 9 | { 10 | /// 11 | /// Execute a task, return the exit code (0 = success) 12 | /// 13 | /// 14 | public Task Launch(); 15 | } 16 | } -------------------------------------------------------------------------------- /sample.console.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSV 3 | IQR 4 | URL -------------------------------------------------------------------------------- /Models/Arguments/CalculateOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CommandLine; 3 | 4 | namespace sample.console.Models.Arguments 5 | { 6 | /// 7 | /// Command line parameters for calculating 8 | /// 9 | [Verb("calc", true, HelpText="Evaluate a mathematical expression")] 10 | public class CalculateOptions 11 | { 12 | [Option('f', "format", Default = OutputFormat.text)] 13 | public OutputFormat Format { get; set; } = default!; 14 | 15 | [Value(0, MetaName = "Expression", Required = true, 16 | HelpText = @"Mathematical expression")] 17 | public IEnumerable Expression { get; set; } = default!; 18 | } 19 | } -------------------------------------------------------------------------------- /Models/Arguments/StatisticsOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CommandLine; 3 | 4 | namespace sample.console.Models.Arguments 5 | { 6 | /// 7 | /// Command line parameters for calculating 8 | /// 9 | [Verb("stats", false, HelpText="Calculate statistics for a list of values")] 10 | public class StatisticsOptions 11 | { 12 | [Option('f', "format", Default = OutputFormat.text)] 13 | public OutputFormat Format { get; set; } = default!; 14 | 15 | [Value(0, MetaName = "Expression", Required = true, 16 | HelpText = @"Mathematical expression")] 17 | public IEnumerable Values { get; set; } = default!; 18 | } 19 | } -------------------------------------------------------------------------------- /sample.console.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "sample.console", "sample.console.csproj", "{61D6F854-4998-4F2A-9AA0-DC1B8D7E3254}" 4 | EndProject 5 | Global 6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 7 | Debug|Any CPU = Debug|Any CPU 8 | Release|Any CPU = Release|Any CPU 9 | EndGlobalSection 10 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 11 | {61D6F854-4998-4F2A-9AA0-DC1B8D7E3254}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 12 | {61D6F854-4998-4F2A-9AA0-DC1B8D7E3254}.Debug|Any CPU.Build.0 = Debug|Any CPU 13 | {61D6F854-4998-4F2A-9AA0-DC1B8D7E3254}.Release|Any CPU.ActiveCfg = Release|Any CPU 14 | {61D6F854-4998-4F2A-9AA0-DC1B8D7E3254}.Release|Any CPU.Build.0 = Release|Any CPU 15 | EndGlobalSection 16 | EndGlobal 17 | -------------------------------------------------------------------------------- /Models/Output/CalculateOutput.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace sample.console.Models.Output 7 | { 8 | public class CalculateOutput: IOutput 9 | { 10 | [DataMember(Order = 1)] 11 | [JsonInclude] 12 | public object? Value { get; init; } 13 | 14 | public static CalculateOutput FromResults(object? results) => 15 | new() 16 | { 17 | Value = results 18 | }; 19 | 20 | /// 21 | /// Output as plain text 22 | /// 23 | /// 24 | public new string ToString() => Value?.ToString() ?? "N/A"; 25 | 26 | /// 27 | /// Output as JSON 28 | /// 29 | /// 30 | public string ToJson() => JsonSerializer.Serialize(this); 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /Services/ConsoleOutput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace sample.console.Services 5 | { 6 | /// 7 | /// Class to abstract console output including mirroring to ILogger 8 | /// 9 | public class ConsoleOutput: IConsoleOutput 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public ConsoleOutput(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | public void WriteLine(string message) 19 | { 20 | Console.WriteLine(message); 21 | _logger.LogInformation("{Message}", message); 22 | } 23 | 24 | public void WriteError(string message) 25 | { 26 | Console.Error.WriteLine(message); 27 | _logger.LogError("{Message}", message); 28 | } 29 | } 30 | 31 | public interface IConsoleOutput 32 | { 33 | void WriteLine(string message); 34 | void WriteError(string message); 35 | } 36 | } -------------------------------------------------------------------------------- /sample.console.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | 9 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Services/Tasks/CalculateTaskFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using CodingSeb.ExpressionEvaluator; 6 | using Microsoft.Extensions.Logging; 7 | using sample.console.Models.Arguments; 8 | using sample.console.Models.Output; 9 | 10 | namespace sample.console.Services.Tasks 11 | { 12 | [SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")] 13 | public class CalculateTaskFactory : ITaskFactory 14 | { 15 | private readonly ExpressionEvaluator _evaluator; 16 | private readonly IConsoleOutput _console; 17 | private readonly ILogger _logger; 18 | private readonly CalculateOptions _options; 19 | 20 | public CalculateTaskFactory( 21 | ExpressionEvaluator evaluator, 22 | IConsoleOutput console, 23 | ILogger logger, 24 | CalculateOptions options) 25 | { 26 | _evaluator = evaluator; 27 | _console = console; 28 | _logger = logger; 29 | _options = options; 30 | } 31 | 32 | /// 33 | /// Outputs a feed to the specified file and format 34 | /// 35 | public Task Launch() => Task.Run(() => 36 | { 37 | try 38 | { 39 | var expression = string.Join(" ", _options.Expression); 40 | _logger.LogDebug("Evaluating {Evaluation}", expression); 41 | var start = DateTime.Now; 42 | var results = _evaluator.Evaluate(expression); 43 | var end = DateTime.Now; 44 | var output = CalculateOutput.FromResults(results); 45 | _console.WriteLine(_options.Format == OutputFormat.text 46 | ? output.ToString() 47 | : output.ToJson()); 48 | _logger.LogDebug("Evaluated in {Elapsed} milliseconds", 49 | end.Subtract(start).TotalMilliseconds); 50 | return 0; 51 | } 52 | catch (Exception ex) 53 | { 54 | _console.WriteError($"Unable to calculate: {ex.Message}"); 55 | return -1; 56 | } 57 | }); 58 | } 59 | } -------------------------------------------------------------------------------- /Services/Tasks/StatisticsTaskFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using MathNet.Numerics.Statistics; 7 | using Microsoft.Extensions.Logging; 8 | using sample.console.Models.Arguments; 9 | using sample.console.Models.Output; 10 | 11 | namespace sample.console.Services.Tasks 12 | { 13 | [SuppressMessage("ReSharper", "ClassWithVirtualMembersNeverInherited.Global")] 14 | public class StatisticsTaskFactory : ITaskFactory 15 | { 16 | private readonly IConsoleOutput _console; 17 | private readonly ILogger _logger; 18 | private readonly StatisticsOptions _options; 19 | 20 | public StatisticsTaskFactory( 21 | IConsoleOutput console, 22 | ILogger logger, 23 | StatisticsOptions options) 24 | { 25 | _console = console; 26 | _logger = logger; 27 | _options = options; 28 | } 29 | 30 | /// 31 | /// Outputs a feed to the specified file and format 32 | /// 33 | public Task Launch() => Task.Run(() => 34 | { 35 | try 36 | { 37 | _logger.LogDebug("Analyzing {Values}", string.Join(",", _options.Values)); 38 | var start = DateTime.Now; 39 | var stats = new DescriptiveStatistics(_options.Values.Select(Convert.ToDouble)); 40 | var end = DateTime.Now; 41 | var results = StatisticsOutput.FromDescriptiveStatistics(stats); 42 | var output = _options.Format == OutputFormat.text 43 | ? string.Join(", ", results.GetType().GetProperties().Select(p => $"{p.Name}={p.GetValue(results)}")) 44 | : JsonSerializer.Serialize(results); 45 | _console.WriteLine(output); 46 | _logger.LogDebug("Analyzed in {Elapsed} milliseconds", 47 | end.Subtract(start).TotalMilliseconds); 48 | return 0; 49 | } 50 | catch (Exception ex) 51 | { 52 | _console.WriteError($"Unable to calculate: {ex.Message}"); 53 | return -1; 54 | } 55 | }); 56 | } 57 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CodingSeb.ExpressionEvaluator; 4 | using CommandLine; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using sample.console.Models.Arguments; 8 | using sample.console.Services; 9 | using sample.console.Services.Tasks; 10 | using Serilog; 11 | 12 | namespace sample.console 13 | { 14 | internal class Program 15 | { 16 | /// 17 | /// Main routine 18 | /// 19 | /// 20 | /// Exit code 21 | public static async Task Main(string[] args) 22 | { 23 | try 24 | { 25 | var host = Host.CreateDefaultBuilder(args) 26 | .ConfigureServices((context, services) => 27 | { 28 | // Configure Serilog 29 | Log.Logger = new LoggerConfiguration() 30 | .ReadFrom.Configuration(context.Configuration) 31 | .CreateLogger(); 32 | 33 | // Set up our console output class 34 | services.AddSingleton(); 35 | 36 | // Based upon verb/options, create services, including the task 37 | var parserResult = Parser.Default.ParseArguments(args); 38 | parserResult 39 | .WithParsed(options => 40 | { 41 | services.AddSingleton(); 42 | services.AddSingleton(options); 43 | services.AddSingleton(); 44 | }) 45 | .WithParsed(options => 46 | { 47 | services.AddSingleton(options); 48 | services.AddSingleton(); 49 | }); 50 | }) 51 | .UseSerilog() 52 | .Build(); 53 | 54 | // If a task was set up to run (i.e. valid command line params) then run it 55 | // and return the results 56 | var task = host.Services.GetService(); 57 | return task == null 58 | ? -1 // This can happen on --help or invalid arguments 59 | : await task.Launch(); 60 | } 61 | catch (Exception ex) 62 | { 63 | // Note that this should only occur if something went wrong with building Host 64 | await Console.Error.WriteLineAsync(ex.Message); 65 | return -1; 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Sample Console Application 2 | 3 | This demonstration project stands up a console app that implements [IHostBuilder](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder?view=dotnet-plat-ext-6.0) to implement dependency injection. It also demonstrates usage of the [CommandLineParser](https://github.com/commandlineparser/commandline) library for handling command line argument parsing. 4 | 5 | > In this demonstration project, IHostBuilder is overkill for what the functionality is. However, IHostBuilder can be useful if you are using code that is implemented via [service extensions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.serviceproviderserviceextensions?view=dotnet-plat-ext-6.0) or if you are building a larger application in which dependency is required to make the code more readable/testable/etc. 6 | 7 | ## License / Usage Rights 8 | 9 | Use and distribute freely with our without attribution. No warranty or claims made as to functionality, bugs, fitness to purpose or any other intent. 10 | 11 | ## Design Objectives 12 | 13 | * [Flexible command line argument processing](https://github.com/commandlineparser/commandline) 14 | * [Dependency injection](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostbuilder?view=dotnet-plat-ext-6.0) 15 | * [Ability to output to Console and ILogger independently](https://serilog.net/) 16 | 17 | ## Requirements 18 | 19 | * [Microsoft .NET 6 Core SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) 20 | 21 | ## Demonstrated Functionality 22 | 23 | This command line utilities performs two functions: 24 | 25 | * Calculation (default): Evaluate the passed parameters and return the results. 26 | * Statistics: Accepts a list of numbers and returns statistic information 27 | 28 | By default, the return format is "plain text". You can optionally return data in JSON format by specifying "-f json" 29 | 30 | ### Examples: 31 | 32 | *Executing via **dotnet** from project directory:* 33 | 34 | * `dotnet run -- 100 + 200 / 2` (Returns **200**) 35 | * `dotnet run -- "(100 + 200) / 2"` (Returns **150**, you'll need the quotes for parenthesis and asterisks) 36 | * `dotnet run -- calc 500 + 2` (Returns **502**, the "calc" is unnecessary but shown for completeness) 37 | * `dotnet run -- -f json 100/2` (Returns **{Value":50}**) 38 | * `dotnet run -- stats 100 200 300 400 500` (Returns **Count=5, Mean=300, Variance=25000, StandardDeviation=158.11388300841898, Skewness=0, Kurtosis=-1.2000000000000002, Maximum=500, Minimum=100**) 39 | * `dotnet run -- stats 100 200 300 400 500` (Returns **Count=5, Mean=300, Variance=25000, StandardDeviation=158.11388300841898, Skewness=0, Kurtosis=-1.2000000000000002, Maximum=500, Minimum=100**) 40 | * `dotnet run -- stats -f json 100 200 300 400 500`: (Returns **{"Count":5,"Mean":300,"Variance":25000,"StandardDeviation":158.11388300841898,"Skewness":0,"Kurtosis":-1.2000000000000002,"Maximum":500,"Minimum":100}**) 41 | 42 | ## Basic Recipe 43 | 44 | 1. In IHostBuilder.ConfigureServices, define injections for any global services and call the default command argument parser's ParseArguments 45 | 2. For parsed options, define verb-specific services, including implementing ITaskFactory (which either implements what you want to do or throws an exception) 46 | 3. Returns zero on happy path or otherwise -1 47 | 48 | > This example executes some functionality and exits. Therefore, it does not implement [IHostLifetime](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostlifetime?view=dotnet-plat-ext-6.0). If you are writing a service/daemon that is going to run persistently, you will want to implement [IHostLifetime](https://andrewlock.net/introducing-ihostlifetime-and-untangling-the-generic-host-startup-interactions/) -------------------------------------------------------------------------------- /Models/Output/StatisticsOutput.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection.Metadata.Ecma335; 2 | using System.Runtime.Serialization; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using MathNet.Numerics.Statistics; 6 | 7 | namespace sample.console.Models.Output 8 | { 9 | public class StatisticsOutput: IOutput 10 | { 11 | public static StatisticsOutput FromDescriptiveStatistics(DescriptiveStatistics statistics) => 12 | new() 13 | { 14 | Count = statistics.Count, 15 | Mean = statistics.Mean, 16 | Variance = statistics.Variance, 17 | StandardDeviation = statistics.StandardDeviation, 18 | Skewness = double.IsNaN(statistics.Skewness) ? null : statistics.Skewness, 19 | Kurtosis = double.IsNaN(statistics.Kurtosis) ? null : statistics.Kurtosis, 20 | Minimum = statistics.Minimum, 21 | Maximum = statistics.Maximum 22 | }; 23 | 24 | /// Gets the size of the sample. 25 | /// The size of the sample. 26 | [DataMember(Order = 1)] 27 | [JsonInclude] 28 | public long Count { get; init; } 29 | 30 | /// Gets the sample mean. 31 | /// The sample mean. 32 | [DataMember(Order = 2)] 33 | [JsonInclude] 34 | public double Mean { get; init; } 35 | 36 | /// 37 | /// Gets the unbiased population variance estimator (on a dataset of size N will use an N-1 normalizer). 38 | /// 39 | /// The sample variance. 40 | [DataMember(Order = 3)] 41 | [JsonInclude] 42 | public double Variance { get; init; } 43 | 44 | /// 45 | /// Gets the unbiased population standard deviation (on a dataset of size N will use an N-1 normalizer). 46 | /// 47 | /// The sample standard deviation. 48 | [DataMember(Order = 4)] 49 | [JsonInclude] 50 | public double StandardDeviation { get; init; } 51 | 52 | /// Gets the sample skewness. 53 | /// The sample skewness. 54 | /// Returns zero if is less than three. 55 | [DataMember(Order = 5)] 56 | [JsonInclude] 57 | public double? Skewness { get; init; } 58 | 59 | /// Gets the sample kurtosis. 60 | /// The sample kurtosis. 61 | /// Returns zero if is less than four. 62 | [DataMember(Order = 6)] 63 | [JsonInclude] 64 | public double? Kurtosis { get; init; } 65 | 66 | /// Gets the maximum sample value. 67 | /// The maximum sample value. 68 | [DataMember(Order = 7)] 69 | [JsonInclude] 70 | public double Maximum { get; init; } 71 | 72 | /// Gets the minimum sample value. 73 | /// The minimum sample value. 74 | [DataMember(Order = 8)] 75 | [JsonInclude] 76 | public double Minimum { get; init; } 77 | 78 | /// 79 | /// Output as plain text 80 | /// 81 | /// 82 | public override string ToString() => 83 | $"Count: {Count}, Mean: {Mean}, Variance: {Variance}, StdDev: {StandardDeviation}, Skewness: {(Skewness == null ? "N/A" : Skewness.ToString())}, Kurtosis: {(Kurtosis == null ? "N/A" : Kurtosis.ToString())}, Maximum: {Maximum}: Minimum: {Minimum}"; 84 | 85 | /// 86 | /// Output as JSON 87 | /// 88 | /// 89 | public string ToJson() => JsonSerializer.Serialize(this); 90 | } 91 | } 92 | --------------------------------------------------------------------------------