From 01600641c0dc1d938f16e94bc5f58c18af4e5538 Mon Sep 17 00:00:00 2001 From: aemarco Date: Sat, 2 May 2026 20:54:00 +0200 Subject: [PATCH 01/11] docs: Clarify SyncFolderByFileName behavior and fix search scope - Improved documentation to better describe operation and limitations - Changed from AllDirectories (recursive) to TopDirectoryOnly for folder sync - Clarified file name matching semantics (case sensitivity, duplicates) - Documented TryDelete failure handling Co-Authored-By: Claude Haiku 4.5 --- Extensions/FileExtensions/DirectoryStuff.cs | 27 ++++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Extensions/FileExtensions/DirectoryStuff.cs b/Extensions/FileExtensions/DirectoryStuff.cs index d6b2e77..2bab36d 100644 --- a/Extensions/FileExtensions/DirectoryStuff.cs +++ b/Extensions/FileExtensions/DirectoryStuff.cs @@ -64,18 +64,21 @@ private static void TryDeleteEmptySubfolders(string path, bool isHome = true) /// - /// Ensures that contains exactly the given files by *file name*. - /// - /// - Existing files with matching names are kept as-is. - /// - Files not listed are deleted. - /// - Files listed but missing are copied from their source paths. - /// - Duplicate file names ?? First file wins, others are ignored - /// - /// Note: Matching is done by file name only (not content or path). + /// Synchronizes to contain exactly the files specified by , matched by file name only. + /// + /// **Operation:** + /// - Files in target matching a source file name are kept as-is (never overwritten). + /// - Files in target with no matching source file name are deleted. + /// - Source files not yet present in target are copied to the target folder root. + /// + /// **Limitations:** + /// - Matching is by file name only (case-sensitive on Linux, case-insensitive on Windows). + /// - If multiple source files have the same name, the first one is used; others are ignored. + /// - Deletions that fail due to file locks are logged but do not stop the operation (via TryDelete). /// - /// absolute target folder path - /// absolute file paths for source files - /// cancellation + /// Target folder to synchronize (will be created if missing) + /// Absolute file paths of files that should exist in target folder + /// Cancellation token public static void SyncFolderByFileName(this DirectoryInfo targetFolder, IEnumerable sourceFiles, CancellationToken cancellationToken = default) { targetFolder.Create(); @@ -86,7 +89,7 @@ public static void SyncFolderByFileName(this DirectoryInfo targetFolder, IEnumer x => Path.Combine(targetFolder.FullName, x.Name)); var existingFiles = targetFolder - .GetFiles("*.*", SearchOption.AllDirectories) + .GetFiles("*", SearchOption.TopDirectoryOnly) .ToList(); //delete obsolete files From e9b1bcc89a6adcc06ddb4a08a6493781882b3cfa Mon Sep 17 00:00:00 2001 From: aemarco Date: Sun, 3 May 2026 11:34:47 +0200 Subject: [PATCH 02/11] feat: Add CheckNetworkPathAccessibility extension for DirectoryInfo Co-Authored-By: Claude Sonnet 4.6 --- .../NetworkExtensions/PathExtensions.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 Extensions/NetworkExtensions/PathExtensions.cs diff --git a/Extensions/NetworkExtensions/PathExtensions.cs b/Extensions/NetworkExtensions/PathExtensions.cs new file mode 100644 index 0000000..bd0c5fa --- /dev/null +++ b/Extensions/NetworkExtensions/PathExtensions.cs @@ -0,0 +1,28 @@ +using System.IO; + +namespace aemarcoCommons.Extensions.NetworkExtensions; + +public record NetworkPathAccessibilityResult( + bool IsAccessible, + Exception? Exception = null); + +public static class PathExtensions +{ + public static NetworkPathAccessibilityResult CheckNetworkPathAccessibility(this DirectoryInfo directory) + { + try + { + var networkRoot = Path.GetPathRoot(directory.FullName); + var isAccessible = !string.IsNullOrEmpty(networkRoot) && Directory.Exists(networkRoot); + return new NetworkPathAccessibilityResult(isAccessible); + } + catch (IOException ex) + { + return new NetworkPathAccessibilityResult(false, ex); + } + catch (UnauthorizedAccessException ex) + { + return new NetworkPathAccessibilityResult(false, ex); + } + } +} From a31ab6b41f70ee255ba9fd1111142fe5c06bd5a8 Mon Sep 17 00:00:00 2001 From: aemarco Date: Sun, 17 May 2026 20:26:07 +0200 Subject: [PATCH 03/11] feat: Add ToStableHash extension for order-independent object fingerprinting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Serializes via STJ with a SortedListConverter so List members are sorted before hashing — two objects differing only in list order produce the same hash. Replaces Newtonsoft-based WallpaperFilter.Signature. Co-Authored-By: Claude Sonnet 4.6 --- Extensions/CryptoExtensions/Base64Stuff.cs | 64 ++++++++++++++++--- .../CryptoExtensionsTests/Base64StuffTests.cs | 31 ++++++++- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/Extensions/CryptoExtensions/Base64Stuff.cs b/Extensions/CryptoExtensions/Base64Stuff.cs index 88e2d07..6503bf2 100644 --- a/Extensions/CryptoExtensions/Base64Stuff.cs +++ b/Extensions/CryptoExtensions/Base64Stuff.cs @@ -1,5 +1,9 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -16,7 +20,7 @@ public static class Base64Stuff public static string ToBase64HashString(this string textToHash) { using var ms = new MemoryStream(Encoding.UTF8.GetBytes(textToHash)); - var hashBytes = ms.ToHashBytes(); // Reuse your existing extension + var hashBytes = ms.ToHashBytes(); return Convert.ToBase64String(hashBytes); } @@ -29,17 +33,10 @@ public static string ToBase64HashString(this string textToHash) public static async Task ToBase64HashStringAsync(this string textToHash, CancellationToken cancellationToken = default) { await using var ms = new MemoryStream(Encoding.UTF8.GetBytes(textToHash)); - var hashBytes = await ms.ToHashBytesAsync(cancellationToken).ConfigureAwait(false); // Async reuse + var hashBytes = await ms.ToHashBytesAsync(cancellationToken).ConfigureAwait(false); return Convert.ToBase64String(hashBytes); } - - - - - - - /// /// Hash the stream to a Base64 string using . /// @@ -64,5 +61,52 @@ public static async Task ToBase64HashStringAsync(this Stream stream, Can } + /// + /// Produces a stable, order-independent Base64 hash of any object. + /// Collections of elements are sorted before hashing, + /// so two objects that differ only in list ordering produce the same hash. + /// Useful as a cache key or equality fingerprint for filter objects. + /// + /// The object to hash. + /// Base64 hash string. + public static string ToStableHash(this object objectToHash) + { + var json = JsonSerializer.Serialize(objectToHash, CacheKeyOptions); + return json.ToBase64HashString(); + } + + private static readonly JsonSerializerOptions CacheKeyOptions = new() + { + Converters = { new SortedListConverterFactory() } + }; + private class SortedListConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) => + typeToConvert.IsGenericType && + typeToConvert.GetGenericTypeDefinition() == typeof(List<>) && + typeToConvert.GetGenericArguments()[0].IsAssignableTo(typeof(IComparable)); + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var elementType = typeToConvert.GetGenericArguments()[0]; + return (JsonConverter)Activator.CreateInstance( + typeof(SortedListConverter<>).MakeGenericType(elementType))!; + } + + private class SortedListConverter : JsonConverter?> where T : IComparable + { + public override List? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + JsonSerializer.Deserialize>(ref reader); + + public override void Write(Utf8JsonWriter writer, List? value, JsonSerializerOptions options) + { + if (value is null) { writer.WriteNullValue(); return; } + writer.WriteStartArray(); + foreach (var item in value.Order()) + JsonSerializer.Serialize(writer, item); + writer.WriteEndArray(); + } + } + } } \ No newline at end of file diff --git a/Tests/ExtensionsTests/CryptoExtensionsTests/Base64StuffTests.cs b/Tests/ExtensionsTests/CryptoExtensionsTests/Base64StuffTests.cs index 88cf3f4..94013c0 100644 --- a/Tests/ExtensionsTests/CryptoExtensionsTests/Base64StuffTests.cs +++ b/Tests/ExtensionsTests/CryptoExtensionsTests/Base64StuffTests.cs @@ -9,10 +9,39 @@ public class Base64StuffTests [TestCase("oMyDear", "d7gPbeNldisHAEjR9Zq7OQ==")] public void ToBase64HashString_Returns_Correctly(string text, string expected) { - var result = text.ToBase64HashString(); result.ShouldBe(expected); } + + // ReSharper disable NotAccessedPositionalProperty.Local + private record StableHashTarget(List? Ids = null, List? Tags = null); + // ReSharper restore NotAccessedPositionalProperty.Local + + [TestCase(new[] { 1, 2, 3 }, new[] { 3, 1, 2 })] + [TestCase(new[] { 10, 1, 5 }, new[] { 5, 10, 1 })] + public void ToStableHash_IntListDifferentOrder_SameHash(int[] a, int[] b) + { + new StableHashTarget([.. a]).ToStableHash() + .ShouldBe(new StableHashTarget([.. b]).ToStableHash()); + } + + [TestCase(new[] { "x", "y", "z" }, new[] { "z", "x", "y" })] + [TestCase(new[] { "banana", "apple" }, new[] { "apple", "banana" })] + public void ToStableHash_StringListDifferentOrder_SameHash(string[] a, string[] b) + { + new StableHashTarget(null, [.. a]).ToStableHash() + .ShouldBe(new StableHashTarget(null, [.. b]).ToStableHash()); + } + + [TestCase(new[] { 1, 2, 3 }, new[] { 1, 2, 4 })] + [TestCase(new[] { 1, 2, 3 }, new[] { 1, 2 })] + public void ToStableHash_DifferentContent_DifferentHash(int[] a, int[] b) + { + new StableHashTarget([.. a]).ToStableHash() + .ShouldNotBe(new StableHashTarget([.. b]).ToStableHash()); + } + + } \ No newline at end of file From 30cca1116fc4b3736c557eed143aff0956db55e1 Mon Sep 17 00:00:00 2001 From: aemarco Date: Tue, 26 May 2026 20:58:06 +0200 Subject: [PATCH 04/11] options not needed --- ToolboxConsole/Power/Json.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ToolboxConsole/Power/Json.cs b/ToolboxConsole/Power/Json.cs index ef2b4ae..2f72d91 100644 --- a/ToolboxConsole/Power/Json.cs +++ b/ToolboxConsole/Power/Json.cs @@ -6,15 +6,9 @@ namespace aemarcoCommons.ToolboxConsole; public static partial class PowerConsole { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - WriteIndented = true - }; public static void WriteAsJson(object obj, JsonSerializerOptions? options = null) { - var json = JsonSerializer.Serialize( - obj, - options ?? JsonSerializerOptions); + var json = JsonSerializer.Serialize(obj, options); AnsiConsole.Write(new JsonText(json)); AnsiConsole.WriteLine(); } From cee7ff6ca8c0130505eb0eef7c7ab66acbb25897 Mon Sep 17 00:00:00 2001 From: aemarco Date: Thu, 4 Jun 2026 13:43:01 +0200 Subject: [PATCH 05/11] fix: EscapeMarkup on selection header to prevent Spectre markup injection Co-Authored-By: Claude Sonnet 4.6 --- ToolboxConsole/Power/Selection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ToolboxConsole/Power/Selection.cs b/ToolboxConsole/Power/Selection.cs index 4de4ac1..1114f16 100644 --- a/ToolboxConsole/Power/Selection.cs +++ b/ToolboxConsole/Power/Selection.cs @@ -13,7 +13,7 @@ public static T EnsureSelection(string header, IEnumerable selectable, Fun { return AnsiConsole.Prompt( new SelectionPrompt() - .Title($"[purple]{header}[/]") + .Title($"[purple]{header.EscapeMarkup()}[/]") .UseConverter(displayProperty) .AddChoices([.. selectable])); } From dc5115dacb03e160315db2871aece6dd46270b38 Mon Sep 17 00:00:00 2001 From: aemarco Date: Sat, 6 Jun 2026 15:41:08 +0200 Subject: [PATCH 06/11] feat: add AddAllImplementationsExtensions for convention-based DI registration Co-Authored-By: Claude Sonnet 4.6 --- .../Ioc/AddAllImplementationsExtensions.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 Toolbox/Ioc/AddAllImplementationsExtensions.cs diff --git a/Toolbox/Ioc/AddAllImplementationsExtensions.cs b/Toolbox/Ioc/AddAllImplementationsExtensions.cs new file mode 100644 index 0000000..1e7f7d4 --- /dev/null +++ b/Toolbox/Ioc/AddAllImplementationsExtensions.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace aemarcoCommons.Toolbox.Ioc; + +public static class AddAllImplementationsExtensions +{ + public static IServiceCollection AddTransientImplementations( + this IServiceCollection services, + params Assembly[] assemblies) => + services.AddImplementations(ServiceLifetime.Transient, assemblies); + + public static IServiceCollection AddScopedImplementations( + this IServiceCollection services, + params Assembly[] assemblies) => + services.AddImplementations(ServiceLifetime.Scoped, assemblies); + + public static IServiceCollection AddSingletonImplementations( + this IServiceCollection services, + params Assembly[] assemblies) => + services.AddImplementations(ServiceLifetime.Singleton, assemblies); + + private static IServiceCollection AddImplementations( + this IServiceCollection services, + ServiceLifetime lifetime, + Assembly[] assemblies) + { + var types = assemblies + .SelectMany(a => a.GetTypes()) + .Where(t => + typeof(TInterface).IsAssignableFrom(t) && + t is { IsAbstract: false, IsInterface: false }); + foreach (var type in types) + services.Add(ServiceDescriptor.Describe(typeof(TInterface), type, lifetime)); + return services; + } +} From a8edff7bbbfcce053c4e24d3cfdf7e8aec96e576 Mon Sep 17 00:00:00 2001 From: aemarco Date: Sat, 6 Jun 2026 16:31:11 +0200 Subject: [PATCH 07/11] chore: misc cleanup and GlobalUsings Co-Authored-By: Claude Sonnet 4.6 --- Toolbox/CustomDatastructures/DropOutStack.cs | 4 +--- Toolbox/GeoTools/GeoServiceSunInfo.cs | 1 - Toolbox/GlobalUsings.cs | 3 +++ Toolbox/Ioc/DecoratorRegistrationExtensions.cs | 1 - Toolbox/Mime/MimeMap.cs | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 Toolbox/GlobalUsings.cs diff --git a/Toolbox/CustomDatastructures/DropOutStack.cs b/Toolbox/CustomDatastructures/DropOutStack.cs index 1c7aab1..9cebfab 100644 --- a/Toolbox/CustomDatastructures/DropOutStack.cs +++ b/Toolbox/CustomDatastructures/DropOutStack.cs @@ -1,6 +1,4 @@ -using System.Linq; - -namespace aemarcoCommons.Toolbox.CustomDatastructures; +namespace aemarcoCommons.Toolbox.CustomDatastructures; public class DropOutStack { diff --git a/Toolbox/GeoTools/GeoServiceSunInfo.cs b/Toolbox/GeoTools/GeoServiceSunInfo.cs index 4a16bd4..a1d423b 100644 --- a/Toolbox/GeoTools/GeoServiceSunInfo.cs +++ b/Toolbox/GeoTools/GeoServiceSunInfo.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using System; using System.Collections.Concurrent; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; diff --git a/Toolbox/GlobalUsings.cs b/Toolbox/GlobalUsings.cs new file mode 100644 index 0000000..10ac625 --- /dev/null +++ b/Toolbox/GlobalUsings.cs @@ -0,0 +1,3 @@ +// Global using directives + +global using System.Linq; \ No newline at end of file diff --git a/Toolbox/Ioc/DecoratorRegistrationExtensions.cs b/Toolbox/Ioc/DecoratorRegistrationExtensions.cs index ed27602..214858e 100644 --- a/Toolbox/Ioc/DecoratorRegistrationExtensions.cs +++ b/Toolbox/Ioc/DecoratorRegistrationExtensions.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using System; -using System.Linq; namespace aemarcoCommons.Toolbox.Ioc; diff --git a/Toolbox/Mime/MimeMap.cs b/Toolbox/Mime/MimeMap.cs index a45d1a1..ad0c6a7 100644 --- a/Toolbox/Mime/MimeMap.cs +++ b/Toolbox/Mime/MimeMap.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; namespace aemarcoCommons.Toolbox.Mime; From 876292b34189ef64e3f30abf529c0c9f3a9ca837 Mon Sep 17 00:00:00 2001 From: aemarco Date: Wed, 22 Jul 2026 00:21:51 +0200 Subject: [PATCH 08/11] chore: bump Microsoft.Windows.Compatibility to 10.0.10; drop IsEnabled log guard Co-Authored-By: Claude Sonnet 4.6 --- Directory.Packages.props | 2 +- ToolboxWeb/Authorization/LanIpAddress.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ff5d2be..6e128b3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,7 +18,7 @@ - + diff --git a/ToolboxWeb/Authorization/LanIpAddress.cs b/ToolboxWeb/Authorization/LanIpAddress.cs index 94cfbb5..26ab224 100644 --- a/ToolboxWeb/Authorization/LanIpAddress.cs +++ b/ToolboxWeb/Authorization/LanIpAddress.cs @@ -144,8 +144,8 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Access granted to {RemoteIp}", remoteIp); + + _logger.LogDebug("Access granted to {RemoteIp}", remoteIp); context.Succeed(requirement); } From 52a1c9a0824dd1be82f704df99898c2e930bbd2e Mon Sep 17 00:00:00 2001 From: aemarco Date: Mon, 27 Jul 2026 18:10:38 +0200 Subject: [PATCH 09/11] feat: add nullable-aware URI validator and secret rule builder Adds BeValidAbsoluteUri supporting nullable trailing-slash intent (null = don't care) and a BeConfiguredSecret rule to catch unconfigured default secrets. --- .../StringRuleBuilderExtensions.cs | 24 ++++++++++++++++--- .../StringRuleBuilderSecretsExtensions.cs | 16 +++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 ToolboxFluentValidation/RuleBuilders/StringRuleBuilderSecretsExtensions.cs diff --git a/ToolboxFluentValidation/RuleBuilders/StringRuleBuilderExtensions.cs b/ToolboxFluentValidation/RuleBuilders/StringRuleBuilderExtensions.cs index f57c80c..b6e5c99 100644 --- a/ToolboxFluentValidation/RuleBuilders/StringRuleBuilderExtensions.cs +++ b/ToolboxFluentValidation/RuleBuilders/StringRuleBuilderExtensions.cs @@ -5,6 +5,22 @@ namespace aemarcoCommons.ToolboxFluentValidation; //https://docs.fluentvalidation.net/en/latest/custom-validators.html public static class StringRuleBuilderExtensions { + + public static IRuleBuilderOptions BeValidAbsoluteUri( + this IRuleBuilder ruleBuilder, bool? trailingSlash = false) + { + return ruleBuilder + .Must(text => IsAbsoluteWebUri(text, trailingSlash)) + .WithMessage( + trailingSlash.HasValue + ? trailingSlash.Value + ? "'{PropertyName}' must be a valid Web-Uri with a trailing slash" + : "'{PropertyName}' must be a valid Web-Uri without a trailing slash" + : "'{PropertyName}' must be a valid Web-Uri"); + } + + + public static IRuleBuilderOptions BeValidAbsoluteWebUri( this IRuleBuilder ruleBuilder, bool allowTrailingSlash = false) { @@ -26,10 +42,12 @@ public static IRuleBuilderOptions BeValidAbsoluteWebUri( - private static bool IsAbsoluteWebUri(string url, bool allowTrailingSlash) => + private static bool IsAbsoluteWebUri(string? url, bool? trailingSlash) => !string.IsNullOrWhiteSpace(url) && - (!url.EndsWith('/') || allowTrailingSlash) && - Uri.TryCreate(url, UriKind.Absolute, out Uri? uriResult) && + (!trailingSlash.HasValue || + (trailingSlash.Value && url.EndsWith('/')) || + (!trailingSlash.Value && !url.EndsWith('/'))) && + Uri.TryCreate(url, UriKind.Absolute, out var uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); diff --git a/ToolboxFluentValidation/RuleBuilders/StringRuleBuilderSecretsExtensions.cs b/ToolboxFluentValidation/RuleBuilders/StringRuleBuilderSecretsExtensions.cs new file mode 100644 index 0000000..b652185 --- /dev/null +++ b/ToolboxFluentValidation/RuleBuilders/StringRuleBuilderSecretsExtensions.cs @@ -0,0 +1,16 @@ +namespace aemarcoCommons.ToolboxFluentValidation.RuleBuilders; + +public static class StringRuleBuilderSecretsExtensions +{ + public static IRuleBuilderOptions BeConfiguredSecret( + this IRuleBuilder ruleBuilder) + { + return ruleBuilder + .Must(text => + text is not null && + text != "secret") + .WithMessage("Secret '{PropertyName}' must be configured"); + } + + +} From a5f6b82dc588ecb2509a32e22941cbc0bfefcfb5 Mon Sep 17 00:00:00 2001 From: aemarco Date: Mon, 27 Jul 2026 19:59:26 +0200 Subject: [PATCH 10/11] refactor: modernize MimeMap and add IdentifyImageAsync for FileInfo MimeMap becomes static with collection expressions; ToolboxImage gains a FileInfo.IdentifyImageAsync helper to replace ad-hoc Image.IdentifyAsync calls in consuming projects. Co-Authored-By: Claude Sonnet 5 --- Toolbox/Mime/MimeMap.cs | 14 +++++++------- ToolboxImage/Extensions/ImageExtensions.cs | 10 ++++++++++ ToolboxImage/GlobalUsings.cs | 3 ++- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Toolbox/Mime/MimeMap.cs b/Toolbox/Mime/MimeMap.cs index ad0c6a7..fb267e3 100644 --- a/Toolbox/Mime/MimeMap.cs +++ b/Toolbox/Mime/MimeMap.cs @@ -4,17 +4,17 @@ namespace aemarcoCommons.Toolbox.Mime; -public class MimeMap +public static class MimeMap { private const string Dot = "."; private const string QuestionMark = "?"; private const string DefaultMimeType = "application/octet-stream"; - private static readonly Lazy> MappingsExtToMime = new Lazy>(BuildMappingsExtToMime); - private static readonly Lazy> MappingsMimeToExt = new Lazy>(BuildMappingsMimeToExt); + private static readonly Lazy> MappingsExtToMime = new(BuildMappingsExtToMime); + private static readonly Lazy> MappingsMimeToExt = new(BuildMappingsMimeToExt); - public static string[] Extensions => MappingsExtToMime.Value.Keys.ToArray(); - public static string[] MimeTypes => MappingsMimeToExt.Value.Values.ToArray(); + public static string[] Extensions => [.. MappingsExtToMime.Value.Keys]; + public static string[] MimeTypes => [.. MappingsMimeToExt.Value.Values]; /// @@ -77,7 +77,7 @@ public static string GetMimeType(string fileNameOrExtension) => : null; } - private static IDictionary BuildMappingsExtToMime() + private static Dictionary BuildMappingsExtToMime() { // ReSharper disable StringLiteralTypo var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -732,7 +732,7 @@ private static IDictionary BuildMappingsExtToMime() // ReSharper restore StringLiteralTypo return mappings; } - private static IDictionary BuildMappingsMimeToExt() + private static Dictionary BuildMappingsMimeToExt() { // ReSharper disable StringLiteralTypo var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/ToolboxImage/Extensions/ImageExtensions.cs b/ToolboxImage/Extensions/ImageExtensions.cs index f6212a3..96b7daf 100644 --- a/ToolboxImage/Extensions/ImageExtensions.cs +++ b/ToolboxImage/Extensions/ImageExtensions.cs @@ -19,6 +19,16 @@ namespace aemarcoCommons.ToolboxImage.Extensions; public static class ImageExtensions { + //read + public static async Task IdentifyImageAsync(this FileInfo file) + { + var result = await Image.IdentifyAsync(file.FullName); + return result; + } + + + //update + /// /// Creates a resized clone of the specified image using the given parameters. /// diff --git a/ToolboxImage/GlobalUsings.cs b/ToolboxImage/GlobalUsings.cs index 1aad611..39f1627 100644 --- a/ToolboxImage/GlobalUsings.cs +++ b/ToolboxImage/GlobalUsings.cs @@ -28,4 +28,5 @@ global using System; global using System.Collections.Generic; global using System.IO; -global using System.Linq; \ No newline at end of file +global using System.Linq; +global using System.Threading.Tasks; \ No newline at end of file From 266f366c58eefe953987a64a332eb25f2ecf781c Mon Sep 17 00:00:00 2001 From: aemarco Date: Mon, 27 Jul 2026 22:30:45 +0200 Subject: [PATCH 11/11] chore: bump version to 10.1 Co-Authored-By: Claude Sonnet 5 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index c601aa1..246ee10 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.0", + "version": "10.1", "publicReleaseRefSpec": [ "^refs/heads/main", "^refs/heads/v\\d+(?:\\.\\d+)?$"