From 1007d919373b429b4902d84eb913919264f06bff Mon Sep 17 00:00:00 2001 From: David Makovsky Date: Fri, 21 Feb 2025 16:20:53 +0100 Subject: [PATCH 1/4] ci: prepare for new version, migrate from fluent assertion to aweasome assertions --- .github/workflows/publish.yml | 4 +-- .github/workflows/verify.yml | 4 +-- .../CommonNet.DependencyInjection.csproj | 2 +- ...erviceCollectionExtensions.AddSingleton.cs | 1 - .../CommonNet.Extensions.Tests.csproj | 8 +++--- .../EmbeddedResourceStreamReader.cs | 2 +- Directory.Build.props | 2 +- Directory.Packages.props | 27 ++++++++++--------- 8 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 812bf15..bf8da1d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,7 +6,7 @@ on: build_version_base: description: 'Build version number X.Y.Z' required: false - default: 0.9.0 + default: 0.10.0 jobs: @@ -37,7 +37,7 @@ jobs: - name: Install .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x # Create the NuGet packages in the folder from the environment variable NuGetDirectory - name: Build NuGet packages diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 72be51d..a3c13d9 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -22,7 +22,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -81,7 +81,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: Install dependencies run: dotnet restore CommonNet.Extensions.Tests/CommonNet.Extensions.Tests.csproj - name: Build Tests diff --git a/CommonNet.DependencyInjection/CommonNet.DependencyInjection.csproj b/CommonNet.DependencyInjection/CommonNet.DependencyInjection.csproj index 3362af9..27b9762 100644 --- a/CommonNet.DependencyInjection/CommonNet.DependencyInjection.csproj +++ b/CommonNet.DependencyInjection/CommonNet.DependencyInjection.csproj @@ -2,7 +2,7 @@ true - Various Dependency Injectionextensions. + Various Dependency Injection extensions. MIT diff --git a/CommonNet.DependencyInjection/Microsoft.Extensions.DependencyInjection/ServiceCollectionExtensions.AddSingleton.cs b/CommonNet.DependencyInjection/Microsoft.Extensions.DependencyInjection/ServiceCollectionExtensions.AddSingleton.cs index d248075..232f74a 100644 --- a/CommonNet.DependencyInjection/Microsoft.Extensions.DependencyInjection/ServiceCollectionExtensions.AddSingleton.cs +++ b/CommonNet.DependencyInjection/Microsoft.Extensions.DependencyInjection/ServiceCollectionExtensions.AddSingleton.cs @@ -210,7 +210,6 @@ public static IServiceCollection AddSingleton(sp => sp.GetRequiredService) .AddSingleton(implementationFactory) .AddSingleton(sp => (sp.GetRequiredService() as TService2)!); } diff --git a/CommonNet.Extensions.Tests/CommonNet.Extensions.Tests.csproj b/CommonNet.Extensions.Tests/CommonNet.Extensions.Tests.csproj index 4619fe0..14cec64 100644 --- a/CommonNet.Extensions.Tests/CommonNet.Extensions.Tests.csproj +++ b/CommonNet.Extensions.Tests/CommonNet.Extensions.Tests.csproj @@ -1,8 +1,8 @@  - net6.0;net7.0;net8.0 - net8.0;net7.0;net6.0;net48 + net8.0;net9.0 + net8.0;net9.0;net48 true @@ -24,8 +24,8 @@ all runtime; build; native; contentfiles; analyzers - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CommonNet.Extensions/System.Reflection/EmbeddedResourceStreamReader.cs b/CommonNet.Extensions/System.Reflection/EmbeddedResourceStreamReader.cs index c7e80b4..847fa31 100644 --- a/CommonNet.Extensions/System.Reflection/EmbeddedResourceStreamReader.cs +++ b/CommonNet.Extensions/System.Reflection/EmbeddedResourceStreamReader.cs @@ -50,7 +50,7 @@ public static byte[] ReadEmbeddedResourceData(this Assembly assembly, string res Guard.IsNotNull(stream, nameof(resourceName)); var data = new byte[stream.Length]; - stream.Read(data, 0, data.Length); + _ = stream.Read(data, 0, data.Length); return data; } diff --git a/Directory.Build.props b/Directory.Build.props index b30fb7f..7211f47 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - netstandard2.0;net6.0;net7.0;net8.0 + netstandard2.0;net8.0;net9.0 Debug;Release latest enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 4800d04..f87e60e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,23 +1,26 @@ - - + + - - - - + + + - - - - + + + + + + + + - - + + \ No newline at end of file From 4f4cb31e005f4fc7910c1d6f672461a70852c211 Mon Sep 17 00:00:00 2001 From: David Makovsky Date: Fri, 21 Feb 2025 20:57:41 +0100 Subject: [PATCH 2/4] chore: optimize dictonary extensions for net8 and newer runtimes --- .../DictionaryExtensions.cs | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/CommonNet.Extensions/System.Collections.Generic/DictionaryExtensions.cs b/CommonNet.Extensions/System.Collections.Generic/DictionaryExtensions.cs index acfc326..3c91ba9 100644 --- a/CommonNet.Extensions/System.Collections.Generic/DictionaryExtensions.cs +++ b/CommonNet.Extensions/System.Collections.Generic/DictionaryExtensions.cs @@ -1,5 +1,6 @@ using CommunityToolkit.Diagnostics; using System.ComponentModel; +using System.Runtime.InteropServices; namespace System.Collections.Generic; @@ -25,14 +26,22 @@ Func valueFactory where TKey : notnull { Guard.IsNotNull(self); - Guard.IsNotNull(key); Guard.IsNotNull(valueFactory); +#if NET8_0_OR_GREATER + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(self, key, out var exists); + if (exists) + { + return value!; + } + value = valueFactory(key); +#else if (!self.TryGetValue(key, out var value)) { value = valueFactory(key); self.Add(key, value); } +#endif return value; } @@ -51,13 +60,21 @@ TValue newValue where TKey : notnull { Guard.IsNotNull(self); - Guard.IsNotNull(key); +#if NET8_0_OR_GREATER + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(self, key, out var exists); + if (exists) + { + return value!; + } + value = newValue; +#else if (!self.TryGetValue(key, out var value)) { value = newValue; self.Add(key, value); } +#endif return value; } @@ -100,21 +117,32 @@ Func updateValueFactory where TKey : notnull { Guard.IsNotNull(self); - Guard.IsNotNull(key); Guard.IsNotNull(updateValueFactory); - TValue newValue; +#if NET8_0_OR_GREATER + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(self, key, out var exists); + if (exists) + { + value = updateValueFactory(key, value!); + } + else + { + value = addValue; + } +#else + TValue value; if (self.TryGetValue(key, out var oldValue)) { - newValue = updateValueFactory(key, oldValue); - self[key] = newValue; + value = updateValueFactory(key, oldValue); + self[key] = value; } else { - newValue = addValue; - self.Add(key, newValue); + value = addValue; + self.Add(key, value); } - return newValue; +#endif + return value; } /// @@ -135,22 +163,33 @@ Func updateValueFactory where TKey : notnull { Guard.IsNotNull(self); - Guard.IsNotNull(key); Guard.IsNotNull(addValueFactory); Guard.IsNotNull(updateValueFactory); - TValue newValue; +#if NET8_0_OR_GREATER + ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(self, key, out var exists); + if (exists) + { + value = updateValueFactory(key, value!); + } + else + { + value = addValueFactory(key); + } +#else + TValue value; if (self.TryGetValue(key, out var oldValue)) { - newValue = updateValueFactory(key, oldValue); - self[key] = newValue; + value = updateValueFactory(key, oldValue); + self[key] = value; } else { - newValue = addValueFactory(key); - self.Add(key, newValue); + value = addValueFactory(key); + self.Add(key, value); } - return newValue; +#endif + return value; } /// From 9c60f32d9d6448dabaef2b7da5230d5201f935bb Mon Sep 17 00:00:00 2001 From: David Makovsky Date: Fri, 21 Feb 2025 21:00:34 +0100 Subject: [PATCH 3/4] feat: wait handle and event waitasync with cancellation token support for net8 and newer --- .../WaitHandleExtensionsTests.cs | 68 +++++++++++++++++++ .../WaitHandleExtensions.cs | 58 ++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/CommonNet.Extensions.Tests/CommonNet.Extensions/WaitHandleExtensionsTests.cs b/CommonNet.Extensions.Tests/CommonNet.Extensions/WaitHandleExtensionsTests.cs index 67e676c..2bed7b7 100644 --- a/CommonNet.Extensions.Tests/CommonNet.Extensions/WaitHandleExtensionsTests.cs +++ b/CommonNet.Extensions.Tests/CommonNet.Extensions/WaitHandleExtensionsTests.cs @@ -63,6 +63,74 @@ public async Task AsTask_WithTimeout_ShouldCancel_WhenTimeoutIsReached() task.IsCanceled.Should().BeTrue(); } + + +#if NET8_0_OR_GREATER + + [Fact] + public async Task WaitAsync_WaitHandle_Signaled_CompletesTask() + { + using var manualEvent = new ManualResetEvent(false); + WaitHandle waitHandle = manualEvent; + var cancellationToken = CancellationToken.None; + + var task = waitHandle.WaitAsync(cancellationToken); + manualEvent.Set(); + + await task.Awaiting(t => t).Should().NotThrowAsync(); + } + + [Fact] + public async Task WaitAsync_WaitHandle_Canceled_ThrowsTaskCanceledException() + { + using var manualEvent = new ManualResetEvent(false); + WaitHandle waitHandle = manualEvent; + var cts = new CancellationTokenSource(); + + var task = waitHandle.WaitAsync(cts.Token); + cts.Cancel(); + + await task.Awaiting(t => t).Should().ThrowAsync(); + } + + [Fact] + public async Task WaitAsync_ManualResetEventSlim_Signaled_CompletesTask() + { + + using var manualResetEvent = new ManualResetEventSlim(false); + var cancellationToken = CancellationToken.None; + + var task = manualResetEvent.WaitAsync(cancellationToken); + manualResetEvent.Set(); + + await task.Awaiting(t => t).Should().NotThrowAsync(); + } + + [Fact] + public async Task WaitAsync_ManualResetEventSlim_Canceled_ThrowsTaskCanceledException() + { + // Arrange + using var manualResetEvent = new ManualResetEventSlim(false); + var cts = new CancellationTokenSource(); + + var task = manualResetEvent.WaitAsync(cts.Token); + cts.Cancel(); + + await task.Awaiting(t => t).Should().ThrowAsync(); + } + + [Fact] + public void WaitAsync_WaitHandle_Null_ThrowsArgumentNullException() + { + WaitHandle waitHandle = null!; + var cancellationToken = CancellationToken.None; + + var func = () => waitHandle.WaitAsync(cancellationToken); + func.Should().ThrowAsync(); + } + +#endif + } #pragma warning restore xUnit1031 diff --git a/CommonNet.Extensions/System.Threading.Tasks/WaitHandleExtensions.cs b/CommonNet.Extensions/System.Threading.Tasks/WaitHandleExtensions.cs index 3b27425..b295df3 100644 --- a/CommonNet.Extensions/System.Threading.Tasks/WaitHandleExtensions.cs +++ b/CommonNet.Extensions/System.Threading.Tasks/WaitHandleExtensions.cs @@ -50,4 +50,62 @@ public static Task AsTask(this WaitHandle handle, TimeSpan timeout) tcs.Task.ContinueWith((_, state) => ((RegisteredWaitHandle)state!).Unregister(null), registration, TaskScheduler.Default); return tcs.Task; } + +#if NET8_0_OR_GREATER + + /// + /// Asynchronously waits for a to be signaled, with support for cancellation. + /// + /// The to wait for. + /// A to observe while waiting for the handle to be signaled. + /// A that completes when the is signaled or the is canceled. + /// Thrown if is null. + /// + /// This method registers a callback with the thread pool to wait for the to be signaled. + /// If the is canceled before the handle is signaled, the returned will be canceled. + /// + public static Task WaitAsync(this WaitHandle waitHandle, CancellationToken cancellationToken = default) + { + Guard.IsNotNull(waitHandle); + + CancellationTokenRegistration cancellationRegistration = default; + + var tcs = new TaskCompletionSource(); + var handle = ThreadPool.RegisterWaitForSingleObject( + waitObject: waitHandle, + callBack: (o, timeout) => + { + _ = cancellationRegistration.Unregister(); + _ = tcs.TrySetResult(); + }, + state: null, + timeout: Timeout.InfiniteTimeSpan, + executeOnlyOnce: true); + + if (cancellationToken.CanBeCanceled) + { + cancellationRegistration = cancellationToken.Register(() => + { + _ = handle.Unregister(waitHandle); + _ = tcs.TrySetCanceled(cancellationToken); + }); + } + + return tcs.Task; + } + + /// + /// Asynchronously waits for a to be set, with support for cancellation. + /// + /// The to wait for. + /// A to observe while waiting for the event to be set. + /// A that completes when the is set or the is canceled. + /// Thrown if is null. + /// + /// This method calls with the of the . + /// + public static Task WaitAsync(this ManualResetEventSlim manualResetEvent, CancellationToken cancellationToken = default) + => WaitAsync(manualResetEvent.WaitHandle, cancellationToken); + +#endif } From 09130a98211bd4fccba6453932f0de6815e108b2 Mon Sep 17 00:00:00 2001 From: David Makovsky Date: Fri, 21 Feb 2025 22:34:18 +0100 Subject: [PATCH 4/4] test: add additional test for improving quality gate score --- .../ServiceCollectionExtensionsTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CommonNet.Extensions.Tests/CommonNet.DependencyInjection/ServiceCollectionExtensionsTests.cs b/CommonNet.Extensions.Tests/CommonNet.DependencyInjection/ServiceCollectionExtensionsTests.cs index 1431454..9f69240 100644 --- a/CommonNet.Extensions.Tests/CommonNet.DependencyInjection/ServiceCollectionExtensionsTests.cs +++ b/CommonNet.Extensions.Tests/CommonNet.DependencyInjection/ServiceCollectionExtensionsTests.cs @@ -69,6 +69,47 @@ public void AddSingleton_ShouldSucceed() bif2.Should().NotBe(bif1); } + [Fact] + public void AddSingletonWithFactory_ShouldSucceed() + { + var services = new ServiceCollection(); + + services + .AddSingletonIf(true, sp => new A4_1()) + .AddSingletonIf(true, sp => new B4_2()); + + using var sp = services.BuildServiceProvider(); + + var iif4 = sp.GetRequiredService(); + iif4.Should().NotBeNull(); + var iif3 = sp.GetRequiredService(); + iif3.Should().NotBeNull(); + var iif2 = sp.GetRequiredService(); + iif2.Should().NotBeNull(); + var bif1 = sp.GetRequiredService(); + bif1.Should().NotBeNull(); + + iif4.Should().Be(iif3); + iif3.Should().Be(iif2); + iif2.Should().Be(bif1); + + + var bif2 = sp.GetRequiredService(); + bif2.Should().NotBeNull(); + var bif3 = sp.GetRequiredService(); + bif3.Should().NotBeNull(); + var bif4 = sp.GetRequiredService(); + bif4.Should().NotBeNull(); + var bif5 = sp.GetRequiredService(); + bif5.Should().NotBeNull(); + + bif2.Should().Be(bif3); + bif3.Should().Be(bif4); + bif4.Should().Be(bif5); + + bif2.Should().NotBe(bif1); + } + [Fact] public void AddSingletonFact_ShouldSucceed() {