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
7 changes: 4 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ These are behavioral contracts rather than formatting rules, which is why they l

- **I/O methods return `bool`** for success or failure, and hand back any additional result through an `out` parameter. They do not signal ordinary I/O failure by throwing.
- **Async methods carry the `Async` suffix** and take an optional `CancellationToken cancellationToken = default`, passed through to the underlying call rather than ignored. An async overload returning several values returns a tuple, since `out` parameters are unavailable there.
- **`Download`** reuses a thread-safe `Lazy<HttpClient>` and reads with `HttpCompletionOption.ResponseHeadersRead`, so a large response streams rather than buffering whole.
- **`FileEx`** wraps its I/O in retry logic configured through `Options`, and honors cancellation from both `Options.Cancel` and the method's own token parameter.
- **`Download`** reuses a thread-safe `Lazy<HttpClient>`. `GetContentInfo()` reads with `HttpCompletionOption.ResponseHeadersRead`, so asking for a size never fetches the body, while `DownloadString()` buffers the whole response and a large body belongs in `DownloadFile()` instead. A download to a file truncates and rewrites the destination in place, so its permissions, ownership, and any links to it survive. The destination is truncated once the response headers are accepted rather than once the body has arrived, so a request that fails before that leaves it untouched, while one that fails partway through the body leaves a short file.
- **`FileEx`** wraps its I/O in retry logic configured through the static `FileEx.Options`, a `FileExOptions`, and honors cancellation from both `FileEx.Options.Cancel` and the method's own token parameter.
- **`StringCompression`** uses Deflate, takes a configurable compression level, and passes `leaveOpen` so the caller keeps ownership of the stream it supplied.
- **`Extensions`** uses the C# `extension` block form inside a static class for its logger and string helpers.
- **`StringHistory`** retains at most `MaxFirstLines` from the head and `MaxLastLines` from the tail. Zero on a single side retains no lines on that side, and zero on both is the one unrestricted mode. Either limit rejects a negative value, and assigning one re-partitions the lines already stored, so the history never holds more than the limits then in force allow. Re-partitioning only discards: once a line has been dropped the head is closed, so a later, larger `MaxFirstLines` never promotes a retained tail line into it.
- **`CompressExtensions`** uses the C# `extension` block form inside a static class for its string helpers, and the internal `LogExtensions` does the same for the logger helpers.
- **Logging is a seam, never a dependency.** The library depends on `Microsoft.Extensions.Logging.Abstractions` and takes an `ILoggerFactory` through `LogOptions`. It references no logging framework or sink, so a consumer chooses its own. `Serilog` appears only in `Sandbox` and the tests, where an application legitimately picks one.
19 changes: 11 additions & 8 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ Some useful and not so useful C# .NET utility classes.

## Release History

