Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Instructions for AI Coding Agents

**Utilities** is a C# .NET library of general-purpose utility classes, published as the NuGet package `InsaneGenius.Utilities` and consumed directly from `main`. The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and an xUnit test project (`UtilitiesTests/`).
**Utilities** is a C# .NET library of general-purpose utility classes, published as the NuGet package `ptr727.Utilities` and consumed directly from `main`. The library ships under [`Utilities/`](./Utilities/), with a `Sandbox/` console app for experimentation and an xUnit test project (`UtilitiesTests/`).

This file is the canonical reference for cross-cutting AI-agent rules. The CI/CD workflow contract and conventions live in [`WORKFLOW.md`](./WORKFLOW.md); C# code-style conventions live in [`CODESTYLE.md`](./CODESTYLE.md). Copilot review *mechanics* are owned by [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) - this file delegates them there explicitly (see "PR Review Etiquette" below). High-level summaries in other docs (e.g. README's Contributing section) are allowed when they link back here; don't duplicate the rules themselves. The library's **project-specific conventions and public-API/behavioral contracts** also live here (the [Library API Conventions](#library-api-conventions) section), **not** in `.github/copilot-instructions.md` - that file targets GitHub Copilot / VS Code specifically, while this file is the agent-agnostic one every coding agent reads, so any rule a reviewer must honor has to live here to be provider-independent.

Expand Down Expand Up @@ -173,7 +173,7 @@ The conventions for everything under `.github/workflows/` - action pinning, file
## Project Structure

- **Utilities** (`Utilities/Utilities.csproj`)
- Core library project, published as NuGet `InsaneGenius.Utilities`. Target framework: .NET 10.0.
- Core library project, published as NuGet `ptr727.Utilities`. Target framework: .NET 10.0.
- **Sandbox** (`Sandbox/Sandbox.csproj`)
- Console app for experimentation; not packaged or published.
- **UtilitiesTests** (`UtilitiesTests/UtilitiesTests.csproj`)
Expand Down
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Some useful and not so useful C# .NET utility classes.
## Release History

- v3.7:
- Renamed the NuGet package and root namespace from `InsaneGenius.Utilities` to `ptr727.Utilities` (a breaking change), aligning with the `ptr727.*` package naming used by the sibling `LanguageTags` project; consumers must update their package reference and change `using InsaneGenius.Utilities;` directives to `using ptr727.Utilities;`. The assembly is now named `Utilities`.
- Replaced the Serilog-coupled logging model with the backend-agnostic `Microsoft.Extensions.Logging` abstraction, matching the sibling `LanguageTags` project.
- Removed the global Serilog `LogOptions.Logger` property (a breaking API change) in favor of a thread-safe, injectable `ILoggerFactory` configured via `LogOptions.SetFactory(...)` / `TrySetFactory(...)`; the library now depends only on `Microsoft.Extensions.Logging.Abstractions`.
- Reworked `FileEx` and `Download` to resolve per-class cached loggers through `LogOptions.CreateLogger(...)` and to emit source-generated `[LoggerMessage]` messages, keeping the build clean under `AnalysisMode=All` and `TreatWarningsAsErrors`.
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ Some useful and not so useful C# .NET utility classes.

**Summary**:

- Adopted an injectable `Microsoft.Extensions.Logging` logging model.
- Renamed the NuGet package and namespace from `InsaneGenius.Utilities` to `ptr727.Utilities` (breaking).
- Update your package reference to `ptr727.Utilities` and change `using InsaneGenius.Utilities;` to `using ptr727.Utilities;`.
- Adopted an injectable `Microsoft.Extensions.Logging` logging model (breaking).
- Configure logging with `LogOptions.SetFactory(ILoggerFactory)` instead of the removed Serilog-typed `LogOptions.Logger` property.
- The library now depends on `Microsoft.Extensions.Logging.Abstractions` and is backend-agnostic; the `Sandbox` shows Serilog console wiring.

Expand All @@ -35,12 +37,12 @@ See [Release History](./HISTORY.md) for complete release notes and older version

```shell
# Add the package to your project
dotnet add package InsaneGenius.Utilities
dotnet add package ptr727.Utilities
```

```csharp
// Include the namespace
using InsaneGenius.Utilities;
using ptr727.Utilities;
```

