diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b43ddf6e..8c2061c0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -8,7 +8,7 @@ updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
- interval: "daily"
+ interval: "weekly"
groups:
actions-deps:
patterns:
@@ -19,7 +19,7 @@ updates:
- package-ecosystem: "nuget"
directory: "/"
schedule:
- interval: "daily"
+ interval: "weekly"
target-branch: "develop"
open-pull-requests-limit: 1
groups:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f807c2e5..f9e60049 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -27,7 +27,7 @@ jobs:
- name: Create packages
run: dotnet pack --configuration Release --output ./packages
- name: Upload a Build Artifact
- uses: actions/upload-artifact@v5
+ uses: actions/upload-artifact@v6
with:
name: NuGet packages
path: packages/*.*
@@ -45,7 +45,7 @@ jobs:
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v5
- - uses: actions/download-artifact@v6
+ - uses: actions/download-artifact@v7
with:
name: NuGet packages
path: packages
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d6b92982..c73c4fab 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -26,8 +26,7 @@ jobs:
dotnet-version: |
6.0.x
8.0.x
- 9.0.x
10.0.x
- name: Build & Test in Release Mode
- run: dotnet test --configuration Release --logger "GitHubActions"
\ No newline at end of file
+ run: dotnet test --configuration Release --report-github
\ No newline at end of file
diff --git a/CommonUtilities.slnx b/CommonUtilities.slnx
index da8c736f..09d7c975 100644
--- a/CommonUtilities.slnx
+++ b/CommonUtilities.slnx
@@ -19,5 +19,5 @@
-
+
diff --git a/Directory.Build.props b/Directory.Build.props
index 9fa329ed..b80be57f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,16 +1,20 @@
$(MSBuildThisFileDirectory)
- preview
- disable
true
true
$(MSBuildThisFileDirectory)
$(RepoRootPath)bin\Packages\$(Configuration)\
- .NET Common Utilities
- Copyright © AnakinRaW 2025
+ preview
+ disable
+ enable
+ true
+
+
+ AnakinRaW Common Utilities
+ Copyright © AnakinRaW 2026
AnakinRaW
AnakinRaW
https://github.com/AnakinRaW/CommonUtilities
@@ -19,8 +23,7 @@
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
+
all
diff --git a/LICENSE b/LICENSE
index e37e0b34..cdb952d1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 AnakinRaW
+Copyright (c) 2026 AnakinRaW
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/global.json b/global.json
new file mode 100644
index 00000000..8f73781c
--- /dev/null
+++ b/global.json
@@ -0,0 +1,5 @@
+{
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj
index b3c74e58..059279dc 100644
--- a/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj
+++ b/src/CommonUtilities.DownloadManager/src/CommonUtilities.DownloadManager.csproj
@@ -1,16 +1,17 @@
-
+
- Simple Download Manager supporting the local file system and HTTP downlaods.
+ CommonUtilities.DownloadManager
+ AnakinRaW.CommonUtilities.DownloadManager
+ Simple DownloadAsync Manager supporting the local file system and HTTP downlaods.
- netstandard2.0;netstandard2.1;net9.0
+ true
+ netstandard2.0;netstandard2.1;net10.0
AnakinRaW.CommonUtilities.DownloadManager
AnakinRaW.CommonUtilities.DownloadManager
- enable
- True
- true
+ en
@@ -21,6 +22,11 @@
true
+
+
+
+
+
all
@@ -30,13 +36,14 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
+
diff --git a/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs b/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs
index bf5be327..c58d51ae 100644
--- a/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs
+++ b/src/CommonUtilities.DownloadManager/src/DownloadFailedException.cs
@@ -38,7 +38,7 @@ public override string Message
/// Initializes a new instance of the class from the specified download failures.
///
/// The failures which occurred during a file download.
- public DownloadFailedException(IEnumerable downloadFailures)
+ internal DownloadFailedException(IEnumerable downloadFailures)
{
DownloadFailures = downloadFailures;
}
diff --git a/src/CommonUtilities.DownloadManager/src/DownloadKind.cs b/src/CommonUtilities.DownloadManager/src/DownloadKind.cs
index e50550cd..932d1f2a 100644
--- a/src/CommonUtilities.DownloadManager/src/DownloadKind.cs
+++ b/src/CommonUtilities.DownloadManager/src/DownloadKind.cs
@@ -14,5 +14,5 @@ public enum DownloadKind
///
/// The provider supports downloading files from the Internet.
///
- Internet,
+ Internet
}
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs
index 55f7b156..8dd40c72 100644
--- a/src/CommonUtilities.DownloadManager/src/DownloadManager.cs
+++ b/src/CommonUtilities.DownloadManager/src/DownloadManager.cs
@@ -29,7 +29,7 @@ public sealed class DownloadManager : IDownloadManager
///
/// Initializes a new instance of the class.
///
- ///
+ /// The service provider.
public DownloadManager(IServiceProvider serviceProvider)
: this(DownloadManagerConfiguration.Default, serviceProvider)
{
@@ -172,7 +172,7 @@ private async Task DownloadWithRetry(
bool validationSuccess;
try
{
- validationSuccess = await validator.Validate(outputStream, summary.DownloadedSize, cancellationToken)
+ validationSuccess = await validator.ValidateAsync(outputStream, summary.DownloadedSize, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception e)
diff --git a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs
index 9af186b6..16d5a4cd 100644
--- a/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs
+++ b/src/CommonUtilities.DownloadManager/src/LeastRecentlyUsedDownloadProviders.cs
@@ -27,6 +27,7 @@ public string? LastSuccessfulProvider
if (value == null)
return;
field = value;
+ // ReSharper disable once InconsistentlySynchronizedField
_preferredProviders.AddOrUpdate(value, 1, (_, existingVal) => ++existingVal);
}
}
diff --git a/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs b/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs
index fc2609d9..ef78a70d 100644
--- a/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs
+++ b/src/CommonUtilities.DownloadManager/src/Providers/HttpClientDownloader.cs
@@ -43,8 +43,8 @@ protected override async Task DownloadAsyncCore(
{
if (response is not null)
{
-#if NET
- await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+#if NET || NETSTANDARD2_1_OR_GREATER
+ await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
#else
using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
#endif
diff --git a/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs b/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs
index 4031c19f..9bf6836e 100644
--- a/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs
+++ b/src/CommonUtilities.DownloadManager/src/Providers/WebClientDownloader.cs
@@ -68,7 +68,7 @@ protected override async Task DownloadAsyncCore(
try
{
summary.DownloadedSize = await StreamUtilities.CopyStreamWithProgressAsync(
- responseStream,
+ responseStream!,
totalStreamLength,
outputStream,
progress,
diff --git a/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs
index 3eac4b98..3bcdf862 100644
--- a/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs
+++ b/src/CommonUtilities.DownloadManager/src/Validation/AlwaysValidDownloadValidator.cs
@@ -19,7 +19,7 @@ private AlwaysValidDownloadValidator()
}
///
- public Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default)
+ public Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default)
{
return Task.FromResult(true);
}
diff --git a/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs
index 636952e5..f29d196f 100644
--- a/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs
+++ b/src/CommonUtilities.DownloadManager/src/Validation/HashDownloadValidator.cs
@@ -37,7 +37,7 @@ public HashDownloadValidator(byte[]? hash, HashTypeKey hashType, IServiceProvide
}
///
- public async Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default)
+ public async Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default)
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
diff --git a/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs
index 9d8f626f..24c231f5 100644
--- a/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs
+++ b/src/CommonUtilities.DownloadManager/src/Validation/IDownloadValidator.cs
@@ -19,5 +19,5 @@ public interface IDownloadValidator
/// The number of bytes downloaded.
/// The cancellation token.
/// if the download is valid; otherwise, .
- Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default);
+ Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default);
}
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs b/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs
index ace67457..4b22c4e4 100644
--- a/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs
+++ b/src/CommonUtilities.DownloadManager/src/Validation/SizeDownloadValidator.cs
@@ -21,7 +21,7 @@ public SizeDownloadValidator(long expectedDownloadBytes)
}
///
- public Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default)
+ public Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default)
{
return Task.FromResult(_expectedDownloadBytes == downloadedBytes);
}
diff --git a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj
index 4ead415b..f91c1181 100644
--- a/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj
+++ b/src/CommonUtilities.DownloadManager/test/CommonUtilities.DownloadManager.Test.csproj
@@ -1,10 +1,11 @@
-
+
net10.0;net8.0
$(TargetFrameworks);net481
false
true
+ Exe
@@ -12,20 +13,17 @@
AnakinRaW.CommonUtilities.DownloadManager.Test
-
- enable
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
+
-
-
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -37,8 +35,12 @@
-
+
+
+
+
+
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs
index 58cb97b9..e02a0225 100644
--- a/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/DownloadManagerTest.cs
@@ -15,7 +15,7 @@
namespace AnakinRaW.CommonUtilities.DownloadManager.Test;
-public class DownloadManagerTest : CommonTestBase
+public class DownloadManagerTest : TestBaseWithFileSystem
{
private const string Destination = "file.txt";
@@ -464,7 +464,7 @@ void ProgressMethod(DownloadUpdate status)
private class ThrowingValidator : IDownloadValidator
{
- public Task Validate(Stream stream, long downloadedBytes, CancellationToken token = default)
+ public Task ValidateAsync(Stream stream, long downloadedBytes, CancellationToken token = default)
{
throw new Exception("Test");
}
diff --git a/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs b/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs
index d0f7a09c..f8ed92d4 100644
--- a/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/LeastRecentlyUsedDownloadProvidersTest.cs
@@ -6,7 +6,7 @@
namespace AnakinRaW.CommonUtilities.DownloadManager.Test;
-public class LeastRecentlyUsedDownloadProvidersTest : CommonTestBase
+public class LeastRecentlyUsedDownloadProvidersTest : TestBaseWithFileSystem
{
private readonly LeastRecentlyUsedDownloadProviders _provider = new();
diff --git a/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs b/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs
index 1458a7db..a155f998 100644
--- a/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs
+++ b/src/CommonUtilities.DownloadManager/test/Providers/DownloadProviderTestBase.cs
@@ -8,7 +8,7 @@
namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Providers;
-public abstract class DownloadProviderTestBase : CommonTestBase
+public abstract class DownloadProviderTestBase : TestBaseWithFileSystem
{
protected abstract Type ExpectedSourceNotFoundExceptionType { get; }
@@ -20,7 +20,7 @@ public abstract class DownloadProviderTestBase : CommonTestBase
public async Task DownloadAsync_SourceNotFound_Throws()
{
var source = CreateSource(false);
- await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await Download(source, new MemoryStream(), null));
+ await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await TestDownloadAsync(source, new MemoryStream(), null, TestContext.Current.CancellationToken));
}
[Theory]
@@ -32,7 +32,7 @@ public async Task DownloadAsync(bool createDefaultOptions)
var source = CreateSource(true);
var options = createDefaultOptions ? new DownloadOptions() : null;
- var result = await Download(source, outStream, options);
+ var result = await TestDownloadAsync(source, outStream, options, TestContext.Current.CancellationToken);
Assert.True(result.DownloadedSize > 0);
Assert.Equal(result.DownloadedSize, outStream.Length);
}
@@ -45,10 +45,10 @@ public async Task DownloadAsync_DownloadCancelled_Throws(bool createDefaultOptio
var outStream = new MemoryStream();
var source = CreateSource(true);
var options = createDefaultOptions ? new DownloadOptions() : null;
- await Assert.ThrowsAnyAsync(async () => await Download(source, outStream, options, new CancellationToken(true)));
+ await Assert.ThrowsAnyAsync(async () => await TestDownloadAsync(source, outStream, options, new CancellationToken(true)));
}
- protected async Task Download(Uri source, Stream outStream, DownloadOptions? options, CancellationToken token = default)
+ protected async Task TestDownloadAsync(Uri source, Stream outStream, DownloadOptions? options, CancellationToken token = default)
{
var provider = CreateProvider();
@@ -65,7 +65,7 @@ protected async Task Download(Uri source, Stream outStream, Down
void Callback(DownloadUpdate status)
{
- Task.Delay(100).Wait();
+ Task.Delay(100, TestContext.Current.CancellationToken).Wait(TestContext.Current.CancellationToken);
callBackFired = true;
}
}
diff --git a/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs b/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs
index 16b71c67..6b1f4a7b 100644
--- a/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/Providers/FileDownloadTest.cs
@@ -4,7 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using AnakinRaW.CommonUtilities.DownloadManager.Providers;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Xunit;
namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Providers;
@@ -41,7 +41,7 @@ public async Task DownloadAsync_ExpectedData()
var source = CreateSource(true);
var outStream = new MemoryStream();
- var result = await Download(source, outStream, null, CancellationToken.None);
+ var result = await TestDownloadAsync(source, outStream, null, CancellationToken.None);
Assert.Equal(Data.Length, result.DownloadedSize);
var copyData = Encoding.Default.GetString(outStream.ToArray());
@@ -53,7 +53,7 @@ public async Task DownloadAsync_UncPath()
{
var source = new Uri("file://server/test.file");
Assert.True(source.IsUnc);
- await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await Download(source, new MemoryStream(), null));
+ await Assert.ThrowsAsync(ExpectedSourceNotFoundExceptionType, async () => await TestDownloadAsync(source, new MemoryStream(), null));
}
[Theory]
@@ -65,6 +65,6 @@ public async Task DownloadAsync_NotAFileSource_Throws(string uri)
{
var source = new Uri(uri);
var outStream = new MemoryStream();
- await Assert.ThrowsAsync(async () => await Download(source, outStream, null, CancellationToken.None));
+ await Assert.ThrowsAsync(async () => await TestDownloadAsync(source, outStream, null, CancellationToken.None));
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs b/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs
index 6dbf15ac..6cb5d536 100644
--- a/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/Providers/InternetDownloadTest.cs
@@ -28,7 +28,7 @@ public async Task DownloadAsync_WithUserAgent()
UserAgent = "AnakinRaw.DownloadManager.Test"
};
- var result = await Download(source, outStream, options);
+ var result = await TestDownloadAsync(source, outStream, options, TestContext.Current.CancellationToken);
Assert.True(result.DownloadedSize > 0);
Assert.Equal(result.DownloadedSize, outStream.Length);
}
@@ -39,7 +39,7 @@ public async Task DownloadAsync_RequiredUserAgentNotSet_Throws()
var outStream = new MemoryStream();
var source = new Uri("https://api.github.com/repos/AnakinRaW/CommonUtilities/releases/latest");
- var exception = await Assert.ThrowsAnyAsync(async () => await Download(source, outStream, null));
+ var exception = await Assert.ThrowsAnyAsync(async () => await TestDownloadAsync(source, outStream, null, TestContext.Current.CancellationToken));
AssertRequiredUserAgentMissingException(exception);
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs b/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs
index febcb410..0c5bd1a6 100644
--- a/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/StreamUtilitiesTest.cs
@@ -30,8 +30,9 @@ public async Task CopyStreamWithProgressAsync_StreamLengthAndCorrectCopy()
Assert.Equal(3, bytesRead);
output.Seek(0, SeekOrigin.Begin);
var outputData = new byte[3];
- output.Read(outputData, 0, 3);
+ var n = output.Read(outputData, 0, 3);
Assert.Equal(inputData, outputData);
+ Assert.Equal(3, n);
}
[Fact]
diff --git a/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs
index dd5b10ea..72bcc79a 100644
--- a/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/Validation/AlwaysValidDownloadValidatorTest.cs
@@ -11,7 +11,7 @@ public class AlwaysValidDownloadValidatorTest
public async Task Validate_IsValid_NullStream_CancelledToken_NegativeBytes()
{
var validator = AlwaysValidDownloadValidator.Instance;
- var result = await validator.Validate(null!, -1, new CancellationToken(true));
+ var result = await validator.ValidateAsync(null!, -1, new CancellationToken(true));
Assert.True(result);
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs
index 1b83269c..013ccc0b 100644
--- a/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/Validation/HashDownloadValidatorTest.cs
@@ -13,7 +13,7 @@
namespace AnakinRaW.CommonUtilities.DownloadManager.Test.Validation;
-public class HashDownloadValidatorTest : CommonTestBase
+public class HashDownloadValidatorTest : TestBaseWithFileSystem
{
protected override void SetupServices(IServiceCollection serviceCollection)
{
@@ -76,14 +76,14 @@ public void Ctor_InvalidateCorrect(HashTypeKey type, byte[] data)
public async Task Validate_NullStream_Throws()
{
var validator = new HashDownloadValidator(null, HashTypeKey.None, ServiceProvider);
- await Assert.ThrowsAsync(async () => await validator.Validate(null!, 0));
+ await Assert.ThrowsAsync(async () => await validator.ValidateAsync(null!, 0, TestContext.Current.CancellationToken));
}
[Fact]
public async Task Validate_NoneHashType()
{
var validator = new HashDownloadValidator(null, HashTypeKey.None, ServiceProvider);
- var result = await validator.Validate(new MemoryStream(new byte[3]), 0);
+ var result = await validator.ValidateAsync(new MemoryStream(new byte[3]), 0, TestContext.Current.CancellationToken);
Assert.True(result);
}
@@ -92,7 +92,7 @@ public async Task Validate_StreamNotSeekable_ThrowsNotSupportedException()
{
var validator = new HashDownloadValidator(null, HashTypeKey.None, ServiceProvider);
await Assert.ThrowsAsync(async () =>
- await validator.Validate(new NonSeekableStream(), 0));
+ await validator.ValidateAsync(new NonSeekableStream(), 0, TestContext.Current.CancellationToken));
}
[Theory]
@@ -110,7 +110,7 @@ public async Task Validate_HashesToNotMatch(HashTypeKey hashType, byte[] notExpe
// notExpectedHash is always empty
var validator = new HashDownloadValidator(notExpectedHash, hashType, ServiceProvider);
- var result = await validator.Validate(dlStream, actualDownloadedBytes);
+ var result = await validator.ValidateAsync(dlStream, actualDownloadedBytes, TestContext.Current.CancellationToken);
Assert.False(result);
}
@@ -138,7 +138,7 @@ public async Task Validate_HashesMatch(HashTypeKey hashType, string expectedHash
var expectedHash = ConvertHexStringToByteArray(expectedHashString);
var validator = new HashDownloadValidator(expectedHash, hashType, ServiceProvider);
- var result = await validator.Validate(dlStream, actualDownloadedBytes);
+ var result = await validator.ValidateAsync(dlStream, actualDownloadedBytes, TestContext.Current.CancellationToken);
Assert.True(result);
if (hashType != HashTypeKey.None)
@@ -163,37 +163,18 @@ public static byte[] ConvertHexStringToByteArray(string hexString)
}
- class NonSeekableStream : Stream
+ private class NonSeekableStream : Stream
{
- public override void Flush()
- {
- throw new NotImplementedException();
- }
-
- public override long Seek(long offset, SeekOrigin origin)
- {
- throw new NotImplementedException();
- }
-
- public override void SetLength(long value)
- {
- throw new NotImplementedException();
- }
-
- public override int Read(byte[] buffer, int offset, int count)
- {
- throw new NotImplementedException();
- }
-
- public override void Write(byte[] buffer, int offset, int count)
- {
- throw new NotImplementedException();
- }
-
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => 0;
public override long Position { get; set; }
+
+ public override void Flush() => throw new NotImplementedException();
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
+ public override void SetLength(long value) => throw new NotImplementedException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
+ public override int Read(byte[] buffer, int offset, int count) => throw new NotImplementedException();
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs b/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs
index af22cff5..d50f8ac4 100644
--- a/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs
+++ b/src/CommonUtilities.DownloadManager/test/Validation/SizeDownloadValidatorTest.cs
@@ -10,7 +10,7 @@ public class SizeDownloadValidatorTest
public async Task Validate_IsValid()
{
var validator = new SizeDownloadValidator(123);
- var result = await validator.Validate(null!, 123);
+ var result = await validator.ValidateAsync(null!, 123, TestContext.Current.CancellationToken);
Assert.True(result);
}
@@ -20,7 +20,7 @@ public async Task Validate_IsValid()
public async Task Validate_IsInvalid(int actualValue)
{
var validator = new SizeDownloadValidator(123);
- var result = await validator.Validate(null!, actualValue);
+ var result = await validator.ValidateAsync(null!, actualValue, TestContext.Current.CancellationToken);
Assert.False(result);
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj
index 06387b7c..ac6a9e5d 100644
--- a/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj
+++ b/src/CommonUtilities.FileSystem/src/Commonutilities.FileSystem.csproj
@@ -1,15 +1,17 @@
-
+
+ CommonUtilities.FileSystem
+ AnakinRaW.CommonUtilities.FileSystem
Helper classes and methods targeting the file system.
- netstandard2.0;netstandard2.1;net9.0
+ true
+ netstandard2.0;netstandard2.1;net10.0
AnakinRaW.CommonUtilities.FileSystem
AnakinRaW.CommonUtilities.FileSystem
- enable
- True
+ en
@@ -22,18 +24,25 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
diff --git a/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs b/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs
index 4d6a90ea..33eb37f8 100644
--- a/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs
+++ b/src/CommonUtilities.FileSystem/src/CompilerServices/CallerArgumentExpressionAttribute.cs
@@ -1,5 +1,7 @@
#if !NET
+#pragma warning disable IDE0130
namespace System.Runtime.CompilerServices;
+#pragma warning restore IDE0130
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute
diff --git a/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs
index 6fdcfb5f..18d69ba3 100644
--- a/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs
+++ b/src/CommonUtilities.FileSystem/src/Extensions/FileExtensions.cs
@@ -70,7 +70,7 @@ public static bool MoveToEx(this IFileInfo source, string destination, bool over
if (destination == null)
throw new ArgumentNullException(nameof(destination));
- return MoveEx(source.FileSystem.File, source.FullName, destination, overwrite);
+ return source.FileSystem.File.MoveEx(source.FullName, destination, overwrite);
}
///
diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs
index cefac2de..a056de79 100644
--- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs
+++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.Join.cs
@@ -110,11 +110,11 @@ public static string Join(this IPath _, ReadOnlySpan path1, ReadOnlySpan path1, ReadOnlySpan path2, ReadOnlySpan path3)
{
if (path1.Length == 0)
- return Join(_, path2, path3);
+ return _.Join(path2, path3);
if (path2.Length == 0)
- return Join(_, path1, path3);
+ return _.Join(path1, path3);
if (path3.Length == 0)
- return Join(_, path1, path2);
+ return _.Join(path1, path2);
return JoinInternal(path1, path2, path3);
}
@@ -187,13 +187,13 @@ public static string Join(this IPath _, ReadOnlySpan path1, ReadOnlySpan path1, ReadOnlySpan path2, ReadOnlySpan path3, ReadOnlySpan path4)
{
if (path1.Length == 0)
- return Join(_, path2, path3, path4);
+ return _.Join(path2, path3, path4);
if (path2.Length == 0)
- return Join(_, path1, path3, path4);
+ return _.Join(path1, path3, path4);
if (path3.Length == 0)
- return Join(_, path1, path2, path4);
+ return _.Join(path1, path2, path4);
if (path4.Length == 0)
- return Join(_, path1, path2, path3);
+ return _.Join(path1, path2, path3);
return JoinInternal(path1, path2, path3, path4);
}
@@ -266,7 +266,7 @@ public static string Join(this IPath _, ReadOnlySpan path1, ReadOnlySpan)paths);
+ return _.Join((ReadOnlySpan)paths);
}
///
@@ -342,11 +342,11 @@ public static bool TryJoin(
return true;
if (path1.Length == 0)
- return TryJoin(_, path2, path3, destination, out charsWritten);
+ return _.TryJoin(path2, path3, destination, out charsWritten);
if (path2.Length == 0)
- return TryJoin(_, path1, path3, destination, out charsWritten);
+ return _.TryJoin(path1, path3, destination, out charsWritten);
if (path3.Length == 0)
- return TryJoin(_, path1, path2, destination, out charsWritten);
+ return _.TryJoin(path1, path2, destination, out charsWritten);
var neededSeparators = HasTrailingDirectorySeparator(path1) || HasLeadingDirectorySeparator(path2) ? 0 : 1;
var needsSecondSeparator = !(HasTrailingDirectorySeparator(path2) || HasLeadingDirectorySeparator(path3));
@@ -357,7 +357,7 @@ public static bool TryJoin(
if (destination.Length < charsNeeded)
return false;
- var result = TryJoin(_, path1, path2, destination, out charsWritten);
+ var result = _.TryJoin(path1, path2, destination, out charsWritten);
Debug.Assert(result, "should never fail joining first two paths");
if (needsSecondSeparator)
diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs
index 28bac173..e671209d 100644
--- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs
+++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.NetFramework.cs
@@ -184,7 +184,7 @@ public static bool IsPathFullyQualified(this IPath _, string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path));
- return IsPathFullyQualified(_, path.AsSpan());
+ return _.IsPathFullyQualified(path.AsSpan());
}
///
diff --git a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs
index bc0aba10..439261bd 100644
--- a/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs
+++ b/src/CommonUtilities.FileSystem/src/Extensions/PathExtensions.cs
@@ -135,8 +135,6 @@ public static bool IsDriveRelative(this IPath fsPath, ReadOnlySpan path, [
driveLetter = path[0];
return true;
}
-
- return false;
}
return false;
}
diff --git a/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs b/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs
index f9d2f73b..2dc0ade0 100644
--- a/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs
+++ b/src/CommonUtilities.FileSystem/src/FileSystemUtilities.cs
@@ -19,7 +19,7 @@ public static class FileSystemUtilities
/// When set to , if all retries are unsuccessful the causing exception will be thrown.
/// Callback which gets invoked if an
/// or is was thrown during the execution.
- /// if the operation was successful. otherwise.
+ /// if the operation was successful; otherwise, .
/// is .
public static bool ExecuteFileSystemActionWithRetry(int retryCount, int retryDelay, Action fileAction,
bool throwOnFailure = true, Func? errorAction = null)
diff --git a/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs b/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs
index 8c1f3e30..bb99d03d 100644
--- a/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs
+++ b/src/CommonUtilities.FileSystem/src/Normalization/PathNormalizer.cs
@@ -6,7 +6,7 @@
namespace AnakinRaW.CommonUtilities.FileSystem.Normalization;
///
-/// Enables customized path normalization.
+/// Provides methods for normalizing file system paths according to specified rules.
///
public static class PathNormalizer
{
diff --git a/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs
index 88952b52..ffc9445b 100644
--- a/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs
+++ b/src/CommonUtilities.FileSystem/src/Utilities/ValueStringBuilder.cs
@@ -1,12 +1,14 @@
using System;
using System.Buffers;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AnakinRaW.CommonUtilities.FileSystem.Utilities;
// Copied from https://github.com/dotnet/runtime
+[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal ref struct ValueStringBuilder
{
private char[]? _arrayToReturnToPool;
@@ -69,8 +71,8 @@ public ref char GetPinnableReference(bool terminate)
{
if (terminate)
{
- EnsureCapacity(Length + 1);
- _chars[Length] = '\0';
+ EnsureCapacity(_pos + 1);
+ _chars[_pos] = '\0';
}
return ref MemoryMarshal.GetReference(_chars);
}
@@ -84,6 +86,11 @@ public ref char this[int index]
}
}
+ // ToString() clears the builder, so we need a side-effect free debugger display.
+ [DebuggerBrowsable(DebuggerBrowsableState.Never)]
+ [ExcludeFromCodeCoverage]
+ private string DebuggerDisplay => AsSpan().ToString();
+
public override string ToString()
{
var s = _chars.Slice(0, _pos).ToString();
@@ -102,8 +109,8 @@ public ReadOnlySpan AsSpan(bool terminate)
{
if (terminate)
{
- EnsureCapacity(Length + 1);
- _chars[Length] = '\0';
+ EnsureCapacity(_pos + 1);
+ _chars[_pos] = '\0';
}
return _chars.Slice(0, _pos);
}
diff --git a/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs b/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs
index 3a675746..d07cb6ce 100644
--- a/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs
+++ b/src/CommonUtilities.FileSystem/src/Validation/CurrentSystemFileNameValidator.cs
@@ -12,6 +12,10 @@ public sealed class CurrentSystemFileNameValidator : FileNameValidator
/// Returns a singleton instance of the class.
///
public static readonly CurrentSystemFileNameValidator Instance = new();
+
+ private readonly FileNameValidator _innerValidator = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ ? WindowsFileNameValidator.Instance
+ : LinuxFileNameValidator.Instance;
private CurrentSystemFileNameValidator()
{
@@ -20,8 +24,6 @@ private CurrentSystemFileNameValidator()
///
public override FileNameValidationResult IsValidFileName(ReadOnlySpan fileName)
{
- return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- ? WindowsFileNameValidator.Instance.IsValidFileName(fileName)
- : LinuxFileNameValidator.Instance.IsValidFileName(fileName);
+ return _innerValidator.IsValidFileName(fileName);
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs b/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs
index 6ba24aa5..81b3883c 100644
--- a/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs
+++ b/src/CommonUtilities.FileSystem/src/Validation/FileNameValidator.cs
@@ -21,5 +21,6 @@ public FileNameValidationResult IsValidFileName(string? fileName)
/// Checks whether a string represent a valid file name
///
/// The string to validate.
+ /// The result of the validation.
public abstract FileNameValidationResult IsValidFileName(ReadOnlySpan fileName);
}
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs b/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs
index 480f1777..cf1a1277 100644
--- a/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs
+++ b/src/CommonUtilities.FileSystem/src/Validation/WindowsFileNameValidator.cs
@@ -36,7 +36,7 @@ private WindowsFileNameValidator()
///
/// The string to validate.
/// Determines whether the check shall include Windows reserved file names (e.g, AUX, LPT1, etc.).
- ///
+ /// if the file name is valid; otherwise, .
public FileNameValidationResult IsValidFileName(ReadOnlySpan fileName, bool checkWindowsReservedNames)
{
if (fileName.Length == 0)
diff --git a/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs b/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs
index 15a3447a..3c5e3ce1 100644
--- a/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs
+++ b/src/CommonUtilities.FileSystem/src/Windows/WindowsFileSystemExtensions.cs
@@ -135,7 +135,7 @@ public static bool DeleteWithRetry(this IFileInfo file, out bool rebootRequired,
rebootRequired = false;
return success;
}
- rebootRequired = DeleteAfterReboot(file);
+ rebootRequired = file.DeleteAfterReboot();
return false;
}
@@ -173,7 +173,7 @@ public static bool DeleteWithRetry(this IDirectoryInfo directory, out bool reboo
return success;
}
- rebootRequired = DeleteAfterReboot(directory);
+ rebootRequired = directory.DeleteAfterReboot();
return false;
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs
index 2cd00d87..040317e9 100644
--- a/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs
+++ b/src/CommonUtilities.FileSystem/src/Windows/WindowsPathExtensions.cs
@@ -38,7 +38,7 @@ public static bool UserHasDirectoryAccessRights(this IDirectoryInfo directoryInf
{
if (!directoryInfo.Exists)
throw new DirectoryNotFoundException($"Unable to find {directoryInfo.FullName}");
- isInRoleWithAccess = TestAccessRightsOnWindows(directoryInfo, accessRights);
+ isInRoleWithAccess = directoryInfo.TestAccessRightsOnWindows(accessRights);
}
catch (UnauthorizedAccessException)
{
diff --git a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj
index f98eaedc..9ffe7cdf 100644
--- a/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj
+++ b/src/CommonUtilities.FileSystem/test/CommonUtilities.FileSystem.Test.csproj
@@ -1,11 +1,11 @@
-
+
net10.0;net8.0
$(TargetFrameworks);net481
false
true
- enable
+ Exe
@@ -15,21 +15,21 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -41,8 +41,12 @@
-
+
+
+
+
+
diff --git a/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs b/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs
index 4249085a..d47770e1 100644
--- a/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs
+++ b/src/CommonUtilities.FileSystem/test/DirectoryCopierTest.cs
@@ -30,15 +30,15 @@ public void MoveDirectory_CopyDirectory_ThrowsArgumentNullExceptions()
public async Task MoveDirectoryAsync_CopyDirectoryAsync_ThrowsArgumentNullExceptionsAsync()
{
var copier = new DirectoryCopier(_fileSystem);
- await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync(null!, "path"));
- await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", null!));
- await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, 0));
- await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, -1));
-
- await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync(null!, "path"));
- await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", null!));
- await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, 0));
- await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, -1));
+ await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync(null!, "path", cancellationToken: TestContext.Current.CancellationToken));
+ await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", null!, cancellationToken: TestContext.Current.CancellationToken));
+ await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, 0, cancellationToken: TestContext.Current.CancellationToken));
+ await Assert.ThrowsAsync(async () => await copier.MoveDirectoryAsync("source", "path", null, null, -1, cancellationToken: TestContext.Current.CancellationToken));
+
+ await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync(null!, "path", cancellationToken: TestContext.Current.CancellationToken));
+ await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", null!, cancellationToken: TestContext.Current.CancellationToken));
+ await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, 0, cancellationToken: TestContext.Current.CancellationToken));
+ await Assert.ThrowsAsync(async () => await copier.CopyDirectoryAsync("source", "path", null, null, -1, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
@@ -101,7 +101,7 @@ public async Task CopyDirectoryAsync()
// https://github.com/Testably/Testably.Abstractions/issues/549
var fsStream = _fileSystem.FileStream.New("test/1.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- await copier.CopyDirectoryAsync("test", "other", progress, Exclude2);
+ await copier.CopyDirectoryAsync("test", "other", progress, Exclude2, cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(1.0, progressValue);
Assert.True(_fileSystem.File.Exists("other/1.txt"));
@@ -207,7 +207,7 @@ public async Task MoveDirectoryAsync()
});
- var delSuc = await copier.MoveDirectoryAsync("test", "other", progress);
+ var delSuc = await copier.MoveDirectoryAsync("test", "other", progress, cancellationToken: TestContext.Current.CancellationToken);
Assert.Equal(1.0, progressValue);
Assert.True(delSuc);
Assert.False(_fileSystem.Directory.Exists("test"));
diff --git a/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs b/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs
index dc93941d..17338f70 100644
--- a/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs
+++ b/src/CommonUtilities.FileSystem/test/DirectoryInfoExtensionsTest.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
using System.Threading.Tasks;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions.Testing;
using Xunit;
@@ -199,7 +199,7 @@ public async Task MoveToAsync_ThrowsDirectoryNotFound()
_fileSystem.Initialize();
var dirToMove = _fileSystem.DirectoryInfo.New("test");
await Assert.ThrowsAsync(async () =>
- await dirToMove.MoveToAsync("test1", null, DirectoryOverwriteOption.NoOverwrite));
+ await dirToMove.MoveToAsync("test1", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
@@ -211,7 +211,7 @@ public async Task MoveToAsync_NoOverwrite_ThrowsIOException()
.WithSubdirectory("other");
var dirToMove = _fileSystem.DirectoryInfo.New("test");
- await Assert.ThrowsAsync(async () => await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.NoOverwrite));
+ await Assert.ThrowsAsync(async () => await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken));
}
@@ -228,7 +228,7 @@ public async Task MoveToAsync_CleanOverride()
var dirToMove = _fileSystem.DirectoryInfo.New("test");
- var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite);
+ var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(delSuc);
Assert.False(_fileSystem.Directory.Exists("test"));
Assert.True(_fileSystem.Directory.Exists("other"));
@@ -252,7 +252,7 @@ public async Task MoveToAsync_MergeOverride()
var dirToMove = _fileSystem.DirectoryInfo.New("test");
- var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.MergeOverwrite);
+ var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.MergeOverwrite, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(delSuc);
Assert.False(_fileSystem.Directory.Exists("test"));
Assert.True(_fileSystem.Directory.Exists("other"));
@@ -289,7 +289,7 @@ public async Task MoveToAsync_WithProgress()
{
progressValue = d;
});
- var delSuc = await dirToMove.MoveToAsync("other", progress, DirectoryOverwriteOption.CleanOverwrite);
+ var delSuc = await dirToMove.MoveToAsync("other", progress, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(delSuc);
Assert.Equal(1.0, progressValue);
@@ -307,7 +307,7 @@ public async Task MoveToAsync_CannotDeleteSource()
var fs = _fileSystem.FileStream.New("test/1.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
- var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite);
+ var delSuc = await dirToMove.MoveToAsync("other", null, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(delSuc);
fs.Dispose();
@@ -395,7 +395,7 @@ public void Copy_AcrossDrives()
public async Task CopyAsync_ThrowsDirectoryNotFound()
{
var dirToCopy = _fileSystem.DirectoryInfo.New("test");
- await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("test1", null, DirectoryOverwriteOption.NoOverwrite));
+ await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("test1", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
@@ -408,7 +408,7 @@ public async Task CopyAsync_NoOverwrite_ThrowsIOException()
var dirToCopy = _fileSystem.DirectoryInfo.New("test");
- await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.NoOverwrite));
+ await Assert.ThrowsAsync(async () => await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.NoOverwrite, cancellationToken: TestContext.Current.CancellationToken));
}
[Fact]
@@ -423,7 +423,7 @@ public async Task CopyAsync_CleanOverwrite()
var dirToCopy = _fileSystem.DirectoryInfo.New("test");
- await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.CleanOverwrite);
+ await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.CleanOverwrite, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(_fileSystem.Directory.Exists("test"));
Assert.True(_fileSystem.Directory.Exists("other"));
Assert.Equal(3, _fileSystem.DirectoryInfo.New("other").GetFiles("*", SearchOption.AllDirectories).Length);
@@ -445,7 +445,7 @@ public async Task CopyAsync_MergeOverwrite()
var dirToCopy = _fileSystem.DirectoryInfo.New("test");
- await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.MergeOverwrite);
+ await dirToCopy.CopyAsync("other", null, DirectoryOverwriteOption.MergeOverwrite, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(_fileSystem.Directory.Exists("test"));
Assert.True(_fileSystem.Directory.Exists("other"));
Assert.Equal(5, _fileSystem.DirectoryInfo.New("other").GetFiles("*", SearchOption.AllDirectories).Length);
diff --git a/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs b/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs
index 2c4682b6..c3a8fb91 100644
--- a/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs
+++ b/src/CommonUtilities.FileSystem/test/FileInfoExtensionsTest.cs
@@ -1,6 +1,6 @@
using System.IO;
using System.Runtime.InteropServices;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions.Testing;
using Xunit;
diff --git a/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs b/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs
index 5c0f8c53..d1ccfdd0 100644
--- a/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs
+++ b/src/CommonUtilities.FileSystem/test/FileSystemInfoExtensionTest.cs
@@ -1,4 +1,4 @@
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions.Testing;
using Xunit;
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs
index 2b040fa5..1b454047 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.AreEqual.cs
@@ -1,6 +1,6 @@
-using AnakinRaW.CommonUtilities.Testing;
-using System;
+using System;
using System.IO.Abstractions;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs
index 0b3f3218..92f3ef97 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetFileName.cs
@@ -2,7 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Abstractions;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
// ReSharper disable InconsistentNaming
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs
index 464f5d83..5b13789c 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetPathRoot.cs
@@ -1,7 +1,7 @@
using System;
using System.IO;
using System.IO.Abstractions;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
// ReSharper disable InconsistentNaming
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs
index 3beb813c..a49b3e1a 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.GetRelativePathEx.cs
@@ -1,6 +1,6 @@
using System;
using System.IO.Abstractions;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Testably.Abstractions.Testing;
using Xunit;
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs
index e33c6220..61d863ac 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasLeadingPathSeparator.cs
@@ -1,6 +1,6 @@
using System;
using System.IO.Abstractions;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
// ReSharper disable InconsistentNaming
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs
index 3422769f..831ab599 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.HasTrailingPathSeparator.cs
@@ -1,6 +1,6 @@
using System;
using System.IO.Abstractions;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
#if NET
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs
index 360083da..5e34019f 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsChildOf.cs
@@ -1,6 +1,6 @@
-using AnakinRaW.CommonUtilities.Testing;
-using System;
+using System;
using System.IO.Abstractions;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions.Testing;
using Xunit;
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs
index c5951ee7..68d1a8b5 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsDriveRelativePath.cs
@@ -1,6 +1,6 @@
-using AnakinRaW.CommonUtilities.Testing;
-using System;
+using System;
using System.IO.Abstractions;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs
index 71886a34..d7426f2c 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.IsPathFullyQualified.cs
@@ -1,6 +1,6 @@
-using AnakinRaW.CommonUtilities.Testing;
-using System;
+using System;
using System.IO.Abstractions;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
@@ -13,6 +13,7 @@ public class IsPathFullyQualifiedTest
[Fact]
public void IsPathFullyQualified_NullArgument()
{
+ // ReSharper disable once RedundantCast
Assert.Throws(() => _fileSystem.Path.IsPathFullyQualified(((string?)null)!));
}
diff --git a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs
index 9cafe96b..2fc37d2c 100644
--- a/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs
+++ b/src/CommonUtilities.FileSystem/test/PathExtensionsTest.Join.cs
@@ -190,6 +190,8 @@ public void TryJoinThreePaths(string? path1, string? path2, string? path3, strin
{ "a", null, null, "b", $"a{Sep}b" }
};
+ // ReSharper disable RedundantExplicitParamsArrayCreation
+
[Theory, MemberData(nameof(TestData_JoinFourPaths))]
public void JoinFourPaths(string? path1, string? path2, string? path3, string? path4, string expected)
{
@@ -247,4 +249,6 @@ public void JoinStringArray_8(string? path1, string? path2, string? path3, strin
Assert.Equal(_fileSystem.Path.Join(fourJoined, fourJoined),
_fileSystem.Path.Join((ReadOnlySpan)new[] { path1, path2, path3, path4, path1, path2, path3, path4 }));
}
+
+ // ReSharper restore RedundantExplicitParamsArrayCreation
}
\ No newline at end of file
diff --git a/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs b/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs
index dbef7e45..8e5db2b4 100644
--- a/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs
+++ b/src/CommonUtilities.FileSystem/test/PathNormalizerTest.Normalize.cs
@@ -13,6 +13,7 @@ public void Normalize_Throws()
{
Assert.Throws(() =>
{
+ // ReSharper disable once RedundantCast
PathNormalizer.Normalize((string)null!, new PathNormalizeOptions());
});
Assert.Throws(() =>
diff --git a/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs b/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs
index 0326e748..5c97c38d 100644
--- a/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs
+++ b/src/CommonUtilities.FileSystem/test/Windows/WindowsPathServiceTest.cs
@@ -1,7 +1,7 @@
using System.IO;
using System.Security.AccessControl;
using AnakinRaW.CommonUtilities.FileSystem.Windows;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
using Testably.Abstractions;
using Xunit;
#if NET
diff --git a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj
index bcd076db..e9e6ffb2 100644
--- a/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj
+++ b/src/CommonUtilities.Registry/src/CommonUtilities.Registry.csproj
@@ -1,15 +1,17 @@
-
+
+ CommonUtilities.Registry
+ AnakinRaW.CommonUtilities.Registry
Platform independent abstraction layer for the Windows Registry.
- netstandard2.0
+ true
+ netstandard2.0;net10.0
AnakinRaW.CommonUtilities.Registry
AnakinRaW.CommonUtilities.Registry
- enable
- True
+ en
@@ -20,13 +22,20 @@
true
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
+
diff --git a/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs b/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs
index cd90ee8c..fc2ec187 100644
--- a/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs
+++ b/src/CommonUtilities.Registry/src/Extensions/RegistryKeyExtensions.cs
@@ -62,8 +62,8 @@ private static async Task WaitForRegistryKeyChangeAsync(
try
{
- InMemoryRegistryKeyData.RegistryChanged += OnRegistryChanged;
- inMemoryRegistryKey.Disposing += OnKeyDisposing;
+ InMemoryRegistryKeyData.RegistryChanged += OnRegistryChanged!;
+ inMemoryRegistryKey.Disposing += OnKeyDisposing!;
// Handle potential race when registering the disposed event
if (inMemoryRegistryKey.IsDisposed)
@@ -73,8 +73,8 @@ private static async Task WaitForRegistryKeyChangeAsync(
}
finally
{
- InMemoryRegistryKeyData.RegistryChanged -= OnRegistryChanged;
- inMemoryRegistryKey.Disposing -= OnKeyDisposing;
+ InMemoryRegistryKeyData.RegistryChanged -= OnRegistryChanged!;
+ inMemoryRegistryKey.Disposing -= OnKeyDisposing!;
}
return;
diff --git a/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs b/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs
index 195b3966..dc33386d 100644
--- a/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs
+++ b/src/CommonUtilities.Registry/src/InMemoryRegistryChangeKind.cs
@@ -4,5 +4,5 @@ internal enum InMemoryRegistryChangeKind
{
TreeCreate,
TreeDelete,
- Value,
+ Value
}
\ No newline at end of file
diff --git a/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs b/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs
index f0bab56a..4b3d0f60 100644
--- a/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs
+++ b/src/CommonUtilities.Registry/src/InMemoryRegistryChangedEventArgs.cs
@@ -2,7 +2,8 @@
namespace AnakinRaW.CommonUtilities.Registry;
-internal class InMemoryRegistryChangedEventArgs(InMemoryRegistryKeyData key, InMemoryRegistryChangeKind kind) : EventArgs
+internal sealed class InMemoryRegistryChangedEventArgs(InMemoryRegistryKeyData key, InMemoryRegistryChangeKind kind)
+ : EventArgs
{
public InMemoryRegistryKeyData KeyData { get; } = key;
public InMemoryRegistryChangeKind Kind { get; } = kind;
diff --git a/src/CommonUtilities.Registry/src/RegistryKeyBase.cs b/src/CommonUtilities.Registry/src/RegistryKeyBase.cs
index a0eefaf4..c11dd1df 100644
--- a/src/CommonUtilities.Registry/src/RegistryKeyBase.cs
+++ b/src/CommonUtilities.Registry/src/RegistryKeyBase.cs
@@ -1,6 +1,8 @@
using System;
using System.Globalization;
+#if NETSTANDARD2_0
using System.Linq;
+#endif
namespace AnakinRaW.CommonUtilities.Registry;
@@ -67,7 +69,7 @@ public abstract class RegistryKeyBase : IRegistryKey
var nonNullableType = Nullable.GetUnderlyingType(type) ?? type;
if (nonNullableType.IsEnum)
- return (T)Enum.Parse(nonNullableType, result.ToString(), true);
+ return (T)Enum.Parse(nonNullableType, result.ToString()!, true);
return (T)Convert.ChangeType(result, nonNullableType, CultureInfo.InvariantCulture);
}
diff --git a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj
index cb770513..227e8107 100644
--- a/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj
+++ b/src/CommonUtilities.Registry/test/CommonUtilities.Registry.Test.csproj
@@ -5,7 +5,7 @@
$(TargetFrameworks);net481
false
true
- enable
+ Exe
@@ -26,12 +26,13 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -43,8 +44,12 @@
-
+
+
+
+
+
diff --git a/src/CommonUtilities.Registry/test/CompilerHelpers/Attributes.cs b/src/CommonUtilities.Registry/test/CompilerHelpers/Attributes.cs
new file mode 100644
index 00000000..e1dc9a46
--- /dev/null
+++ b/src/CommonUtilities.Registry/test/CompilerHelpers/Attributes.cs
@@ -0,0 +1,45 @@
+#if !NET5_0_OR_GREATER
+// ReSharper disable CheckNamespace
+// ReSharper disable InconsistentNaming
+namespace System.Runtime.Versioning;
+
+///
+/// Base type for all platform-specific API attributes.
+///
+
+internal abstract class OSPlatformAttribute(string platformName) : Attribute
+{
+ public string PlatformName { get; } = platformName;
+}
+
+///
+/// Records the platform that the project targeted.
+///
+[AttributeUsage(AttributeTargets.Assembly)]
+internal sealed class TargetPlatformAttribute(string platformName) : OSPlatformAttribute(platformName);
+
+///
+/// Records the operating system (and minimum version) that supports an API. Multiple attributes can be
+/// applied to indicate support on multiple operating systems.
+///
+///
+/// Callers can apply a
+/// or use guards to prevent calls to APIs on unsupported operating systems.
+///
+/// A given platform should only be specified once.
+///
+[AttributeUsage(AttributeTargets.Assembly |
+ AttributeTargets.Class |
+ AttributeTargets.Constructor |
+ AttributeTargets.Enum |
+ AttributeTargets.Event |
+ AttributeTargets.Field |
+ AttributeTargets.Interface |
+ AttributeTargets.Method |
+ AttributeTargets.Module |
+ AttributeTargets.Property |
+ AttributeTargets.Struct,
+ AllowMultiple = true, Inherited = false)]
+internal sealed class SupportedOSPlatformAttribute(string platformName) : OSPlatformAttribute(platformName);
+// ReSharper restore InconsistentNaming
+#endif
\ No newline at end of file
diff --git a/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs b/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs
index fa8b58da..905ed32a 100644
--- a/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs
+++ b/src/CommonUtilities.Registry/test/Extensions/RegistryKeyExtensionsTestBase.cs
@@ -20,7 +20,7 @@ public abstract class RegistryKeyExtensionsTestBase
public async Task AwaitRegKeyChange()
{
using var test = CreateTestKey();
- var changeWatcherTask = test.Key.WaitForChangeAsync();
+ var changeWatcherTask = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.False(changeWatcherTask.IsCompleted);
test.Key.SetValue("a", "b");
await changeWatcherTask;
@@ -32,13 +32,13 @@ public async Task AwaitRegKeyChange()
public async Task AwaitRegKeyChange_CreateOtherUnrelatedKey_DoesNotNotify()
{
using var test = CreateTestKey();
- var changeWatcherTask = test.Key.WaitForChangeAsync();
+ var changeWatcherTask = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.False(changeWatcherTask.IsCompleted);
using var other = CreateTestKey();
other.Key.CreateSubKey("otherSub");
- var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay));
+ var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken));
Assert.NotSame(changeWatcherTask, completedTask);
}
@@ -46,12 +46,12 @@ public async Task AwaitRegKeyChange_CreateOtherUnrelatedKey_DoesNotNotify()
public async Task AwaitRegKeyChange_SubkeyFilterDoesNotNotifyOnValueChanges()
{
using var test = CreateTestKey();
- var changeWatcherTask = test.Key.WaitForChangeAsync(change: RegistryChangeNotificationFilters.Subkey);
+ var changeWatcherTask = test.Key.WaitForChangeAsync(change: RegistryChangeNotificationFilters.Subkey, cancellationToken: TestContext.Current.CancellationToken);
Assert.False(changeWatcherTask.IsCompleted);
test.Key.SetValue("a", "b");
- var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay));
+ var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken));
Assert.NotSame(changeWatcherTask, completedTask);
}
@@ -63,8 +63,8 @@ public async Task AwaitRegKeyChange_TwoAtOnce_SameKeyHandle()
try
{
- var changeWatcherTask1 = test.Key.WaitForChangeAsync();
- var changeWatcherTask2 = test.Key.WaitForChangeAsync();
+ var changeWatcherTask1 = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken);
+ var changeWatcherTask2 = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken);
Assert.False(changeWatcherTask1.IsCompleted);
Assert.False(changeWatcherTask2.IsCompleted);
@@ -89,7 +89,7 @@ public async Task AwaitRegKeyChange_NoChange()
Assert.False(changeWatcherTask.IsCompleted);
// Give a bit of time to confirm the task will not complete.
- var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay));
+ var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken));
Assert.NotSame(changeWatcherTask, completedTask);
}
@@ -145,7 +145,7 @@ public async Task AwaitRegKeyChange_SelfDeleted_AlwaysNotifies(RegistryChangeNot
try
{
- var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false ,change: filter);
+ var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false ,change: filter, TestContext.Current.CancellationToken);
test.Key.DeleteKey(GetRegistryKeySubName(subKey.Name), false);
await changeWatcherTask;
}
@@ -212,7 +212,7 @@ public async Task AwaitRegKeyChange_SubKeyDeleted_ValueFilterDoesNotNotify(bool
var changeWatcherTask = test.Key.WaitForChangeAsync(watchSubtree: watchSubtree, RegistryChangeNotificationFilters.Value, cancellationToken: test.FinishedToken);
test.Key.DeleteKey(GetRegistryKeySubName(subKey.Name), false);
- var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay));
+ var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken));
Assert.NotSame(changeWatcherTask, completedTask);
}
finally
@@ -252,7 +252,7 @@ public async Task AwaitRegKeyChange_SubSubKeyDeleted_NoWatchSubtree()
var changeWatcherTask = test.Key.WaitForChangeAsync(watchSubtree: false, cancellationToken: test.FinishedToken);
test.Key.DeleteKey("sub\\subsub", false);
- var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay));
+ var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken));
Assert.NotSame(changeWatcherTask, completedTask);
}
finally
@@ -271,7 +271,8 @@ public async Task AwaitRegKeyChange_ParentKeyDeletedWhileAwaiting()
try
{
// Only watch for value changes, not tree changes, so we don't notify
- var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false, RegistryChangeNotificationFilters.Value);
+ var changeWatcherTask = subKey.WaitForChangeAsync(watchSubtree: false,
+ RegistryChangeNotificationFilters.Value, cancellationToken: TestContext.Current.CancellationToken);
// Delete the parent key
test.Key.DeleteKey(string.Empty, true);
@@ -300,7 +301,7 @@ public async Task AwaitRegKeyChange_NoWatchSubtree()
// We do not expect changes to sub-keys to complete the task, so give a bit of time to confirm
// the task doesn't complete.
- var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay));
+ var completedTask = await Task.WhenAny(changeWatcherTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken));
Assert.NotSame(changeWatcherTask, completedTask);
}
finally
@@ -335,7 +336,7 @@ public async Task AwaitRegKeyChange_KeyDisposedWhileWatching()
Task watchingTask;
using (var test = CreateTestKey())
{
- watchingTask = test.Key.WaitForChangeAsync();
+ watchingTask = test.Key.WaitForChangeAsync(cancellationToken: TestContext.Current.CancellationToken);
}
// We expect the task to quietly complete (without throwing any exception).
@@ -382,7 +383,7 @@ public async Task AwaitRegKeyChange_CallingThreadDestroyed()
thread.Join();
// Verify that the watching task is still watching.
- var completedTask = await Task.WhenAny(watchingTask, Task.Delay(AsyncDelay));
+ var completedTask = await Task.WhenAny(watchingTask, Task.Delay(AsyncDelay, TestContext.Current.CancellationToken));
Assert.NotSame(watchingTask, completedTask);
test.CreateSubKey().Dispose();
diff --git a/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs b/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs
index ad03bf7f..05c01967 100644
--- a/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs
+++ b/src/CommonUtilities.Registry/test/Extensions/WindowsKeyExtensionsTest.cs
@@ -1,10 +1,12 @@
#if Windows
+using System.Runtime.Versioning;
using AnakinRaW.CommonUtilities.Registry.Windows;
namespace AnakinRaW.CommonUtilities.Registry.Test.Extensions;
// ReSharper disable once UnusedMember.Global
+[SupportedOSPlatform("windows")]
public class WindowsKeyExtensionsTest : RegistryKeyExtensionsTestBase
{
protected override RegKeyTest CreateTestKey()
diff --git a/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs b/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs
index 8aba1041..c25eca30 100644
--- a/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs
+++ b/src/CommonUtilities.Registry/test/RegistryKey_DeleteKey_Recursive.cs
@@ -1,6 +1,6 @@
using System;
using System.Linq;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Extensions;
using Xunit;
namespace AnakinRaW.CommonUtilities.Registry.Test;
diff --git a/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs b/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs
index 2938a299..c8811aca 100644
--- a/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs
+++ b/src/CommonUtilities.Registry/test/RegistryKey_TypeLimit.cs
@@ -1,5 +1,5 @@
using System;
-using AnakinRaW.CommonUtilities.Testing;
+using AnakinRaW.CommonUtilities.Testing.Extensions;
using Xunit;
namespace AnakinRaW.CommonUtilities.Registry.Test;
@@ -17,13 +17,14 @@ public void SetValue_InvalidDataTypes_WindowsCompatibility()
// Should throw because only String[] (REG_MULTI_SZ) and byte[] (REG_BINARY) are supported.
// RegistryKey.SetValue does not support arrays of type UInt32[].
- AssertExtensions.Throws(null, () => TestRegistryKey.SetValue("IntArray", value: new[] { 1, 2, 3 }));
+ AssertExtensions.Throws(null, () => TestRegistryKey.SetValue("IntArray", value: (int[])[1, 2, 3
+ ]));
}
else
{
TestRegistryKey.SetValue("StringArr", value: new string[1]);
- Assert.Equal([null!], TestRegistryKey.GetValue("StringArr")!);
- TestRegistryKey.SetValue("IntArray", value: new[] { 1, 2, 3 });
+ Assert.Equal((string[])[null!], TestRegistryKey.GetValue("StringArr")!);
+ TestRegistryKey.SetValue("IntArray", value: (int[])[1, 2, 3]);
Assert.Equal([1, 2, 3], TestRegistryKey.GetValue("IntArray")!);
}
}
diff --git a/src/CommonUtilities.Registry/test/TestData.cs b/src/CommonUtilities.Registry/test/TestData.cs
index 953febf6..cdbcb48b 100644
--- a/src/CommonUtilities.Registry/test/TestData.cs
+++ b/src/CommonUtilities.Registry/test/TestData.cs
@@ -32,10 +32,10 @@ static TestData()
["Test_06", (uint)rand.Next(0, int.MaxValue)],
["Test_07", (long)rand.Next(int.MinValue, int.MaxValue)],
["Test_08", (ulong)rand.Next(0, int.MaxValue)],
- ["Test_09", new decimal(((double)decimal.MaxValue) * rand.NextDouble())],
- ["Test_10", new decimal(((double)decimal.MinValue) * rand.NextDouble())],
- ["Test_11", new decimal(((double)decimal.MinValue) * rand.NextDouble())],
- ["Test_12", new decimal(((double)decimal.MaxValue) * rand.NextDouble())],
+ ["Test_09", new decimal((double)decimal.MaxValue * rand.NextDouble())],
+ ["Test_10", new decimal((double)decimal.MinValue * rand.NextDouble())],
+ ["Test_11", new decimal((double)decimal.MinValue * rand.NextDouble())],
+ ["Test_12", new decimal((double)decimal.MaxValue * rand.NextDouble())],
["Test_13", int.MaxValue *rand.NextDouble()],
["Test_14", int.MinValue * rand.NextDouble()],
["Test_15", int.MaxValue * (float)rand.NextDouble()],
diff --git a/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs b/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs
index c78fc9a2..1fb98f5b 100644
--- a/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs
+++ b/src/CommonUtilities.Registry/test/WindowsRegistryTests.cs
@@ -3,8 +3,8 @@
using AnakinRaW.CommonUtilities.Registry.Windows;
using Microsoft.Win32;
using Xunit;
-using AnakinRaW.CommonUtilities.Testing;
using System;
+using AnakinRaW.CommonUtilities.Testing.Attributes;
#if NET
diff --git a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj
index 318ea7f4..cc8f91ca 100644
--- a/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj
+++ b/src/CommonUtilities.SimplePipeline/src/CommonUtilities.SimplePipeline.csproj
@@ -1,15 +1,17 @@
-
+
+ CommonUtilities.SimplePipeline
+ AnakinRaW.CommonUtilities.SimplePipeline
Implements pipeline runners to run steps synchronized or in parallel.
- netstandard2.0
- enable
- true
+ true
+ netstandard2.0;netstandard2.1;net10.0
AnakinRaW.CommonUtilities.SimplePipeline
AnakinRaW.CommonUtilities.SimplePipeline
+ en
@@ -20,13 +22,17 @@
true
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -41,4 +47,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Extensions.cs b/src/CommonUtilities.SimplePipeline/src/Extensions.cs
index fc6bdd49..5d5637f2 100644
--- a/src/CommonUtilities.SimplePipeline/src/Extensions.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Extensions.cs
@@ -7,15 +7,27 @@ internal static class Extensions
{
extension(Exception error)
{
- public bool IsExceptionType() where T : Exception
+ internal bool IsExceptionType() where T : Exception
{
return error switch
{
- T _ => true,
+ T => true,
AggregateException aggregateException => aggregateException.InnerExceptions.Any(p =>
p.IsExceptionType()),
_ => false
};
}
+
+ internal T? FindException() where T : Exception
+ {
+ return error switch
+ {
+ T t => t,
+ AggregateException aggregateException => aggregateException.InnerExceptions
+ .Select(p => p.FindException())
+ .FirstOrDefault(p => p is not null),
+ _ => null
+ };
+ }
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/IParallelStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/IParallelStepRunner.cs
deleted file mode 100644
index 09b7bad5..00000000
--- a/src/CommonUtilities.SimplePipeline/src/IParallelStepRunner.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline;
-
-///
-/// A specialized which allows for synchronous waiting.
-///
-public interface IParallelStepRunner : IStepRunner
-{
- ///
- /// Gets an aggregated exception of all failed steps or if no step failed.
- ///
- public AggregateException? Exception { get; }
-
- ///
- /// Gets the number of parallel workers the uses.
- ///
- public int WorkerCount { get; }
-
- ///
- /// Synchronously waits for this stepRunner for all of its steps to be finished.
- ///
- /// If any of the steps failed with an exception.
- void Wait();
-
- ///
- /// Synchronously waits for this stepRunner for all of its steps to be finished.
- ///
- /// The time duration to wait.
- /// If expired.
- /// If any of the steps failed with an exception.
- void Wait(TimeSpan waitDuration);
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/IStep.cs b/src/CommonUtilities.SimplePipeline/src/IStep.cs
index 112be06c..3e78c9bd 100644
--- a/src/CommonUtilities.SimplePipeline/src/IStep.cs
+++ b/src/CommonUtilities.SimplePipeline/src/IStep.cs
@@ -1,5 +1,7 @@
using System;
+using System.Runtime.CompilerServices;
using System.Threading;
+using System.Threading.Tasks;
namespace AnakinRaW.CommonUtilities.SimplePipeline;
@@ -9,13 +11,37 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline;
public interface IStep : IDisposable
{
///
- /// The exception, if any, that happened while running this step.
+ /// Gets a value indicating whether the step has been cancelled.
///
+ ///
+ /// if the step was cancelled; otherwise, .
+ ///
+ public bool IsCancelled { get; }
+
+ ///
+ /// Gets the exception that occurred during the execution of the step, if any.
+ ///
+ ///
+ /// An representing the error that occurred during the step's execution,
+ /// or if no error occurred.
+ ///
Exception? Error { get; }
///
/// Run the step's action.
///
/// Provided to allow step cancellation.
- void Run(CancellationToken token);
+ Task RunAsync(CancellationToken token);
+
+ ///
+ /// Gets an awaiter used to await this .
+ ///
+ TaskAwaiter GetAwaiter();
+
+ ///
+ /// Configures an awaiter used to await this .
+ ///
+ ///
+ /// An object used to await this task.
+ ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs
index 415fff55..1101b314 100644
--- a/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs
+++ b/src/CommonUtilities.SimplePipeline/src/IStepRunner.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -15,6 +16,34 @@ public interface IStepRunner
///
event EventHandler? Error;
+ ///
+ /// Gets an aggregated exception of all failed steps or if no step failed.
+ ///
+ public AggregateException? Exception { get; }
+
+ ///
+ /// Gets the number of parallel workers the uses.
+ ///
+ public int WorkerCount { get; }
+
+ ///
+ /// Gets a value indicating whether the is currently executing steps.
+ ///
+ ///
+ /// if the runner is actively executing steps; otherwise, .
+ ///
+ bool IsRunning { get; }
+
+ ///
+ /// Gets a value indicating whether the step runner has been cancelled.
+ ///
+ ///
+ /// This property returns if the step runner was cancelled during its execution,
+ /// typically due to a cancellation request via a .
+ /// Otherwise, it returns .
+ ///
+ bool IsCancelled { get; }
+
///
/// Gets a read-only list of only those steps were executed by the .
///
@@ -33,4 +62,51 @@ public interface IStepRunner
/// The step to app.
/// /// is .
void AddStep(IStep step);
+
+ ///
+ /// Synchronously waits for this stepRunner for all of its steps to be finished.
+ ///
+ /// If any of the steps failed with an exception.
+ void Wait();
+
+ ///
+ /// Synchronously waits for this stepRunner for all of its steps to be finished.
+ ///
+ /// The time duration to wait.
+ /// If expired.
+ /// If any of the steps failed with an exception.
+ ///
+ /// is a negative number other than -1 milliseconds, which represents an infinite time-out.
+ /// or is greater than Int32.MaxValue.
+ ///
+ void Wait(TimeSpan waitDuration);
+
+ ///
+ /// Gets an awaiter used to await this .
+ ///
+ ///
+ ///
+ /// This method enables the to be used with the await keyword.
+ ///
+ ///
+ /// If called before has been invoked, the awaiter will block until
+ /// the runner is started and has completed execution of all steps.
+ ///
+ ///
+ /// If called during execution, the awaiter will block until all steps have finished.
+ ///
+ ///
+ /// The awaiter does not throw exceptions for failed steps. Any errors that occurred during
+ /// execution are available through the property.
+ ///
+ ///
+ /// A instance that can be used to await the runner's completion.
+ TaskAwaiter GetAwaiter();
+
+ ///
+ /// Configures an awaiter used to await this .
+ ///
+ ///
+ /// An object used to await this task.
+ ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext);
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs
index 469a948c..db3341fa 100644
--- a/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/IPipeline.cs
@@ -5,26 +5,24 @@
namespace AnakinRaW.CommonUtilities.SimplePipeline;
///
-/// Represents an execution pipeline can run multiple instanes.
+/// Represents an execution pipeline that can be prepared and run.
///
public interface IPipeline : IDisposable
-{
+{
///
/// Prepares the pipeline for execution.
///
- ///
- /// Preparation can only be done once per instance.
- ///
- /// A task that completes when the preparation is completed.
- Task PrepareAsync();
-
+ /// Token to cancel the preparation.
+ /// Cancellation was requested.
+ /// The pipeline was disposed.
+ Task PrepareAsync(CancellationToken token = default);
+
///
- /// Runs pipeline synchronously.
+ /// Runs the pipeline asynchronously.
///
- /// Provided to allow cancellation.
- /// A task that represents the operation completion.
- /// The pipeline was cancelled was requested for cancellation.
- /// The pipeline may throw this exception if one or many steps failed.
+ /// Token to cancel the execution.
+ /// Cancellation was requested.
+ /// The pipeline was disposed.
Task RunAsync(CancellationToken token = default);
///
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelPipeline.cs
deleted file mode 100644
index 77d7d582..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelPipeline.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using AnakinRaW.CommonUtilities.SimplePipeline.Runners;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline;
-
-///
-/// A simple pipeline that runs all steps on the thread pool in parallel.
-///
-public abstract class ParallelPipeline : StepRunnerPipeline
-{
- private readonly int _workerCount;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The service provider for dependency injection within the pipeline.
- /// The number of worker threads to be used for parallel execution.
- /// A value indicating whether the pipeline should fail fast.
- /// is .
- protected ParallelPipeline(IServiceProvider serviceProvider, int workerCount = 4, bool failFast = true) : base(serviceProvider, failFast)
- {
- _workerCount = workerCount;
- }
-
- ///
- protected sealed override ParallelStepRunner CreateRunner()
- {
- return new ParallelStepRunner(_workerCount, ServiceProvider);
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs
deleted file mode 100644
index 40c3f69f..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Pipelines/ParallelProducerConsumerPipeline.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-using AnakinRaW.CommonUtilities.SimplePipeline.Runners;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline;
-
-///
-/// A simple pipeline that runs all steps on the thread pool in parallel. Allows to run the pipeline even if preparation is not completed.
-///
-///
-/// Useful, if preparation is work intensive.
-///
-public abstract class ParallelProducerConsumerPipeline : Pipeline
-{
- private readonly ParallelProducerConsumerStepRunner _stepRunner;
-
- private Exception? _preparationException;
-
- ///
- protected override bool FailFast { get; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The service provider for dependency injection within the pipeline.
- /// The number of worker threads to be used for parallel execution.
- /// A value indicating whether the pipeline should fail fast.
- protected ParallelProducerConsumerPipeline(int workerCount, bool failFast, IServiceProvider serviceProvider) : base(serviceProvider)
- {
- FailFast = failFast;
- _stepRunner = new ParallelProducerConsumerStepRunner(workerCount, serviceProvider);
- }
-
- ///
- public sealed override async Task RunAsync(CancellationToken token = default)
- {
- ThrowIfDisposed();
-
- LinkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
-
- if (!Prepared)
- {
- Task.Run(async () =>
- {
- try
- {
- await PrepareAsync().ConfigureAwait(false);
- }
- catch (Exception e)
- {
- PipelineFailed = true;
- _preparationException = e;
-
- if (FailFast)
- Cancel();
- }
- finally
- {
- _stepRunner.Finish();
- }
- }, LinkedCancellationTokenSource.Token).Forget();
- }
-
- try
- {
- await RunCoreAsync(LinkedCancellationTokenSource.Token).ConfigureAwait(false);
- LinkedCancellationTokenSource.Token.ThrowIfCancellationRequested();
- }
- catch (Exception)
- {
- PipelineFailed = true;
- throw;
- }
- finally
- {
- if (LinkedCancellationTokenSource is not null)
- {
- LinkedCancellationTokenSource.Dispose();
- LinkedCancellationTokenSource = null;
- }
- }
- }
-
- ///
- /// Builds the steps in the order they should be executed within the pipeline.
- ///
- /// A list of steps in the order they should be executed.
- protected abstract IAsyncEnumerable BuildSteps();
-
- ///
- protected override async Task PrepareCoreAsync()
- {
- await foreach (var step in BuildSteps().ConfigureAwait(false))
- _stepRunner.AddStep(step);
- _stepRunner.Finish();
- return true;
- }
-
- ///
- protected override async Task RunCoreAsync(CancellationToken token)
- {
- try
- {
- _stepRunner.Error += OnError;
- await _stepRunner.RunAsync(token).ConfigureAwait(false);
- }
- finally
- {
- _stepRunner.Error -= OnError;
- }
-
- if (!PipelineFailed)
- return;
-
- if (_preparationException is not null)
- throw _preparationException;
-
- ThrowIfAnyStepsFailed(_stepRunner.ExecutedSteps);
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs
index 02651235..a0842c80 100644
--- a/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/Pipeline.cs
@@ -1,158 +1,243 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace AnakinRaW.CommonUtilities.SimplePipeline;
///
-/// Base implementation for an
+/// Base implementation for an .
///
public abstract class Pipeline : DisposableObject, IPipeline
{
+ private Task? _preparationTask;
+ private Task? _runTask;
+
+#if NET10_0_OR_GREATER
+ private readonly Lock _reentryLock = new();
+#else
+ private readonly object _reentryLock = new();
+#endif
+
///
- /// The cancellation token source used by this pipeline to send cancellation request.
+ /// Gets the used to cancel the execution of the pipeline.
+ /// Returns if the execution is not started or already finished.
///
- protected CancellationTokenSource? LinkedCancellationTokenSource;
-
+ protected CancellationTokenSource? CancellationTokenSource;
+
///
- /// Returns the service provider of the .
+ /// Gets the for the pipeline.
///
protected readonly IServiceProvider ServiceProvider;
-
+
///
- /// Returns the logger of the .
+ /// Gets the for the pipeline or if no logger is registered.
///
protected readonly ILogger? Logger;
///
- /// Gets a value indicating whether the preparation of the was successful.
+ /// Gets a value indicating whether the pipeline has been successfully prepared.
///
- protected bool Prepared { get; set; }
+ ///
+ /// if the pipeline preparation task has completed successfully; otherwise, .
+ ///
+ protected internal bool IsPrepared =>
+#if NETSTANDARD2_0 || NETFRAMEWORK
+ _preparationTask is { Status: TaskStatus.RanToCompletion, IsCompleted: true };
+#else
+ _preparationTask?.IsCompletedSuccessfully is true;
+#endif
///
- /// Gets a value indicating whether the execution of the pipeline has encountered a failure.
+ /// Gets a value indicating whether the pipeline has encountered a failure during its execution.
///
- public bool PipelineFailed { get; protected set; }
-
+ ///
+ /// This property is set to if an exception occurs during the execution of the pipeline.
+ ///
+ public bool Failed { get; protected set; }
+
///
- /// Gets a value indicating the pipeline shall abort execution on the first received error.
+ /// Gets a value indicating whether the pipeline has been cancelled.
///
- protected virtual bool FailFast => false;
-
+ ///
+ /// This property is set to when the pipeline is explicitly cancelled
+ /// or when an is thrown during execution.
+ ///
+ public bool Cancelled { get; protected set; }
+
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class with the specified service provider.
///
+ /// The used to resolve dependencies for the pipeline.
/// is .
protected Pipeline(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
Logger = serviceProvider.GetService()?.CreateLogger(GetType());
}
-
- ///
- public async Task PrepareAsync()
+
+ ///
+ /// Returns a string representation of the current instance.
+ ///
+ ///
+ /// A that represents the name of the current pipeline type.
+ ///
+ [ExcludeFromCodeCoverage]
+ public override string ToString() => GetType().Name;
+
+ ///
+ /// Prepares the pipeline for execution.
+ ///
+ /// A to observe while waiting for the preparation to complete.
+ /// A that represents the asynchronous preparation operation.
+ /// The pipeline already is prepared or preparation has been started.
+ /// The pipeline has been disposed.
+ /// The operation is canceled.
+ public Task PrepareAsync(CancellationToken token = default)
{
ThrowIfDisposed();
- if (!Prepared)
+ token.ThrowIfCancellationRequested();
+ lock (_reentryLock)
{
+ if (_preparationTask is not null)
+ throw new InvalidOperationException("Pipeline preparation has already been started.");
+
try
{
- await PrepareCoreAsync().ConfigureAwait(false);
+ _preparationTask = PrepareCoreAsync(token);
}
- finally
+ catch (Exception ex)
{
- Prepared = true;
+ _preparationTask = Task.FromException(ex);
+ throw;
}
+ return _preparationTask;
}
}
- ///
- public virtual async Task RunAsync(CancellationToken token = default)
+ ///
+ /// Executes the pipeline asynchronously.
+ ///
+ /// A to observe while waiting for the task to complete.
+ /// A that represents the asynchronous operation.
+ /// Thrown when the pipeline has already been started or is executing.
+ /// Thrown when the pipeline has been disposed.
+ public Task RunAsync(CancellationToken token = default)
{
ThrowIfDisposed();
-
- await PrepareAsync().ConfigureAwait(false);
-
- try
+ lock (_reentryLock)
{
- try
- {
- LinkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
- await RunCoreAsync(LinkedCancellationTokenSource.Token).ConfigureAwait(false);
- LinkedCancellationTokenSource.Token.ThrowIfCancellationRequested();
- }
- finally
- {
- if (LinkedCancellationTokenSource is not null)
- {
- LinkedCancellationTokenSource.Dispose();
- LinkedCancellationTokenSource = null;
- }
- }
- }
- catch (Exception)
- {
- PipelineFailed = true;
- throw;
+ if (_runTask is not null)
+ throw new InvalidOperationException("Pipeline has already been started.");
+
+ _runTask = RunCoreAsync(token);
+ return _runTask;
}
}
- ///
+ ///
+ /// Cancels the execution of the pipeline.
+ ///
+ ///
+ /// This method ensures that the pipeline's execution is stopped by canceling the associated
+ /// . Once canceled, the property is set to true.
+ ///
+ /// Thrown if the pipeline or its associated resources have already been disposed.
public void Cancel()
{
- LinkedCancellationTokenSource?.Cancel();
- }
-
- ///
- [ExcludeFromCodeCoverage]
- public override string ToString()
- {
- return GetType().Name;
+ var cts = CancellationTokenSource;
+ if (cts != null)
+ {
+ cts.Cancel();
+ Cancelled = true;
+ }
}
///
/// Performs the actual preparation of this instance.
///
- /// if the planning was successful; otherwise.
- protected abstract Task PrepareCoreAsync();
+ protected abstract Task PrepareCoreAsync(CancellationToken token);
///
- /// Implements the run logic of this instance.
+ /// Implements the actual execution logic of this instance.
///
/// It's assured this instance is already prepared when this method gets called.
- /// Provided to allow cancellation.
- protected abstract Task RunCoreAsync(CancellationToken token);
+ protected abstract Task ExecuteAsync(CancellationToken token);
///
- /// Throws an if any of the passed steps ended with an error that is not the result of cancellation.
+ /// Orchestrates and executes the pipeline.
///
- /// The steps that were executed by the pipeline.
- /// If any of has an error that is not the result of cancellation.
- protected void ThrowIfAnyStepsFailed(IEnumerable steps)
+ ///
+ ///
+ /// Override this method to customize execution flow.
+ ///
+ ///
+ /// The default implementation calls if needed,
+ /// sets up cancellation, and delegates to .
+ ///
+ ///
+ protected virtual async Task RunCoreAsync(CancellationToken token)
{
- var failedBuildSteps = steps
- .Where(p => p.Error != null && !p.Error.IsExceptionType())
- .ToList();
+ CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
+
+ try
+ {
+ await WaitForPreparationAsync(CancellationTokenSource.Token).ConfigureAwait(false);
+ await ExecuteAsync(CancellationTokenSource.Token).ConfigureAwait(false);
+ CancellationTokenSource.Token.ThrowIfCancellationRequested();
+ }
+ catch (OperationCanceledException)
+ {
+ Cancelled = true;
+ throw;
+ }
+ catch
+ {
+ Failed = true;
+ throw;
+ }
+ finally
+ {
+ CancellationTokenSource?.Dispose();
+ CancellationTokenSource = null;
+ }
+ }
- if (failedBuildSteps.Any())
- throw new StepFailureException(failedBuildSteps);
+ ///
+ /// Releases resources used by the pipeline, including any tasks and cancellation tokens.
+ ///
+ protected override void DisposeResources()
+ {
+ lock (_reentryLock)
+ {
+ _preparationTask?.Dispose();
+ _runTask?.Dispose();
+ CancellationTokenSource?.Dispose();
+ CancellationTokenSource = null;
+
+ // Safe because we explicitly check the methods if disposed
+ _preparationTask = null;
+ _runTask = null;
+ }
+
+ base.DisposeResources();
}
///
- /// The default event handler that can be used when an error occurs within a step.
- /// is set to . When is , the pipeline gets cancelled.
+ /// Waits for preparation or starts it if not yet started.
///
- /// The sender of the event.
- /// The event arguments.
- protected virtual void OnError(object sender, StepRunnerErrorEventArgs e)
+ protected Task WaitForPreparationAsync(CancellationToken token)
{
- PipelineFailed = true;
- if (FailFast || e.Cancel)
- Cancel();
+ Task task;
+
+ lock (_reentryLock)
+ {
+ _preparationTask ??= PrepareCoreAsync(token);
+ task = _preparationTask;
+ }
+ return task.WaitAsync(token);
+
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/ProducerConsumerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/ProducerConsumerPipeline.cs
new file mode 100644
index 00000000..2f73a947
--- /dev/null
+++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/ProducerConsumerPipeline.cs
@@ -0,0 +1,148 @@
+using AnakinRaW.CommonUtilities.SimplePipeline.Runners;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AnakinRaW.CommonUtilities.SimplePipeline;
+
+///
+/// A pipeline that runs preparation and execution in parallel using a producer/consumer pattern.
+///
+///
+/// Steps are added to the runner while execution is already in progress.
+/// Useful when preparation is work-intensive.
+///
+public abstract class ProducerConsumerPipeline : StepRunnerPipelineBase
+{
+ private readonly int _workerCount;
+
+ ///
+ /// Initializes a new instance of the class with the specified worker count and service provider.
+ ///
+ /// The number of workers to be used in the pipeline. Must be between 1 and 64 inclusive.
+ /// The used to resolve dependencies for the pipeline.
+ /// is less than 1 or greater than 64.
+ /// is .
+ protected ProducerConsumerPipeline(int workerCount, IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ if (workerCount is < 1 or > 64)
+ throw new ArgumentOutOfRangeException(nameof(workerCount), "worker count must be between 1 and 64 inclusive");
+ _workerCount = workerCount;
+ }
+
+ ///
+ /// Asynchronously builds a collection of steps to be executed by the pipeline.
+ ///
+ /// A to observe while waiting for the steps to be built.
+ /// An asynchronous enumerable of instances representing the steps to be executed.
+ ///
+ /// This method is intended to be overridden in derived classes to provide the logic for preparing the steps
+ /// that will be executed by the pipeline. The steps are produced asynchronously, allowing for efficient
+ /// preparation of steps in scenarios where preparation is computationally intensive or involves I/O operations.
+ ///
+ protected abstract IAsyncEnumerable BuildStepsAsync(CancellationToken token);
+
+ ///
+ /// Creates an instance of to execute the steps in the pipeline using a producer/consumer pattern.
+ ///
+ ///
+ /// The is initialized with the specified number of workers and the service provider.
+ /// This method ensures that the runner is properly configured to handle the producer/consumer pattern for step execution.
+ ///
+ ///
+ /// A configured instance of .
+ ///
+ protected sealed override ProducerConsumerStepRunner CreateRunner()
+ {
+ return new ProducerConsumerStepRunner(_workerCount, ServiceProvider);
+ }
+
+ ///
+ /// Prepares the pipeline by initializing the step runner and adding steps to it asynchronously.
+ ///
+ /// A to observe while waiting for the preparation to complete.
+ /// A that represents the asynchronous preparation operation.
+ ///
+ /// This method initializes the step runner and asynchronously builds and adds steps to the runner.
+ /// Once all steps are added, the runner is marked as finished.
+ ///
+ protected sealed override async Task PrepareCoreAsync(CancellationToken token)
+ {
+ _ = StepRunner;
+ await foreach (var step in BuildStepsAsync(token).ConfigureAwait(false))
+ {
+ token.ThrowIfCancellationRequested();
+ if (!StepRunner.TryAddStep(step) && !Cancelled)
+ throw new InvalidOperationException("Unable to add write steps to underlying runner");
+ }
+ StepRunner.Finish();
+ }
+
+ ///
+ /// Executes the core logic of the pipeline asynchronously.
+ ///
+ /// A to observe while waiting for the task to complete.
+ /// A that represents the asynchronous operation.
+ ///
+ /// This method initializes the runner, links the provided cancellation token, and ensures the pipeline gets prepared.
+ /// It handles cancellation and failure scenarios, ensuring proper cleanup of resources.
+ ///
+ /// Thrown when the operation is canceled.
+ /// Thrown when an error occurs during execution.
+ protected sealed override async Task RunCoreAsync(CancellationToken token)
+ {
+ CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token);
+ var linkedToken = CancellationTokenSource.Token;
+
+ if (!IsPrepared)
+ {
+ Task.Run(() => RunPreparationAsync(linkedToken), CancellationToken.None).Forget();
+ }
+
+ try
+ {
+ await ExecuteAsync(linkedToken).ConfigureAwait(false);
+ await WaitForPreparationAsync(token).ConfigureAwait(false);
+ linkedToken.ThrowIfCancellationRequested();
+ }
+ catch (OperationCanceledException)
+ {
+ Cancelled = true;
+ throw;
+ }
+ catch
+ {
+ Failed = true;
+ throw;
+ }
+ finally
+ {
+ CancellationTokenSource?.Dispose();
+ CancellationTokenSource = null;
+ }
+ }
+
+ private async Task RunPreparationAsync(CancellationToken token)
+ {
+ try
+ {
+ await WaitForPreparationAsync(token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ Cancelled = true;
+ Cancel();
+ }
+ catch (Exception)
+ {
+ Failed = true;
+ if (FailFast)
+ Cancel();
+ }
+ finally
+ {
+ StepRunner.Finish();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs
index 97e2a292..421f7128 100644
--- a/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/SequentialPipeline.cs
@@ -6,20 +6,28 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline;
///
/// A simple pipeline that runs all steps sequentially.
///
-public abstract class SequentialPipeline : StepRunnerPipeline
+public abstract class SequentialPipeline : StepRunnerPipeline
{
///
/// Initializes a new instance of the class.
///
/// The service provider for dependency injection within the pipeline.
- /// A value indicating whether the pipeline should fail fast.
/// is .
- protected SequentialPipeline(IServiceProvider serviceProvider, bool failFast = true) : base(serviceProvider, failFast)
+ protected SequentialPipeline(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
- ///
- protected sealed override SequentialStepRunner CreateRunner()
+ ///
+ /// Creates an instance of to execute the steps in the pipeline sequentially.
+ ///
+ ///
+ /// This method returns a .
+ /// The runner ensures that all steps are executed one after another in a sequential manner.
+ ///
+ ///
+ /// An instance of that executes steps sequentially.
+ ///
+ protected sealed override IStepRunner CreateRunner()
{
return new SequentialStepRunner(ServiceProvider);
}
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs
index 0e27fa19..8b1cf9d1 100644
--- a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipeline.cs
@@ -1,84 +1,64 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
namespace AnakinRaW.CommonUtilities.SimplePipeline;
///
-/// Base class for a pipeline implementation utilizing an as its primary execution engine.
+/// Base class for pipelines that use an with sequential preparation and execution.
///
-/// The type of the step stepRunner.
-public abstract class StepRunnerPipeline : Pipeline where TRunner : IStepRunner
+///
+///
+/// This class follows a sequential pattern: preparation completes fully before execution begins.
+///
+///
+/// For pipelines that need to run preparation and execution in parallel (producer/consumer pattern),
+/// use instead.
+///
+///
+public abstract class StepRunnerPipeline(IServiceProvider serviceProvider) : StepRunnerPipelineBase(serviceProvider)
{
- private IStepRunner _buildStepRunner = null!;
-
- ///
- protected override bool FailFast { get; }
-
///
- /// Initializes a new instance of the class.
+ /// Creates a collection of steps to be executed by the .
///
- /// The service provider the pipeline.
- /// A value indicating whether the pipeline should fail fast.
+ ///
+ /// A that can be used to cancel the step creation process.
+ ///
+ ///
+ /// A task that represents the asynchronous operation. The task result contains a list of steps
+ /// () to be added to the .
+ ///
///
- /// The parameter determines whether the pipeline should stop executing immediately upon encountering the first failure.
+ /// This method is abstract and must be implemented by derived classes to define the specific steps
+ /// required for the pipeline. The steps are created during the preparation phase of the pipeline.
///
- /// is .
- protected StepRunnerPipeline(IServiceProvider serviceProvider, bool failFast = true) : base(serviceProvider)
- {
- FailFast = failFast;
- }
-
- ///
- [ExcludeFromCodeCoverage]
- public override string ToString()
- {
- return GetType().Name;
- }
-
- ///
- /// Creates the step stepRunner for the pipeline.
- ///
- /// The step stepRunner instance.
- protected abstract TRunner CreateRunner();
+ protected abstract Task> CreateRunnerSteps(CancellationToken token);
///
- /// Builds the steps that should be executed within the pipeline.
+ /// Prepares the pipeline by initializing the step runner and adding the steps to be executed.
///
+ /// A to observe while waiting for the task to complete.
///
- /// The order of the steps might be relevant, depending on the type of .
+ /// This method initializes the step runner and sequentially adds the steps created by
+ /// to the runner. It ensures that the preparation phase
+ /// is fully completed before the execution phase begins.
///
- /// A task that returns a list of steps.
- protected abstract Task> BuildSteps();
-
- ///
- protected override async Task PrepareCoreAsync()
+ ///
+ /// The step runner is not properly initialized before adding steps.
+ ///
+ /// A that represents the asynchronous operation.
+ protected sealed override async Task PrepareCoreAsync(CancellationToken token)
{
- _buildStepRunner = CreateRunner() ?? throw new InvalidOperationException("RunnerFactory created null value!");
- var steps = await BuildSteps().ConfigureAwait(false);
- foreach (var step in steps)
- _buildStepRunner.AddStep(step);
- return true;
+ _ = StepRunner;
+ var steps = await CreateRunnerSteps(token).ConfigureAwait(false);
+ foreach (var step in steps)
+ StepRunner.AddStep(step);
}
///
- protected override async Task RunCoreAsync(CancellationToken token)
+ protected sealed override Task RunCoreAsync(CancellationToken token)
{
- try
- {
- _buildStepRunner.Error += OnError;
- await _buildStepRunner.RunAsync(token).ConfigureAwait(false);
- }
- finally
- {
- _buildStepRunner.Error -= OnError;
- }
-
- if (!PipelineFailed)
- return;
-
- ThrowIfAnyStepsFailed(_buildStepRunner.ExecutedSteps);
+ return base.RunCoreAsync(token);
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs
new file mode 100644
index 00000000..c165aaa1
--- /dev/null
+++ b/src/CommonUtilities.SimplePipeline/src/Pipelines/StepRunnerPipelineBase.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AnakinRaW.CommonUtilities.SimplePipeline;
+
+///
+/// Represents a base class for pipelines that execute steps using a .
+///
+/// The type of the step runner used to execute the steps.
+///
+///
+/// Running the pipeline may throw a if one or many steps produce errors.
+///
+///
+/// This class provides functionality for managing a step runner, executing steps, handling errors,
+/// and disposing resources. Derived classes must implement the method
+/// to provide a specific step runner implementation.
+///
+///
+public abstract class StepRunnerPipelineBase : Pipeline where TStepRunner : class, IStepRunner
+{
+ private readonly Lazy _stepRunnerLazy;
+
+ ///
+ /// Initializes a new instance of the class with the specified service provider.
+ ///
+ /// The used to resolve dependencies for the pipeline.
+ /// is .
+ protected StepRunnerPipelineBase(IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _stepRunnerLazy = new Lazy(EnsureRunner, LazyThreadSafetyMode.ExecutionAndPublication);
+ }
+
+ ///
+ /// Gets the step runner used to execute the steps in the pipeline.
+ ///
+ ///
+ /// The step runner of type .
+ ///
+ ///
+ ///
+ /// The step runner is lazily initialized when accessed for the first time, using .
+ /// It is guaranteed to be non- once initialized.
+ ///
+ ///
+ /// Derived classes can use this property to add steps to the runner or to
+ /// perform operations specific to the step runner implementation.
+ ///
+ ///
+ protected internal TStepRunner StepRunner => _stepRunnerLazy.Value;
+
+ private TStepRunner EnsureRunner()
+ {
+ return CreateRunner() ?? throw new InvalidOperationException("CreateRunner must not return null.");
+ }
+
+ ///
+ /// Gets a value indicating whether the step runner has been initialized.
+ ///
+ ///
+ /// if the step runner has been initialized; otherwise, .
+ ///
+ ///
+ /// When this property returns , the property is guaranteed
+ /// to return a non- value.
+ ///
+ protected internal bool IsStepRunnerInitialized => _stepRunnerLazy.IsValueCreated;
+
+ ///
+ /// Gets or sets a value indicating whether the pipeline should terminate execution immediately
+ /// upon encountering an error.
+ ///
+ ///
+ /// if the pipeline should stop execution on the first error; otherwise, .
+ ///
+ ///
+ /// When set to , the pipeline will cancel further processing as soon as an error occurs.
+ /// This is useful for scenarios where continuing execution after an error is not desirable.
+ ///
+ public bool FailFast { get; protected set; } = false;
+
+ ///
+ /// Creates an instance of the step runner used to execute the steps in the pipeline.
+ ///
+ /// An instance of representing the step runner.
+ ///
+ /// Derived classes must implement this method to provide a specific implementation of the step runner.
+ /// The returned step runner must not be
+ ///
+ protected abstract TStepRunner CreateRunner();
+
+ ///
+ /// Executes the pipeline asynchronously, running all steps added to .
+ ///
+ /// A to observe while waiting for the task to complete.
+ ///
+ /// A that represents the asynchronous execution of the pipeline.
+ ///
+ ///
+ /// This method attaches an error handler to the step runner, executes the steps asynchronously,
+ /// and ensures that any failed steps throw an exception after execution.
+ ///
+ ///
+ /// Thrown when if any executed step failed excluding those which represent a cancelled Step.
+ ///
+ protected sealed override async Task ExecuteAsync(CancellationToken token)
+ {
+ OnExecuteStarted();
+ try
+ {
+ StepRunner.Error += OnRunnerExecutionError!;
+ await StepRunner.RunAsync(token).ConfigureAwait(false);
+ }
+ finally
+ {
+ StepRunner.Error -= OnRunnerExecutionError!;
+ }
+ OnRunnerExecuted();
+
+ var failedSteps = GetFailedSteps(StepRunner.ExecutedSteps).ToList();
+ if (failedSteps.Count > 0)
+ throw new StepFailureException(failedSteps);
+ OnExecuteCompleted();
+ }
+
+ ///
+ /// Retrieves the steps that have failed during execution.
+ ///
+ /// The collection of steps to evaluate for failures.
+ ///
+ /// A collection of steps that encountered errors during execution.
+ /// Each step in the returned collection has a non-null property.
+ ///
+ ///
+ /// This method filters the provided steps to identify those that have an associated error.
+ /// Derived classes can override this method to customize the logic for determining failed steps.
+ ///
+ protected virtual IEnumerable GetFailedSteps(IEnumerable steps)
+ {
+ return steps.Where(step => step is { Error: not null });
+ }
+
+ ///
+ /// Called when the execution of the pipeline starts, before the step runner begins processing steps.
+ ///
+ ///
+ /// Override this method to perform custom logic at the start of execution.
+ ///
+ protected virtual void OnExecuteStarted()
+ {
+ }
+
+ ///
+ /// Called after the step runner has completed executing all steps, before checking for step failures.
+ ///
+ ///
+ /// Override this method to perform custom logic after the runner has executed but before failure validation.
+ ///
+ protected virtual void OnRunnerExecuted()
+ {
+ }
+
+ ///
+ /// Called when the execution of the pipeline has completed successfully.
+ ///
+ ///
+ /// This method is only called if no is thrown.
+ /// Override this method to perform custom logic upon successful completion.
+ ///
+ protected virtual void OnExecuteCompleted()
+ {
+ }
+
+ ///
+ /// Handles errors that occur during the execution of the .
+ ///
+ /// The source of the event, typically the instance.
+ /// The containing details about the error.
+ ///
+ /// This method updates the pipeline's state based on the error details, including whether the pipeline
+ /// should be cancelled or marked as failed. If is enabled or the error indicates
+ /// cancellation, the pipeline will be cancelled immediately.
+ ///
+ protected virtual void OnRunnerExecutionError(object sender, StepRunnerErrorEventArgs e)
+ {
+ var isCancel = IsCancel(e);
+ Cancelled |= isCancel;
+ Failed |= !isCancel;
+
+ if (FailFast || isCancel)
+ Cancel();
+ }
+
+ private static bool IsCancel(StepRunnerErrorEventArgs e)
+ {
+ return e.Cancel || e.Exception.IsExceptionType();
+ }
+
+ ///
+ /// Releases the resources used by the instance.
+ ///
+ ///
+ /// This method ensures that any resources associated with the pipeline, including the step runner, are properly disposed of.
+ ///
+ protected override void DisposeResources()
+ {
+ base.DisposeResources();
+ if (IsStepRunnerInitialized)
+ StepRunner.Error -= OnRunnerExecutionError!;
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs b/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs
index 22fbd48a..c5760fbc 100644
--- a/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Progress/AggregatedProgressReporter.cs
@@ -110,15 +110,14 @@ protected AggregatedProgressReporter(
{
if (!_progressSteps.Add(step))
continue;
- step.Progress += OnStepProgress;
+ step.Progress += OnStepProgress!;
TotalSize += step.Size;
}
}
private void OnStepProgress(object sender, ProgressEventArgs e)
{
- if (sender is not TStep step)
- throw new InvalidCastException($"Cannot cast '{sender.GetType()}' to {typeof(TStep)}");
+ var step = (TStep)sender;
if (!_progressSteps.Contains(step))
return;
@@ -134,7 +133,7 @@ private void OnStepProgress(object sender, ProgressEventArgs e)
protected override void DisposeResources()
{
foreach (var step in _progressSteps)
- step.Progress -= OnStepProgress;
+ step.Progress -= OnStepProgress!;
_progressSteps.Clear();
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs
new file mode 100644
index 00000000..96b922ac
--- /dev/null
+++ b/src/CommonUtilities.SimplePipeline/src/Runners/AsyncStepRunner.cs
@@ -0,0 +1,303 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
+
+///
+/// Represents an asynchronous step runner that manages the execution of steps in a pipeline.
+///
+///
+///
+/// This class provides functionality to add steps, execute them asynchronously, and handle errors during execution.
+/// It supports multiple workers for parallel step execution and ensures proper cancellation and error handling.
+///
+///
+/// Thread Safety: This class and its derivatives are not thread-safe for concurrent operations
+/// such as adding steps from a different thread while step execution is processing.
+///
+///
+public class AsyncStepRunner : IStepRunner
+{
+ ///
+ public event EventHandler? Error;
+
+ private readonly ConcurrentQueue _pendingSteps = new();
+ private readonly ConcurrentBag _executedSteps = [];
+ private readonly ConcurrentBag _exceptions = [];
+ private readonly TaskCompletionSource _completionSource = new();
+ private Task? _exposedTask;
+
+ ///
+ public AggregateException? Exception => _exceptions.IsEmpty ? null : new AggregateException(_exceptions);
+
+ ///
+ /// Gets a value indicating whether the steps in the pipeline are executed sequentially.
+ ///
+ ///
+ /// if the steps are executed sequentially; otherwise, .
+ ///
+ ///
+ /// The execution is considered sequential when the is set to 1.
+ ///
+ public bool IsSequential => WorkerCount == 1;
+
+ ///
+ public bool IsRunning { get; private set; }
+
+ ///
+ public int WorkerCount { get; }
+
+ ///
+ public IReadOnlyCollection ExecutedSteps => _executedSteps.ToArray();
+
+ ///
+ public bool IsCancelled { get; private set; }
+
+ ///
+ /// Gets the logger instance used for logging messages related to the execution of the step runner.
+ ///
+ protected ILogger? Logger { get; }
+
+ ///
+ /// Initializes a new instance of the class with the specified number of workers and a service provider.
+ ///
+ /// The number of workers to use for executing steps. Must be between 1 and 64, inclusive.
+ /// The service provider used to resolve dependencies required by the runner.
+ /// Thrown when is less than 1 or greater than 64.
+ /// Thrown when is .
+ public AsyncStepRunner(int workerCount, IServiceProvider serviceProvider)
+ {
+ if (workerCount is < 1 or > 64)
+ throw new ArgumentOutOfRangeException(nameof(workerCount));
+ if (serviceProvider == null)
+ throw new ArgumentNullException(nameof(serviceProvider));
+ Logger = serviceProvider.GetService()?.CreateLogger(GetType());
+ WorkerCount = workerCount;
+ }
+
+ ///
+ public virtual void AddStep(IStep step)
+ {
+ if (step == null)
+ throw new ArgumentNullException(nameof(step));
+ _pendingSteps.Enqueue(step);
+ }
+
+ ///
+ public Task RunAsync(CancellationToken token)
+ {
+ if (IsRunning)
+ throw new InvalidOperationException("The step runner is already running.");
+
+ var task = CreateRunnerTask(token);
+ _completionSource.TrySetResult(task);
+ return GetRunnerTask();
+ }
+
+ ///
+ public TaskAwaiter GetAwaiter()
+ {
+ return GetRunnerTask().GetAwaiter();
+ }
+
+ ///
+ public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
+ {
+ return GetRunnerTask().ConfigureAwait(continueOnCapturedContext);
+ }
+
+ private Task GetRunnerTask()
+ {
+ var existing = Volatile.Read(ref _exposedTask);
+ if (existing is not null)
+ return existing;
+
+ var tcsTask = _completionSource.Task;
+ if (tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion })
+ {
+ var result = tcsTask.Result;
+ var original = Interlocked.CompareExchange(ref _exposedTask, result, null);
+ return original ?? result;
+ }
+
+ var newTask = CreateAwaitableTask();
+ var prev = Interlocked.CompareExchange(ref _exposedTask, newTask, null);
+ return prev ?? newTask;
+ }
+
+ private async Task CreateAwaitableTask()
+ {
+ var task = await _completionSource.Task.ConfigureAwait(false);
+ await task.ConfigureAwait(false);
+ }
+
+ ///
+ public void Wait()
+ {
+ Wait(Timeout.InfiniteTimeSpan);
+ }
+
+ ///
+ public void Wait(TimeSpan timeout)
+ {
+ var totalMilliseconds = (long)timeout.TotalMilliseconds;
+ if (totalMilliseconds is < -1 or > int.MaxValue)
+ throw new ArgumentOutOfRangeException(nameof(timeout));
+
+ var task = GetRunnerTask();
+
+ var completed = true;
+ try
+ {
+ completed = task.Wait(timeout);
+ }
+ catch
+ {
+ // Ignore
+ }
+
+ if (!completed)
+ throw new TimeoutException();
+
+ var exception = Exception;
+ if (exception != null)
+ throw exception;
+ }
+
+ ///
+ /// Asynchronously retrieves the next step to be executed from the pending steps queue.
+ ///
+ /// A to observe while waiting for the next step.
+ ///
+ /// A representing the asynchronous operation.
+ /// The result contains the next to be executed, or if no steps are available.
+ ///
+ ///
+ /// This method is designed to be overridden in derived classes to customize the behavior of step retrieval.
+ ///
+ protected virtual ValueTask TakeNextStepAsync(CancellationToken cancellationToken)
+ {
+ return new ValueTask(_pendingSteps.TryDequeue(out var step) ? step : null);
+ }
+
+ ///
+ /// Allows an overriding class to handle step errors and raises the event.
+ ///
+ /// The exception that caused the error.
+ /// The event args to use.
+ protected virtual void OnError(Exception exception, StepRunnerErrorEventArgs stepError)
+ {
+ Error?.Invoke(this, stepError);
+ IsCancelled |= stepError.Cancel;
+ }
+
+ ///
+ /// Throws an if the given token was requested for cancellation.
+ ///
+ /// The token to check for cancellation.
+ /// If the token was requested for cancellation.
+ protected void ThrowIfCancelled(CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+ if (IsCancelled)
+ throw new OperationCanceledException(token);
+ }
+
+ ///
+ /// Allows an overriding class to perform cleanup actions once the runner was requested to stop execution.
+ ///
+ protected virtual void OnRunnerStopped()
+ {
+ }
+
+ private async Task CreateRunnerTask(CancellationToken token)
+ {
+ try
+ {
+ IsRunning = true;
+ if (WorkerCount == 1)
+ await Task.Run(() => RunWorkerAsync(token), CancellationToken.None).ConfigureAwait(false);
+ else
+ {
+ var workers = new Task[WorkerCount];
+ for (var i = 0; i < WorkerCount; i++)
+ workers[i] = Task.Factory.StartNew(
+ () => RunWorkerAsync(token),
+ CancellationToken.None,
+ TaskCreationOptions.LongRunning,
+ TaskScheduler.Default).Unwrap();
+ await Task.WhenAll(workers).ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ IsRunning = false;
+ }
+ }
+
+ private async Task RunWorkerAsync(CancellationToken token)
+ {
+ var alreadyCancelled = false;
+ try
+ {
+ while (await TakeNextStepAsync(token).ConfigureAwait(false) is { } step)
+ {
+ try
+ {
+ ThrowIfCancelled(token);
+ _executedSteps.Add(step);
+ await step.RunAsync(token).ConfigureAwait(false);
+ }
+ catch (StopRunnerException e)
+ {
+ _exceptions.Add(e);
+ Logger?.LogTrace("Stop subsequent steps");
+ IsCancelled = true;
+
+ var error = new StepRunnerErrorEventArgs(e, step)
+ {
+ Cancel = true
+ };
+ OnError(e, error);
+
+ OnRunnerStopped();
+
+ break;
+ }
+ catch (Exception e)
+ {
+ _exceptions.Add(e);
+ if (!alreadyCancelled)
+ {
+ if (e.IsExceptionType())
+ Logger?.LogTrace("Step {Step} cancelled", step);
+ else
+ Logger?.LogTrace(e, "Step {Step} threw an exception: {Exception}: {EMessage}", step, e.GetType(), e.Message);
+ }
+
+ var error = new StepRunnerErrorEventArgs(e, step)
+ {
+ Cancel = token.IsCancellationRequested || IsCancelled || e.IsExceptionType()
+ };
+ if (error.Cancel)
+ alreadyCancelled = true;
+ OnError(e, error);
+ }
+ }
+ }
+ catch (OperationCanceledException e)
+ {
+ IsCancelled = true;
+ OnError(e, new StepRunnerErrorEventArgs(e, null)
+ {
+ Cancel = true
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelProducerConsumerStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ParallelProducerConsumerStepRunner.cs
deleted file mode 100644
index 3d045d50..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelProducerConsumerStepRunner.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
-
-///
-/// Runner engine, which executes all queued _steps parallel. Steps may be queued while step execution has been started.
-/// The execution can finish only if was called explicitly.
-///
-public sealed class ParallelProducerConsumerStepRunner : ParallelStepRunnerBase
-{
- private BlockingCollection StepQueue { get; } = new();
-
- ///
- /// Initializes a new instance of the class with the specified number of workers.
- ///
- /// The number of parallel workers.
- /// The service provider.
- /// If the number of workers is below 1.
- /// is .
- public ParallelProducerConsumerStepRunner(int workerCount, IServiceProvider serviceProvider) : base(workerCount, serviceProvider)
- {
- }
-
- ///
- /// Signals this instance does not expect any more steps.
- ///
- public void Finish()
- {
- StepQueue.CompleteAdding();
- }
-
- ///
- public override void AddStep(IStep step)
- {
- if (step is null)
- throw new ArgumentNullException(nameof(step));
- StepQueue.Add(step, CancellationToken.None);
- }
-
- ///
- protected override bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step)
- {
- step = null;
- if (StepQueue.IsCompleted)
- return false;
-
- try
- {
- step = StepQueue.Take(cancellationToken);
- return true;
- }
- catch (InvalidOperationException)
- {
- return false;
- }
- }
-
- ///
- protected override void OnRunnerStopped()
- {
- Finish();
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunner.cs
deleted file mode 100644
index 637fc80b..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunner.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
-
-///
-/// Runner engine, which executes all queued steps parallel.
-///
-public class ParallelStepRunner : ParallelStepRunnerBase
-{
- private ConcurrentQueue StepQueue { get; } = new();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The number of parallel workers.
- /// The service provider.
- /// If the number of workers is below 1 or above 64.
- /// is .
- public ParallelStepRunner(int workerCount, IServiceProvider serviceProvider) : base(workerCount, serviceProvider)
- {
- }
-
- ///
- public override void AddStep(IStep step)
- {
- if (step == null)
- throw new ArgumentNullException(nameof(step));
- StepQueue.Enqueue(step);
- }
-
- ///
- protected override bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step)
- {
- return StepQueue.TryDequeue(out step);
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunnerBase.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunnerBase.cs
deleted file mode 100644
index aa662d5d..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Runners/ParallelStepRunnerBase.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
-
-///
-/// Base class for an that allows parallel step execution on the thread pool.
-///
-public abstract class ParallelStepRunnerBase : StepRunnerBase, IParallelStepRunner
-{
- private readonly ConcurrentBag _exceptions;
- private readonly Task[] _tasks;
-
- ///
- /// Gets the number of parallel workers.
- ///
- public int WorkerCount { get; }
-
- ///
- public AggregateException? Exception => _exceptions.Count > 0 ? new AggregateException(_exceptions) : null;
-
- ///
- /// Initializes a new instance of the class with the specified number of workers.
- ///
- /// The number of parallel workers.
- /// The service provider.
- /// If the number of workers is below 1 or above 64.
- /// is .
- protected ParallelStepRunnerBase(int workerCount, IServiceProvider serviceProvider) : base(serviceProvider)
- {
- if (workerCount is < 1 or > 64)
- throw new ArgumentOutOfRangeException(nameof(workerCount));
- WorkerCount = workerCount;
- _exceptions = [];
- _tasks = new Task[workerCount];
- }
-
- ///
- public override Task RunAsync(CancellationToken token)
- {
- for (var index = 0; index < WorkerCount; ++index)
- _tasks[index] = Task.Factory.StartNew(() => RunSteps(token), TaskCreationOptions.LongRunning);
- return Task.WhenAll(_tasks);
- }
-
- ///
- public void Wait()
- {
- Wait(Timeout.InfiniteTimeSpan);
- }
-
- ///
- public void Wait(TimeSpan timeout)
- {
- if (!Task.WaitAll(_tasks, timeout))
- throw new TimeoutException();
-
- var exception = Exception;
- if (exception != null)
- throw exception;
- }
-
- ///
- protected override void OnError(Exception exception, StepRunnerErrorEventArgs stepError)
- {
- _exceptions.Add(exception);
- base.OnError(exception, stepError);
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs
new file mode 100644
index 00000000..eb4f52f0
--- /dev/null
+++ b/src/CommonUtilities.SimplePipeline/src/Runners/ProducerConsumerStepRunner.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Threading;
+using System.Threading.Channels;
+using System.Threading.Tasks;
+
+namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
+
+///
+/// Represents a step runner that processes steps using a producer-consumer pattern.
+///
+///
+///
+/// This class allows steps to be added dynamically while the runner is executing.
+///
+///
+/// Call to signal completion of step additions,
+/// otherwise the runner will block indefinitely unless cancelled via .
+///
+///
+/// Thread Safety: While this class supports adding steps while the runner is executing, it is not
+/// thread-safe by design. Adding steps from different threads or concurrently with cancellation/error handling
+/// may lead to race conditions where the consequence might be that steps added after cancellation or an error
+/// might still get scheduled for execution.
+///
+///
+public class ProducerConsumerStepRunner(int workerCount, IServiceProvider serviceProvider)
+ : AsyncStepRunner(workerCount, serviceProvider)
+{
+ private readonly Channel _stepChannel = Channel.CreateUnbounded();
+
+ ///
+ /// Adds a step to the runner for execution.
+ ///
+ /// The step to add to the runner.
+ /// Thrown when the is .
+ /// Thrown when the runner has already been finished and cannot accept new steps.
+ public override void AddStep(IStep step)
+ {
+ if (!TryAddStep(step))
+ throw new InvalidOperationException("Runner has been finished.");
+ }
+
+ ///
+ /// Attempts to add a step to the runner for execution.
+ ///
+ /// The step to add to the runner.
+ ///
+ /// if the step was successfully added; otherwise, .
+ ///
+ /// Thrown when the is .
+ public bool TryAddStep(IStep step)
+ {
+ if (step == null)
+ throw new ArgumentNullException(nameof(step));
+ return _stepChannel.Writer.TryWrite(step);
+ }
+
+ ///
+ /// Signals this instance does not expect any more steps.
+ ///
+ ///
+ ///
+ /// This method must be called to allow to complete normally.
+ /// After calling this method, attempting to add more steps via
+ /// will throw an .
+ ///
+ ///
+ /// It is safe to call this method before, during, or after execution.
+ ///
+ ///
+ public void Finish()
+ {
+ _stepChannel.Writer.TryComplete();
+ }
+
+ ///
+ /// Asynchronously retrieves the next step to be executed from the internal queue.
+ ///
+ /// A token to monitor for cancellation requests.
+ ///
+ /// A task that represents the asynchronous operation. The task result contains the next to be executed,
+ /// or if no more steps are available.
+ ///
+ ///
+ /// This method waits for a step to become available in the queue. If the queue is empty and no more steps will be added,
+ /// it returns . The operation can be cancelled by the provided .
+ ///
+ /// The operation is cancelled via the .
+ protected override async ValueTask TakeNextStepAsync(CancellationToken cancellationToken)
+ {
+ while (await _stepChannel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false))
+ {
+ if (_stepChannel.Reader.TryRead(out var step))
+ return step;
+ }
+ return null;
+ }
+
+ ///
+ /// Handles errors that occur during the execution of a step in the producer-consumer step runner.
+ ///
+ /// The exception that was thrown during the execution of the step.
+ ///
+ /// The containing details about the error, including the step
+ /// that caused the error and whether the runner should cancel further processing.
+ ///
+ ///
+ /// This method is invoked when an error occurs during the execution of a step. If the
+ /// property is set to true, the runner will terminate further processing by calling .
+ ///
+ protected override void OnError(Exception exception, StepRunnerErrorEventArgs stepError)
+ {
+ base.OnError(exception, stepError);
+ if (stepError.Cancel)
+ Finish();
+ }
+
+ ///
+ /// Performs cleanup actions when the runner is requested to stop execution.
+ ///
+ ///
+ /// This method overrides the base implementation to ensure that the runner
+ /// completes its processing by signaling that no more steps are expected.
+ ///
+ protected override void OnRunnerStopped()
+ {
+ base.OnRunnerStopped();
+ Finish();
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/SequentialStepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/SequentialStepRunner.cs
new file mode 100644
index 00000000..0ea4e8ea
--- /dev/null
+++ b/src/CommonUtilities.SimplePipeline/src/Runners/SequentialStepRunner.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
+
+///
+/// A that executes steps sequentially using a single worker.
+///
+public class SequentialStepRunner(IServiceProvider serviceProvider) : AsyncStepRunner(1, serviceProvider);
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunner.cs b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunner.cs
deleted file mode 100644
index e04e368e..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunner.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
-
-///
-/// Runner engine, which executes all queued sequentially in the order they are queued.
-///
-public sealed class SequentialStepRunner : StepRunnerBase
-{
- private ConcurrentQueue StepQueue { get; } = new();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The service provider for this instance.
- /// is .
- public SequentialStepRunner(IServiceProvider services) : base(services)
- {
- }
-
- ///
- public override Task RunAsync(CancellationToken token)
- {
- return Task.Run(() =>
- {
- RunSteps(token);
- }, CancellationToken.None);
- }
-
- ///
- public override void AddStep(IStep step)
- {
- if (step == null)
- throw new ArgumentNullException(nameof(step));
- StepQueue.Enqueue(step);
- }
-
- ///
- protected override bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step)
- {
- return StepQueue.TryDequeue(out step);
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs b/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs
deleted file mode 100644
index 2db72601..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Runners/StepRunnerBase.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline.Runners;
-
-///
-/// Base class for an .
-///
-public abstract class StepRunnerBase : IStepRunner
-{
- ///
- public event EventHandler? Error;
-
- ///
- /// Gets a modifiable bag of all executed steps.
- ///
- protected readonly ConcurrentBag ExecutedStepsBag = [];
-
- ///
- /// Gets the logger instance of this stepRunner.
- ///
- protected ILogger? Logger { get; }
-
- ///
- public IReadOnlyCollection ExecutedSteps => ExecutedStepsBag.ToArray();
-
- internal bool IsCancelled { get; private set; }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The service provider for this instance.
- /// is .
- protected StepRunnerBase(IServiceProvider services)
- {
- if (services == null)
- throw new ArgumentNullException(nameof(services));
- Logger = services.GetService()?.CreateLogger(GetType());
- }
-
- ///
- public abstract Task RunAsync(CancellationToken token);
-
- ///
- public abstract void AddStep(IStep step);
-
- ///
- /// Tries to get the next step from the step queue.
- ///
- /// The cancellation token
- /// When this method returns, contains the next step to execute, or if there is no step to execute.
- /// when there exists a next step; otherwise, .
- protected abstract bool TakeNextStep(CancellationToken cancellationToken, [NotNullWhen(true)] out IStep? step);
-
- ///
- /// Takes steps from the step queue and executes it.
- ///
- /// The cancellation token.
- protected void RunSteps(CancellationToken token)
- {
- var alreadyCancelled = false;
- try
- {
- while (TakeNextStep(token, out var step))
- {
- try
- {
- ThrowIfCancelled(token);
-
- ExecutedStepsBag.Add(step);
- step.Run(token);
- }
- catch (StopRunnerException)
- {
- OnRunnerStopped();
- Logger?.LogTrace("Stop subsequent steps");
- break;
- }
- catch (Exception e)
- {
- if (!alreadyCancelled)
- {
- if (e.IsExceptionType())
- Logger?.LogTrace("Step {Step} cancelled", step);
- else
- Logger?.LogTrace(e, "Step {Step} threw an exception: {Exception}: {EMessage}", step, e.GetType(), e.Message);
- }
-
- var error = new StepRunnerErrorEventArgs(e, step)
- {
- Cancel = token.IsCancellationRequested || IsCancelled || e.IsExceptionType()
- };
- if (error.Cancel)
- alreadyCancelled = true;
- OnError(e, error);
- }
- }
- }
- catch (OperationCanceledException e)
- {
- OnError(e, new StepRunnerErrorEventArgs(e, null));
- IsCancelled = true;
- }
- }
-
- ///
- /// Allows an overriding class to handle step errors and raises the event.
- ///
- /// The exception that caused the error.
- /// The event args to use.
- protected virtual void OnError(Exception exception, StepRunnerErrorEventArgs stepError)
- {
- Error?.Invoke(this, stepError);
- if (!stepError.Cancel)
- return;
- IsCancelled |= stepError.Cancel;
- }
-
- ///
- /// Throws an if the given token was requested for cancellation.
- ///
- /// The token to check for cancellation.
- /// If the token was requested for cancellation.
- protected void ThrowIfCancelled(CancellationToken token)
- {
- token.ThrowIfCancellationRequested();
- if (IsCancelled)
- throw new OperationCanceledException(token);
- }
-
- ///
- /// Allows an overriding class to perform cleanup actions once the runner was requested to stop execution.
- ///
- protected virtual void OnRunnerStopped()
- {
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs b/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs
index f5ef830b..5955fd5b 100644
--- a/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs
+++ b/src/CommonUtilities.SimplePipeline/src/StepFailureException.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using System.Text;
namespace AnakinRaW.CommonUtilities.SimplePipeline;
@@ -10,7 +11,13 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline;
///
public sealed class StepFailureException : Exception
{
- private readonly IEnumerable _failedSteps;
+ ///
+ /// Gets the collection of steps that failed.
+ ///
+ ///
+ /// A read-only collection of instances representing the failed steps.
+ ///
+ public IReadOnlyCollection FailedSteps { get; }
///
public override string Message => Error;
@@ -22,11 +29,14 @@ private string Error
{
if (field is not null)
return field;
-
- var stringBuilder = new StringBuilder();
-
- foreach (var step in _failedSteps)
- stringBuilder.Append($"Step '{step}' failed with error: {step.Error?.Message};");
+ var stringBuilder = new StringBuilder($"{FailedSteps.Count} Failed Step(s)");
+ if (FailedSteps.Count > 0)
+ {
+ stringBuilder.Append(':');
+ stringBuilder.Append(' ');
+ }
+ foreach (var step in FailedSteps)
+ stringBuilder.Append($"Step '{step}' failed with error: {step.Error?.Message ?? "n/a"};");
field = stringBuilder.ToString().TrimEnd(';');
return field;
}
@@ -38,6 +48,8 @@ private string Error
/// The failed steps.
public StepFailureException(IEnumerable failedSteps)
{
- _failedSteps = failedSteps ?? throw new ArgumentNullException(nameof(failedSteps));
+ if (failedSteps == null)
+ throw new ArgumentNullException(nameof(failedSteps));
+ FailedSteps = failedSteps.ToList();
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/AsyncStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/AsyncStep.cs
deleted file mode 100644
index a14b6de8..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Steps/AsyncStep.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-//using Microsoft.Extensions.DependencyInjection;
-//using Microsoft.Extensions.Logging;
-//using System;
-//using System.Runtime.CompilerServices;
-//using System.Threading;
-//using System.Threading.Tasks;
-
-//namespace AnakinRaW.CommonUtilities.StepRunnerPipeline.Steps;
-
-/////
-///// A that can be awaited on.
-/////
-//public abstract class AsyncStep : DisposableObject, IStep
-//{
-// private readonly TaskCompletionSource _taskCompletion = new();
-
-// ///
-// /// Gets the service provider of this step.
-// ///
-// protected IServiceProvider Services { get; }
-
-// ///
-// /// Gets the logger of this step.
-// ///
-// protected ILogger? Logger { get; }
-
-// ///
-// public Exception? Error
-// {
-// get
-// {
-// if (_taskCompletion.Task.IsFaulted)
-// return _taskCompletion.Task.Exception?.InnerException;
-
-// if (!_taskCompletion.Task.IsCompleted)
-// return null;
-
-// if (_taskCompletion.Task.Result.IsFaulted)
-// return _taskCompletion.Task.Result.Exception?.InnerException;
-
-// return null;
-// }
-// }
-
-// ///
-// /// Initializes a new instance of the class.
-// ///
-// /// The service provider.
-// /// is .
-// protected AsyncStep(IServiceProvider serviceProvider)
-// {
-// Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
-// Logger = serviceProvider.GetService()?.CreateLogger(GetType());
-// }
-
-// ///
-// /// Gets an awaiter used to await this .
-// ///
-// /// An awaiter instance.
-// public TaskAwaiter GetAwaiter()
-// {
-// if (_taskCompletion.Task.IsCompleted)
-// return _taskCompletion.Task.Result.GetAwaiter();
-
-// return Task.Run(async () =>
-// {
-// var task = await _taskCompletion.Task.ConfigureAwait(false);
-// await task.ConfigureAwait(false);
-// }).GetAwaiter();
-// }
-
-// ///
-// /// Run the step's action and returns the operation as a task reference.
-// ///
-// ///
-// /// The task that represents the operation of this step.
-// protected abstract Task RunAsync(CancellationToken token);
-
-// ///
-// public void Run(CancellationToken token)
-// {
-// Logger?.LogTrace($"BEGIN on thread-pool: {this}");
-// var task = RunAsync(token);
-// _taskCompletion.SetResult(task);
-// }
-//}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs
index 94f4c41d..b57de664 100644
--- a/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Steps/PipelineStep.cs
@@ -1,8 +1,9 @@
-using System;
-using System.Linq;
-using System.Threading;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps;
@@ -11,6 +12,9 @@ namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps;
///
public abstract class PipelineStep : DisposableObject, IStep
{
+ private readonly TaskCompletionSource _completionSource = new();
+ private Task? _exposedTask;
+
///
/// Returns the service provider of this step.
///
@@ -22,9 +26,28 @@ public abstract class PipelineStep : DisposableObject, IStep
protected readonly ILogger? Logger;
///
- /// Gets the exception that occurred during execution or if no error occurred.
+ /// Gets the exception that occurred during the execution of the step, if any.
///
- public Exception? Error { get; internal set; }
+ ///
+ ///
+ /// If the step is cancelled by an
+ /// (which may also be wrapped inside an ),
+ /// this property contains the underlying cause of the cancellation when available,
+ /// otherwise it may be .
+ ///
+ ///
+ /// If the step throws a , this property is always .
+ /// This is because a does not indicate a failure of the step itself.
+ ///
+ ///
+ /// For all other failures, this property contains the exception that caused
+ /// the step to fail.
+ ///
+ ///
+ public Exception? Error { get; private set; }
+
+ ///
+ public bool IsCancelled { get; private set; }
///
/// Initializes a new instance of the class.
@@ -36,55 +59,112 @@ protected PipelineStep(IServiceProvider serviceProvider)
Services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
Logger = serviceProvider.GetService()?.CreateLogger(GetType());
}
-
+
+ ///
+ public Task RunAsync(CancellationToken token)
+ {
+ var task = ExecuteStepAsync(token);
+ _completionSource.TrySetResult(task);
+ return GetStepTask();
+ }
+
+ ///
+ public TaskAwaiter GetAwaiter()
+ {
+ return GetStepTask().GetAwaiter();
+ }
+
///
- public void Run(CancellationToken token)
+ public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
+ {
+ return GetStepTask().ConfigureAwait(continueOnCapturedContext);
+ }
+
+ ///
+ /// Returns a string that represents the current instance.
+ ///
+ ///
+ /// A string that represents the current instance, typically the name of the step's type.
+ ///
+ public override string ToString()
+ {
+ return GetType().Name;
+ }
+
+ /// 0
+ /// Executes this step.
+ ///
+ /// Provided to allow cancellation.
+ protected abstract Task RunCoreAsync(CancellationToken token);
+
+ private async Task CreateAwaitableTask()
+ {
+ var task = await _completionSource.Task.ConfigureAwait(false);
+ await task.ConfigureAwait(false);
+ }
+
+ private Task GetStepTask()
+ {
+ var existing = Volatile.Read(ref _exposedTask);
+ if (existing is not null)
+ return existing;
+
+ var tcsTask = _completionSource.Task;
+ if (tcsTask is { IsCompleted: true, Status: TaskStatus.RanToCompletion })
+ {
+ var result = tcsTask.Result;
+ var original = Interlocked.CompareExchange(ref _exposedTask, result, null);
+ return original ?? result;
+ }
+
+ var newTask = CreateAwaitableTask();
+ var prev = Interlocked.CompareExchange(ref _exposedTask, newTask, null);
+ return prev ?? newTask;
+ }
+
+ private async Task ExecuteStepAsync(CancellationToken token)
{
Logger?.LogTrace("BEGIN: {Step}", this);
try
{
- RunCore(token);
+ await RunCoreAsync(token).ConfigureAwait(false);
Logger?.LogTrace("END: {Step}", this);
}
catch (OperationCanceledException ex)
{
Error = ex.InnerException;
+ IsCancelled = true;
throw;
}
catch (StopRunnerException)
{
throw;
}
- catch (AggregateException ex)
+ catch (AggregateException e)
{
- if (!ex.IsExceptionType())
- LogFaultException(ex);
+ if (e.IsExceptionType())
+ {
+ Error = e.FindException()?.InnerException;
+ IsCancelled = true;
+ }
else
- Error = ex.InnerExceptions.FirstOrDefault(p => p.IsExceptionType())?.InnerException;
+ {
+ Error = e;
+ LogFaultException(e);
+ }
+
throw;
}
catch (Exception e)
{
+ Error = e;
LogFaultException(e);
throw;
}
}
- ///
- public override string ToString()
- {
- return GetType().Name;
- }
-
- ///
- /// Executes this step.
- ///
- /// Provided to allow cancellation.
- protected abstract void RunCore(CancellationToken token);
-
private void LogFaultException(Exception ex)
{
- Error = ex;
Logger?.LogError(ex, ex.InnerException?.Message ?? ex.Message);
}
}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs
index 7a13f29b..85f243ad 100644
--- a/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Steps/RunPipelineStep.cs
@@ -1,34 +1,55 @@
using System;
-using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps;
///
-/// A step that executes a pipeline and waits for the pipeline to end.
+/// Represents a pipeline step that executes a specific .
///
-/// The pipeline to execute.
-/// The service provider
-public class RunPipelineStep(IPipeline pipeline, IServiceProvider serviceProvider) : SynchronizedStep(serviceProvider)
+///
+/// This step is responsible for running the provided pipeline and managing its lifecycle,
+/// including handling exceptions and disposing of resources.
+///
+public sealed class RunPipelineStep : PipelineStep
{
- private readonly IPipeline _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
+ private readonly IPipeline _pipeline;
- ///
- protected override void RunSynchronized(CancellationToken token)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The pipeline to be executed by this step.
+ /// The service provider used to resolve dependencies.
+ ///
+ /// Thrown when or is .
+ ///
+ public RunPipelineStep(IPipeline pipeline, IServiceProvider serviceProvider) : base(serviceProvider)
+ {
+ _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline));
+ }
+
+ ///
+ /// Executes the core logic of the pipeline step asynchronously.
+ ///
+ ///
+ /// This method is responsible for running the associated and handling its lifecycle.
+ /// It logs the start and completion of the pipeline execution, and captures any exceptions that occur during execution.
+ ///
+ /// A to observe while waiting for the task to complete.
+ /// A representing the asynchronous operation.
+ /// The pipeline execution encounters an error.
+ protected override async Task RunCoreAsync(CancellationToken token)
{
Logger?.LogTrace("Running {Pipeline}...", _pipeline);
try
{
- // ReSharper disable once MethodSupportsCancellation
- _pipeline.RunAsync(token).Wait();
+ await _pipeline.RunAsync(token).ConfigureAwait(false);
Logger?.LogTrace("Finished {Pipeline}", _pipeline);
}
- catch (AggregateException e)
+ catch (Exception e)
{
- var root = e.InnerExceptions.FirstOrDefault();
- if (root is not null)
- throw root;
+ Logger?.LogError(e, "Pipeline {Pipeline} finished with exception: {Message}", _pipeline, e.Message);
throw;
}
}
diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/SynchronizedStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/SynchronizedStep.cs
deleted file mode 100644
index 618c1ba3..00000000
--- a/src/CommonUtilities.SimplePipeline/src/Steps/SynchronizedStep.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using System;
-using System.Threading;
-
-namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps;
-
-///
-/// A step that can be waited for.
-///
-public abstract class SynchronizedStep : PipelineStep
-{
- ///
- /// Event gets raised if this instance failed with an .
- ///
- public event EventHandler? Canceled;
-
- private readonly ManualResetEvent _handle;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The service provider.
- /// is .
- protected SynchronizedStep(IServiceProvider serviceProvider) : base(serviceProvider)
- {
- _handle = new ManualResetEvent(false);
- }
-
- ///
- /// Waits until the predefined stepRunner has finished.
- ///
- public void Wait()
- {
- Wait(Timeout.InfiniteTimeSpan);
- }
-
- ///
- /// Waits until the predefined stepRunner has finished.
- ///
- /// The time duration to wait.
- /// If .
- public void Wait(TimeSpan timeout)
- {
- if (!_handle.WaitOne(timeout))
- throw new TimeoutException();
- }
-
- ///
- /// Executes this step.
- ///
- ///
- protected abstract void RunSynchronized(CancellationToken token);
-
- ///
- protected override void DisposeResources()
- {
- base.DisposeResources();
- _handle.Dispose();
- }
-
- ///
- protected sealed override void RunCore(CancellationToken token)
- {
- try
- {
- RunSynchronized(token);
- }
- catch (Exception ex)
- {
- if (ex.IsExceptionType())
- Canceled?.Invoke(this, EventArgs.Empty);
- throw;
- }
- finally
- {
- _handle.Set();
- }
- }
-}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs b/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs
index d8a56160..8f2b4dd2 100644
--- a/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs
+++ b/src/CommonUtilities.SimplePipeline/src/Steps/WaitStep.cs
@@ -1,16 +1,17 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
+using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace AnakinRaW.CommonUtilities.SimplePipeline.Steps;
///
-/// A step that waits for a given to finish.
+/// A step that waits for a given to finish.
///
public sealed class WaitStep : PipelineStep
{
- private readonly IParallelStepRunner _stepRunner;
+ private readonly IStepRunner _stepRunner;
///
/// Initializes a new instance of the class with the specified stepRunner.
@@ -18,29 +19,26 @@ public sealed class WaitStep : PipelineStep
/// The step runner.
/// The service provider.
/// or is .
- public WaitStep(IParallelStepRunner stepRunner, IServiceProvider serviceProvider) : base(serviceProvider)
+ public WaitStep(IStepRunner stepRunner, IServiceProvider serviceProvider) : base(serviceProvider)
{
_stepRunner = stepRunner ?? throw new ArgumentNullException(nameof(stepRunner));
}
///
[ExcludeFromCodeCoverage]
- public override string ToString() => "Waiting for other steps";
+ public override string ToString() => "Waiting for other step runner";
///
/// Waits for the instance's parallel stepRunner.
///
/// Provided to allow cancellation.
/// If awaiting the stepRunner failed with an exception.
- protected override void RunCore(CancellationToken token)
+ protected override async Task RunCoreAsync(CancellationToken token)
{
- try
+ await _stepRunner;
+ if (_stepRunner.Exception is not null)
{
- _stepRunner.Wait();
- }
- catch
- {
- Logger?.LogTrace("Wait step is stopping all subsequent steps");
+ Logger?.LogTrace("The other step runner finished with failed steps. Stopping current runner.");
throw new StopRunnerException();
}
}
diff --git a/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs b/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs
index 645e2690..2154c29c 100644
--- a/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs
+++ b/src/CommonUtilities.SimplePipeline/src/StopRunnerException.cs
@@ -2,4 +2,31 @@
namespace AnakinRaW.CommonUtilities.SimplePipeline;
-internal class StopRunnerException : Exception;
\ No newline at end of file
+///
+/// The exception that is thrown to signal an the termination of step execution.
+///
+///
+/// This exception is typically thrown by an to indicate that its associated
+/// should stop executing any further steps.
+///
+public sealed class StopRunnerException : Exception
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// This constructor creates a default instance of the exception without any additional context or message.
+ /// It is typically used to signal the termination of step execution in a pipeline.
+ ///
+ public StopRunnerException() : base("Stopping step runner.")
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The message that describes the error.
+ public StopRunnerException(string? message) : base(message)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj
index fd1fc46e..4dc7d23b 100644
--- a/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj
+++ b/src/CommonUtilities.SimplePipeline/test/CommonUtilities.SimplePipeline.Test.csproj
@@ -1,10 +1,11 @@
-
+
net10.0;net8.0
$(TargetFrameworks);net481
false
true
+ Exe
@@ -12,20 +13,16 @@
AnakinRaW.CommonUtilities.SimplePipeline.Test
-
- enable
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
-
+
+
+
+
+
+
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
@@ -37,8 +34,8 @@
-
+
diff --git a/src/CommonUtilities.SimplePipeline/test/ConfigureAwaitTestExtensions.cs b/src/CommonUtilities.SimplePipeline/test/ConfigureAwaitTestExtensions.cs
new file mode 100644
index 00000000..8526973b
--- /dev/null
+++ b/src/CommonUtilities.SimplePipeline/test/ConfigureAwaitTestExtensions.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace AnakinRaW.CommonUtilities.SimplePipeline.Test;
+
+// Based on https://github.com/dotnet/runtime TaskAwaiterTests.cs
+
+public static class ConfigureAwaitTestExtensions
+{
+ public static void AwaiterAndAwaitableEquality(
+ Func instanceFactory,
+ Func awaitableFactory,
+ Func configuredAwaitableFactory)
+ {
+ var instance = instanceFactory();
+
+ // TaskAwaiter
+ Assert.Equal(awaitableFactory(instance), awaitableFactory(instance));
+
+ // ConfiguredTaskAwaitable
+ Assert.Equal(configuredAwaitableFactory(instance, false), configuredAwaitableFactory(instance, false));
+ Assert.NotEqual(configuredAwaitableFactory(instance, false), configuredAwaitableFactory(instance, true));
+ Assert.NotEqual(configuredAwaitableFactory(instance, true), configuredAwaitableFactory(instance, false));
+
+ // ConfiguredTaskAwaitable.ConfiguredTaskAwaiter
+ Assert.Equal(configuredAwaitableFactory(instance, false).GetAwaiter(), configuredAwaitableFactory(instance, false).GetAwaiter());
+ Assert.NotEqual(configuredAwaitableFactory(instance, false).GetAwaiter(), configuredAwaitableFactory(instance, true).GetAwaiter());
+ Assert.NotEqual(configuredAwaitableFactory(instance, true).GetAwaiter(), configuredAwaitableFactory(instance, false).GetAwaiter());
+ }
+
+ public static void TestOnCompletedCompletesInAnotherSynchronizationContext(
+ bool? continueOnCapturedContext,
+ Func instanceFactory,
+ Func awaitableFactory,
+ Func configuredAwaitableFactory,
+ Action completeInstance)
+ {
+ var origCtx = SynchronizationContext.Current;
+ try
+ {
+ var validateCtx = new ValidateCorrectContextSynchronizationContext();
+ Assert.Equal(0, validateCtx.PostCount);
+ SynchronizationContext.SetSynchronizationContext(validateCtx);
+
+ var mre = new ManualResetEventSlim();
+
+ var instance = instanceFactory();
+
+ // Hook up a callback
+ var postedInContext = false;
+ var callback = () =>
+ {
+ postedInContext = ValidateCorrectContextSynchronizationContext.IsPostedInContext;
+ mre.Set();
+ };
+
+ if (continueOnCapturedContext.HasValue)
+ configuredAwaitableFactory(instance, continueOnCapturedContext.Value).GetAwaiter().OnCompleted(callback);
+ else
+ awaitableFactory(instance).OnCompleted(callback);
+
+ Assert.False(mre.IsSet, "Callback should not yet have run.");
+
+ // Complete the task in another context and wait for the callback to run
+ Task.Run(() => completeInstance(instance), TestContext.Current.CancellationToken);
+ mre.Wait(TestContext.Current.CancellationToken);
+
+ // Validate the callback ran and in the correct context
+ var shouldHavePosted = !continueOnCapturedContext.HasValue || continueOnCapturedContext.Value;
+ Assert.Equal(shouldHavePosted ? 1 : 0, validateCtx.PostCount);
+ Assert.Equal(shouldHavePosted, postedInContext);
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(origCtx);
+ }
+ }
+
+ private class ValidateCorrectContextSynchronizationContext : SynchronizationContext
+ {
+ [ThreadStatic]
+ internal static bool IsPostedInContext;
+
+ internal int PostCount;
+ private int _sendCount;
+
+ public override void Post(SendOrPostCallback d, object? state)
+ {
+ Interlocked.Increment(ref PostCount);
+ Task.Run(() =>
+ {
+ SetSynchronizationContext(this);
+ try
+ {
+ IsPostedInContext = true;
+ d(state);
+ }
+ finally
+ {
+ IsPostedInContext = false;
+ SetSynchronizationContext(null);
+ }
+ });
+ }
+
+ public override void Send(SendOrPostCallback d, object? state)
+ {
+ Interlocked.Increment(ref _sendCount);
+ d(state);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs b/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs
index ba25f272..dc2ac2aa 100644
--- a/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs
+++ b/src/CommonUtilities.SimplePipeline/test/ExtensionsTest.cs
@@ -1,26 +1,168 @@
-using System;
+using AnakinRaW.CommonUtilities.Testing;
+using System;
using System.Collections.Generic;
using System.IO;
+using System.Reflection;
using Xunit;
namespace AnakinRaW.CommonUtilities.SimplePipeline.Test;
-public class ExtensionsTest
-{
- [Fact]
- public void IsExceptionType()
+public class ExtensionsTest : TestBaseWithServiceProvider
+{
+ public static IEnumerable