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..7433d94b 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` option 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