## Contributing
Expand Down Expand Up @@ -69,8 +71,8 @@ Licensed under the [MIT License][license-link]\
[lastcommit-shield]: https://img.shields.io/github/last-commit/ptr727/Utilities?logo=github&label=Last%20Commit
[license-link]: ./LICENSE
[license-shield]: https://img.shields.io/github/license/ptr727/Utilities?label=License
[nuget-link]: https://www.nuget.org/packages/InsaneGenius.Utilities/
[nugetreleaseversion-shield]: https://img.shields.io/nuget/v/InsaneGenius.Utilities?logo=nuget&label=NuGet%20Release
[nuget-link]: https://www.nuget.org/packages/ptr727.Utilities/
[nugetreleaseversion-shield]: https://img.shields.io/nuget/v/ptr727.Utilities?logo=nuget&label=NuGet%20Release
[prereleaseversion-shield]: https://img.shields.io/github/v/release/ptr727/Utilities?include_prereleases&filter=*-g*&label=GitHub%20Pre-Release&logo=github
[releasebuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/Utilities/publish-release.yml?logo=github&label=Releases%20Build
[releases-link]: https://github.com/ptr727/Utilities/releases
Expand Down
1 change: 1 addition & 0 deletions Sandbox/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Serilog;
3 changes: 1 addition & 2 deletions Sandbox/LoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using System.Globalization;
using Serilog;
using Serilog.Extensions.Logging;
using Serilog.Sinks.SystemConsole.Themes;

namespace InsaneGenius.Utilities.Sandbox;
namespace ptr727.Utilities.Sandbox;

/// <summary>
/// Configures a Serilog console logger and exposes it as a
Expand Down
5 changes: 2 additions & 3 deletions Sandbox/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System.Diagnostics;
using System.Reflection;
using InsaneGenius.Utilities;
using InsaneGenius.Utilities.Sandbox;
using Serilog;
using ptr727.Utilities;
using ptr727.Utilities.Sandbox;

// Configure logging: build a Serilog console logger and inject it into the library.
Log.Logger = LoggerFactory.Create();
Expand Down
2 changes: 1 addition & 1 deletion Sandbox/Sandbox.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>InsaneGenius.Utilities.Sandbox</RootNamespace>
<RootNamespace>ptr727.Utilities.Sandbox</RootNamespace>
<PublishAot>true</PublishAot>
<InvariantGlobalization>false</InvariantGlobalization>
<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>
Expand Down
14 changes: 4 additions & 10 deletions Utilities/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,14 @@ dotnet_diagnostic.CA1711.severity = none

# Library-scoped analyzer exceptions. Each is a deliberate, documented decision
# for this published library, not a brownfield blanket-relax (see CODESTYLE.md
# "Analyzer Diagnostics and Suppressions").
# CA1002: the published InsaneGenius.Utilities surface intentionally exposes
# "Analyzer Diagnostics and Suppressions"). Localized single-symbol exceptions
# (CA1024, CA1034, CA1054, CA5394) are suppressed with [SuppressMessage]
# attributes at the specific symbol instead of here.
# CA1002: the published ptr727.Utilities surface intentionally exposes
# List<T> (FileEx.EnumerateDirectory, StringHistory.StringList); changing to
# Collection<T> is a breaking API change.
dotnet_diagnostic.CA1002.severity = suggestion
# CA1024: Download.GetHttpClient() is intentionally a method, not a property.
dotnet_diagnostic.CA1024.severity = suggestion
# CA1034: nested types generated by the C# extension members in Extensions.cs.
dotnet_diagnostic.CA1034.severity = suggestion
# CA1054: Download URL parameters are intentionally string, not System.Uri.
dotnet_diagnostic.CA1054.severity = suggestion
# CA2007: await using / await foreach disposal sites; the awaited async calls
# already use ConfigureAwait(false) and a ConfiguredAsyncDisposable rewrite
# hurts readability.
dotnet_diagnostic.CA2007.severity = suggestion
# CA5394: Random is used for retry jitter and temp-name generation, not security.
dotnet_diagnostic.CA5394.severity = suggestion
2 changes: 1 addition & 1 deletion Utilities/CommandLineEx.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Provides command-line argument parsing utilities.
Expand Down
2 changes: 1 addition & 1 deletion Utilities/ConsoleEx.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Globalization;

namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Provides enhanced console output utilities with color and timestamp support.
Expand Down
42 changes: 34 additions & 8 deletions Utilities/Download.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using System.Net.Http.Headers;
using System.Reflection;
Comment thread
ptr727 marked this conversation as resolved.
using Microsoft.Extensions.Logging;

namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Provides HTTP download utilities for files and strings.
Expand Down Expand Up @@ -201,6 +200,11 @@ public static bool DownloadString(Uri uri, out string value)
/// Gets the shared HttpClient instance.
/// </summary>
/// <returns>The HttpClient instance.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"CA1024:Use properties where appropriate",
Justification = "Exposed as a method so callers see they receive the shared, lazily-initialized HttpClient rather than a lightweight property value."
)]
public static HttpClient GetHttpClient() => s_httpClient.Value;

