Skip to content
Merged

10.1 #21

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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Http.Polly" Version="10.0.6" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.6" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="10.0.6" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="10.0.10" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.6" />
<PackageVersion Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
Expand Down
64 changes: 54 additions & 10 deletions Extensions/CryptoExtensions/Base64Stuff.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}

Expand All @@ -29,17 +33,10 @@ public static string ToBase64HashString(this string textToHash)
public static async Task<string> 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);
}








/// <summary>
/// Hash the stream to a Base64 string using <see cref="ByteStuff.ToHashBytes"/>.
/// </summary>
Expand All @@ -64,5 +61,52 @@ public static async Task<string> ToBase64HashStringAsync(this Stream stream, Can
}


/// <summary>
/// Produces a stable, order-independent Base64 hash of any object.
/// Collections of <see cref="IComparable"/> 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.
/// </summary>
/// <param name="objectToHash">The object to hash.</param>
/// <returns>Base64 hash string.</returns>
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<T> : JsonConverter<List<T>?> where T : IComparable
{
public override List<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
JsonSerializer.Deserialize<List<T>>(ref reader);

public override void Write(Utf8JsonWriter writer, List<T>? value, JsonSerializerOptions options)
{
if (value is null) { writer.WriteNullValue(); return; }
writer.WriteStartArray();
foreach (var item in value.Order())
JsonSerializer.Serialize(writer, item);
writer.WriteEndArray();
}
}
}

}
27 changes: 15 additions & 12 deletions Extensions/FileExtensions/DirectoryStuff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,21 @@ private static void TryDeleteEmptySubfolders(string path, bool isHome = true)


/// <summary>
/// Ensures that <paramref name="targetFolder"/> 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 <paramref name="targetFolder"/> to contain exactly the files specified by <paramref name="sourceFiles"/>, 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).
/// </summary>
/// <param name="targetFolder">absolute target folder path</param>
/// <param name="sourceFiles">absolute file paths for source files</param>
/// <param name="cancellationToken">cancellation</param>
/// <param name="targetFolder">Target folder to synchronize (will be created if missing)</param>
/// <param name="sourceFiles">Absolute file paths of files that should exist in target folder</param>
/// <param name="cancellationToken">Cancellation token</param>
public static void SyncFolderByFileName(this DirectoryInfo targetFolder, IEnumerable<string> sourceFiles, CancellationToken cancellationToken = default)
{
targetFolder.Create();
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions Extensions/NetworkExtensions/PathExtensions.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
31 changes: 30 additions & 1 deletion Tests/ExtensionsTests/CryptoExtensionsTests/Base64StuffTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>? Ids = null, List<string>? 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());
}


}
4 changes: 1 addition & 3 deletions Toolbox/CustomDatastructures/DropOutStack.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Linq;

namespace aemarcoCommons.Toolbox.CustomDatastructures;
namespace aemarcoCommons.Toolbox.CustomDatastructures;

public class DropOutStack<T>
{
Expand Down
1 change: 0 additions & 1 deletion Toolbox/GeoTools/GeoServiceSunInfo.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;

Expand Down
3 changes: 3 additions & 0 deletions Toolbox/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Global using directives

global using System.Linq;
37 changes: 37 additions & 0 deletions Toolbox/Ioc/AddAllImplementationsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;

namespace aemarcoCommons.Toolbox.Ioc;

public static class AddAllImplementationsExtensions
{
public static IServiceCollection AddTransientImplementations<TInterface>(
this IServiceCollection services,
params Assembly[] assemblies) =>
services.AddImplementations<TInterface>(ServiceLifetime.Transient, assemblies);

public static IServiceCollection AddScopedImplementations<TInterface>(
this IServiceCollection services,
params Assembly[] assemblies) =>
services.AddImplementations<TInterface>(ServiceLifetime.Scoped, assemblies);

public static IServiceCollection AddSingletonImplementations<TInterface>(
this IServiceCollection services,
params Assembly[] assemblies) =>
services.AddImplementations<TInterface>(ServiceLifetime.Singleton, assemblies);

private static IServiceCollection AddImplementations<TInterface>(
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;
}
}
1 change: 0 additions & 1 deletion Toolbox/Ioc/DecoratorRegistrationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Linq;


namespace aemarcoCommons.Toolbox.Ioc;
Expand Down
15 changes: 7 additions & 8 deletions Toolbox/Mime/MimeMap.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;

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<IDictionary<string, string>> MappingsExtToMime = new Lazy<IDictionary<string, string>>(BuildMappingsExtToMime);
private static readonly Lazy<IDictionary<string, string>> MappingsMimeToExt = new Lazy<IDictionary<string, string>>(BuildMappingsMimeToExt);
private static readonly Lazy<IDictionary<string, string>> MappingsExtToMime = new(BuildMappingsExtToMime);
private static readonly Lazy<IDictionary<string, string>> 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];


/// <summary>
Expand Down Expand Up @@ -78,7 +77,7 @@ public static string GetMimeType(string fileNameOrExtension) =>
: null;
}

private static IDictionary<string, string> BuildMappingsExtToMime()
private static Dictionary<string, string> BuildMappingsExtToMime()
{
// ReSharper disable StringLiteralTypo
var mappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
Expand Down Expand Up @@ -733,7 +732,7 @@ private static IDictionary<string, string> BuildMappingsExtToMime()
// ReSharper restore StringLiteralTypo
return mappings;
}
private static IDictionary<string, string> BuildMappingsMimeToExt()
private static Dictionary<string, string> BuildMappingsMimeToExt()
{
// ReSharper disable StringLiteralTypo
var mappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
Expand Down
8 changes: 1 addition & 7 deletions ToolboxConsole/Power/Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion ToolboxConsole/Power/Selection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static T EnsureSelection<T>(string header, IEnumerable<T> selectable, Fun
{
return AnsiConsole.Prompt(
new SelectionPrompt<T>()
.Title($"[purple]{header}[/]")
.Title($"[purple]{header.EscapeMarkup()}[/]")
.UseConverter(displayProperty)
.AddChoices([.. selectable]));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ namespace aemarcoCommons.ToolboxFluentValidation;
//https://docs.fluentvalidation.net/en/latest/custom-validators.html
public static class StringRuleBuilderExtensions
{

public static IRuleBuilderOptions<T, string?> BeValidAbsoluteUri<T>(
this IRuleBuilder<T, string?> 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<T, string> BeValidAbsoluteWebUri<T>(
this IRuleBuilder<T, string> ruleBuilder, bool allowTrailingSlash = false)
{
Expand All @@ -26,10 +42,12 @@ public static IRuleBuilderOptions<T, string> BeValidAbsoluteWebUri<T>(



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);


Expand Down
Loading