├── 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 |
--------------------------------------------------------------------------------