/// <summary>
Expand All @@ -211,6 +215,11 @@ public static bool DownloadString(Uri uri, out string value)
/// <param name="password">The password (optional).</param>
/// <returns>The constructed URI.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="url"/> is null.</exception>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"CA1054:URI parameters should not be strings",
Justification = "The url parameter is intentionally a string; CreateUri builds the Uri from raw string input supplied by callers."
)]
public static Uri CreateUri(string url, string? userName = null, string? password = null)
{
ArgumentNullException.ThrowIfNull(url);
Expand All @@ -233,13 +242,30 @@ private static HttpClient CreateHttpClient()
{
HttpClient client = new() { Timeout = TimeSpan.FromSeconds(TimeoutSeconds) };

Assembly assembly = Assembly.GetExecutingAssembly();
string productName = assembly.GetName().Name ?? "InsaneGenius.Utilities";
string productVersion = assembly.GetName().Version?.ToString() ?? "1.0.0";
// Identify the consuming application (the caller), never this library. Assembly-to-file
// metadata is unreliable under NativeAOT, so try the managed entry assembly name, then
// the OS-level process executable name, then a generic value.
Assembly? entryAssembly = Assembly.GetEntryAssembly();
string? processPath = Environment.ProcessPath;
string? processName = string.IsNullOrEmpty(processPath)
? null
: Path.GetFileNameWithoutExtension(processPath);
string productName = entryAssembly?.GetName().Name ?? processName ?? "Unknown";
string productVersion = entryAssembly?.GetName().Version?.ToString() ?? "1.0.0";

client.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue(productName, productVersion)
);
// The derived name may not be a valid HTTP token (e.g. a process name with spaces);
// fall back to a guaranteed-valid token so a User-Agent is always set.
if (
!ProductInfoHeaderValue.TryParse(
$"{productName}/{productVersion}",
out ProductInfoHeaderValue? userAgent
)
)
{
userAgent = new ProductInfoHeaderValue("Unknown", productVersion);
}

client.DefaultRequestHeaders.UserAgent.Add(userAgent);

return client;
}
Expand Down
8 changes: 6 additions & 2 deletions Utilities/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
using System.IO.Compression;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;

namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Provides extension methods for string compression.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Design",
"CA1034:Nested types should not be visible",
Justification = "The visible nested type is generated by the C# extension members feature; https://github.com/dotnet/sdk/issues/51681"
)]
public static class CompressExtensions
{
/// <summary>
Expand Down
13 changes: 11 additions & 2 deletions Utilities/FileEx.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using Microsoft.Extensions.Logging;

namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Provides extended file and directory operation utilities with retry logic and cancellation support.
Expand Down Expand Up @@ -1097,6 +1096,11 @@ public static string TimeStampFileName(string filePath, DateTime timeStamp)
/// <param name="name">The file path to create.</param>
/// <param name="size">The size of the file in bytes.</param>
/// <returns>True if successful, false otherwise.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Security",
"CA5394:Do not use insecure randomness",
Justification = "Random fills the file with non-cryptographic placeholder data, not security-sensitive values."
)]
public static bool CreateRandomFilledFile(string name, long size)
{
try
Expand Down Expand Up @@ -1149,6 +1153,11 @@ public static bool CreateRandomFilledFile(string name, long size)
/// <param name="size">The size of the file in bytes.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>True if successful, false otherwise.</returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Security",
"CA5394:Do not use insecure randomness",
Justification = "Random fills the file with non-cryptographic placeholder data, not security-sensitive values."
)]
public static async Task<bool> CreateRandomFilledFileAsync(
string name,
long size,
Expand Down
2 changes: 1 addition & 1 deletion Utilities/FileExOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Configuration options for FileEx operations.
Expand Down
14 changes: 9 additions & 5 deletions Utilities/Format.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Provides formatting utilities for byte sizes and size constants.
Expand Down Expand Up @@ -70,8 +70,10 @@ public static string BytesToKibi(long value, string units = "B")
double fraction = Math.Round(value / Math.Pow(KiB, magnitude), 1);
double truncate = Math.Truncate(fraction);
return fraction.Equals(truncate)
? $"{Convert.ToInt64(truncate):D}{s_kibiSuffix[magnitude]}{units}"
: $"{fraction:F}{s_kibiSuffix[magnitude]}{units}";
? FormattableString.Invariant(
$"{Convert.ToInt64(truncate):D}{s_kibiSuffix[magnitude]}{units}"
)
: FormattableString.Invariant($"{fraction:F}{s_kibiSuffix[magnitude]}{units}");
}

private static readonly string[] s_kibiSuffix = ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
Expand Down Expand Up @@ -101,8 +103,10 @@ public static string BytesToKilo(long value, string units = "B")
double fraction = Math.Round(value / Math.Pow(KB, magnitude), 1);
double truncate = Math.Truncate(fraction);
return fraction.Equals(truncate)
? $"{Convert.ToInt64(truncate):D}{s_kiloSuffix[magnitude]}{units}"
: $"{fraction:F}{s_kiloSuffix[magnitude]}{units}";
? FormattableString.Invariant(
$"{Convert.ToInt64(truncate):D}{s_kiloSuffix[magnitude]}{units}"
)
: FormattableString.Invariant($"{fraction:F}{s_kiloSuffix[magnitude]}{units}");
}

private static readonly string[] s_kiloSuffix = ["", "K", "M", "G", "T", "P", "E"];
Expand Down
1 change: 1 addition & 0 deletions Utilities/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using Microsoft.Extensions.Logging;
3 changes: 1 addition & 2 deletions Utilities/LogOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Provides global logging configuration for the library.
Expand Down
2 changes: 1 addition & 1 deletion Utilities/StringCompression.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.IO.Compression;
using System.Text;

namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

// https://stackoverflow.com/questions/7343465/compression-decompression-string-with-c-sharp

Expand Down
2 changes: 1 addition & 1 deletion Utilities/StringHistory.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace InsaneGenius.Utilities;
namespace ptr727.Utilities;

/// <summary>
/// Manages a history of strings with configurable limits on the number of first and last lines to retain.
Expand Down
8 changes: 4 additions & 4 deletions Utilities/Utilities.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
<Version>1.1.1.1</Version>
<FileVersion>1.1.1.1</FileVersion>
<AssemblyVersion>1.1.1.0</AssemblyVersion>
<PackageId>InsaneGenius.Utilities</PackageId>
<AssemblyName>InsaneGenius.Utilities</AssemblyName>
<RootNamespace>InsaneGenius.Utilities</RootNamespace>
<PackageId>ptr727.Utilities</PackageId>
<AssemblyName>Utilities</AssemblyName>
<RootNamespace>ptr727.Utilities</RootNamespace>
<NeutralLanguage>en</NeutralLanguage>
<PackageProjectUrl>https://github.com/ptr727/Utilities</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand All @@ -30,7 +30,7 @@
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="InsaneGenius.Utilities.Tests" />
<InternalsVisibleTo Include="UtilitiesTests" />
</ItemGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="" />
Expand Down
12 changes: 0 additions & 12 deletions UtilitiesTests/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,15 @@ root = false
# Allow underscores in test method names
dotnet_diagnostic.CA1707.severity = none

# Ignore unused private members
dotnet_diagnostic.IDE0052.severity = none

# Test-scoped analyzer exceptions: rules that target production-code concerns
# and don't apply to xUnit test code. Each is documented, not a brownfield
# blanket-relax (see CODESTYLE.md "Analyzer Diagnostics and Suppressions").
# CA1063: test IDisposable implementations are intentionally simple.
dotnet_diagnostic.CA1063.severity = suggestion
# CA1307: test string operations use the default comparison intentionally.
dotnet_diagnostic.CA1307.severity = suggestion
# CA1515: xUnit requires public test classes, so they can't be made internal.
dotnet_diagnostic.CA1515.severity = suggestion
# CA1823: xUnit fixture fields are injected for lifetime/collection wiring and
# are not always referenced directly.
dotnet_diagnostic.CA1823.severity = suggestion
# CA1849: synchronous calls inside async test paths are kept intentionally.
dotnet_diagnostic.CA1849.severity = suggestion
# CA2000: test disposable ownership is transferred or scoped to the test, so
# scope-based disposal analysis reports false positives.
dotnet_diagnostic.CA2000.severity = suggestion
# CA2007: ConfigureAwait(false) is not used in xUnit tests (see xUnit1030).
dotnet_diagnostic.CA2007.severity = suggestion
# CA5394: Random in tests is for test data, not security.
dotnet_diagnostic.CA5394.severity = suggestion
8 changes: 2 additions & 6 deletions UtilitiesTests/CommandLineTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
using Xunit;
namespace ptr727.Utilities.Tests;

namespace InsaneGenius.Utilities.Tests;

public class CommandLineTests(UtilitiesTests fixture) : IClassFixture<UtilitiesTests>
public class CommandLineTests
{
private readonly UtilitiesTests _fixture = fixture;

[Fact]
public void ParseArguments()
{
Expand Down
Loading