- v4.1:
- Fixed `Download.DownloadFile()` and `DownloadFileAsync()` corrupting the destination file: both opened it with `File.OpenWrite()`, which does not truncate, so a download over a longer existing file left that file's trailing bytes after the downloaded content and reported success. Both now truncate the destination explicitly and rewrite it in place, which keeps its permissions, ownership, and any links to it. The truncation happens once the response headers are accepted rather than once the body has arrived, so a download that fails partway now leaves a short file where it previously left the original bytes behind the new ones.
- Tightened the `StringHistory` limit contract: `MaxFirstLines` and `MaxLastLines` now document zero as retaining no lines on that side rather than as no limit (both at zero remains the unrestricted mode), reject a negative value with `ArgumentOutOfRangeException` at the constructor and at the property rather than at a later `AppendLine()`, and re-partition the lines already stored when assigned, so a limit set after appending is honored instead of ignored. `AppendLine()` changed with them: what is retained is now always a prefix of the appended lines followed by a suffix of them, so once a line has been discarded the head is trimmed but never refilled, and a later, larger `MaxFirstLines` raises the ceiling without adopting retained tail lines as first lines.
- v4.0:
- Added `HttpClientFactory`, a reusable resilient HTTP client factory built on `Microsoft.Extensions.Http.Resilience` (Polly) with retry, circuit breaker, and connection pooling, tunable through the new `HttpClientOptions`; it exposes a shared singleton client, caller-owned clients, and the resilience handler for callers that build their own client with a custom base address or headers.
- Added `HttpClientFactory`, a reusable resilient HTTP client factory built on `Microsoft.Extensions.Http.Resilience` (Polly) with retry, circuit breaker, and connection pooling, tunable through the new `HttpClientOptions`. It exposes a shared singleton client, caller-owned clients, and the resilience handler for callers that build their own client with a custom base address or headers.
- Added `AssemblyInfo`, an AOT safe assembly and application identity helper whose `For<T>()` substitutes for `Assembly.GetExecutingAssembly()` (unreliable under Native AOT), and which supplies the consuming application name, version, and a default User-Agent.
- Reworked `Download` to build its `HttpClient` through `HttpClientFactory`, so downloads now flow through the shared retry and circuit-breaker pipeline; the `TimeoutSeconds` property and all method signatures are unchanged.
- Changed public members that exposed `List<T>` to safe collection types (a breaking API change): `FileEx.EnumerateDirectories` / `EnumerateDirectory` now return `Collection<T>` out parameters and accept `IEnumerable<string>`, and `StringHistory.StringList` is now a `ReadOnlyCollection<string>`.
- Reworked `Download` to build its `HttpClient` through `HttpClientFactory`, so downloads now flow through the shared retry and circuit-breaker pipeline. The `TimeoutSeconds` property and all method signatures are unchanged.
- Changed public members that exposed `List<T>` to safe collection types (a breaking API change): `FileEx.EnumerateDirectories()` / `EnumerateDirectory()` now return `Collection<T>` out parameters and accept `IEnumerable<string>`, and `StringHistory.StringList` is now a `ReadOnlyCollection<string>`.
- Gated the library's reference AOT verification behind an explicit `PublishAot` opt-in, and turned the `Sandbox` project into a Native AOT smoke test (published and run as AOT) that proves the resilience pipeline and assembly-identity resolution work under Native AOT.
- Renamed the NuGet package and root namespace from `InsaneGenius.Utilities` to `ptr727.Utilities` (a breaking change); consumers must update their package reference and change `using InsaneGenius.Utilities;` directives to `using ptr727.Utilities;`. The assembly is now named `Utilities`.
- Renamed the NuGet package and root namespace from `InsaneGenius.Utilities` to `ptr727.Utilities`, a breaking change. 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.
- 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`.
- 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`.
- Moved the `LogAndHandle` / `LogAndPropagate` helpers onto `Microsoft.Extensions.Logging.ILogger` as internal extensions (exposed to tests via `InternalsVisibleTo`).
- Renamed the public `Extensions` class to `CompressExtensions` (a breaking API change for direct references; instance-style extension calls such as `value.Compress()` are unaffected), resolving the naming clash with the `Microsoft.Extensions` namespace.
- Moved the `LogAndHandle()` / `LogAndPropagate()` helpers onto `Microsoft.Extensions.Logging.ILogger` as internal extensions (exposed to tests via `InternalsVisibleTo`).
- Renamed the public `Extensions` class to `CompressExtensions` (a breaking API change for direct references, though instance-style extension calls such as `value.Compress()` are unaffected), resolving the naming clash with the `Microsoft.Extensions` namespace.
- Updated the `Sandbox` example to configure a Serilog console logger and inject it through a `SerilogLoggerFactory`.
- Dropped the library's Serilog dependency and its `IL3058` AOT warning suppression.
- v3.6:
Expand All @@ -24,7 +27,7 @@ Some useful and not so useful C# .NET utility classes.
- Added `WORKFLOW.md`, the canonical CI/CD specification, and `repo-config/`, the rulesets and repository settings as code.
- Bundled the build output and NuGet packages into a `Utilities.7z` asset on each GitHub release.
- v3.5:
- Re-synced the repository structure and agent documentation with the upstream ProjectTemplate: added this `HISTORY.md` and a `CODESTYLE.md` .NET style guide, narrowed `.github/copilot-instructions.md` to the Copilot review runbook, and refreshed `AGENTS.md` conventions.
- Re-synced the repository structure and agent documentation: added this `HISTORY.md` and a `CODESTYLE.md` .NET style guide, narrowed `.github/copilot-instructions.md` to the Copilot review runbook, and refreshed `AGENTS.md` conventions.
- Corrected the versioning policy to bump `version.json` only for functional changes.
- Swapped the recommended Todo VS Code add-on from Todo Tree to Better Todo Tree.
- v3.4:
Expand Down
13 changes: 3 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,10 @@ Some useful and not so useful C# .NET utility classes.

