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: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
[Oo]bj/
.artifacts

# Test coverage output (e.g. dotnet test --results-directory ./coverage)
[Cc]overage/
TestResults/

.idea
.vs

Expand Down
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<Project>
<ItemGroup>
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.300" />
<PackageVersion Include="Serilog" Version="4.3.1" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="10.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.analyzers" Version="1.27.0" />
Expand Down
8 changes: 8 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Some useful and not so useful C# .NET utility classes.

## Release History

- v3.7:
- 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`.
- 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.
- Updated the `Sandbox` example to configure a Serilog console logger and inject it through a `SerilogLoggerFactory`, borrowing the pattern from `LanguageTagsCreate`.
- Dropped the library's Serilog dependency and its `IL3058` AOT warning suppression.
- v3.6:
- Reworked the CI/CD pipeline to the branch-scoped self-publishing model: `main` publishes stable releases and `develop` publishes prereleases, each branch publishing itself when a shipped input changes.
- Switched NuGet publishing to keyless OIDC trusted publishing, removing the `NUGET_API_KEY` secret.
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,13 @@ Some useful and not so useful C# .NET utility classes.

### Release Notes

**Version: 3.6**:
**Version: 3.7**:

**Summary**:

- Internal CI/CD rework, no library API changes.
- Branch-scoped self-publishing workflows.
- Keyless OIDC NuGet publishing.
- Hardened repository configuration, see [WORKFLOW.md](./WORKFLOW.md).
- Adopted an injectable `Microsoft.Extensions.Logging` logging model.
- 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.

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

Expand Down
44 changes: 44 additions & 0 deletions Sandbox/LoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Globalization;
using Serilog;
using Serilog.Extensions.Logging;
using Serilog.Sinks.SystemConsole.Themes;

namespace InsaneGenius.Utilities.Sandbox;

/// <summary>
/// Configures a Serilog console logger and exposes it as a
/// <see cref="Microsoft.Extensions.Logging.ILoggerFactory"/> for the library.
/// </summary>
internal static class LoggerFactory
{
private static readonly Lazy<SerilogLoggerFactory> s_serilogLoggerFactory = new(() =>
{
// Use the already configured Log.Logger if set, else create a new logger.
ILogger logger = ReferenceEquals(Log.Logger, Serilog.Core.Logger.None)
? Create()
: Log.Logger;
bool disposeLogger = !ReferenceEquals(logger, Log.Logger);
return new SerilogLoggerFactory(logger, dispose: disposeLogger);
});

/// <summary>
/// Creates a Serilog console logger.
/// </summary>
/// <returns>The configured Serilog logger.</returns>
internal static ILogger Create() =>
new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console(
theme: AnsiConsoleTheme.Code,
formatProvider: CultureInfo.InvariantCulture,
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"
)
.CreateLogger();

/// <summary>
/// Gets the logger factory wrapping the configured Serilog logger.
/// </summary>
/// <returns>The logger factory.</returns>
internal static Microsoft.Extensions.Logging.ILoggerFactory CreateLoggerFactory() =>
s_serilogLoggerFactory.Value;
}
27 changes: 20 additions & 7 deletions Sandbox/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
using System.Diagnostics;
using System.Reflection;
using InsaneGenius.Utilities;
using InsaneGenius.Utilities.Sandbox;
using Serilog;

// Get the assembly directory
Assembly? entryAssembly = Assembly.GetEntryAssembly();
Debug.Assert(entryAssembly != null);
string? assemblyDirectory = Path.GetDirectoryName(AppContext.BaseDirectory);
Debug.Assert(assemblyDirectory != null);
string projectDirectory = Path.GetFullPath(Path.Combine(assemblyDirectory, "../../../../"));
Log.Logger.Information("Project directory: {ProjectDirectory}", projectDirectory);
// Configure logging: build a Serilog console logger and inject it into the library.
Log.Logger = LoggerFactory.Create();
LogOptions.SetFactory(LoggerFactory.CreateLoggerFactory());

try
{
// Get the assembly directory
Assembly? entryAssembly = Assembly.GetEntryAssembly();
Debug.Assert(entryAssembly != null);
string? assemblyDirectory = Path.GetDirectoryName(AppContext.BaseDirectory);
Debug.Assert(assemblyDirectory != null);
string projectDirectory = Path.GetFullPath(Path.Combine(assemblyDirectory, "../../../../"));
Log.Logger.Information("Project directory: {ProjectDirectory}", projectDirectory);
}
finally
{
Log.CloseAndFlush();
}
2 changes: 2 additions & 0 deletions Sandbox/Sandbox.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>InsaneGenius.Utilities.Sandbox</RootNamespace>
<PublishAot>true</PublishAot>
<InvariantGlobalization>false</InvariantGlobalization>
<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>
Expand All @@ -10,6 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.Console" />
</ItemGroup>
<ItemGroup>
Expand Down
18 changes: 12 additions & 6 deletions Utilities/Download.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Net.Http.Headers;
using System.Reflection;
using Microsoft.Extensions.Logging;

namespace InsaneGenius.Utilities;

Expand All @@ -9,6 +10,11 @@ namespace InsaneGenius.Utilities;
public static class Download
{
private static readonly Lazy<HttpClient> s_httpClient = new(CreateHttpClient);
private static readonly Lazy<ILogger> s_logger = new(() =>
LogOptions.CreateLogger(typeof(Download).FullName!)
);

private static ILogger Log => s_logger.Value;

/// <summary>
/// Gets or sets the HTTP client timeout in seconds. Default is 180 seconds.
Expand Down Expand Up @@ -41,7 +47,7 @@ public static bool GetContentInfo(Uri uri, out long size, out DateTime modifiedT
size = httpResponse.Content.Headers.ContentLength ?? 0;
modifiedTime = httpResponse.Content.Headers.LastModified?.DateTime ?? DateTime.MinValue;
}
catch (Exception e) when (LogOptions.Logger.LogAndHandle(e))
catch (Exception e) when (Log.LogAndHandle(e))
{
return false;
}
Expand Down Expand Up @@ -75,7 +81,7 @@ public static bool GetContentInfo(Uri uri, out long size, out DateTime modifiedT
httpResponse.Content.Headers.LastModified?.DateTime ?? DateTime.MinValue;
return (true, size, modifiedTime);
}
catch (Exception e) when (LogOptions.Logger.LogAndHandle(e))
catch (Exception e) when (Log.LogAndHandle(e))
{
return (false, 0, DateTime.MinValue);
}
Expand All @@ -99,7 +105,7 @@ public static bool DownloadFile(Uri uri, string fileName)
using FileStream fileStream = File.OpenWrite(fileName);
httpStream.CopyTo(fileStream);
}
catch (Exception e) when (LogOptions.Logger.LogAndHandle(e))
catch (Exception e) when (Log.LogAndHandle(e))
{
return false;
}
Expand Down Expand Up @@ -132,7 +138,7 @@ public static async Task<bool> DownloadFileAsync(
await using FileStream fileStream = File.OpenWrite(fileName);
await httpStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
catch (Exception e) when (LogOptions.Logger.LogAndHandle(e))
catch (Exception e) when (Log.LogAndHandle(e))
{
return false;
}
Expand All @@ -156,7 +162,7 @@ public static bool DownloadString(Uri uri, out string value)
{
value = GetHttpClient().GetStringAsync(uri).GetAwaiter().GetResult();
}
catch (Exception e) when (LogOptions.Logger.LogAndHandle(e))
catch (Exception e) when (Log.LogAndHandle(e))
{
return false;
}
Expand Down Expand Up @@ -185,7 +191,7 @@ public static bool DownloadString(Uri uri, out string value)
.ConfigureAwait(false);
return (true, value);
}
catch (Exception e) when (LogOptions.Logger.LogAndHandle(e))
catch (Exception e) when (Log.LogAndHandle(e))
{
return (false, string.Empty);
}
Expand Down
99 changes: 89 additions & 10 deletions Utilities/Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
using System.IO.Compression;
using System.Runtime.CompilerServices;
using Serilog;
using Microsoft.Extensions.Logging;

namespace InsaneGenius.Utilities;

/// <summary>
/// Provides extension methods for string compression and logger error handling.
/// Provides extension methods for string compression.
/// </summary>
#pragma warning disable CA1708 // Identifiers should differ by more than case
public static class Extensions
#pragma warning restore CA1708 // Identifiers should differ by more than case
public static class CompressExtensions
{
/// <summary>
/// Extension methods for string compression and decompression.
Expand Down Expand Up @@ -54,9 +52,15 @@ public Task<string> CompressAsync(
public Task<string> DecompressAsync(CancellationToken cancellationToken = default) =>
StringCompression.DecompressAsync(uncompressedString, cancellationToken);
}
}

/// <summary>
/// Provides logger error handling helpers and strongly-typed, source-generated log messages.
/// </summary>
internal static partial class LogExtensions
{
/// <summary>
/// Extension methods for Serilog ILogger error handling.
/// Extension methods for Microsoft.Extensions.Logging ILogger error handling.
/// </summary>
extension(ILogger logger)
{
Expand All @@ -66,12 +70,12 @@ public Task<string> DecompressAsync(CancellationToken cancellationToken = defaul
/// <param name="exception">The exception to log.</param>
/// <param name="function">The function name (automatically captured).</param>
/// <returns>Always returns false to allow exception to propagate.</returns>
public bool LogAndPropagate(
internal bool LogAndPropagate(
Exception exception,
[CallerMemberName] string function = "unknown"
)
{
logger.Error(exception, "{Function}", function);
logger.LogCatchException(function, exception);
return false;
}

Expand All @@ -81,13 +85,88 @@ public bool LogAndPropagate(
/// <param name="exception">The exception to log.</param>
/// <param name="function">The function name (automatically captured).</param>
/// <returns>Always returns true to indicate exception was handled.</returns>
public bool LogAndHandle(
internal bool LogAndHandle(
Exception exception,
[CallerMemberName] string function = "unknown"
)
{
logger.Error(exception, "{Function}", function);
logger.LogCatchException(function, exception);
return true;
}
}

[LoggerMessage(Message = "Exception in {Function}", Level = LogLevel.Error)]
internal static partial void LogCatchException(
this ILogger logger,
string function,
Exception exception
);

[LoggerMessage(
Message = "Deleting ({RetryCount} / {OptionsRetryCount}) : {FileName}",
Level = LogLevel.Information
)]
internal static partial void LogDeletingFile(
this ILogger logger,
int retryCount,
int optionsRetryCount,
string fileName
);

[LoggerMessage(
Message = "Deleting ({RetryCount} / {OptionsRetryCount}) : {Directory}",
Level = LogLevel.Information
)]
internal static partial void LogDeletingDirectory(
this ILogger logger,
int retryCount,
int optionsRetryCount,
string directory
);

[LoggerMessage(
Message = "Renaming file failed due to invalid path(s) : {OriginalName} to {NewName}",
Level = LogLevel.Error
)]
internal static partial void LogRenameInvalidPath(
this ILogger logger,
string originalName,
string newName
);

[LoggerMessage(
Message = "Renaming ({RetryCount} / {OptionsRetryCount}) : {OriginalDirectory} : {OriginalFile} to {NewFile}",
Level = LogLevel.Information
)]
internal static partial void LogRenamingInDirectory(
this ILogger logger,
int retryCount,
int optionsRetryCount,
string originalDirectory,
string originalFile,
string newFile
);

[LoggerMessage(
Message = "Renaming ({RetryCount} / {OptionsRetryCount}) : {OriginalName} to {NewName}",
Level = LogLevel.Information
)]
internal static partial void LogRenaming(
this ILogger logger,
int retryCount,
int optionsRetryCount,
string originalName,
string newName
);

[LoggerMessage(
Message = "Waiting for file to become readable ({RetryCount} / {OptionsRetryCount}) : {Name}",
Level = LogLevel.Information
)]
internal static partial void LogWaitingForReadable(
this ILogger logger,
int retryCount,
int optionsRetryCount,
string name
);
}
Loading