diff --git a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs index ac6b8d718..b5d98cb98 100644 --- a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs +++ b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs @@ -96,4 +96,14 @@ public async Task GivenASequenceOfSequencesInterleaveReturnsTheExpectedSequence( expected = (expected % 4) + 1; } } + + [Fact] + public async Task CancellationIsPropagated() + { + // System.Linq.Async throws for an already canceled token, so the interleaved sequence + // is expected to be canceled after the inner sequences have observed the token. + var canceledToken = new CancellationToken(canceled: true); + var interleaved = new AssertIsCancellationRequestedAsyncSequence().Interleave(new AssertIsCancellationRequestedAsyncSequence()); + await Assert.ThrowsAsync(async () => await interleaved.ToListAsync(canceledToken)); + } } diff --git a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/MergeTest.cs b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/MergeTest.cs index eac5b503c..80126316f 100644 --- a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/MergeTest.cs +++ b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/MergeTest.cs @@ -75,4 +75,14 @@ public Task MergeASequenceWithADifferentComparer() return AsyncAssert.Equal(expected, sequence1.Merge(sequence2, DescendingIntComparer.Create())); } + + [Fact] + public async Task CancellationIsPropagated() + { + // System.Linq.Async throws for an already canceled token, so the merged sequence + // is expected to be canceled after the inner sequences have observed the token. + var canceledToken = new CancellationToken(canceled: true); + var merged = new AssertIsCancellationRequestedAsyncSequence().Merge(new AssertIsCancellationRequestedAsyncSequence()); + await Assert.ThrowsAsync(async () => await merged.ToListAsync(canceledToken)); + } } diff --git a/Funcky.Async/CompatibilitySuppressions.xml b/Funcky.Async/CompatibilitySuppressions.xml new file mode 100644 index 000000000..e8efe9589 --- /dev/null +++ b/Funcky.Async/CompatibilitySuppressions.xml @@ -0,0 +1,82 @@ + + + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1},System.Collections.Generic.IEqualityComparer{``1}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1},System.Func{``0,``2},System.Collections.Generic.IEqualityComparer{``1}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1},System.Func{``0,``2}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwait``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.Tasks.ValueTask{``1}},System.Collections.Generic.IEqualityComparer{``1}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwait``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.Tasks.ValueTask{``1}}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwait``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.Tasks.ValueTask{``1}},System.Func{``0,System.Threading.Tasks.ValueTask{``2}},System.Collections.Generic.IEqualityComparer{``1}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwait``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.Tasks.ValueTask{``1}},System.Func{``0,System.Threading.Tasks.ValueTask{``2}}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwaitWithCancellation``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``1}},System.Collections.Generic.IEqualityComparer{``1}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwaitWithCancellation``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``1}}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwaitWithCancellation``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``1}},System.Func{``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``2}},System.Collections.Generic.IEqualityComparer{``1}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwaitWithCancellation``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``1}},System.Func{``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``2}}) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + + CP0002 + M:Funcky.Extensions.AsyncEnumerableExtensions.Chunk``1(System.Collections.Generic.IAsyncEnumerable{``0},System.Int32) + lib/net9.0/Funcky.Async.dll + lib/net10.0/Funcky.Async.dll + + \ No newline at end of file diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Interleave.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Interleave.cs index 123181aaa..7189c9e2a 100644 --- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Interleave.cs +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Interleave.cs @@ -34,7 +34,7 @@ private static async IAsyncEnumerable InterleaveInternal( try { - await foreach (var element in InterleaveEnumeratorAsync(enumerators).ConfigureAwait(false)) + await foreach (var element in InterleaveEnumeratorAsync(enumerators, cancellationToken).ConfigureAwait(false)) { yield return element; } @@ -54,11 +54,11 @@ private static ImmutableList> GetInterleaveEnumerators CancellationToken cancellationToken) => source.Select(s => s.GetAsyncEnumerator(cancellationToken)).ToImmutableList(); - private static async IAsyncEnumerable InterleaveEnumeratorAsync(ImmutableList> enumerators) + private static async IAsyncEnumerable InterleaveEnumeratorAsync(ImmutableList> enumerators, [EnumeratorCancellation] CancellationToken cancellationToken) { while (!enumerators.IsEmpty) { - enumerators = enumerators.RemoveRange(await enumerators.ToAsyncEnumerable().WhereAwait(async f => await HasMoreElements(f).ConfigureAwait(false)).ToListAsync().ConfigureAwait(false)); + enumerators = enumerators.RemoveRange(await enumerators.ToAsyncEnumerable().WhereAwait(async f => await HasMoreElements(f).ConfigureAwait(false)).ToListAsync(cancellationToken).ConfigureAwait(false)); foreach (var enumerator in enumerators) { yield return enumerator.Current; diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs index 1460aaba9..7d1b12f83 100644 --- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Runtime.CompilerServices; namespace Funcky.Extensions; @@ -55,13 +56,20 @@ public static IAsyncEnumerable Merge(this IAsyncEnumerableThe type elements in the sequences. /// The merged sequences in the same order as the given sequences. [Pure] - public static async IAsyncEnumerable Merge(this IEnumerable> sources, Option> comparer = default) + public static IAsyncEnumerable Merge(this IEnumerable> sources, Option> comparer = default) + => MergeInternal(sources, comparer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static async IAsyncEnumerable MergeInternal( + IEnumerable> sources, + Option> comparer, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var enumerators = GetMergeEnumerators(sources); + var enumerators = GetMergeEnumerators(sources, cancellationToken); try { - await foreach (var element in MergeEnumerators(enumerators.RemoveRange(await enumerators.ToAsyncEnumerable().WhereAwait(async f => await HasMoreElements(f).ConfigureAwait(false)).ToListAsync().ConfigureAwait(false)), GetMergeComparer(comparer)).ConfigureAwait(false)) + await foreach (var element in MergeEnumerators(enumerators.RemoveRange(await enumerators.ToAsyncEnumerable().WhereAwait(async f => await HasMoreElements(f).ConfigureAwait(false)).ToListAsync(cancellationToken).ConfigureAwait(false)), GetMergeComparer(comparer)).ConfigureAwait(false)) { yield return element; } @@ -75,8 +83,8 @@ public static async IAsyncEnumerable Merge(this IEnumerable> GetMergeEnumerators(IEnumerable> sources) - => ImmutableList.Create>().AddRange(sources.Select(s => s.GetAsyncEnumerator())); + private static ImmutableList> GetMergeEnumerators(IEnumerable> sources, CancellationToken cancellationToken) + => ImmutableList.Create>().AddRange(sources.Select(s => s.GetAsyncEnumerator(cancellationToken))); private static IComparer GetMergeComparer(Option> comparer = default) => comparer.GetOrElse(Comparer.Default); diff --git a/Funcky.Async/Funcky.Async.csproj b/Funcky.Async/Funcky.Async.csproj index 48f8169c2..0394fa70d 100644 --- a/Funcky.Async/Funcky.Async.csproj +++ b/Funcky.Async/Funcky.Async.csproj @@ -1,12 +1,16 @@ + net9.0 - $(FunckyNewestTargetFramework);net8.0;net5.0;netstandard2.1;netstandard2.0 + net10.0;$(FunckyNewestTargetFramework);net8.0;net5.0;netstandard2.1;netstandard2.0 preview enable - Extends Funcky with support for IAsyncEnumerable and Tasks. + Extends Funcky with support for IAsyncEnumerable and Tasks on frameworks older than .NET 10. On .NET 10 and newer these APIs are part of the Funcky package and this package only forwards to them. Functional Async Monad Linq - 1.4.1 + 1.5.0 true snupkg icon.png @@ -23,6 +27,9 @@ Funcky $(DefineConstants);CONTRACTS_FULL + + true + true true @@ -30,7 +37,12 @@ true - + + + + + + @@ -42,11 +54,11 @@ + - diff --git a/Funcky.Async/TypeForwards.cs b/Funcky.Async/TypeForwards.cs new file mode 100644 index 000000000..c2eca273b --- /dev/null +++ b/Funcky.Async/TypeForwards.cs @@ -0,0 +1,22 @@ +// On .NET 10 and newer, Funcky's async APIs are part of the Funcky package itself. +// This asset of Funcky.Async only exists so that projects referencing both packages +// (e.g. after upgrading from an older framework) keep compiling: every public type +// that used to live here is forwarded to its new home in Funcky. +using System.Runtime.CompilerServices; +using Funcky; + +[assembly: TypeForwardedTo(typeof(AsyncFunctional))] +[assembly: TypeForwardedTo(typeof(AsyncSequence))] +[assembly: TypeForwardedTo(typeof(IAsyncBuffer<>))] +[assembly: TypeForwardedTo(typeof(AsyncEnumerableExtensions))] +[assembly: TypeForwardedTo(typeof(OptionAsyncExtensions))] +[assembly: TypeForwardedTo(typeof(ResultAsyncExtensions))] +[assembly: TypeForwardedTo(typeof(EitherAsyncExtensions))] +[assembly: TypeForwardedTo(typeof(OptionTaskAwaiter))] +[assembly: TypeForwardedTo(typeof(OptionTaskAwaiter<>))] +[assembly: TypeForwardedTo(typeof(OptionValueTaskAwaiter))] +[assembly: TypeForwardedTo(typeof(OptionValueTaskAwaiter<>))] +[assembly: TypeForwardedTo(typeof(ConfiguredOptionTaskAwaitable))] +[assembly: TypeForwardedTo(typeof(ConfiguredOptionTaskAwaitable<>))] +[assembly: TypeForwardedTo(typeof(ConfiguredOptionValueTaskAwaitable))] +[assembly: TypeForwardedTo(typeof(ConfiguredOptionValueTaskAwaitable<>))] diff --git a/Funcky.Async/package.md b/Funcky.Async/package.md index 837026449..2c24ed9bf 100644 --- a/Funcky.Async/package.md +++ b/Funcky.Async/package.md @@ -1,6 +1,11 @@ ## About Provides async counterparts for [Funcky]'s `IEnumerable` extensions, -`IEnumerable` generators, traversal and retrying. +`IEnumerable` generators, traversal and retrying for frameworks older than .NET 10. + +Starting with .NET 10, `IAsyncEnumerable` LINQ is part of the base class library and +these APIs are part of the [Funcky] package itself. The `net10.0` asset of this package +only forwards its types to `Funcky`, so existing projects keep compiling. Once you target +.NET 10 or newer, you can remove the reference to `Funcky.Async`. ## Main Types * `Funcky.AsyncSequence`—Generate asynchronous sequences. diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs index 9dbbb5ea8..659685322 100644 --- a/Funcky.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs +++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs @@ -97,5 +97,12 @@ public async Task GivenASequenceOfSequencesInterleaveReturnsTheExpectedSequence( expected = (expected % 4) + 1; } } + + [Fact] + public async Task CancellationIsPropagated() + { + var canceledToken = new CancellationToken(canceled: true); + _ = await new AssertIsCancellationRequestedAsyncSequence().Interleave(new AssertIsCancellationRequestedAsyncSequence()).ToListAsync(canceledToken); + } } #endif diff --git a/Funcky/Funcky.csproj b/Funcky/Funcky.csproj index 5fb6f812e..8f65f835d 100644 --- a/Funcky/Funcky.csproj +++ b/Funcky/Funcky.csproj @@ -7,7 +7,7 @@ Funcky Funcky is a functional C# library Functional Monad Linq - 3.6.0 + 3.6.1 true snupkg README.md diff --git a/Funcky/Functional/RetryWithException.cs b/Funcky/Functional/RetryWithException.cs index c46a8da1a..8ce436f5f 100644 --- a/Funcky/Functional/RetryWithException.cs +++ b/Funcky/Functional/RetryWithException.cs @@ -7,7 +7,7 @@ public static partial class Functional /// Retries a producer as long as an exception matching the predicate is thrown. /// When all retries are exhausted, the exception is propagated to the caller. /// Note that this function uses for the delay. - /// Consider using RetryAsync from the Funcky.Async package instead. + /// Consider using AsyncFunctional.RetryAsync instead (part of Funcky on .NET 10 and newer, part of the Funcky.Async package on older frameworks). public static TResult Retry(Func producer, Func shouldRetry, IRetryPolicy retryPolicy) { var retryCount = 0; diff --git a/README.md b/README.md index 6b2a1bff8..4bab8a73a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Funcky is a functional library for C# which encourages correct usage of the func * **Funcky** \ [![NuGet Version](https://img.shields.io/nuget/v/Funcky)](https://www.nuget.org/packages/Funcky) -* **Funcky.Async** \ +* **Funcky.Async**: Async extensions for frameworks older than .NET 10. On .NET 10 and newer these APIs are part of **Funcky** itself. \ [![NuGet Version](https://img.shields.io/nuget/v/Funcky.Async)](https://www.nuget.org/packages/Funcky.Async) * **Funcky.XUnit** \ [![NuGet Version](https://img.shields.io/nuget/v/Funcky.XUnit)](https://www.nuget.org/packages/Funcky.XUnit) diff --git a/changelog.md b/changelog.md index 08f5421e9..165512dd2 100644 --- a/changelog.md +++ b/changelog.md @@ -3,13 +3,30 @@ All notable changes to this project will be documented in this file. Funcky adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Funcky 3.6.1 | Funcky.Async 1.5.0 + +* Add a `net10.0` target to `Funcky.Async` that only forwards all its types to `Funcky`. + Projects that reference both `Funcky` and `Funcky.Async` no longer get ambiguous type errors + (`CS0433`) when they move to .NET 10. On .NET 10 you can simply remove the `Funcky.Async` reference. +* Fix: `Interleave` and `Merge` for `IAsyncEnumerable` now pass the cancellation token to the inner + enumerators (same fix as in Funcky 3.6.0). + ## Funcky 3.6.0 This update is mainly to update to .NET 10. -* remove dependency to System.Linq.Async because it is integrated into .NET 10 -* Integrate Funcky.Async into Funcky, as the async extensions are now in the same assembly as the sync ones. - * This means that there is no longer a separate `Funcky.Async` package. +* Funcky now targets `net10.0`. +* On .NET 10 and newer, the `IAsyncEnumerable` extensions, `AsyncSequence`, `AsyncFunctional` and the + async `Option`/`Result`/`Either` extensions are part of the `Funcky` package itself, because + `System.Linq.AsyncEnumerable` is now part of the base class library. Funcky does not depend on + `System.Linq.Async` on this framework. +* `Funcky.Async` stays the home of these APIs on .NET 9 and older frameworks, where it continues to + depend on `System.Linq.Async`. +* The `Chunk(int)` overload for `IAsyncEnumerable` is not provided on .NET 10; the identically named + method in the base class library is used instead and returns arrays rather than `IReadOnlyList`. + The `Chunk` overloads with a `resultSelector` are unchanged. +* `AdjacentGroupBy` for `IAsyncEnumerable` returns `IGrouping` on .NET 10 + instead of the `IAsyncGrouping` type from `System.Linq.Async`. ## Funcky 3.5.1 | Funcky.Async 1.4.1 | Funcky.Xunit 2.1.1 | Funcky.Analyzers 1.4.1