### Release Notes

**Version: 4.0**:
**Version: 4.1**:

**⚠️ Breaking Changes**:

- Renamed the NuGet package and namespace from `InsaneGenius.Utilities` to `ptr727.Utilities`.
- Replaced Serilog with an injectable `Microsoft.Extensions.Logging` logging model.
- Replaced `List<T>` in public methods with `Collection<T>` and `ReadOnlyCollection<T>`.

**Summary**:

- Added `HttpClientFactory`, a reusable resilient HTTP client factory (Polly retry and circuit breaker) with an AOT safe `AssemblyInfo` identity helper and a tunable `HttpClientOptions`.
- Fixed `Download.DownloadFile()` and `DownloadFileAsync()` corrupting the destination when downloading over a longer existing file. The destination is now truncated and rewritten in place, keeping its permissions and any links to it. A download that fails partway leaves a short file rather than a mix of the new content and the old.
- Fixed the `StringHistory` limit properties: `MaxFirstLines` and `MaxLastLines` now honor a limit assigned after lines have been appended, document zero on one side as retaining no lines on that side rather than as no limit, with both at zero remaining the one unrestricted mode, and reject a negative value with `ArgumentOutOfRangeException`.

See [Release History](./HISTORY.md) for complete release notes and older versions.

Expand Down
24 changes: 22 additions & 2 deletions Utilities/Download.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,18 @@ public static bool DownloadFile(Uri uri, string fileName)
try
{
using Stream httpStream = GetHttpClient().GetStreamAsync(uri).GetAwaiter().GetResult();
using FileStream fileStream = File.OpenWrite(fileName);

// Rewriting in place keeps the destination's own permissions, ownership, and links.
// OpenOrCreate rather than Create, which on Windows refuses a hidden destination.
using FileStream fileStream = new(
fileName,
FileMode.OpenOrCreate,
FileAccess.Write,
FileShare.None
);

// Truncate explicitly, which OpenWrite never did, leaving a longer file's tail behind.
fileStream.SetLength(0);
httpStream.CopyTo(fileStream);
}
catch (Exception e) when (Log.LogAndHandle(e))
Expand Down Expand Up @@ -137,9 +148,18 @@ public static async Task<bool> DownloadFileAsync(
.ConfigureAwait(false);
await using (httpStream.ConfigureAwait(false))
{
FileStream fileStream = File.OpenWrite(fileName);
// Rewriting in place keeps the destination's permissions, ownership, and links.
// OpenOrCreate rather than Create, which on Windows refuses a hidden destination.
FileStream fileStream = new(
fileName,
FileMode.OpenOrCreate,
FileAccess.Write,
FileShare.None
);
await using (fileStream.ConfigureAwait(false))
{
// Truncate explicitly, which OpenWrite never did, leaving a longer file's tail.
fileStream.SetLength(0);
await httpStream
.CopyToAsync(fileStream, cancellationToken)
.ConfigureAwait(false);
Expand Down
Loading