Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit>().Interleave(new AssertIsCancellationRequestedAsyncSequence<Unit>());
await Assert.ThrowsAsync<OperationCanceledException>(async () => await interleaved.ToListAsync(canceledToken));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit>().Merge(new AssertIsCancellationRequestedAsyncSequence<Unit>());
await Assert.ThrowsAsync<OperationCanceledException>(async () => await merged.ToListAsync(canceledToken));
}
}
82 changes: 82 additions & 0 deletions Funcky.Async/CompatibilitySuppressions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1},System.Collections.Generic.IEqualityComparer{``1})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1},System.Func{``0,``2},System.Collections.Generic.IEqualityComparer{``1})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupBy``3(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,``1},System.Func{``0,``2})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwait``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.Tasks.ValueTask{``1}},System.Collections.Generic.IEqualityComparer{``1})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwait``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.Tasks.ValueTask{``1}})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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}})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.AdjacentGroupByAwaitWithCancellation``2(System.Collections.Generic.IAsyncEnumerable{``0},System.Func{``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask{``1}})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>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}})</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Funcky.Extensions.AsyncEnumerableExtensions.Chunk``1(System.Collections.Generic.IAsyncEnumerable{``0},System.Int32)</Target>
<Left>lib/net9.0/Funcky.Async.dll</Left>
<Right>lib/net10.0/Funcky.Async.dll</Right>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private static async IAsyncEnumerable<TSource> InterleaveInternal<TSource>(

try
{
await foreach (var element in InterleaveEnumeratorAsync(enumerators).ConfigureAwait(false))
await foreach (var element in InterleaveEnumeratorAsync(enumerators, cancellationToken).ConfigureAwait(false))
{
yield return element;
}
Expand All @@ -54,11 +54,11 @@ private static ImmutableList<IAsyncEnumerator<TSource>> GetInterleaveEnumerators
CancellationToken cancellationToken)
=> source.Select(s => s.GetAsyncEnumerator(cancellationToken)).ToImmutableList();

private static async IAsyncEnumerable<TSource> InterleaveEnumeratorAsync<TSource>(ImmutableList<IAsyncEnumerator<TSource>> enumerators)
private static async IAsyncEnumerable<TSource> InterleaveEnumeratorAsync<TSource>(ImmutableList<IAsyncEnumerator<TSource>> 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;
Expand Down
18 changes: 13 additions & 5 deletions Funcky.Async/Extensions/AsyncEnumerableExtensions/Merge.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Runtime.CompilerServices;

namespace Funcky.Extensions;

Expand Down Expand Up @@ -55,13 +56,20 @@ public static IAsyncEnumerable<TSource> Merge<TSource>(this IAsyncEnumerable<TSo
/// <typeparam name="TSource">The type elements in the sequences.</typeparam>
/// <returns>The merged sequences in the same order as the given sequences.</returns>
[Pure]
public static async IAsyncEnumerable<TSource> Merge<TSource>(this IEnumerable<IAsyncEnumerable<TSource>> sources, Option<IComparer<TSource>> comparer = default)
public static IAsyncEnumerable<TSource> Merge<TSource>(this IEnumerable<IAsyncEnumerable<TSource>> sources, Option<IComparer<TSource>> comparer = default)
=> MergeInternal(sources, comparer);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async IAsyncEnumerable<TSource> MergeInternal<TSource>(
IEnumerable<IAsyncEnumerable<TSource>> sources,
Option<IComparer<TSource>> 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;
}
Expand All @@ -75,8 +83,8 @@ public static async IAsyncEnumerable<TSource> Merge<TSource>(this IEnumerable<IA
}
}

private static ImmutableList<IAsyncEnumerator<TSource>> GetMergeEnumerators<TSource>(IEnumerable<IAsyncEnumerable<TSource>> sources)
=> ImmutableList.Create<IAsyncEnumerator<TSource>>().AddRange(sources.Select(s => s.GetAsyncEnumerator()));
private static ImmutableList<IAsyncEnumerator<TSource>> GetMergeEnumerators<TSource>(IEnumerable<IAsyncEnumerable<TSource>> sources, CancellationToken cancellationToken)
=> ImmutableList.Create<IAsyncEnumerator<TSource>>().AddRange(sources.Select(s => s.GetAsyncEnumerator(cancellationToken)));

private static IComparer<TSource> GetMergeComparer<TSource>(Option<IComparer<TSource>> comparer = default)
=> comparer.GetOrElse(Comparer<TSource>.Default);
Expand Down
22 changes: 17 additions & 5 deletions Funcky.Async/Funcky.Async.csproj
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Starting with .NET 10, IAsyncEnumerable LINQ is part of the base class library and Funcky's
async APIs live in the Funcky package itself. The net10.0 asset of this package therefore only
contains type forwards to Funcky, so projects that reference both packages keep compiling.
The public API is tracked on the newest framework that still carries the implementation. -->
<FunckyNewestTargetFramework>net9.0</FunckyNewestTargetFramework>
<TargetFrameworks>$(FunckyNewestTargetFramework);net8.0;net5.0;netstandard2.1;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net10.0;$(FunckyNewestTargetFramework);net8.0;net5.0;netstandard2.1;netstandard2.0</TargetFrameworks>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<Description>Extends Funcky with support for IAsyncEnumerable and Tasks.</Description>
<Description>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.</Description>
<PackageTags>Functional Async Monad Linq</PackageTags>
<VersionPrefix>1.4.1</VersionPrefix>
<VersionPrefix>1.5.0</VersionPrefix>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageIcon>icon.png</PackageIcon>
Expand All @@ -23,14 +27,22 @@
<RootNamespace>Funcky</RootNamespace>
<DefineConstants>$(DefineConstants);CONTRACTS_FULL</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">
<FunckyAsyncIntegrated>true</FunckyAsyncIntegrated>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ItemGroup Condition="'$(FunckyAsyncIntegrated)' == 'true'">
<Compile Remove="**\*.cs" />
<Compile Include="TypeForwards.cs" />
</ItemGroup>
<ItemGroup Condition="'$(FunckyAsyncIntegrated)' != 'true'">
<Compile Remove="TypeForwards.cs" />
<Compile Include="..\Funcky\Internal\Aggregators\DecimalAverageAggregator.cs" Link="Internal\Aggregators\DecimalAverageAggregator.cs" />
<Compile Include="..\Funcky\Internal\Aggregators\DoubleAverageAggregator.cs" Link="Internal\Aggregators\DoubleAverageAggregator.cs" />
<Compile Include="..\Funcky\Internal\Aggregators\FloatAverageAggregator.cs" Link="Internal\Aggregators\FloatAverageAggregator.cs" />
Expand All @@ -42,11 +54,11 @@
<Compile Include="..\Funcky\Internal\Validators\WindowWidthValidator.cs" Link="Internal\Validators\WindowWidthValidator.cs" />
<Compile Include="..\Funcky\Internal\Validators\ChunkSizeValidator.cs" Link="Internal\Validators\ChunkSizeValidator.cs" />
<Compile Include="..\Funcky\Internal\PartitionBuilder.cs" Link="Internal\PartitionBuilder.cs" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="PolySharp" PrivateAssets="all" />
<PackageReference Include="Polyadic.Build.SemanticVersioning" PrivateAssets="all" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Funcky\Funcky.csproj" />
Expand Down
22 changes: 22 additions & 0 deletions Funcky.Async/TypeForwards.cs
Original file line number Diff line number Diff line change
@@ -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<>))]
7 changes: 6 additions & 1 deletion Funcky.Async/package.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Unit>().Interleave(new AssertIsCancellationRequestedAsyncSequence<Unit>()).ToListAsync(canceledToken);
}
}
#endif
2 changes: 1 addition & 1 deletion Funcky/Funcky.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Product>Funcky</Product>
<Description>Funcky is a functional C# library</Description>
<PackageTags>Functional Monad Linq</PackageTags>
<VersionPrefix>3.6.0</VersionPrefix>
<VersionPrefix>3.6.1</VersionPrefix>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageReadmeFile>README.md</PackageReadmeFile>
Expand Down
2 changes: 1 addition & 1 deletion Funcky/Functional/RetryWithException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public static partial class Functional
/// <summary>Retries a producer as long as an exception matching the <paramref name="shouldRetry"/> predicate is thrown.
/// When all retries are exhausted, the exception is propagated to the caller.</summary>
/// <remarks>Note that this function uses <see cref="Thread.Sleep(TimeSpan)"/> for the delay.
/// Consider using <c>RetryAsync</c> from the <c>Funcky.Async</c> package instead.</remarks>
/// Consider using <c>AsyncFunctional.RetryAsync</c> instead (part of Funcky on .NET 10 and newer, part of the <c>Funcky.Async</c> package on older frameworks).</remarks>
public static TResult Retry<TResult>(Func<TResult> producer, Func<Exception, bool> shouldRetry, IRetryPolicy retryPolicy)
{
var retryCount = 0;
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading