From 960bdad19f362db11a210cff3817d834ae5b090c Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 28 Jan 2026 13:58:54 -0800 Subject: [PATCH 1/3] Refactor Signed-off-by: Pieter Viljoen --- Benchmarks/Benchmarks.csproj | 2 +- CodeGen/ApiNinjas.cs | 47 +++++ CodeGen/CodeGen.csproj | 10 +- CodeGen/CodeGenBuilder.cs | 62 ++++++ CodeGen/CommandLine.cs | 29 +-- CodeGen/Program.cs | 136 ++---------- Console/CommandLine.cs | 71 +++++-- Console/Console.csproj | 10 +- Console/Program.cs | 35 ++-- Library/.editorconfig | 7 + Library/Library.csproj | 6 +- ProjectTemplate.code-workspace | 6 + ProjectTemplate.slnx | 4 +- README.md | 366 +++++++++++++++------------------ Tests/Tests.csproj | 1 + 15 files changed, 421 insertions(+), 371 deletions(-) create mode 100644 CodeGen/ApiNinjas.cs create mode 100644 CodeGen/CodeGenBuilder.cs create mode 100644 Library/.editorconfig diff --git a/Benchmarks/Benchmarks.csproj b/Benchmarks/Benchmarks.csproj index 61fabb07..84b9a369 100644 --- a/Benchmarks/Benchmarks.csproj +++ b/Benchmarks/Benchmarks.csproj @@ -2,10 +2,10 @@ latest-all true + false enable Exe ptr727.ProjectTemplate.Benchmarks - false net10.0 diff --git a/CodeGen/ApiNinjas.cs b/CodeGen/ApiNinjas.cs new file mode 100644 index 00000000..bc880f98 --- /dev/null +++ b/CodeGen/ApiNinjas.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ptr727.ProjectTemplate.CodeGen; + +internal sealed class ApiNinjas(string apiKey, CancellationToken cancellationToken) +{ + internal async Task GetQuoteOfTheDayAsync() + { + // https://api-ninjas.com/api/quotes#v2-quoteoftheday + using HttpRequestMessage request = new( + HttpMethod.Get, + "https://api.api-ninjas.com/v2/quotes?categories=philosophy" + ); + request.Headers.Add("X-Api-Key", apiKey); + + using HttpResponseMessage response = await HttpClientFactory + .GetHttpClient() + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + _ = response.EnsureSuccessStatusCode(); + + using Stream responseStream = await response + .Content.ReadAsStreamAsync(cancellationToken) + .ConfigureAwait(false); + QuoteOfTheDayItem[]? items = await JsonSerializer + .DeserializeAsync( + responseStream, + QuoteOfTheDayJsonContext.Default.QuoteOfTheDayItemArray, + cancellationToken + ) + .ConfigureAwait(false); + + string? quote = items?.FirstOrDefault()?.Quote; + return string.IsNullOrWhiteSpace(quote) + ? throw new InvalidOperationException( + "Quote of the day response did not include a quote." + ) + : quote; + } +} + +internal sealed record QuoteOfTheDayItem([property: JsonPropertyName("quote")] string Quote); + +[JsonSerializable(typeof(QuoteOfTheDayItem[]))] +internal sealed partial class QuoteOfTheDayJsonContext : JsonSerializerContext; diff --git a/CodeGen/CodeGen.csproj b/CodeGen/CodeGen.csproj index c829befa..0a6ec544 100644 --- a/CodeGen/CodeGen.csproj +++ b/CodeGen/CodeGen.csproj @@ -2,18 +2,18 @@ latest-all true + 1.0.0-pre false enable Exe + false ptr727.ProjectTemplate.CodeGen net10.0 1.0.0 - $(Version)+$(SourceRevisionId) - - true + + true + true true diff --git a/CodeGen/CodeGenBuilder.cs b/CodeGen/CodeGenBuilder.cs new file mode 100644 index 00000000..1157b75b --- /dev/null +++ b/CodeGen/CodeGenBuilder.cs @@ -0,0 +1,62 @@ +using System.IO; +using System.Text; + +namespace ptr727.ProjectTemplate.CodeGen; + +internal sealed class CodeGenBuilder(string outputPath, CancellationToken cancellationToken) +{ + internal async Task CodeGenAsync(string quote) + { + // Codegen example + string codeGen = $$""" + namespace ptr727.ProjectTemplate.CodeGen; + + internal static class CodeGen + { + private const string QuoteOfTheDay = {{ToCSharpStringLiteral(quote)}}; + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1303:Do not pass literals as localized parameters", + Justification = "Demonstration code." + )] + internal static void Quote() + { + string dateTime = $"{{DateTime.UtcNow:o}}"; + Console.WriteLine($"{dateTime} : {QuoteOfTheDay}"); + Log.Logger.Information("Quote of the Day: {DateTime} : {Quote}", dateTime, QuoteOfTheDay); + } + } + """; + + // Write code to file + await File.WriteAllTextAsync(outputPath, codeGen, cancellationToken).ConfigureAwait(false); + } + + private static string ToCSharpStringLiteral(string value) + { + StringBuilder sb = new(value.Length + 2); + _ = sb.Append('"'); + foreach (char c in value) + { + _ = sb.Append( + c switch + { + '\\' => "\\\\", + '\"' => "\\\"", + '\r' => "\\r", + '\n' => "\\n", + '\t' => "\\t", + '\0' => "\\0", + '\b' => "\\b", + '\f' => "\\f", + '\u2019' => "'", + _ when char.IsControl(c) => $"\\u{(int)c:X4}", + _ => c.ToString(), + } + ); + } + _ = sb.Append('"'); + return sb.ToString(); + } +} diff --git a/CodeGen/CommandLine.cs b/CodeGen/CommandLine.cs index 63e99f29..9916cc6d 100644 --- a/CodeGen/CommandLine.cs +++ b/CodeGen/CommandLine.cs @@ -6,35 +6,36 @@ namespace ptr727.ProjectTemplate.CodeGen; internal sealed class CommandLine { - internal sealed class Context + internal CommandLine(string[] args) + { + Root = CreateRootCommand(); + Result = Root.Parse(args); + } + + internal sealed class Options { internal required DirectoryInfo CodePath { get; init; } internal required string APIKey { get; init; } } - internal static async Task<( - CommandLine commandLine, - RootCommand rootCommand - )> CreateRootCommandWithCommandLine() + internal RootCommand Root { get; init; } + internal ParseResult Result { get; init; } + + internal RootCommand CreateRootCommand() { - CommandLine commandLine = new(); - RootCommand rootCommand = new("C# .NET codegen project") - { - commandLine._codePathOption, - commandLine._apiKeyOption, - }; + RootCommand rootCommand = new("C# .NET codegen project") { _codePathOption, _apiKeyOption }; rootCommand.SetAction( (parseResult, cancellationToken) => { - Program program = new(commandLine.CreateContext(parseResult), cancellationToken); + Program program = new(CreateOptions(parseResult), cancellationToken); return program.ExecuteAsync(); } ); - return (commandLine, rootCommand); + return rootCommand; } - internal Context CreateContext(ParseResult parseResult) => + internal Options CreateOptions(ParseResult parseResult) => new() { CodePath = parseResult.GetValue(_codePathOption)!, diff --git a/CodeGen/Program.cs b/CodeGen/Program.cs index 2c66f032..1a17a0e2 100644 --- a/CodeGen/Program.cs +++ b/CodeGen/Program.cs @@ -1,37 +1,26 @@ -using System.CommandLine; using System.IO; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; using Serilog.Sinks.SystemConsole.Themes; namespace ptr727.ProjectTemplate.CodeGen; internal sealed class Program( - CommandLine.Context commandLineContext, + CommandLine.Options commandLineOptions, CancellationToken cancellationToken ) { - internal CommandLine.Context GetCommandLineContext() => commandLineContext; + internal CommandLine.Options GetCommandLineOptions() => commandLineOptions; internal CancellationToken GetCancellationToken() => cancellationToken; - private readonly HttpClient _httpClient = HttpClientFactory.GetHttpClient(); - - internal HttpClient GetHttpClient() => _httpClient; - internal static async Task Main(string[] args) { // Parse commandline - (CommandLine _, RootCommand rootCommand) = await CommandLine - .CreateRootCommandWithCommandLine() - .ConfigureAwait(false); - ParseResult parseResult = rootCommand.Parse(args); + CommandLine commandLine = new(args); - // Bypass startup for help and version commands - if (CommandLine.BypassStartup(parseResult)) + // Bypass startup for errors or help and version commands + if (CommandLine.BypassStartup(commandLine.Result)) { - return await parseResult.InvokeAsync().ConfigureAwait(false); + return await commandLine.Result.InvokeAsync().ConfigureAwait(false); } // Configure logging @@ -44,19 +33,29 @@ internal static async Task Main(string[] args) Log.Logger = loggerConfiguration.CreateLogger(); // Invoke command - return await parseResult.InvokeAsync().ConfigureAwait(false); + return await commandLine.Result.InvokeAsync().ConfigureAwait(false); } internal async Task ExecuteAsync() { try { + Log.Information("Executing codegen command..."); + string quoteoftheday = "No API key provided."; - if (!string.IsNullOrEmpty(commandLineContext.APIKey)) + if (!string.IsNullOrEmpty(commandLineOptions.APIKey)) { - quoteoftheday = await GetQuoteOfTheDayAsync().ConfigureAwait(false); + Log.Information("Retrieving quote from API Ninjas..."); + ApiNinjas apiNinjas = new(commandLineOptions.APIKey, cancellationToken); + quoteoftheday = await apiNinjas.GetQuoteOfTheDayAsync().ConfigureAwait(false); } - await CodeGenAsync(quoteoftheday).ConfigureAwait(false); + Log.Information("Quote: {Quote}", quoteoftheday); + + string outputPath = Path.Combine(commandLineOptions.CodePath.FullName, "CodeGen.cs"); + Log.Information("Writing quote to {OutputPath}", outputPath); + CodeGenBuilder codegenBuilder = new(outputPath, cancellationToken); + await codegenBuilder.CodeGenAsync(quoteoftheday).ConfigureAwait(false); + return 0; } catch (Exception ex) when (Log.Logger.LogAndHandle(ex)) @@ -64,99 +63,4 @@ internal async Task ExecuteAsync() return 1; } } - - private async Task GetQuoteOfTheDayAsync() - { - // https://api-ninjas.com/api/quotes#v2-quoteoftheday - using HttpRequestMessage request = new( - HttpMethod.Get, - "https://api.api-ninjas.com/v2/quotes?categories=philosophy" - ); - request.Headers.Add("X-Api-Key", commandLineContext.APIKey); - - using HttpResponseMessage response = await GetHttpClient() - .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, GetCancellationToken()) - .ConfigureAwait(false); - _ = response.EnsureSuccessStatusCode(); - - using Stream responseStream = await response - .Content.ReadAsStreamAsync(GetCancellationToken()) - .ConfigureAwait(false); - QuoteOfTheDayItem[]? items = await JsonSerializer - .DeserializeAsync( - responseStream, - QuoteOfTheDayJsonContext.Default.QuoteOfTheDayItemArray, - GetCancellationToken() - ) - .ConfigureAwait(false); - - string? quote = items?.FirstOrDefault()?.Quote; - return string.IsNullOrWhiteSpace(quote) - ? throw new InvalidOperationException( - "Quote of the day response did not include a quote." - ) - : quote; - } - - private async Task CodeGenAsync(string quote) - { - // Codegen example - string codeGen = $$""" - namespace ptr727.ProjectTemplate.CodeGen; - - internal static class CodeGen - { - private const string QuoteOfTheDay = {{ToCSharpStringLiteral(quote)}}; - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1303:Do not pass literals as localized parameters", - Justification = "Demonstration code." - )] - internal static void Quote() - { - string dateTime = $"{{DateTime.UtcNow:o}}"; - Console.WriteLine($"{dateTime} : {QuoteOfTheDay}"); - Log.Logger.Information("Quote of the Day: {DateTime} : {Quote}", dateTime, QuoteOfTheDay); - } - } - """; - - // Write code to file - string outputPath = Path.Combine(commandLineContext.CodePath.FullName, "CodeGen.cs"); - await File.WriteAllTextAsync(outputPath, codeGen, GetCancellationToken()) - .ConfigureAwait(false); - } - - private static string ToCSharpStringLiteral(string value) - { - StringBuilder sb = new(value.Length + 2); - _ = sb.Append('"'); - foreach (char c in value) - { - _ = sb.Append( - c switch - { - '\\' => "\\\\", - '\"' => "\\\"", - '\r' => "\\r", - '\n' => "\\n", - '\t' => "\\t", - '\0' => "\\0", - '\b' => "\\b", - '\f' => "\\f", - '\u2019' => "'", - _ when char.IsControl(c) => $"\\u{(int)c:X4}", - _ => c.ToString(), - } - ); - } - _ = sb.Append('"'); - return sb.ToString(); - } } - -internal sealed record QuoteOfTheDayItem([property: JsonPropertyName("quote")] string Quote); - -[JsonSerializable(typeof(QuoteOfTheDayItem[]))] -internal sealed partial class QuoteOfTheDayJsonContext : JsonSerializerContext; diff --git a/Console/CommandLine.cs b/Console/CommandLine.cs index e628b19e..bba3c155 100644 --- a/Console/CommandLine.cs +++ b/Console/CommandLine.cs @@ -6,35 +6,63 @@ namespace ptr727.ProjectTemplate.Console; internal sealed class CommandLine { - internal sealed class Context + internal CommandLine(string[] args) + { + Root = CreateRootCommand(); + Result = Root.Parse(args); + } + + internal sealed class Options { internal required LoggerFactory.Options LogOptions { get; init; } + internal required string TestOption { get; init; } } - internal static async Task<( - CommandLine commandLine, - RootCommand rootCommand - )> CreateRootCommandWithCommandLine() + internal RootCommand Root { get; init; } + internal ParseResult Result { get; init; } + + internal RootCommand CreateRootCommand() { - CommandLine commandLine = new(); + // Default root command RootCommand rootCommand = new("C# .NET console project") { - commandLine._logLevelOption, - commandLine._logFileOption, - commandLine._logFileClearOption, + // Global options (set Recursive to true to apply to subcommands) + _logLevelOption, + _logFileOption, + _logFileClearOption, }; rootCommand.SetAction( (parseResult, cancellationToken) => { - Program program = new(commandLine.CreateContext(parseResult), cancellationToken); + Program program = new(CreateOptions(parseResult), cancellationToken); return program.ExecuteAsync(); } ); - return (commandLine, rootCommand); + // Sub commands + rootCommand.Subcommands.Add(CreateTestCommand()); + + return rootCommand; + } + + internal Command CreateTestCommand() + { + Command testCommand = new("test", "Test command") + { + // Test command options + _testOption, + }; + testCommand.SetAction( + (parseResult, cancellationToken) => + { + Program program = new(CreateOptions(parseResult), cancellationToken); + return program.ExecuteTestAsync(); + } + ); + return testCommand; } - internal Context CreateContext(ParseResult parseResult) => + internal Options CreateOptions(ParseResult parseResult) => new() { LogOptions = new LoggerFactory.Options @@ -43,16 +71,20 @@ internal Context CreateContext(ParseResult parseResult) => File = parseResult.GetValue(_logFileOption) ?? string.Empty, FileClear = parseResult.GetValue(_logFileClearOption), }, + TestOption = parseResult.GetValue(_testOption) ?? string.Empty, }; private readonly Option _logLevelOption = CreateLogLevelOption(); private readonly Option _logFileOption = CreateLogFileOption(); private readonly Option _logFileClearOption = CreateLogFileClearOption(); + private readonly Option _testOption = CreateTestOption(); + private static Option CreateLogFileClearOption() => new("--logfile-clear", "-c") { Description = "Clear the log file before writing (default: false).", + Recursive = true, }; private static Option CreateLogLevelOption() => @@ -60,10 +92,21 @@ private static Option CreateLogLevelOption() => { Description = "Set the log level (default: Information).", DefaultValueFactory = _ => LogEventLevel.Information, + Recursive = true, }; - private static Option CreateLogFileOption() => - new("--logfile", "-f") { Description = "Write logs to the specified file (optional)." }; + private static Option CreateLogFileOption() + { + Option option = new("--logfile", "-f") + { + Description = "Write logs to the specified file (optional).", + Recursive = true, + }; + return option.AcceptLegalFileNamesOnly(); + } + + private static Option CreateTestOption() => + new("--test", "-t") { Description = "Test command option (optional)." }; internal static bool BypassStartup(ParseResult parseResult) => parseResult.Errors.Count > 0 diff --git a/Console/Console.csproj b/Console/Console.csproj index fd011401..478a9a15 100644 --- a/Console/Console.csproj +++ b/Console/Console.csproj @@ -2,18 +2,18 @@ latest-all true + 1.0.0-pre false enable Exe + false ptr727.ProjectTemplate.Console net10.0 1.0.0 - $(Version)+$(SourceRevisionId) - - true + + true + true true diff --git a/Console/Program.cs b/Console/Program.cs index 116a83a4..255afa97 100644 --- a/Console/Program.cs +++ b/Console/Program.cs @@ -1,4 +1,3 @@ -using System.CommandLine; using System.Diagnostics; using Microsoft.Extensions.Logging.Abstractions; using ptr727.ProjectTemplate.Library; @@ -6,30 +5,27 @@ namespace ptr727.ProjectTemplate.Console; internal sealed class Program( - CommandLine.Context commandLineContext, + CommandLine.Options commandLineOptions, CancellationToken cancellationToken ) { - internal CommandLine.Context GetCommandLineContext() => commandLineContext; + internal CommandLine.Options GetCommandLineOptions() => commandLineOptions; internal CancellationToken GetCancellationToken() => cancellationToken; internal static async Task Main(string[] args) { // Parse commandline - (CommandLine commandLine, RootCommand rootCommand) = await CommandLine - .CreateRootCommandWithCommandLine() - .ConfigureAwait(false); - ParseResult parseResult = rootCommand.Parse(args); + CommandLine commandLine = new(args); - // Bypass startup for help and version commands - if (CommandLine.BypassStartup(parseResult)) + // Bypass startup for errors or help and version commands + if (CommandLine.BypassStartup(commandLine.Result)) { - return await parseResult.InvokeAsync().ConfigureAwait(false); + return await commandLine.Result.InvokeAsync().ConfigureAwait(false); } // Create logger - _ = LoggerFactory.Create(commandLine.CreateContext(parseResult).LogOptions); + _ = LoggerFactory.Create(commandLine.CreateOptions(commandLine.Result).LogOptions); Log.Logger.LogOverrideContext().Information("Starting: {Args}", args); // Initialize library with logger @@ -40,13 +36,28 @@ internal static async Task Main(string[] args) templateLibrary.Test(); // Invoke command - return await parseResult.InvokeAsync().ConfigureAwait(false); + return await commandLine.Result.InvokeAsync().ConfigureAwait(false); } internal async Task ExecuteAsync() { try { + Log.Information("Executing root command..."); + await Task.Delay(1000, cancellationToken).ConfigureAwait(false); + return 0; + } + catch (Exception ex) when (Log.Logger.LogAndHandle(ex)) + { + return 1; + } + } + + internal async Task ExecuteTestAsync() + { + try + { + Log.Information("Executing test command..."); await Task.Delay(1000, cancellationToken).ConfigureAwait(false); return 0; } diff --git a/Library/.editorconfig b/Library/.editorconfig new file mode 100644 index 00000000..f1906f4a --- /dev/null +++ b/Library/.editorconfig @@ -0,0 +1,7 @@ +root = false + +# C# files +[*.cs] + +# Ignore missing XML comment warnings +dotnet_diagnostic.CS1591.severity = none diff --git a/Library/Library.csproj b/Library/Library.csproj index a3694938..bd5934e7 100644 --- a/Library/Library.csproj +++ b/Library/Library.csproj @@ -9,10 +9,12 @@ true true 1.0.0.0 + true true true 1.0.0-pre true + true enable ptr727.ProjectTemplate.Library MIT @@ -30,10 +32,6 @@ true - - true - $(NoWarn);1591 - - + + + diff --git a/README.md b/README.md index 9989975b..b9dfe2cb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ C# .NET project template. + + ## Build and Distribution - **Source Code**: [GitHub][github-link] - Source code, issues, discussions, and CI/CD pipelines. @@ -29,12 +31,12 @@ C# .NET project template. **Version: 1.0**: -**Summary:** +**Summary**: - Something. - And something else. -> **⚠️ Breaking Changes:** +> **⚠️ Breaking Changes**: > > - Something. > - And something else. @@ -49,9 +51,15 @@ Get started with ProjectTemplate in three easy steps: > > **ℹ️ Note**: Some interesting note. -```shell -ls -la -``` +1. **Install ProjectTemplate**: + - Do something. +2. **Configure ProjectTemplate**: + - Then something else. +3. **Run ProjectTemplate**: + + ```shell + Console --loglevel=Debug + ``` See [Installation](#installation) for detailed setup instructions. @@ -67,9 +75,9 @@ See [Installation](#installation) for detailed setup instructions. - [Installation](#installation) - [Configuration](#configuration) - [Usage](#usage) - - [Commands Quick Reference](#commands-quick-reference) + - [Command Quick Reference](#command-quick-reference) - [Global Options](#global-options) - - [Some Command](#some-command) + - [Test Command](#test-command) - [Questions or Issues](#questions-or-issues) - [Development Environment Setup](#development-environment-setup) - [Template Project Setup](#template-project-setup) @@ -95,68 +103,106 @@ Choose an installation method based on your platform and requirements: - ⚠️ Some not so good reason. - ❌ Strong reason to avoid. - Best for: Linux, NAS devices, servers, cross-platform deployments. +- **Method 2**: Custom configuration options. + - ✅ Some good reason. + - ⚠️ Some not so good reason. + - ❌ Strong reason to avoid. + - Best for: Specialized devices. ## Configuration -> **⚠️ Important**: The default settings file must be edited to match your requirements before processing media files. +> **⚠️ Important**: The spinner setting must be configured before first use. + +**Required configuration**: -Describe configuration steps. +- Set `foo` to something. +- Set `bar` to something else. + +**Optional configuration**: + +- Set `advanced` to `special`. +- Some other custom option. ## Usage -### Commands Quick Reference +`Console [global options] [command options]` + +### Command Quick Reference | Command | Description | Notes | | ------- | ------- | ----------- | -| `somecommand` | Do something useful | First time setup | -| `othercommand` | Do something else useful | Some note | +| default | Default action when no command is specified | First time setup | +| `test` | Do something else useful | Some note | +| `--help` | Show help output | Use ` --help` for command specific help | +| `--version` | Show version output | | + +Use the `--help` to get a list of all commands and global options.\ +To get help for a specific command run `Console --help`. -See detailed command documentation below for all options and usage examples. +### Global Options ---- +**Global options apply to all commands**: -Use the `--help` commandline option to get a list of commands and options.\ -To get help for a specific command run `Console --help`. +| Option | Description | Default | +| ------- | ------- | ----------- | +| `--logfile` | Debug log file | Optional | +| `--loglevel` | Debug log level | Default is `Information` | +| `--logfile-clear` | Clear log file at startup | Default is `false` | + +**General help**: ```text -> Console --help +>.\Console\bin\Debug\net10.0\Console --help Description: C# .NET console project -``` -### Global Options +Usage: + Console [command] [options] -Global options apply to all commands: +Options: + -l, --loglevel Set the log level (default: Information). [default: Information] + -f, --logfile Write logs to the specified file (optional). + -c, --logfile-clear Clear the log file before writing (default: false). + -?, -h, --help Show help and usage information + --version Show version information + +Commands: + test Test command +``` + +### Test Command -- `--logfile`: - - Path to the log file. +**Test command options**: | Option | Description | Default | | ------- | ------- | ----------- | -| `--logfile` | Do something useful | Required | -| `--debuglevel` | Set the debug log level | `Information` | +| `--test` | Test options | Optional | -### Some Command +**Test command help**: ```text -> PlexCleaner process --help +>.\Console\bin\Debug\net10.0\Console test --help Description: - Process media files -``` + Test command -Options: +Usage: + Console test [options] -- `--settingsfile`: (required) - - Path to the JSON settings file. - - Something else that is relevant. +Options: + -t, --test Test command option (optional). + -?, -h, --help Show help and usage information + -l, --loglevel Set the log level (default: Information). [default: Information] + -f, --logfile Write logs to the specified file (optional). + -c, --logfile-clear Clear the log file before writing (default: false). +``` ## Questions or Issues -**For General Questions:** +**For General Questions**: -- Use the [Discussions][discussions-link] forum for general questions, feature requests, and sharing working configurations. +- Use the [Discussions][discussions-link] forum for general questions. -**For Bug Reports:** +**For Bug Reports**: - Ask in the [Discussions][discussions-link] forum if you are not sure if it is a bug. - Check the existing [Issues][issues-link] tracker for known problems. @@ -164,219 +210,167 @@ Options: ## Development Environment Setup -- **Install Developer Tools:** +- **Install Developer Tools**: - Install [.NET SDK](https://dotnet.microsoft.com/en-us/download): ```shell + # Windows winget install Microsoft.DotNet.SDK.10 - winget upgrade Microsoft.DotNet.SDK.10 + + # Linux + apt install dotnet-sdk-10.0 ``` - Install [Visual Studio Code](https://code.visualstudio.com/download): ```shell + # Windows winget install Microsoft.VisualStudioCode - winget upgrade Microsoft.VisualStudioCode ``` - Install [Visual Studio](https://visualstudio.microsoft.com/downloads/): ```shell + # Windows winget install Microsoft.VisualStudio.Community - winget upgrade Microsoft.VisualStudio.Community ``` -- **Clone and Configure the Project:** +- **Clone and Configure Project**: - Clone the repository and initialize tools: ```shell - git clone -b main https://github.com/ptr727/[ProjectTemplate].git ./[NewProject] + # Clone from CLI (or clone from VSCode) + git clone -b main https://github.com/ptr727/[Project].git ./[Project] + + # Initialize dotnet tools + cd ./[Project] dotnet tool restore dotnet husky install ``` - - Open `[ProjectTemplate].code-workspace` in Visual Studio Code. - - Open `[ProjectTemplate].slnx` in Visual Studio. + - Open `[Project].code-workspace` in Visual Studio Code. + - Open `[Project].slnx` in Visual Studio. + +## 3rd Party Tools + +**3rd Party tools used in this project**: + +- [API Ninjas][apininjas-link] +- [AwesomeAssertions][awesomeassertions-link] +- [Bring Your Own Badge][byob-link] +- [Create Pull Request][createpr-link] +- [CSharpier][csharpier-link] +- [GH Release][ghrelease-link] +- [Git Auto Commit][ghautocommit-link] +- [GitHub Actions][ghactions-link] +- [GitHub Dependabot][ghdependabot-link] +- [Husky.Net][huskynet-link] +- [Nerdbank.GitVersioning][nerbankgitversion-link] +- [Serilog][serilog-link] +- [xUnit.Net][xunit-link] + +## License + +Licensed under the [MIT License][license-link]\ +![GitHub License][license-shield] + + ## Template Project Setup ### Template - TODO List +- [ ] Configure git for SSH signing and SSH forwarding in dev containers. - [ ] Start on Linux to avoid file permission issues when moving from Windows. - [ ] Configure the [Developer Environment](#template---developer-environment-setup). -- [ ] Configure the [Global Git Setup](#template---global-git-setup) and the [Project Git Setup](#template---project-git-setup). -- [ ] Open the project directory in Visual Studio Code, and rename (Ctrl-Shift-H) all instances of `ProjectTemplate` to `[NewProject]` in code. +- [ ] Open the project directory (*not the workspace*) in Visual Studio Code, and rename (Ctrl-Shift-H) all instances of `ProjectTemplate` to `[NewProject]` in code. - [ ] Rename `ProjectTemplate.code-workspace` to `[NewProject].code-workspace` and `ProjectTemplate.slnx` to `[NewProject].slnx`. - [ ] Open `[NewProject].code-workspace` workspace in Visual Studio Code. - [ ] Delete any projects and associated actions that will not be used, update dependencies in actions to remove deleted actions. - [ ] Rename projects to match the naming, update `.slnx` and `.csproj` files, and update actions to match the naming. - [ ] Update the `namespace` in `.cs` and `.csproj` files to match the naming. - [ ] Update all ref-links in `README.md` to point to the naming. -- [ ] Publish to GitHub to create a new empty GitHub repository. +- [ ] Publish to GitHub from VSCode to create a new empty GitHub repository. - [ ] Commit and push the `first-branch`. - [ ] Edit and iterate only in `first-branch` until ready to start with git history. -- [ ] Setup `main` as the [First Permanent Branch](#template---git-permanent-branch) when ready. +- [ ] Setup `main` as the first permanent branch when ready. - [ ] Configure [GitHub](#template---github-setup) for the new repository. -- [ ] Follow the [Branching Workflow](#template---branching-workflow), create `develop` from `main`, PR from `feature-branch` to `develop` to `main`. +- [ ] Follow the [Branching Workflow](#template---branching-workflow). - [ ] Delete the `Project Template Setup` section from `README.md`. ### Template - Developer Environment Setup -#### Template - Tools Setup - -- Install [.NET SDK](https://dotnet.microsoft.com/en-us/download): - - ```shell - winget install Microsoft.DotNet.SDK.10 - winget upgrade Microsoft.DotNet.SDK.10 - ``` - -- Install [Visual Studio Code](https://code.visualstudio.com/download): - - ```shell - winget install Microsoft.VisualStudioCode - winget upgrade Microsoft.VisualStudioCode - ``` - -- Install [Visual Studio](https://visualstudio.microsoft.com/downloads/): - - ```shell - winget install Microsoft.VisualStudio.Community - winget upgrade Microsoft.VisualStudio.Community - ``` - -- Install [Nektos ACT](https://nektosact.com/): - - ```shell - winget install nektos.act - winget upgrade nektos.act - ``` - -#### Template - Global Git Setup +#### Template - Git Setup -- Configure Git options: - - ```shell - git config --global credential.helper "cache --timeout=3600" - git config --global user.name "Pieter Viljoen" - git config --global user.email "ptr727@users.noreply.github.com" - git config --global core.sharedRepository group - git config --global --add safe.directory '*' - git config --list --show-origin - ``` - -- [Register](https://github.com/settings/keys) SSH key for Authentication and Signing on GitHub. - - ```shell - ssh-keygen -t ed25519 # If not already created - cat ~/.ssh/id_ed25519.pub # Paste into GitHub - ssh-keyscan github.com >> ~/.ssh/known_hosts - ssh -v -T git@github.com - ``` - -- Configure Git for [SSH signing](https://docs.github.com/en/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key): - - ```shell - git config --global gpg.format ssh - git config --global user.signingkey "~/.ssh/id_ed25519.pub" - git config --global commit.gpgsign true - git config --global tag.gpgsign true - mkdir -p ~/.config/git - echo "$(git config --get user.email) namespaces=\"git\" $(cat ~/.ssh/id_ed25519.pub)" >> ~/.config/git/allowed_signers - git config --global gpg.ssh.allowedSignersFile "~/.config/git/allowed_signers" - git log --show-signature - git config --list --show-origin - ``` - -#### Template - Project Git Setup - -- Template project setup: +- **⚠️ Prerequisites**: + - Configure git for SSH signing. + - Configure SSH forwarding for dev containers. +- Setup new project from template: ```shell + # Clone the template project git clone -b main https://github.com/ptr727/ProjectTemplate.git ./[NewProject] + + # Reset git to start a new repo rm -r ./[NewProject]/.git cd ./[NewProject] git init -b first-branch + + # Init dotnet tools dotnet tool restore dotnet husky install - ``` - -#### Template - Git Permanent Branch -- Create `main` branch from `first-branch` with no history: - - ```shell - # When you're ready to create main with ONLY ONE squashed commit: - git checkout --orphan main # creates main with no history - git commit --allow-empty -m "temp" # required so we can merge into it - - git merge --squash first-branch # bring in final state as staged changes - git commit -m "Initial import (squashed)" # main now has exactly 1 real commit - - git reset --hard HEAD~1 # drop the temporary commit (leaves your squashed commit as the first) - - # delete the feature branch - git branch -D feature-big-branch + # Update dotnet tools + dotnet tool update --all + dotnet outdated --upgrade:prompt ``` -#### Template - Project Workspace Setup +- Setup new project from scratch: -- Setup new project environment: + > **⚠️ Linux**: Start configuration on Linux to avoid file permission issues. ```shell + # Init git + mkdir ./[NewProject] + cd ./[NewProject] git init -b first-branch + + # Init dotnet tools dotnet new tool-manifest dotnet tool install csharpier dotnet tool install husky dotnet tool install dotnet-outdated-tool dotnet husky install dotnet husky add pre-commit -c "dotnet husky run" - ``` -- New pull of existing project: - - ```shell - dotnet tool restore - dotnet husky install + # Make sure pre-commit is executable on Linux chmod +x ./.husky/pre-commit ``` -- Update tools in existing project: +- Use `first-branch` for all the initial project setup and testing. +- When ready, *only when ready*, create `main` branch from `first-branch` with no history: ```shell - dotnet tool update --all - dotnet outdated --upgrade:prompt - ``` + # Create main branch with no history + git checkout --orphan main + git commit --allow-empty -m "temp" -- Linux / macOS: - - Verify that shell files are `+x` executable and `LF` line ending mode. - - Verify that there are no duplicate files with different case names. + # Squash merge changes + git merge --squash first-branch + git commit -m "Initial import (squashed)" -#### Template - GitHub Local Actions Setup + # Drop the temporary commit + git reset --hard HEAD~1 -- Install [Nektos ACT](https://nektosact.com/): - - ```shell - winget install nektos.act - winget upgrade nektos.act + # Delete first-branch + git branch -D first-branch ``` -- Install [GitHub Local Actions](https://marketplace.visualstudio.com/items?itemName=SanjulaGanepola.github-local-actions) Visual Studio Code extension. -- Update [settings](https://nektosact.com/usage/index.html#action-artifacts) to always start the artifact server. - - ```json - "githubLocalActions.actCommand": "act --artifact-server-path ./.artifacts", - ``` - -- Update local secrets: - - Save the existing [Docker Hub Personal Access Token](https://app.docker.com/accounts/ptr727/settings/personal-access-tokens) as `DOCKER_HUB_ACCESS_TOKEN` and `DOCKER_HUB_USERNAME`. - - Create a [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens) as `GITHUB_TOKEN`. - ### Template - GitHub Setup -#### Template - GitHub Secrets Setup +**GitHub secrets setup**: - Create a [NuGet API Key](https://www.nuget.org/account/apikeys). - Save the Key as `NUGET_API_KEY` in: @@ -388,7 +382,7 @@ Options: - GitHub project security Settings / Secrets / Actions. - GitHub project security Settings / Secrets / Dependabot. -#### Template - GitHub Project Settings +**GitHub project settings**: - General: - Default branch: `main` @@ -416,44 +410,18 @@ Options: - Actions / General: - `Allow GitHub Actions to create and approve pull requests` -#### Template - Branching Workflow +### Template - Branching Workflow - Create persistent `main` and `develop` branches. -- Protect `main` and `develop` branches with [branch protection rules](#template---github-project-settings). +- Protect `main` and `develop` branches with branch protection rules. - Make sure that `main` and `develop` are always building error free. - Create feature branches from the `develop` branch. +- Only commit to feature branches, do not commit directly to `develop` or to `main` - Always "Squash and merge" from feature branches to the `develop` branch to minimize change history. - Always "Squash and merge" from `develop` to `main` to maintain a linear history. +- Bot generated pull requests will always merge to `main`, keep feature branches updated when merging to `develop` to merge to `main`. -#### Template - GitHub Actions Workflow - -- Use reusable tasks to eliminate duplication. -- Create one pull request test action, and register that task as a [branch rule](#template---github-project-settings) check. - -## 3rd Party Tools - -**3rd Party tools used in this project:** - -- [API Ninjas][apininjas-link] -- [AwesomeAssertions][awesomeassertions-link] -- [Bring Your Own Badge][byob-link] -- [Create Pull Request][createpr-link] -- [CSharpier][csharpier-link] -- [GH Release][ghrelease-link] -- [Git Auto Commit][ghautocommit-link] -- [GitHub Actions][ghactions-link] -- [GitHub Dependabot][ghdependabot-link] -- [Husky.Net][huskynet-link] -- [Nerdbank.GitVersioning][nerbankgitversion-link] -- [Serilog][serilog-link] -- [xUnit.Net][xunit-link] - -## License - -Licensed under the [MIT License][license-link]\ -![GitHub License][license-shield] - - + [github-link]: https://github.com/ptr727/ProjectTemplate [actions-link]: https://github.com/ptr727/ProjectTemplate/actions @@ -481,7 +449,7 @@ Licensed under the [MIT License][license-link]\ [nugetreleaseversion-shield]: https://img.shields.io/nuget/v/ptr727.ProjectTemplate.Library?logo=nuget&label=NuGet%20Release [nugetprereleaseversion-shield]: https://img.shields.io/nuget/vpre/ptr727.ProjectTemplate.Library?logo=nuget&&label=NuGet%20Pre-Release&color=orange - + [apininjas-link]: https://api-ninjas.com/api/quotes [awesomeassertions-link]: https://awesomeassertions.org/ diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index d9c6f038..dfbbaee4 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -5,6 +5,7 @@ false true enable + ptr727.ProjectTemplate.Tests net10.0 From dc0fde516458e6c7ef3ebb58670d307f5339506a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 28 Jan 2026 14:07:44 -0800 Subject: [PATCH 2/3] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9dfe2cb..064fabb6 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Choose an installation method based on your platform and requirements: | `--help` | Show help output | Use ` --help` for command specific help | | `--version` | Show version output | | -Use the `--help` to get a list of all commands and global options.\ +Use the `--help` option to get a list of all commands and global options.\ To get help for a specific command run `Console --help`. ### Global Options From a17da4576385a011fe329f34c9c08f457ba042ba Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Wed, 28 Jan 2026 14:07:57 -0800 Subject: [PATCH 3/3] Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 064fabb6..7433d94b 100644 --- a/README.md +++ b/README.md @@ -416,7 +416,7 @@ Licensed under the [MIT License][license-link]\ - Protect `main` and `develop` branches with branch protection rules. - Make sure that `main` and `develop` are always building error free. - Create feature branches from the `develop` branch. -- Only commit to feature branches, do not commit directly to `develop` or to `main` +- Only commit to feature branches, do not commit directly to `develop` or to `main`. - Always "Squash and merge" from feature branches to the `develop` branch to minimize change history. - Always "Squash and merge" from `develop` to `main` to maintain a linear history. - Bot generated pull requests will always merge to `main`, keep feature branches updated when merging to `develop` to merge to `main`.