├── .gitignore ├── banner.png ├── icon.png ├── AsyncEnumerable.pfx ├── src ├── IAsyncDisposable.cs ├── Extensions │ ├── ForEachAsyncBreakException.cs │ ├── ParallelForEachException.cs │ ├── ForEachAsync.cs │ ├── AdapterExtensions.cs │ └── ForEachAsyncExtensions.cs ├── AsyncEnumerationCanceledException.cs ├── Internals │ ├── CancellationTokenEx.cs │ ├── EmptyAsyncEnumerable.cs │ ├── EmptyAsyncEnumerator.cs │ ├── CurrentValueContainer.cs │ ├── TaskEx.cs │ ├── AsyncEnumerableWrapper.cs │ ├── EnumerableAdapter.cs │ ├── EnumeratorAdapter.cs │ ├── AsyncEnumeratorWrapper.cs │ └── TaskCompletionSource.cs ├── AssemblyInfo.cs ├── IAsyncEnumerator.cs ├── AsyncEnumerable.csproj ├── IAsyncEnumerable.cs ├── AsyncEnumerable.cs └── AsyncEnumerator.cs ├── netfx45 ├── tests │ ├── packages.config │ ├── AssemblyInfo.cs │ └── AsyncEnumerable.Tests.NetFx45.csproj └── lib │ ├── packages.config │ └── AsyncEnumerable.NetFx45.csproj ├── Tests ├── AsyncEnumerable.Tests.csproj ├── Program.cs ├── PerformanceTests.cs ├── ForEachAsyncTests.cs ├── AsyncEnumeratorTests.cs ├── EnumeratorLinqStyleExtensionsTests.cs └── EnumerableLinqStyleExtensionsTests.cs ├── LICENSE ├── AsyncEnumerable.Symbols.nuspec ├── AsyncEnumerable.sln ├── NUGET-DOC.md ├── AsyncEnumerable.nuspec └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | bin/ 3 | .vs/ 4 | *.user 5 | packages/ 6 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dasync/AsyncEnumerable/HEAD/banner.png -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dasync/AsyncEnumerable/HEAD/icon.png -------------------------------------------------------------------------------- /AsyncEnumerable.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dasync/AsyncEnumerable/HEAD/AsyncEnumerable.pfx -------------------------------------------------------------------------------- /src/IAsyncDisposable.cs: -------------------------------------------------------------------------------- 1 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 2 | using System.Threading.Tasks; 3 | 4 | namespace Dasync.Collections 5 | { 6 | public interface IAsyncDisposable 7 | { 8 | ValueTask DisposeAsync(); 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /src/Extensions/ForEachAsyncBreakException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dasync.Collections 4 | { 5 | /// 6 | /// This exception is thrown when you call . 7 | /// 8 | public sealed class ForEachAsyncBreakException : OperationCanceledException { } 9 | } 10 | -------------------------------------------------------------------------------- /src/AsyncEnumerationCanceledException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dasync.Collections 4 | { 5 | /// 6 | /// This exception is thrown when you call 7 | /// or when the enumerator is disposed before reaching the end of enumeration. 8 | /// 9 | public sealed class AsyncEnumerationCanceledException : OperationCanceledException { } 10 | } 11 | -------------------------------------------------------------------------------- /netfx45/tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /netfx45/lib/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Internals/CancellationTokenEx.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace Dasync.Collections.Internals 4 | { 5 | internal static class CancellationTokenEx 6 | { 7 | public static readonly CancellationToken Canceled; 8 | 9 | static CancellationTokenEx() 10 | { 11 | var cts = new CancellationTokenSource(); 12 | cts.Cancel(); 13 | Canceled = cts.Token; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Extensions/ParallelForEachException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Dasync.Collections 5 | { 6 | /// 7 | /// Used in ParallelForEachAsync<T> extension method 8 | /// 9 | public class ParallelForEachException : AggregateException 10 | { 11 | /// 12 | /// Constructor 13 | /// 14 | public ParallelForEachException(IEnumerable innerExceptions) 15 | : base(innerExceptions) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Extensions/ForEachAsync.cs: -------------------------------------------------------------------------------- 1 | namespace Dasync.Collections 2 | { 3 | /// 4 | /// Class to provide access to static method. 5 | /// 6 | public static class ForEachAsync 7 | { 8 | /// 9 | /// Stops ForEachAsync iteration (similar to 'break' statement) 10 | /// 11 | /// Always throws this exception to stop the ForEachAsync iteration 12 | public static void Break() 13 | { 14 | throw new ForEachAsyncBreakException(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Internals/EmptyAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using Dasync.Collections; 4 | 5 | namespace Dasync.Collections.Internals 6 | { 7 | internal sealed class EmptyAsyncEnumerable : IAsyncEnumerable 8 | { 9 | public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) 10 | => AsyncEnumerator.Empty; 11 | 12 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 13 | IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) 14 | => AsyncEnumerator.Empty; 15 | #endif 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyVersion("4.0.2")] 6 | [assembly: AssemblyFileVersion("4.0.2")] 7 | [assembly: AssemblyTitle("AsyncEnumerable")] 8 | [assembly: AssemblyProduct("AsyncEnumerable")] 9 | [assembly: AssemblyDescription("Asynchronous enumeration with IAsyncEnumerator, ForEachAsync(), and ParallelForEachAsync()")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("D-ASYNC")] 12 | [assembly: AssemblyCopyright("Copyright © 2019")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | [assembly: CLSCompliant(true)] 16 | [assembly: ComVisible(false)] 17 | [assembly: Guid("111536c3-dabf-48cf-b993-e504c0bc5ce7")] -------------------------------------------------------------------------------- /src/Internals/EmptyAsyncEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Dasync.Collections; 5 | 6 | namespace Dasync.Collections.Internals 7 | { 8 | internal sealed class EmptyAsyncEnumerator : IAsyncEnumerator, IAsyncEnumerator 9 | { 10 | public T Current 11 | { 12 | get 13 | { 14 | throw new InvalidOperationException("The enumerator has reached the end of the collection"); 15 | } 16 | } 17 | 18 | object IAsyncEnumerator.Current => Current; 19 | 20 | public ValueTask MoveNextAsync() => new ValueTask(false); 21 | 22 | public ValueTask DisposeAsync() => new ValueTask(); 23 | 24 | public void Dispose() { } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Internals/CurrentValueContainer.cs: -------------------------------------------------------------------------------- 1 | namespace Dasync.Collections.Internals 2 | { 3 | /// 4 | /// Internal base type for and 5 | /// 6 | public abstract class CurrentValueContainer : AsyncEnumerator 7 | { 8 | private T _currentValue; 9 | 10 | internal T CurrentValue 11 | { 12 | get 13 | { 14 | return _currentValue; 15 | } 16 | set 17 | { 18 | _currentValue = value; 19 | HasCurrentValue = true; 20 | } 21 | } 22 | 23 | internal bool HasCurrentValue 24 | { 25 | get; 26 | private set; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internals/TaskEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Dasync.Collections.Internals 5 | { 6 | internal static class TaskEx 7 | { 8 | public static readonly Task True = Task.FromResult(true); 9 | public static readonly Task False = Task.FromResult(false); 10 | public static readonly Task Completed = 11 | #if NETFX4_5 || NETFX4_5_2 12 | True; 13 | #else 14 | Task.CompletedTask; 15 | #endif 16 | 17 | public static Task FromException(Exception ex) 18 | { 19 | #if NETFX4_5 || NETFX4_5_2 20 | var tcs = new TaskCompletionSource(); 21 | tcs.SetException(ex); 22 | return tcs.Task; 23 | #else 24 | return Task.FromException(ex); 25 | #endif 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Tests/AsyncEnumerable.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.0 5 | False 6 | Tests 7 | Tests 8 | exe 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 D-ASYNC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Internals/AsyncEnumerableWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using Dasync.Collections; 4 | 5 | namespace Dasync.Collections.Internals 6 | { 7 | internal sealed class AsyncEnumerableWrapper : IAsyncEnumerable, IAsyncEnumerable 8 | { 9 | private readonly IEnumerable _enumerable; 10 | private readonly bool _runSynchronously; 11 | 12 | public AsyncEnumerableWrapper(IEnumerable enumerable, bool runSynchronously) 13 | { 14 | _enumerable = enumerable; 15 | _runSynchronously = runSynchronously; 16 | } 17 | 18 | IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) 19 | => new AsyncEnumeratorWrapper(_enumerable.GetEnumerator(), _runSynchronously) 20 | { MasterCancellationToken = cancellationToken }; 21 | 22 | public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) 23 | => new AsyncEnumeratorWrapper(_enumerable.GetEnumerator(), _runSynchronously) 24 | { MasterCancellationToken = cancellationToken }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Internals/EnumerableAdapter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using Dasync.Collections; 4 | 5 | namespace Dasync.Collections.Internals 6 | { 7 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 8 | internal sealed class EnumerableAdapter : IEnumerable 9 | { 10 | private readonly IAsyncEnumerable _asyncEnumerable; 11 | 12 | public EnumerableAdapter(IAsyncEnumerable asyncEnumerable) 13 | { 14 | _asyncEnumerable = asyncEnumerable; 15 | } 16 | 17 | public IEnumerator GetEnumerator() => 18 | new EnumeratorAdapter(_asyncEnumerable.GetAsyncEnumerator()); 19 | } 20 | #endif 21 | 22 | internal sealed class EnumerableAdapter : IEnumerable, IEnumerable 23 | { 24 | private readonly IAsyncEnumerable _asyncEnumerable; 25 | 26 | public EnumerableAdapter(IAsyncEnumerable asyncEnumerable) 27 | { 28 | _asyncEnumerable = asyncEnumerable; 29 | } 30 | 31 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 32 | 33 | public IEnumerator GetEnumerator() => 34 | new EnumeratorAdapter(_asyncEnumerable.GetAsyncEnumerator()); 35 | } 36 | } -------------------------------------------------------------------------------- /AsyncEnumerable.Symbols.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AsyncEnumerator 5 | 4.0.2 6 | 7 | AsyncEnumerator symbols 8 | AsyncEnumerator symbols 9 | AsyncEnumerator symbols 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/IAsyncEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Dasync.Collections 5 | { 6 | /// 7 | /// Supports a simple asynchronous iteration over a non-generic collection 8 | /// 9 | public interface IAsyncEnumerator : IDisposable, IAsyncDisposable 10 | { 11 | /// 12 | /// Gets the current element in the collection. 13 | /// 14 | object Current { get; } 15 | 16 | /// 17 | /// Advances the enumerator to the next element of the collection asynchronously 18 | /// 19 | /// Returns a Task that does transition to the next element. The result of the task is True if the enumerator was successfully advanced to the next element, or False if the enumerator has passed the end of the collection. 20 | ValueTask MoveNextAsync(); 21 | } 22 | 23 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 24 | /// 25 | /// Supports a simple asynchronous iteration over a collection of typed items 26 | /// 27 | /// The type of items in the collection 28 | public interface IAsyncEnumerator : IAsyncEnumerator 29 | { 30 | /// 31 | /// Gets the element in the collection at the current position of the enumerator. 32 | /// 33 | new T Current { get; } 34 | } 35 | #endif 36 | } 37 | -------------------------------------------------------------------------------- /netfx45/tests/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2019")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("9557379c-26e0-448f-ab22-b197885b05e2")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/AsyncEnumerable.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.4;net461;netstandard2.0;netstandard2.1 5 | True 6 | ../AsyncEnumerable.pfx 7 | False 8 | False 9 | AsyncEnumerable 10 | Dasync.Collections 11 | false 12 | latest 13 | bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/IAsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace Dasync.Collections 4 | { 5 | /// 6 | /// Exposes an asynchronous enumerator, which supports a simple iteration over a non-generic collection 7 | /// 8 | public interface IAsyncEnumerable 9 | { 10 | /// 11 | /// Creates an enumerator that iterates through a collection asynchronously 12 | /// 13 | /// A cancellation token to cancel creation of the enumerator in case if it takes a lot of time 14 | /// Returns a task with the created enumerator as result on completion 15 | IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default); 16 | } 17 | 18 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 19 | /// 20 | /// Exposes the asynchronous enumerator, which supports a simple iteration over a collection of typed items 21 | /// 22 | /// The type of items in the collection 23 | public interface IAsyncEnumerable : IAsyncEnumerable 24 | { 25 | /// 26 | /// Creates an enumerator that iterates through a collection asynchronously 27 | /// 28 | /// A cancellation token to cancel creation of the enumerator in case if it takes a lot of time 29 | /// Returns a task with the created enumerator as result on completion 30 | new IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default); 31 | } 32 | #endif 33 | } 34 | -------------------------------------------------------------------------------- /src/Internals/EnumeratorAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Dasync.Collections; 5 | 6 | namespace Dasync.Collections.Internals 7 | { 8 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 9 | internal sealed class EnumeratorAdapter : IEnumerator 10 | { 11 | private readonly IAsyncEnumerator _asyncEnumerator; 12 | 13 | public EnumeratorAdapter(IAsyncEnumerator asyncEnumerator) 14 | { 15 | _asyncEnumerator = asyncEnumerator; 16 | } 17 | 18 | public object Current => _asyncEnumerator.Current; 19 | 20 | public bool MoveNext() => _asyncEnumerator.MoveNextAsync().GetAwaiter().GetResult(); 21 | 22 | public void Reset() => throw new NotSupportedException("The IEnumerator.Reset() method is obsolete. Create a new enumerator instead."); 23 | 24 | public void Dispose() => _asyncEnumerator.Dispose(); 25 | } 26 | #endif 27 | 28 | internal sealed class EnumeratorAdapter : IEnumerator, IEnumerator 29 | { 30 | private readonly IAsyncEnumerator _asyncEnumerator; 31 | 32 | public EnumeratorAdapter(IAsyncEnumerator asyncEnumerator) 33 | { 34 | _asyncEnumerator = asyncEnumerator; 35 | } 36 | 37 | public T Current => _asyncEnumerator.Current; 38 | 39 | object IEnumerator.Current => Current; 40 | 41 | public bool MoveNext() => _asyncEnumerator.MoveNextAsync().GetAwaiter().GetResult(); 42 | 43 | public void Reset() => throw new NotSupportedException("The IEnumerator.Reset() method is obsolete. Create a new enumerator instead."); 44 | 45 | public void Dispose() => 46 | _asyncEnumerator.DisposeAsync().GetAwaiter().GetResult(); 47 | } 48 | } -------------------------------------------------------------------------------- /Tests/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Threading; 6 | using NUnit.Framework.Api; 7 | using NUnit.Framework.Interfaces; 8 | using NUnit.Framework.Internal; 9 | 10 | namespace Tests 11 | { 12 | class Program 13 | { 14 | static void Main(string[] args) 15 | { 16 | var builder = new DefaultTestAssemblyBuilder(); 17 | var runner = new NUnitTestAssemblyRunner(builder); 18 | runner.Load(typeof(Program).GetTypeInfo().Assembly, settings: new Dictionary { }); 19 | runner.Run(new ConsoleTestListener(), TestFilter.Empty); 20 | while (runner.IsTestRunning) 21 | Thread.Sleep(500); 22 | } 23 | 24 | class ConsoleTestListener : ITestListener 25 | { 26 | private TextWriter _output = Console.Out; 27 | 28 | public void TestStarted(ITest test) 29 | { 30 | } 31 | 32 | public void TestFinished(ITestResult result) 33 | { 34 | var message = $"{result.ResultState.Status.ToString().ToUpperInvariant()} - {result.Test.FullName}"; 35 | 36 | if (result.ResultState.Status == TestStatus.Failed) 37 | message += Environment.NewLine + " " + result.Message; 38 | 39 | _output.WriteLine(message); 40 | } 41 | 42 | public void TestOutput(TestOutput output) 43 | { 44 | _output.WriteLine(output.Text); 45 | } 46 | 47 | public void SendMessage(TestMessage message) 48 | { 49 | _output.WriteLine(message.Message); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Internals/AsyncEnumeratorWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Dasync.Collections; 6 | 7 | namespace Dasync.Collections.Internals 8 | { 9 | internal sealed class AsyncEnumeratorWrapper : IAsyncEnumerator, IAsyncEnumerator 10 | { 11 | private readonly IEnumerator _enumerator; 12 | private readonly bool _runSynchronously; 13 | 14 | public AsyncEnumeratorWrapper(IEnumerator enumerator, bool runSynchronously) 15 | { 16 | _enumerator = enumerator; 17 | _runSynchronously = runSynchronously; 18 | } 19 | 20 | internal CancellationToken MasterCancellationToken; 21 | 22 | public T Current => _enumerator.Current; 23 | 24 | object IAsyncEnumerator.Current => Current; 25 | 26 | public ValueTask MoveNextAsync() 27 | { 28 | if (_runSynchronously) 29 | { 30 | try 31 | { 32 | return new ValueTask(_enumerator.MoveNext()); 33 | } 34 | catch (Exception ex) 35 | { 36 | var tcs = new TaskCompletionSource(); 37 | tcs.SetException(ex); 38 | return new ValueTask(tcs.Task); 39 | } 40 | } 41 | else 42 | { 43 | return new ValueTask(Task.Run(() => _enumerator.MoveNext(), MasterCancellationToken)); 44 | } 45 | } 46 | 47 | public void Dispose() 48 | { 49 | _enumerator.Dispose(); 50 | } 51 | 52 | public ValueTask DisposeAsync() 53 | { 54 | Dispose(); 55 | return new ValueTask(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/PerformanceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Dasync.Collections; 7 | using NUnit.Framework; 8 | 9 | namespace Tests 10 | { 11 | [TestFixture] 12 | public class PerformanceTests 13 | { 14 | [Test] 15 | [Ignore("Manual testing, no assertions")] 16 | public async Task MeasureEnumerationTime() 17 | { 18 | var iterations = 1000000; 19 | var enumerator = new AsyncEnumerator(async yield => 20 | { 21 | for (int i = 0; i < iterations; i++) 22 | { 23 | await yield.ReturnAsync(1); 24 | } 25 | }); 26 | 27 | var sw = Stopwatch.StartNew(); 28 | int sum = 0; 29 | 30 | while (await enumerator.MoveNextAsync()) 31 | sum += enumerator.Current; 32 | 33 | var time = sw.Elapsed; 34 | Console.WriteLine($"Time taken: {time}, Sum: {sum}"); 35 | 36 | 37 | 38 | sw = Stopwatch.StartNew(); 39 | sum = 0; 40 | 41 | foreach (var number in EnumerateNumbers()) 42 | sum += number; 43 | 44 | time = sw.Elapsed; 45 | Console.WriteLine($"Time taken: {time}, Sum: {sum}"); 46 | 47 | 48 | 49 | sw = Stopwatch.StartNew(); 50 | sum = 0; 51 | 52 | int _lock = 0; 53 | for (int i = 0; i < iterations; i++) 54 | { 55 | Interlocked.CompareExchange(ref _lock, 1, 0); 56 | var tcs = new TaskCompletionSource(); 57 | tcs.TrySetResult(1); 58 | sum += await tcs.Task; 59 | //await Task.Yield(); 60 | //await Task.Yield(); 61 | Interlocked.Exchange(ref _lock, 0); 62 | } 63 | 64 | time = sw.Elapsed; 65 | Console.WriteLine($"Time taken: {time}, Sum: {sum}"); 66 | 67 | 68 | 69 | sw = Stopwatch.StartNew(); 70 | sum = 0; 71 | 72 | var t = Task.FromResult(1); 73 | for (int i = 0; i < iterations; i++) 74 | sum += await t; 75 | 76 | time = sw.Elapsed; 77 | Console.WriteLine($"Time taken: {time}, Sum: {sum}"); 78 | } 79 | 80 | public IEnumerable EnumerateNumbers() 81 | { 82 | for (int i = 0; i < 1000000; i++) 83 | yield return 1; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Tests/ForEachAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Dasync.Collections; 5 | using NUnit.Framework; 6 | 7 | #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously 8 | 9 | namespace Tests 10 | { 11 | [TestFixture] 12 | public class ForEachAsyncTests 13 | { 14 | [Test] 15 | public void SimpleSyncForEach() 16 | { 17 | IAsyncEnumerable enumerable = new AsyncEnumerable( 18 | async yield => 19 | { 20 | for (int i = 0; i < 5; i++) 21 | await yield.ReturnAsync(i); 22 | }); 23 | 24 | #pragma warning disable CS0612 // Type or member is obsolete 25 | int counter = 0; 26 | foreach (var number in enumerable.ToEnumerable()) 27 | { 28 | Assert.AreEqual(counter, number); 29 | counter++; 30 | } 31 | #pragma warning restore CS0612 // Type or member is obsolete 32 | } 33 | 34 | [Test] 35 | public async Task SimpleAsyncForEachWithSyncBreak() 36 | { 37 | IAsyncEnumerable enumerable = new AsyncEnumerable( 38 | async yield => 39 | { 40 | for (int i = 0; i < 5; i++) 41 | await yield.ReturnAsync(i); 42 | }); 43 | 44 | int counter = 0; 45 | await enumerable.ForEachAsync( 46 | number => 47 | { 48 | Assert.AreEqual(counter, number); 49 | counter++; 50 | if (counter == 3) ForEachAsync.Break(); 51 | }); 52 | 53 | Assert.AreEqual(3, counter); 54 | } 55 | 56 | [Test] 57 | public async Task SimpleAsyncForEachWithAsyncBreak() 58 | { 59 | IAsyncEnumerable enumerable = new AsyncEnumerable( 60 | async yield => 61 | { 62 | for (int i = 0; i < 5; i++) 63 | await yield.ReturnAsync(i); 64 | }); 65 | 66 | int counter = 0; 67 | await enumerable.ForEachAsync( 68 | async number => 69 | { 70 | Assert.AreEqual(counter, number); 71 | counter++; 72 | if (counter == 2) ForEachAsync.Break(); 73 | }); 74 | 75 | Assert.AreEqual(2, counter); 76 | } 77 | 78 | [Test] 79 | public async Task SimpleAsyncForEach() 80 | { 81 | IAsyncEnumerable enumerable = new AsyncEnumerable( 82 | async yield => 83 | { 84 | for (int i = 0; i < 5; i++) 85 | await yield.ReturnAsync(i); 86 | }); 87 | 88 | int counter = 0; 89 | await enumerable.ForEachAsync( 90 | number => 91 | { 92 | Assert.AreEqual(counter, number); 93 | counter++; 94 | }); 95 | 96 | Assert.AreEqual(5, counter); 97 | } 98 | 99 | [Test] 100 | public async Task RethrowProducerException() 101 | { 102 | IAsyncEnumerable enumerable = new AsyncEnumerable( 103 | async yield => 104 | { 105 | throw new ArgumentException("test"); 106 | }); 107 | 108 | try 109 | { 110 | await enumerable.ForEachAsync( 111 | number => 112 | { 113 | Assert.Fail("must never be called due to the exception"); 114 | }); 115 | } 116 | catch (ArgumentException) 117 | { 118 | Assert.Pass(); 119 | } 120 | 121 | Assert.Fail("Expected an exception to be thrown"); 122 | } 123 | 124 | [Test] 125 | public async Task RethrowConsumerException() 126 | { 127 | IAsyncEnumerable enumerable = new AsyncEnumerable( 128 | async yield => 129 | { 130 | await yield.ReturnAsync(123); 131 | }); 132 | 133 | try 134 | { 135 | await enumerable.ForEachAsync( 136 | number => 137 | { 138 | throw new ArgumentException("test"); 139 | }); 140 | } 141 | catch (ArgumentException) 142 | { 143 | Assert.Pass(); 144 | } 145 | 146 | Assert.Fail("Expected an exception to be thrown"); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /netfx45/tests/AsyncEnumerable.Tests.NetFx45.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | {9557379C-26E0-448F-AB22-B197885B05E2} 9 | Library 10 | Properties 11 | Tests 12 | Tests 13 | v4.5 14 | 512 15 | 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\..\packages\NUnit.3.11.0\lib\net45\nunit.framework.dll 38 | 39 | 40 | 41 | ..\..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll 42 | 43 | 44 | ..\..\packages\System.Threading.Tasks.Extensions.4.5.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll 45 | 46 | 47 | ..\..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll 48 | 49 | 50 | 51 | 52 | EnumerableLinqStyleExtensionsTests.cs 53 | 54 | 55 | EnumeratorLinqStyleExtensionsTests.cs 56 | 57 | 58 | 59 | AsyncEnumeratorTests.cs 60 | 61 | 62 | ForEachAsyncTests.cs 63 | 64 | 65 | PerformanceTests.cs 66 | 67 | 68 | 69 | 70 | {111536c3-dabf-48cf-b993-e504c0bc5ce7} 71 | AsyncEnumerable.NetFx45 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 84 | 85 | 86 | 87 | 94 | -------------------------------------------------------------------------------- /src/AsyncEnumerable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Dasync.Collections.Internals; 6 | 7 | namespace Dasync.Collections 8 | { 9 | /// 10 | /// Base abstract class that implements . 11 | /// Use concrete implementation or . 12 | /// 13 | public abstract class AsyncEnumerable : IAsyncEnumerable 14 | { 15 | /// 16 | /// Returns pre-cached empty collection 17 | /// 18 | public static IAsyncEnumerable Empty() => AsyncEnumerable.Empty; 19 | 20 | IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) 21 | { 22 | throw new NotImplementedException(); 23 | } 24 | } 25 | 26 | /// 27 | /// Helps to enumerate items in a collection asynchronously 28 | /// 29 | /// 30 | /// 31 | /// IAsyncEnumerable<int> ProduceNumbers(int start, int end) 32 | /// { 33 | /// return new AsyncEnumerable<int>(async yield => { 34 | /// for (int number = start; number <= end; number++) 35 | /// await yield.ReturnAsync(number); 36 | /// }); 37 | /// } 38 | /// 39 | /// async Task ConsumeAsync() 40 | /// { 41 | /// var asyncEnumerableCollection = ProduceNumbers(start: 1, end: 10); 42 | /// await asyncEnumerableCollection.ForEachAsync(async number => { 43 | /// await Console.Out.WriteLineAsync(number); 44 | /// }); 45 | /// } 46 | /// 47 | /// 48 | public class AsyncEnumerable : AsyncEnumerable, IAsyncEnumerable, IAsyncEnumerable 49 | { 50 | private readonly Func.Yield, Task> _enumerationFunction; 51 | 52 | /// 53 | /// A pre-cached empty collection 54 | /// 55 | public readonly static IAsyncEnumerable Empty = new EmptyAsyncEnumerable(); 56 | 57 | /// 58 | /// Constructor 59 | /// 60 | /// A function that enumerates items in a collection asynchronously 61 | public AsyncEnumerable(Func.Yield, Task> enumerationFunction) 62 | { 63 | _enumerationFunction = enumerationFunction; 64 | } 65 | 66 | /// 67 | /// Creates an enumerator that iterates through a collection asynchronously 68 | /// 69 | /// A cancellation token to cancel creation of the enumerator in case if it takes a lot of time 70 | /// Returns a task with the created enumerator as result on completion 71 | public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) 72 | => new AsyncEnumerator(_enumerationFunction) { MasterCancellationToken = cancellationToken }; 73 | 74 | IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) 75 | => new AsyncEnumerator(_enumerationFunction) { MasterCancellationToken = cancellationToken }; 76 | } 77 | 78 | /// 79 | /// Similar to , but allows you to pass a state object into the enumeration function, what can be 80 | /// used for performance optimization, so don't have to create a delegate on the fly every single time you create the enumerator. 81 | /// 82 | /// Type of items returned by 83 | /// Type of the state object 84 | public class AsyncEnumerableWithState : AsyncEnumerable, IAsyncEnumerable, IAsyncEnumerable 85 | { 86 | private readonly Func.Yield, TState, Task> _enumerationFunction; 87 | 88 | /// 89 | /// Constructor 90 | /// 91 | /// A function that enumerates items in a collection asynchronously 92 | /// A state object that is passed to the 93 | public AsyncEnumerableWithState(Func.Yield, TState, Task> enumerationFunction, TState state) 94 | { 95 | _enumerationFunction = enumerationFunction; 96 | State = state; 97 | } 98 | 99 | /// 100 | /// A user state that gets passed into the enumeration function. 101 | /// 102 | protected TState State { get; } 103 | 104 | /// 105 | /// Creates an enumerator that iterates through a collection asynchronously 106 | /// 107 | /// Returns a task with the created enumerator as result on completion 108 | public virtual IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) 109 | => new AsyncEnumeratorWithState(_enumerationFunction, State) 110 | { MasterCancellationToken = cancellationToken }; 111 | 112 | /// 113 | /// Creates an enumerator that iterates through a collection asynchronously 114 | /// 115 | /// Returns a task with the created enumerator as result on completion 116 | IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) 117 | => new AsyncEnumeratorWithState(_enumerationFunction, State) 118 | { MasterCancellationToken = cancellationToken }; 119 | } 120 | } -------------------------------------------------------------------------------- /AsyncEnumerable.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28606.126 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncEnumerable.NetFx45", "netfx45\lib\AsyncEnumerable.NetFx45.csproj", "{111536C3-DABF-48CF-B993-E504C0BC5CE7}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".NET Framework 4.5", ".NET Framework 4.5", "{6D19B0B6-35B3-4A6B-B060-60DD1E7F8CAB}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsyncEnumerable.Tests.NetFx45", "netfx45\tests\AsyncEnumerable.Tests.NetFx45.csproj", "{9557379C-26E0-448F-AB22-B197885B05E2}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".NET Standard", ".NET Standard", "{FEFF0146-6799-40F0-A9CF-AB5AAB6584C9}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncEnumerable", "src\AsyncEnumerable.csproj", "{2299336C-BC95-41DB-8697-5127E3754370}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{6E8E24A4-11BF-46D5-B060-E993E070ED65}" 17 | ProjectSection(SolutionItems) = preProject 18 | AsyncEnumerable.nuspec = AsyncEnumerable.nuspec 19 | LICENSE = LICENSE 20 | README.md = README.md 21 | EndProjectSection 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncEnumerable.Tests", "Tests\AsyncEnumerable.Tests.csproj", "{FFD079F6-2A1E-4D81-BE07-005DD274D336}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Debug|x64 = Debug|x64 29 | Debug|x86 = Debug|x86 30 | Release|Any CPU = Release|Any CPU 31 | Release|x64 = Release|x64 32 | Release|x86 = Release|x86 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Debug|x64.Build.0 = Debug|Any CPU 39 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Debug|x86.Build.0 = Debug|Any CPU 41 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Release|x64.ActiveCfg = Release|Any CPU 44 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Release|x64.Build.0 = Release|Any CPU 45 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Release|x86.ActiveCfg = Release|Any CPU 46 | {111536C3-DABF-48CF-B993-E504C0BC5CE7}.Release|x86.Build.0 = Release|Any CPU 47 | {9557379C-26E0-448F-AB22-B197885B05E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {9557379C-26E0-448F-AB22-B197885B05E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {9557379C-26E0-448F-AB22-B197885B05E2}.Debug|x64.ActiveCfg = Debug|Any CPU 50 | {9557379C-26E0-448F-AB22-B197885B05E2}.Debug|x64.Build.0 = Debug|Any CPU 51 | {9557379C-26E0-448F-AB22-B197885B05E2}.Debug|x86.ActiveCfg = Debug|Any CPU 52 | {9557379C-26E0-448F-AB22-B197885B05E2}.Debug|x86.Build.0 = Debug|Any CPU 53 | {9557379C-26E0-448F-AB22-B197885B05E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {9557379C-26E0-448F-AB22-B197885B05E2}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {9557379C-26E0-448F-AB22-B197885B05E2}.Release|x64.ActiveCfg = Release|Any CPU 56 | {9557379C-26E0-448F-AB22-B197885B05E2}.Release|x64.Build.0 = Release|Any CPU 57 | {9557379C-26E0-448F-AB22-B197885B05E2}.Release|x86.ActiveCfg = Release|Any CPU 58 | {9557379C-26E0-448F-AB22-B197885B05E2}.Release|x86.Build.0 = Release|Any CPU 59 | {2299336C-BC95-41DB-8697-5127E3754370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {2299336C-BC95-41DB-8697-5127E3754370}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {2299336C-BC95-41DB-8697-5127E3754370}.Debug|x64.ActiveCfg = Debug|Any CPU 62 | {2299336C-BC95-41DB-8697-5127E3754370}.Debug|x64.Build.0 = Debug|Any CPU 63 | {2299336C-BC95-41DB-8697-5127E3754370}.Debug|x86.ActiveCfg = Debug|Any CPU 64 | {2299336C-BC95-41DB-8697-5127E3754370}.Debug|x86.Build.0 = Debug|Any CPU 65 | {2299336C-BC95-41DB-8697-5127E3754370}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {2299336C-BC95-41DB-8697-5127E3754370}.Release|Any CPU.Build.0 = Release|Any CPU 67 | {2299336C-BC95-41DB-8697-5127E3754370}.Release|x64.ActiveCfg = Release|Any CPU 68 | {2299336C-BC95-41DB-8697-5127E3754370}.Release|x64.Build.0 = Release|Any CPU 69 | {2299336C-BC95-41DB-8697-5127E3754370}.Release|x86.ActiveCfg = Release|Any CPU 70 | {2299336C-BC95-41DB-8697-5127E3754370}.Release|x86.Build.0 = Release|Any CPU 71 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 72 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Debug|Any CPU.Build.0 = Debug|Any CPU 73 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Debug|x64.ActiveCfg = Debug|Any CPU 74 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Debug|x64.Build.0 = Debug|Any CPU 75 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Debug|x86.ActiveCfg = Debug|Any CPU 76 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Debug|x86.Build.0 = Debug|Any CPU 77 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Release|Any CPU.ActiveCfg = Release|Any CPU 78 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Release|Any CPU.Build.0 = Release|Any CPU 79 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Release|x64.ActiveCfg = Release|Any CPU 80 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Release|x64.Build.0 = Release|Any CPU 81 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Release|x86.ActiveCfg = Release|Any CPU 82 | {FFD079F6-2A1E-4D81-BE07-005DD274D336}.Release|x86.Build.0 = Release|Any CPU 83 | EndGlobalSection 84 | GlobalSection(SolutionProperties) = preSolution 85 | HideSolutionNode = FALSE 86 | EndGlobalSection 87 | GlobalSection(NestedProjects) = preSolution 88 | {111536C3-DABF-48CF-B993-E504C0BC5CE7} = {6D19B0B6-35B3-4A6B-B060-60DD1E7F8CAB} 89 | {9557379C-26E0-448F-AB22-B197885B05E2} = {6D19B0B6-35B3-4A6B-B060-60DD1E7F8CAB} 90 | {2299336C-BC95-41DB-8697-5127E3754370} = {FEFF0146-6799-40F0-A9CF-AB5AAB6584C9} 91 | {FFD079F6-2A1E-4D81-BE07-005DD274D336} = {FEFF0146-6799-40F0-A9CF-AB5AAB6584C9} 92 | EndGlobalSection 93 | GlobalSection(ExtensibilityGlobals) = postSolution 94 | SolutionGuid = {98CBAF14-E09D-4B95-ABA5-0028ADD420B1} 95 | EndGlobalSection 96 | EndGlobal 97 | -------------------------------------------------------------------------------- /NUGET-DOC.md: -------------------------------------------------------------------------------- 1 | __1: How to use this library?__ 2 | 3 | See examples above. You can the core code and lots of useful extension methods in the `Dasync.Collections` namespace. 4 | 5 | 6 | __2: Using CancellationToken__ 7 | 8 | ```csharp 9 | using Dasync.Collections; 10 | 11 | IAsyncEnumerable ProduceNumbers() 12 | { 13 | return new AsyncEnumerable(async yield => { 14 | 15 | await FooAsync(yield.CancellationToken); 16 | }); 17 | } 18 | ``` 19 | 20 | __3: Always remember about ConfigureAwait(false)__ 21 | 22 | To avoid performance degradation and possible dead-locks in ASP.NET or WPF applications (or any `SynchronizationContext`-dependent environment), you should always put `ConfigureAwait(false)` in your `await` statements: 23 | 24 | ```csharp 25 | using Dasync.Collections; 26 | 27 | IAsyncEnumerable GetValues() 28 | { 29 | return new AsyncEnumerable(async yield => 30 | { 31 | await FooAsync().ConfigureAwait(false); 32 | 33 | // Yes, it's even needed for 'yield.ReturnAsync' 34 | await yield.ReturnAsync(123).ConfigureAwait(false); 35 | }); 36 | } 37 | ``` 38 | 39 | __4: Clean-up on incomplete enumeration__ 40 | 41 | Imagine such situation: 42 | 43 | ```csharp 44 | using Dasync.Collections; 45 | 46 | IAsyncEnumerable ReadValuesFromQueue() 47 | { 48 | return new AsyncEnumerable(async yield => 49 | { 50 | using (var queueClient = CreateQueueClient()) 51 | { 52 | while (true) 53 | { 54 | var message = queueClient.DequeueMessageAsync(); 55 | if (message == null) 56 | break; 57 | 58 | await yield.ReturnAsync(message.Value); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | Task ReadFirstValueOrDefaultAsync() 65 | { 66 | return ReadValuesFromQueue().FirstOrDefaultAsync(); 67 | } 68 | ``` 69 | 70 | The `FirstOrDefaultAsync` method will try to read first value from the `IAsyncEnumerator`, and then will just dispose it. However, disposing `AsyncEnumerator` does not mean that the `queueClient` in the lambda function will be disposed automatically as well, because async methods are just state machines which need somehow to go to a particular state to do the clean-up. 71 | To provide such behavior, when you dispose an `AsyncEnumerator` before you reach the end of enumeration, it will tell to resume your async lambda function (at `await yield.ReturnAsync()`) with the `AsyncEnumerationCanceledException` (derives from `OperationCanceledException`). Having such exception in your lambda method will break normal flow of enumeration and will go to terminal state of the underlying state machine, what will do the clean-up, i.e. dispose the `queueClient` in this case. You don't need (and shouldn't) catch that exception type, because it's handled internally by `AsyncEnumerator`. The same exception is thrown when you call `yield.Break()`. 72 | 73 | There is another option to do the cleanup on `Dispose`: 74 | 75 | ```csharp 76 | using Dasync.Collections; 77 | 78 | IAsyncEnumerator GetQueueEnumerator() 79 | { 80 | var queueClient = CreateQueueClient(); 81 | 82 | return new AsyncEnumerable(async yield => 83 | { 84 | while (true) 85 | { 86 | var message = queueClient.DequeueMessageAsync(); 87 | if (message == null) 88 | break; 89 | 90 | await yield.ReturnAsync(message.Value); 91 | } 92 | }, 93 | onDispose: () => queueClient.Dispose()); 94 | } 95 | ``` 96 | 97 | __5: Why is GetAsyncEnumeratorAsync async?__ 98 | 99 | The `IAsyncEnumerable.GetAsyncEnumeratorAsync()` method is async and returns a `Task`, where the current implementation of `AsyncEnumerable` always runs that method synchronously and just returns an instance of `AsyncEnumerator`. Having interfaces allows you to do your own implementation, where classes mentioned above are just helpers. The initial idea was to be able to support database-like scenarios, where `GetAsyncEnumeratorAsync()` executes a query first (what internally returns a pointer), and the `MoveNextAsync()` enumerates through rows (by using that pointer). 100 | 101 | __6: Returning IAsyncEnumerable vs IAsyncEnumerator__ 102 | 103 | When you implement a method that returns an async-enumerable collection you have a choice to return either `IAsyncEnumerable` or `IAsyncEnumerator` - the constructors of the helper classes `AsyncEnumerable` and `AsyncEnumerator` are absolutely identical. Both interfaces have same set of useful extension methods, like `ForEachAsync`. 104 | 105 | When you create an 'enumerable', you create a factory that produces 'enumerators', i.e. you can enumerate through a collection many times. On the other hand, creating an 'enumerator' is needed when you can through a collection only once. 106 | 107 | __7: Where is Reset or ResetAsync?__ 108 | 109 | The `Reset` method must not be on the `IEnumerator` interface, and should be considered as deprecated. Create a new enumerator instead. This is the reason why the 'oneTimeUse' flag was removed in version 2 of this library. 110 | 111 | __8: How can I do synchronous for-each enumeration through IAsyncEnumerable?__ 112 | 113 | You can use extension methods like `IAsyncEnumerable.ToEnumerable()` to use built-in `foreach` enumeration, BUT you should never do that! The general idea of this library is to avoid thread-blocking calls on worker threads, where converting an `IAsyncEnumerable` to `IEnumerable` will just defeat the whole purpose of this library. This is the reason why such synchronous extension methods are marked with `[Obsolete]` attribute. 114 | 115 | __9: What's the difference between ForEachAsync and ParallelForEachAsync?__ 116 | 117 | The `ForEachAsync` allows you to go through a collection and perform an action on every single item in sequential manner. On the other hand, `ParallelForEachAsync` allows you to run the action on multiple items at the same time where the sequential 118 | order of completion is not guaranteed. For the latter, the degree of the parallelism is controlled by the `maxDegreeOfParalellism` 119 | argument, however it does not guarantee to spin up the exact amount of threads, because it depends on the [thread pool size](https://msdn.microsoft.com/en-us/library/system.threading.threadpool.setmaxthreads(v=vs.110).aspx) and its occupancy at a moment of time. Such parallel approach is much better than trying to create a task for an action for every single item on the collection and then awaiting on all of them with `Task.WhenAll`, because it adds less overhead to the runtime, better with memory usage, and helps with throttling-sensitive scenarios. -------------------------------------------------------------------------------- /AsyncEnumerable.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AsyncEnumerator 5 | 4.0.2 6 | sergiis,dasync 7 | MIT 8 | https://github.com/Dasync/AsyncEnumerable 9 | 10 | false 11 | https://raw.githubusercontent.com/Dasync/AsyncEnumerable/master/icon.png 12 | C# Async Streams features 13 | Introduces IAsyncEnumerable, IAsyncEnumerator, ForEachAsync(), and ParallelForEachAsync() 14 | GitHub: https://github.com/Dasync/AsyncEnumerable 15 | 16 | PROBLEM SPACE 17 | 18 | Helps to (a) create an element provider, where producing an element can take a lot of time 19 | due to dependency on other asynchronous events (e.g. wait handles, network streams), and 20 | (b) a consumer that processes those element as soon as they are ready without blocking 21 | the thread (the processing is scheduled on a worker thread instead). 22 | 23 | 24 | EXAMPLE 25 | 26 | using Dasync.Collections; 27 | 28 | static IAsyncEnumerable<int> ProduceAsyncNumbers(int start, int end) 29 | { 30 | return new AsyncEnumerable<int>(async yield => { 31 | 32 | // Just to show that ReturnAsync can be used multiple times 33 | await yield.ReturnAsync(start); 34 | 35 | for (int number = start + 1; number <= end; number++) 36 | await yield.ReturnAsync(number); 37 | 38 | // You can break the enumeration loop with the following call: 39 | yield.Break(); 40 | 41 | // This won't be executed due to the loop break above 42 | await yield.ReturnAsync(12345); 43 | }); 44 | } 45 | 46 | // Just to compare with synchronous version of enumerator 47 | static IEnumerable<int> ProduceNumbers(int start, int end) 48 | { 49 | yield return start; 50 | 51 | for (int number = start + 1; number <= end; number++) 52 | yield return number; 53 | 54 | yield break; 55 | 56 | yield return 12345; 57 | } 58 | 59 | static async Task ConsumeNumbersAsync() 60 | { 61 | var asyncEnumerableCollection = ProduceAsyncNumbers(start: 1, end: 10); 62 | await asyncEnumerableCollection.ForEachAsync(async number => { 63 | await Console.Out.WriteLineAsync($"{number}"); 64 | }); 65 | } 66 | 67 | // Just to compare with synchronous version of enumeration 68 | static void ConsumeNumbers() 69 | { 70 | var enumerableCollection = ProduceNumbers(start: 1, end: 10); 71 | foreach (var number in enumerableCollection) { 72 | Console.Out.WriteLine($"{number}"); 73 | } 74 | } 75 | Introduces IAsyncEnumerable, IAsyncEnumerator, ForEachAsync(), and ParallelForEachAsync() 76 | 77 | 4.0.0: Use interfaces from Microsoft.Bcl.AsyncInterfaces package in NET Standard 2.0. 78 | 3.1.0: Add support for NET Standard 2.1, consolidate interface with Microsoft's implementation. 79 | 2.2.0: New extension methods: SelectMany, Append, Prepend, OfType, Concat, Distinct, ToDictionaryAsync, ToLookupAsync, AggregateAsync. 80 | 2.1.0: New extension methods: Batch, UnionAll, Single, SingleOrDefault, DefaultIfEmpty, Cast. 81 | 2.0.0: Revise design of the library: same features, but slight paradigm shift and interface breaking changes. 82 | 1.5.0: Add support for .NET Standard, minor improvements. 83 | 1.4.2: Add finalizer to AsyncEnumerator and call Dispose in ForEachAsync and ParallelForEachAsync extension methods. 84 | 1.4.0: Add new generic type AsyncEnumeratorWithState for performance optimization. 85 | Now IAsyncEnumerator<T> is covariant. 86 | Add ForEachAsync, ParallelForeachAsync, and LINQ-style extension methods for IAsyncEnumerator. 87 | 1.2.1: New Linq-style extension methods in System.Collections.Async namespace. 88 | 1.1.0: Add ParallelForEachAsync extension methods for IEnumerable<T> and IAsyncEnumerable<T> in System.Collections.Async namespace. 89 | 90 | IAsyncEnumerable IAsyncEnumerator ForEachAsync ParallelForEachAsync async await foreach parallel async-streams linq charp .net 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/Internals/TaskCompletionSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using System.Runtime.CompilerServices; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dasync.Collections.Internals 8 | { 9 | /// 10 | /// Utility methods for 11 | /// 12 | public static class TaskCompletionSource 13 | { 14 | private static Func _resetTaskFunc; 15 | 16 | static TaskCompletionSource() 17 | { 18 | // Collect all necessary fields of a Task that needs to be reset. 19 | #if NETSTANDARD 20 | var m_stateFlags = typeof(Task).GetTypeInfo().GetDeclaredField("m_stateFlags"); 21 | var m_continuationObject = typeof(Task).GetTypeInfo().GetDeclaredField("m_continuationObject"); 22 | var m_taskId = typeof(Task).GetTypeInfo().GetDeclaredField("m_taskId"); 23 | var m_stateObject = typeof(Task).GetTypeInfo().GetDeclaredField("m_stateObject"); 24 | #else 25 | var m_stateFlags = typeof(Task).GetField("m_stateFlags", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 26 | var m_continuationObject = typeof(Task).GetField("m_continuationObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 27 | var m_taskId = typeof(Task).GetField("m_taskId", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 28 | var m_stateObject = typeof(Task).GetField("m_stateObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 29 | #endif 30 | 31 | // Make sure that all of them available (has been checked with .NET Framework 4.5 only). 32 | if (m_stateFlags != null && m_continuationObject != null && m_taskId != null && m_stateObject != null) 33 | try 34 | { 35 | /* Using Linq Expressions compile a simple function: 36 | * 37 | * int ResetTask(Task task, object state) 38 | * { 39 | * task.m_stateFlags = ; 40 | * task.m_continuationObject = null; 41 | * task.m_taskId = 0; 42 | * m_stateObject = state; 43 | * return 0; 44 | * } 45 | */ 46 | 47 | var defaultStateFlags = (int)m_stateFlags.GetValue(new TaskCompletionSource().Task); 48 | 49 | var targetArg = Expression.Parameter(typeof(Task), "task"); 50 | var stateObjectArg = Expression.Parameter(typeof(object), "stateObject"); 51 | 52 | var body = Expression.Block( 53 | Expression.Assign(Expression.MakeMemberAccess(targetArg, m_stateFlags), Expression.Constant(defaultStateFlags, typeof(int))), 54 | Expression.Assign(Expression.MakeMemberAccess(targetArg, m_continuationObject), Expression.Constant(null, typeof(object))), 55 | Expression.Assign(Expression.MakeMemberAccess(targetArg, m_taskId), Expression.Constant(0, typeof(int))), 56 | Expression.Assign(Expression.MakeMemberAccess(targetArg, m_stateObject), stateObjectArg), 57 | Expression.Constant(0, typeof(int)) // this can be anything of any type - lambda expression allows to compile Func<> only, but not an Action<> 58 | ); 59 | 60 | var lambda = Expression.Lambda(body, targetArg, stateObjectArg); 61 | _resetTaskFunc = (Func)lambda.Compile(); 62 | 63 | // Do initial testing of the reset function 64 | TestResetFunction(); 65 | } 66 | catch 67 | { 68 | // If something goes wrong, the feature just won't be enabled. 69 | _resetTaskFunc = null; 70 | } 71 | } 72 | 73 | private static void TestResetFunction() 74 | { 75 | var stateObject1 = new object(); 76 | var stateObject2 = new object(); 77 | var tcs = new TaskCompletionSource(); 78 | 79 | // Test reset before SetResult 80 | _resetTaskFunc(tcs.Task, stateObject1); 81 | if (tcs.Task.IsCanceled || tcs.Task.IsCompleted || tcs.Task.IsFaulted || tcs.Task.AsyncState != stateObject1) 82 | { 83 | _resetTaskFunc = null; 84 | return; 85 | } 86 | 87 | // Test SetResult 88 | tcs.SetResult(123); 89 | if (tcs.Task.IsCanceled || !tcs.Task.IsCompleted || tcs.Task.IsFaulted) 90 | { 91 | _resetTaskFunc = null; 92 | return; 93 | } 94 | 95 | // Test reset before SetCanceled 96 | _resetTaskFunc(tcs.Task, stateObject2); 97 | if (tcs.Task.IsCanceled || tcs.Task.IsCompleted || tcs.Task.IsFaulted || tcs.Task.AsyncState != stateObject2) 98 | { 99 | _resetTaskFunc = null; 100 | return; 101 | } 102 | 103 | // Test SetCanceled 104 | tcs.SetCanceled(); 105 | if (!tcs.Task.IsCanceled || !tcs.Task.IsCompleted || tcs.Task.IsFaulted) 106 | { 107 | _resetTaskFunc = null; 108 | return; 109 | } 110 | 111 | // Test reset before SetException 112 | _resetTaskFunc(tcs.Task, stateObject1); 113 | if (tcs.Task.IsCanceled || tcs.Task.IsCompleted || tcs.Task.IsFaulted || tcs.Task.AsyncState != stateObject1) 114 | { 115 | _resetTaskFunc = null; 116 | return; 117 | } 118 | 119 | // Test SetException 120 | var ex = new Exception(); 121 | tcs.SetException(ex); 122 | if (tcs.Task.IsCanceled || !tcs.Task.IsCompleted || !tcs.Task.IsFaulted || tcs.Task.Exception.InnerException != ex) 123 | { 124 | _resetTaskFunc = null; 125 | return; 126 | } 127 | } 128 | 129 | /// 130 | /// Forcibly disables re-use of instances in the method. 131 | /// This is just a safety switch in case when something goes wrong with re-using instances of . 132 | /// 133 | public static void DisableTaskCompletionSourceReUse() 134 | { 135 | _resetTaskFunc = null; 136 | } 137 | 138 | /// 139 | /// Resets a to initial incomplete state. 140 | /// This method by default re-uses the same instance of the by re-setting internal state of its using reflection. 141 | /// If such feature is not available or explicitly disable with the method, it just returns a new instance of a . 142 | /// 143 | /// Type of the result value 144 | /// Target to be reset or recreated. It's safe to pass null. 145 | /// Optional state object that you pass into constructor. 146 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 147 | public static void Reset(ref TaskCompletionSource taskCompletionSource, object stateObject = null) 148 | { 149 | if (_resetTaskFunc != null && taskCompletionSource != null) 150 | { 151 | _resetTaskFunc(taskCompletionSource.Task, stateObject); 152 | } 153 | else 154 | { 155 | taskCompletionSource = new TaskCompletionSource(); 156 | } 157 | } 158 | } 159 | } -------------------------------------------------------------------------------- /Tests/AsyncEnumeratorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Dasync.Collections; 6 | using NUnit.Framework; 7 | 8 | namespace Tests 9 | { 10 | [TestFixture] 11 | public class AsyncEnumeratorTests 12 | { 13 | [Test] 14 | public async Task RaceConditionOnEndOfEnumeration() 15 | { 16 | var enumerator = new AsyncEnumerator(async yield => 17 | { 18 | await Task.Run(async () => 19 | { 20 | await yield.ReturnAsync(1); 21 | }); 22 | }); 23 | 24 | var moveResult1 = await enumerator.MoveNextAsync(); 25 | var moveResult2 = await enumerator.MoveNextAsync(); 26 | var moveResult3 = await enumerator.MoveNextAsync(); 27 | 28 | Assert.IsTrue(moveResult1); 29 | Assert.IsFalse(moveResult2); 30 | Assert.IsFalse(moveResult3); 31 | } 32 | 33 | [Test] 34 | public void CancelEnumeration() 35 | { 36 | var cts = new CancellationTokenSource(); 37 | cts.Cancel(); 38 | 39 | var enumerable = new AsyncEnumerable(async yield => 40 | { 41 | await Task.Yield(); 42 | yield.CancellationToken.ThrowIfCancellationRequested(); 43 | }); 44 | 45 | Assert.ThrowsAsync(() => enumerable.ToListAsync(cts.Token)); 46 | } 47 | 48 | [Test] 49 | public async Task DisposeAfterPartialEnumeration() 50 | { 51 | // ARRANGE 52 | 53 | var testDisposable = new TestDisposable(); 54 | var enumerator = new AsyncEnumerator(async yield => 55 | { 56 | using (testDisposable) 57 | { 58 | await yield.ReturnAsync(1); 59 | await yield.ReturnAsync(2); 60 | await yield.ReturnAsync(3); 61 | } 62 | }); 63 | 64 | // ACT 65 | 66 | await enumerator.MoveNextAsync(); 67 | enumerator.Dispose(); 68 | 69 | // ASSERT 70 | 71 | Assert.IsTrue(testDisposable.HasDisposed); 72 | } 73 | 74 | [Test] 75 | public async Task DisposeByGCAfterPartialEnumeration() 76 | { 77 | // ARRANGE 78 | 79 | var testDisposable = new TestDisposable(); 80 | 81 | void CreateEnumeratorAndMoveNext() 82 | { 83 | var enumerator = new AsyncEnumerator(async yield => 84 | { 85 | using (testDisposable) 86 | { 87 | await yield.ReturnAsync(1); 88 | await yield.ReturnAsync(2); 89 | await yield.ReturnAsync(3); 90 | } 91 | }); 92 | 93 | // Do partial enumeration. 94 | enumerator.MoveNextAsync().GetAwaiter().GetResult(); 95 | } 96 | 97 | // ACT 98 | CreateEnumeratorAndMoveNext(); 99 | 100 | // Instead of calling enumerator.Dispose(), do garbage collection. 101 | GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true); 102 | 103 | // Give some time to other thread that does the disposal of the enumerator. 104 | // (see finalizer of the AsyncEnumerator for details) 105 | await Task.Delay(16); 106 | 107 | // ASSERT 108 | 109 | Assert.IsTrue(testDisposable.HasDisposed); 110 | } 111 | 112 | [Test] 113 | public void OnDisposeActionIsCalled() 114 | { 115 | // ARRANGE 116 | 117 | var testDisposable = new TestDisposable(); 118 | 119 | var enumerator = new AsyncEnumerator(async yield => 120 | { 121 | await yield.ReturnAsync(1); 122 | }, 123 | onDispose: () => testDisposable.Dispose()); 124 | 125 | // ACT 126 | 127 | enumerator.Dispose(); 128 | 129 | // ASSERT 130 | 131 | Assert.IsTrue(testDisposable.HasDisposed); 132 | } 133 | 134 | [Test] 135 | public void OnDisposeMustBeCalledOnGcWhenEnumerationHasNotBeenStarted() 136 | { 137 | // ARRANGE 138 | 139 | var testDisposable = new TestDisposable(); 140 | 141 | void CreateEnumerator() 142 | { 143 | var enumerator = new AsyncEnumerator(async yield => 144 | { 145 | await yield.ReturnAsync(1); 146 | }, 147 | onDispose: () => testDisposable.Dispose()); 148 | } 149 | 150 | // ACT 151 | 152 | CreateEnumerator(); 153 | GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true); 154 | Thread.Sleep(16); 155 | 156 | // ASSERT 157 | 158 | Assert.IsTrue(testDisposable.HasDisposed); 159 | } 160 | 161 | [Test] 162 | public async Task DisposeWaitsForFinalization() 163 | { 164 | var tcs = new TaskCompletionSource(); 165 | var isFinalized = false; 166 | 167 | var enumerable = new AsyncEnumerable(async yield => 168 | { 169 | try 170 | { 171 | await yield.ReturnAsync(1); 172 | await yield.ReturnAsync(2); 173 | } 174 | finally 175 | { 176 | await tcs.Task; 177 | isFinalized = true; 178 | } 179 | }); 180 | 181 | var enumerator = enumerable.GetAsyncEnumerator(); 182 | await enumerator.MoveNextAsync(); 183 | 184 | var disposeTask = enumerator.DisposeAsync(); 185 | await Task.Yield(); 186 | Assert.IsFalse(disposeTask.IsCompleted); 187 | 188 | tcs.SetResult(null); 189 | await disposeTask; 190 | Assert.IsTrue(isFinalized); 191 | } 192 | 193 | private class TestDisposable : IDisposable 194 | { 195 | public bool HasDisposed { get; private set; } 196 | public void Dispose() 197 | { 198 | HasDisposed = true; 199 | } 200 | } 201 | 202 | [Test] 203 | public async Task EnumerationMustEndAfterDispose() 204 | { 205 | // ARRANGE 206 | 207 | var enumerator = new AsyncEnumerator(async yield => 208 | { 209 | await yield.ReturnAsync(1); 210 | await yield.ReturnAsync(2); 211 | }); 212 | 213 | await enumerator.MoveNextAsync(); 214 | 215 | // ACT 216 | 217 | enumerator.Dispose(); 218 | bool moveNextResult = await enumerator.MoveNextAsync(); 219 | int currentElement = enumerator.Current; 220 | 221 | // ASSERT 222 | 223 | Assert.IsFalse(moveNextResult, "MoveNextAsync must return False after Dispose"); 224 | Assert.AreEqual(1, currentElement, "Current must not change after Dispose"); 225 | } 226 | 227 | [Test] 228 | public async Task YieldBreak() 229 | { 230 | // ARRANGE 231 | 232 | var asyncEnumerationCanceledExceptionRaised = false; 233 | 234 | var enumerator = new AsyncEnumerator(async yield => 235 | { 236 | try 237 | { 238 | yield.Break(); 239 | } 240 | catch (AsyncEnumerationCanceledException) 241 | { 242 | asyncEnumerationCanceledExceptionRaised = true; 243 | } 244 | 245 | await yield.ReturnAsync(1); 246 | }); 247 | 248 | // ACT 249 | 250 | var result = await enumerator.MoveNextAsync(); 251 | 252 | await Task.Yield(); 253 | 254 | // ASSERT 255 | 256 | Assert.IsFalse(result, "MoveNextAsync must return False due to Yield.Break"); 257 | Assert.IsTrue(asyncEnumerationCanceledExceptionRaised, "Yield.Break must throw AsyncEnumerationCanceledException so the enumerator body can perform finalization"); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Tests/EnumeratorLinqStyleExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Dasync.Collections; 5 | using NUnit.Framework; 6 | 7 | namespace Tests 8 | { 9 | [TestFixture] 10 | public class EnumeratorLinqStyleExtensionsTests 11 | { 12 | [Test] 13 | public async Task Select() 14 | { 15 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 16 | var actualResult = await collection.Select(x => x.ToString()).ToArrayAsync(); 17 | var expectedResult = new string[] { "1", "2", "3" }; 18 | Assert.AreEqual(expectedResult, actualResult); 19 | } 20 | 21 | [Test] 22 | public async Task SelectWithIndex() 23 | { 24 | var collection = new int[] { 1, 1, 1 }.GetAsyncEnumerator(); 25 | var actualResult = await collection.Select((x, i) => x + i).ToArrayAsync(); 26 | var expectedResult = new long[] { 1, 2, 3 }; 27 | Assert.AreEqual(expectedResult, actualResult); 28 | } 29 | 30 | [Test] 31 | public async Task First() 32 | { 33 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 34 | var actualResult = await collection.FirstAsync(); 35 | Assert.AreEqual(1, actualResult); 36 | } 37 | 38 | [Test] 39 | public void First_Empty() 40 | { 41 | var collection = AsyncEnumerable.Empty; 42 | Assert.ThrowsAsync(() => collection.FirstAsync()); 43 | } 44 | 45 | [Test] 46 | public async Task First_Predicate() 47 | { 48 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 49 | var actualResult = await collection.FirstAsync(x => x > 1); 50 | Assert.AreEqual(2, actualResult); 51 | } 52 | 53 | [Test] 54 | public void First_Predicate_Empty() 55 | { 56 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 57 | Assert.ThrowsAsync(() => collection.FirstAsync(x => x > 3)); 58 | } 59 | 60 | [Test] 61 | public async Task FirstOrDefault() 62 | { 63 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 64 | var actualResult = await collection.FirstAsync(); 65 | Assert.AreEqual(1, actualResult); 66 | } 67 | 68 | [Test] 69 | public async Task FirstOrDefault_Empty() 70 | { 71 | var collection = AsyncEnumerable.Empty; 72 | var actualResult = await collection.FirstOrDefaultAsync(); 73 | Assert.AreEqual(0, actualResult); 74 | } 75 | 76 | [Test] 77 | public async Task FirstOrDefault_Predicate() 78 | { 79 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 80 | var actualResult = await collection.FirstOrDefaultAsync(x => x > 1); 81 | Assert.AreEqual(2, actualResult); 82 | } 83 | 84 | [Test] 85 | public async Task FirstOrDefault_Predicate_Empty() 86 | { 87 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 88 | var actualResult = await collection.FirstOrDefaultAsync(x => x > 3); 89 | Assert.AreEqual(0, actualResult); 90 | } 91 | 92 | [Test] 93 | public async Task Take() 94 | { 95 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 96 | var actualResult = await collection.Take(2).ToArrayAsync(); 97 | var expectedResult = new int[] { 1, 2 }; 98 | Assert.AreEqual(expectedResult, actualResult); 99 | } 100 | 101 | [Test] 102 | public async Task Take_Zero() 103 | { 104 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 105 | var actualResult = await collection.Take(0).ToArrayAsync(); 106 | var expectedResult = new int[] { }; 107 | Assert.AreEqual(expectedResult, actualResult); 108 | } 109 | 110 | [Test] 111 | public async Task Take_More() 112 | { 113 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 114 | var actualResult = await collection.Take(int.MaxValue).ToArrayAsync(); 115 | var expectedResult = new int[] { 1, 2, 3 }; 116 | Assert.AreEqual(expectedResult, actualResult); 117 | } 118 | 119 | [Test] 120 | public async Task TakeWhile() 121 | { 122 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 123 | var actualResult = await collection.TakeWhile(x => x < 3).ToArrayAsync(); 124 | var expectedResult = new int[] { 1, 2 }; 125 | Assert.AreEqual(expectedResult, actualResult); 126 | } 127 | 128 | [Test] 129 | public async Task TakeWhile_None() 130 | { 131 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 132 | var actualResult = await collection.TakeWhile(x => x < 1).ToArrayAsync(); 133 | var expectedResult = new int[] { }; 134 | Assert.AreEqual(expectedResult, actualResult); 135 | } 136 | 137 | [Test] 138 | public async Task TakeWhile_All() 139 | { 140 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 141 | var actualResult = await collection.TakeWhile(x => x > 0).ToArrayAsync(); 142 | var expectedResult = new int[] { 1, 2, 3 }; 143 | Assert.AreEqual(expectedResult, actualResult); 144 | } 145 | 146 | [Test] 147 | public async Task Skip() 148 | { 149 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 150 | var actualResult = await collection.Skip(2).ToArrayAsync(); 151 | var expectedResult = new int[] { 3 }; 152 | Assert.AreEqual(expectedResult, actualResult); 153 | } 154 | 155 | [Test] 156 | public async Task Skip_Zero() 157 | { 158 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 159 | var actualResult = await collection.Skip(0).ToArrayAsync(); 160 | var expectedResult = new int[] { 1, 2, 3 }; 161 | Assert.AreEqual(expectedResult, actualResult); 162 | } 163 | 164 | [Test] 165 | public async Task Skip_More() 166 | { 167 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 168 | var actualResult = await collection.Skip(1000).ToArrayAsync(); 169 | var expectedResult = new int[] { }; 170 | Assert.AreEqual(expectedResult, actualResult); 171 | } 172 | 173 | [Test] 174 | public async Task SkipWhile() 175 | { 176 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 177 | var actualResult = await collection.SkipWhile(x => x < 3).ToArrayAsync(); 178 | var expectedResult = new int[] { 3 }; 179 | Assert.AreEqual(expectedResult, actualResult); 180 | } 181 | 182 | [Test] 183 | public async Task SkipWhile_None() 184 | { 185 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 186 | var actualResult = await collection.SkipWhile(x => x > 3).ToArrayAsync(); 187 | var expectedResult = new int[] { 1, 2, 3 }; 188 | Assert.AreEqual(expectedResult, actualResult); 189 | } 190 | 191 | [Test] 192 | public async Task SkipWhile_All() 193 | { 194 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 195 | var actualResult = await collection.SkipWhile(x => x > 0).ToArrayAsync(); 196 | var expectedResult = new int[] { }; 197 | Assert.AreEqual(expectedResult, actualResult); 198 | } 199 | 200 | [Test] 201 | public async Task Where() 202 | { 203 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 204 | var actualResult = await collection.Where(x => x != 2).ToArrayAsync(); 205 | var expectedResult = new int[] { 1, 3 }; 206 | Assert.AreEqual(expectedResult, actualResult); 207 | } 208 | 209 | [Test] 210 | public async Task Where_None() 211 | { 212 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 213 | var actualResult = await collection.Where(x => x > 3).ToArrayAsync(); 214 | var expectedResult = new int[] { }; 215 | Assert.AreEqual(expectedResult, actualResult); 216 | } 217 | 218 | [Test] 219 | public async Task Where_All() 220 | { 221 | var collection = new int[] { 1, 2, 3 }.GetAsyncEnumerator(); 222 | var actualResult = await collection.Where(x => x > 0).ToArrayAsync(); 223 | var expectedResult = new int[] { 1, 2, 3 }; 224 | Assert.AreEqual(expectedResult, actualResult); 225 | } 226 | 227 | [Test] 228 | public async Task WhereWithIndex() 229 | { 230 | var collection = new int[] { 1, 2, 1 }.GetAsyncEnumerator(); 231 | var actualResult = await collection.Where((x, i) => (x + i) != 3).ToArrayAsync(); 232 | var expectedResult = new int[] { 1 }; 233 | Assert.AreEqual(expectedResult, actualResult); 234 | } 235 | } 236 | } -------------------------------------------------------------------------------- /netfx45/lib/AsyncEnumerable.NetFx45.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Debug 9 | AnyCPU 10 | {111536C3-DABF-48CF-B993-E504C0BC5CE7} 11 | Library 12 | Properties 13 | System.Collections.Async 14 | AsyncEnumerable 15 | v4.5 16 | 512 17 | 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | TRACE;DEBUG;NETFX4_5 25 | prompt 26 | 4 27 | bin\Debug\AsyncEnumerable.xml 28 | latest 29 | 30 | 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE;NETFX4_5 35 | prompt 36 | 4 37 | bin\Release\AsyncEnumerable.xml 38 | latest 39 | 40 | 41 | true 42 | 43 | 44 | ..\..\AsyncEnumerable.pfx 45 | 46 | 47 | 48 | 49 | ..\..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll 50 | 51 | 52 | ..\..\packages\System.Threading.Tasks.Extensions.4.5.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll 53 | 54 | 55 | 56 | 57 | AssemblyInfo.cs 58 | 59 | 60 | AsyncEnumerable.cs 61 | 62 | 63 | AsyncEnumerationCanceledException.cs 64 | 65 | 66 | AsyncEnumerator.cs 67 | 68 | 69 | Extensions\AdapterExtensions.cs 70 | 71 | 72 | Extensions\ForEachAsyncExtensions.cs 73 | 74 | 75 | Extensions\ForEachAsync.cs 76 | 77 | 78 | Extensions\ForEachAsyncBreakException.cs 79 | 80 | 81 | Extensions\IAsyncEnumerableExtensions.cs 82 | 83 | 84 | Extensions\IAsyncEnumeratorExtensions.cs 85 | 86 | 87 | Extensions\ParallelForEachException.cs 88 | 89 | 90 | Extensions\ParallelForEachExtensions.cs 91 | 92 | 93 | IAsyncDisposable.cs 94 | 95 | 96 | IAsyncEnumerable.cs 97 | 98 | 99 | IAsyncEnumerator.cs 100 | 101 | 102 | Internals\AsyncEnumerableWrapper.cs 103 | 104 | 105 | Internals\AsyncEnumeratorWrapper.cs 106 | 107 | 108 | Internals\CancellationTokenEx.cs 109 | 110 | 111 | Internals\CurrentValueContainer.cs 112 | 113 | 114 | Internals\EmptyAsyncEnumerable.cs 115 | 116 | 117 | Internals\EmptyAsyncEnumerator.cs 118 | 119 | 120 | Internals\EnumerableAdapter.cs 121 | 122 | 123 | Internals\EnumeratorAdapter.cs 124 | 125 | 126 | Internals\TaskCompletionSource.cs 127 | 128 | 129 | Internals\TaskEx.cs 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 157 | -------------------------------------------------------------------------------- /src/Extensions/AdapterExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Threading; 7 | using Dasync.Collections.Internals; 8 | 9 | namespace Dasync.Collections 10 | { 11 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 12 | /// 13 | /// Converts generic IEnumerable to IAsyncEnumerable 14 | /// 15 | [EditorBrowsable(EditorBrowsableState.Never)] 16 | public static class EnumerableExtensions 17 | { 18 | /// 19 | /// Creates adapter for 20 | /// 21 | /// The instance of to convert 22 | /// If True the enumeration will be performed on the same thread, otherwise the MoveNext will be executed on a separate thread with Task.Run method 23 | /// Returns an instance of implementation 24 | public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable enumerable, bool runSynchronously = true) 25 | { 26 | if (enumerable == null) 27 | throw new ArgumentNullException(nameof(enumerable)); 28 | return enumerable as IAsyncEnumerable ?? new AsyncEnumerableWrapper(enumerable.Cast(), runSynchronously); 29 | } 30 | } 31 | #endif 32 | } 33 | 34 | namespace Dasync.Collections 35 | { 36 | /// 37 | /// Converts generic IEnumerable to IAsyncEnumerable 38 | /// 39 | [EditorBrowsable(EditorBrowsableState.Never)] 40 | public static class GenericEnumerableExtensions 41 | { 42 | /// 43 | /// Creates adapter for 44 | /// 45 | /// The element type 46 | /// The instance of to convert 47 | /// If True the enumeration will be performed on the same thread, otherwise the MoveNext will be executed on a separate thread with Task.Run method 48 | /// Returns an instance of implementation 49 | public static IAsyncEnumerable ToAsyncEnumerable(this IEnumerable enumerable, bool runSynchronously = true) 50 | { 51 | if (enumerable == null) 52 | throw new ArgumentNullException(nameof(enumerable)); 53 | if (ReferenceEquals(enumerable, Enumerable.Empty())) 54 | return AsyncEnumerable.Empty; 55 | return enumerable as IAsyncEnumerable ?? new AsyncEnumerableWrapper(enumerable, runSynchronously); 56 | } 57 | 58 | /// 59 | /// Creates adapter for the enumerator of 60 | /// 61 | /// The element type 62 | /// The instance of to convert 63 | /// If True the enumeration will be performed on the same thread, otherwise the MoveNext will be executed on a separate thread with Task.Run method 64 | /// Returns an instance of implementation 65 | public static IAsyncEnumerator GetAsyncEnumerator(this IEnumerable enumerable, bool runSynchronously = true) 66 | { 67 | if (enumerable == null) 68 | throw new ArgumentNullException(nameof(enumerable)); 69 | 70 | if (enumerable is IAsyncEnumerable asyncEnumerable) 71 | return asyncEnumerable.GetAsyncEnumerator(); 72 | 73 | var enumerator = enumerable.GetEnumerator(); 74 | return new AsyncEnumeratorWrapper(enumerator, runSynchronously); 75 | } 76 | 77 | /// 78 | /// Creates adapter for 79 | /// 80 | /// The element type 81 | /// The instance of to convert 82 | /// If True the enumeration will be performed on the same thread, otherwise the MoveNext will be executed on a separate thread with Task.Run method 83 | /// Returns an instance of implementation 84 | public static IAsyncEnumerator ToAsyncEnumerator(this IEnumerator enumerator, bool runSynchronously = true) 85 | { 86 | if (enumerator == null) 87 | throw new ArgumentNullException(nameof(enumerator)); 88 | return enumerator as IAsyncEnumerator ?? new AsyncEnumeratorWrapper(enumerator, runSynchronously); 89 | } 90 | } 91 | } 92 | 93 | namespace Dasync.Collections 94 | { 95 | /// 96 | /// Extension methods for for backward compatibility with version 1 of this libraray. 97 | /// Not recommended to use. 98 | /// 99 | [EditorBrowsable(EditorBrowsableState.Never)] 100 | public static class AsyncEnumerableAdapterExtensions 101 | { 102 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 103 | /// 104 | /// Converts to . 105 | /// This method is marked as [Obsolete] to discourage you from doing such conversion, 106 | /// which defeats the whole purpose of having a non-blocking async enumeration, 107 | /// and what might lead to dead-locks in ASP.NET or WPF applications. 108 | /// 109 | [Obsolete] 110 | public static IEnumerable ToEnumerable(this IAsyncEnumerable asyncEnumerable) 111 | { 112 | if (asyncEnumerable == null) 113 | throw new ArgumentNullException(nameof(asyncEnumerable)); 114 | if (asyncEnumerable is IEnumerable enumerable) 115 | return enumerable; 116 | return new EnumerableAdapter(asyncEnumerable); 117 | } 118 | 119 | /// 120 | /// Converts to . 121 | /// This method is marked as [Obsolete] to discourage you from doing such conversion, 122 | /// which defeats the whole purpose of having a non-blocking async enumeration, 123 | /// and what might lead to dead-locks in ASP.NET or WPF applications. 124 | /// 125 | [Obsolete] 126 | public static IEnumerator ToEnumerator(this IAsyncEnumerator asyncEnumerator) 127 | { 128 | if (asyncEnumerator == null) 129 | throw new ArgumentNullException(nameof(asyncEnumerator)); 130 | if (asyncEnumerator is IEnumerator enumerator) 131 | return enumerator; 132 | return new EnumeratorAdapter(asyncEnumerator); 133 | } 134 | 135 | /// 136 | /// Creates an enumerator that iterates through a collection synchronously. 137 | /// This method is marked as [Obsolete] to discourage you from using this synchronous version of 138 | /// the method instead of , 139 | /// what might lead to dead-locks in ASP.NET or WPF applications. 140 | /// 141 | [Obsolete] 142 | public static IEnumerator GetEnumerator(this IAsyncEnumerable asyncEnumerable) 143 | { 144 | if (asyncEnumerable == null) 145 | throw new ArgumentNullException(nameof(asyncEnumerable)); 146 | return asyncEnumerable.GetAsyncEnumerator().ToEnumerator(); 147 | } 148 | 149 | /// 150 | /// Advances the enumerator to the next element of the collection synchronously. 151 | /// This method is marked as [Obsolete] to discourage you from using this synchronous version of 152 | /// the method instead of , 153 | /// what might lead to dead-locks in ASP.NET or WPF applications. 154 | /// 155 | [Obsolete] 156 | public static bool MoveNext(this IAsyncEnumerator asyncEnumerator) 157 | { 158 | if (asyncEnumerator == null) 159 | throw new ArgumentNullException(nameof(asyncEnumerator)); 160 | return asyncEnumerator.MoveNextAsync().GetAwaiter().GetResult(); 161 | } 162 | #endif 163 | 164 | /// 165 | /// Converts to . 166 | /// This method is marked as [Obsolete] to discourage you from doing such conversion, 167 | /// which defeats the whole purpose of having a non-blocking async enumeration, 168 | /// and what might lead to dead-locks in ASP.NET or WPF applications. 169 | /// 170 | [Obsolete] 171 | public static IEnumerable ToEnumerable(this IAsyncEnumerable asyncEnumerable) 172 | { 173 | if (asyncEnumerable == null) 174 | throw new ArgumentNullException(nameof(asyncEnumerable)); 175 | if (asyncEnumerable is IEnumerable enumerable) 176 | return enumerable; 177 | return new EnumerableAdapter(asyncEnumerable); 178 | } 179 | 180 | /// 181 | /// Converts to . 182 | /// This method is marked as [Obsolete] to discourage you from doing such conversion, 183 | /// which defeats the whole purpose of having a non-blocking async enumeration, 184 | /// and what might lead to dead-locks in ASP.NET or WPF applications. 185 | /// 186 | [Obsolete] 187 | public static IEnumerator ToEnumerator(this IAsyncEnumerator asyncEnumerator) 188 | { 189 | if (asyncEnumerator == null) 190 | throw new ArgumentNullException(nameof(asyncEnumerator)); 191 | if (asyncEnumerator is IEnumerator enumerator) 192 | return enumerator; 193 | return new EnumeratorAdapter(asyncEnumerator); 194 | } 195 | 196 | /// 197 | /// Creates an enumerator that iterates through a collection synchronously. 198 | /// This method is marked as [Obsolete] to discourage you from using this synchronous version of 199 | /// the method instead of , 200 | /// what might lead to dead-locks in ASP.NET or WPF applications. 201 | /// 202 | [Obsolete] 203 | public static IEnumerator GetEnumerator(this IAsyncEnumerable asyncEnumerable) 204 | { 205 | if (asyncEnumerable == null) 206 | throw new ArgumentNullException(nameof(asyncEnumerable)); 207 | return asyncEnumerable.GetAsyncEnumerator().ToEnumerator(); 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /src/AsyncEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Dasync.Collections.Internals; 7 | 8 | namespace Dasync.Collections 9 | { 10 | /// 11 | /// Base type for and 12 | /// 13 | public abstract class AsyncEnumerator 14 | { 15 | /// 16 | /// Returns an empty . Safe to use by multiple threads. 17 | /// 18 | public static IAsyncEnumerator Empty() => AsyncEnumerator.Empty; 19 | } 20 | 21 | /// 22 | /// Helps to enumerate items in a collection asynchronously. 23 | /// Provides exactly the same functionality as , 24 | /// but allows to pass a user state object in the enumeration function, 25 | /// what can be used for performance optimization. 26 | /// 27 | public class AsyncEnumeratorWithState : CurrentValueContainer, IAsyncEnumerator, IAsyncEnumerator 28 | { 29 | private static readonly Action OnEnumerationCompleteAction = OnEnumerationComplete; 30 | 31 | private Func.Yield, TState, Task> _enumerationFunction; 32 | private Action _onDisposeAction; 33 | private AsyncEnumerator.Yield _yield; 34 | private Task _enumerationTask; 35 | 36 | /// 37 | /// Constructor 38 | /// 39 | /// A function that enumerates items in a collection asynchronously 40 | /// Any state object that is passed to the 41 | /// Optional action that gets invoked on Dispose() 42 | public AsyncEnumeratorWithState( 43 | Func.Yield, TState, Task> enumerationFunction, 44 | TState state, 45 | Action onDispose = null) 46 | { 47 | _enumerationFunction = enumerationFunction ?? throw new ArgumentNullException(nameof(enumerationFunction)); 48 | _onDisposeAction = onDispose; 49 | State = state; 50 | 51 | // If dispose action has not been defined and enumeration has not been started, there is nothing to finilize. 52 | if (onDispose == null) 53 | GC.SuppressFinalize(this); 54 | } 55 | 56 | /// 57 | /// Finalizer 58 | /// 59 | ~AsyncEnumeratorWithState() 60 | { 61 | Dispose(manualDispose: false); 62 | } 63 | 64 | internal CancellationToken MasterCancellationToken; 65 | 66 | /// 67 | /// A user state that gets passed into the enumeration function. 68 | /// 69 | protected TState State { get; } 70 | 71 | /// 72 | /// Gets the element in the collection at the current position of the enumerator 73 | /// 74 | public virtual TItem Current 75 | { 76 | get 77 | { 78 | if (!HasCurrentValue) 79 | throw new InvalidOperationException("Call MoveNextAsync() or MoveNext() before accessing the Current item"); 80 | return CurrentValue; 81 | } 82 | } 83 | 84 | /// 85 | /// Tells if enumeration is complete. Returns True only after MoveNextAsync returns False. 86 | /// 87 | public bool IsEnumerationComplete => _yield != null && _yield.IsComplete; 88 | 89 | object IAsyncEnumerator.Current => Current; 90 | 91 | /// 92 | /// Advances the enumerator to the next element of the collection asynchronously 93 | /// 94 | /// Returns a Task that does transition to the next element. The result of the task is True if the enumerator was successfully advanced to the next element, or False if the enumerator has passed the end of the collection. 95 | public virtual ValueTask MoveNextAsync() 96 | { 97 | if (_enumerationFunction == null) 98 | return new ValueTask(false); 99 | 100 | if (_yield == null) 101 | _yield = new AsyncEnumerator.Yield(this, MasterCancellationToken); 102 | 103 | var moveNextCompleteTask = _yield.OnMoveNext(); 104 | 105 | if (_enumerationTask == null) 106 | { 107 | // Register for finalization, which might be needed if caller 108 | // doesn't not finish the enumeration and does not call Dispose(). 109 | GC.ReRegisterForFinalize(this); 110 | 111 | _enumerationTask = 112 | _enumerationFunction(_yield, State) 113 | .ContinueWith(OnEnumerationCompleteAction, this, TaskContinuationOptions.ExecuteSynchronously); 114 | } 115 | 116 | return moveNextCompleteTask; 117 | } 118 | 119 | /// 120 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources 121 | /// 122 | public void Dispose() 123 | { 124 | DisposeAsync().GetAwaiter().GetResult(); 125 | } 126 | 127 | /// 128 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources 129 | /// 130 | public async ValueTask DisposeAsync() 131 | { 132 | var enumTask = _enumerationTask; 133 | 134 | Dispose(manualDispose: true); 135 | 136 | if (enumTask != null) 137 | { 138 | try 139 | { 140 | await enumTask; 141 | } 142 | catch 143 | { 144 | } 145 | } 146 | 147 | GC.SuppressFinalize(this); 148 | } 149 | 150 | /// 151 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources 152 | /// 153 | /// True if called from Dispose() method, otherwise False - called by GC 154 | protected virtual void Dispose(bool manualDispose) 155 | { 156 | if (manualDispose) 157 | { 158 | _yield?.SetCanceled(); 159 | } 160 | else if (_yield != null && !_yield.IsComplete) 161 | { 162 | var yield = _yield; 163 | Task.Run(() => yield.SetCanceled()); // don't block the GC thread 164 | } 165 | 166 | _enumerationTask = null; 167 | _yield = null; 168 | 169 | _onDisposeAction?.Invoke(State); 170 | _onDisposeAction = null; 171 | _enumerationFunction = null; 172 | } 173 | 174 | private static void OnEnumerationComplete(Task task, object state) 175 | { 176 | var enumerator = (AsyncEnumeratorWithState)state; 177 | 178 | // When en enumeration is complete, there is nothing to dispose. 179 | GC.SuppressFinalize(enumerator); 180 | 181 | if (task.IsFaulted) 182 | { 183 | if (task.Exception.GetBaseException() is AsyncEnumerationCanceledException) 184 | { 185 | enumerator._yield?.SetCanceled(); 186 | } 187 | else 188 | { 189 | enumerator._yield?.SetFailed(task.Exception); 190 | } 191 | } 192 | else if (task.IsCanceled) 193 | { 194 | enumerator._yield?.SetCanceled(); 195 | } 196 | else 197 | { 198 | enumerator._yield?.SetComplete(); 199 | } 200 | } 201 | } 202 | 203 | /// 204 | /// Helps to enumerate items in a collection asynchronously 205 | /// 206 | public class AsyncEnumerator : AsyncEnumeratorWithState.NoStateAdapter> 207 | { 208 | /// 209 | /// An empty . Safe to use by multiple threads. 210 | /// 211 | public static readonly IAsyncEnumerator Empty = new EmptyAsyncEnumerator(); 212 | 213 | /// 214 | /// The asynchronous version of the 'yield' construction 215 | /// 216 | public sealed class Yield 217 | { 218 | private TaskCompletionSource _resumeEnumerationTcs; // Can be of any type - there is no non-generic version of the TaskCompletionSource 219 | private TaskCompletionSource _moveNextCompleteTcs; 220 | private CurrentValueContainer _currentValueContainer; 221 | 222 | internal Yield(CurrentValueContainer currentValueContainer, CancellationToken cancellationToken) 223 | { 224 | _currentValueContainer = currentValueContainer; 225 | CancellationToken = cancellationToken; 226 | } 227 | 228 | /// 229 | /// Gets the cancellation token that was passed to the method 230 | /// 231 | public CancellationToken CancellationToken { get; private set; } 232 | 233 | internal bool IsComplete { get; private set; } 234 | 235 | /// 236 | /// Yields an item asynchronously (similar to 'yield return' statement) 237 | /// 238 | /// The item of the collection to yield 239 | /// Returns a Task which tells if when you can continue to yield the next item 240 | #pragma warning disable AsyncMethodMustTakeCancellationToken // Does not take a CancellationToken by design 241 | public Task ReturnAsync(T item) 242 | #pragma warning restore AsyncMethodMustTakeCancellationToken 243 | { 244 | TaskCompletionSource.Reset(ref _resumeEnumerationTcs); 245 | _currentValueContainer.CurrentValue = item; 246 | _moveNextCompleteTcs.TrySetResult(true); 247 | return _resumeEnumerationTcs.Task; 248 | } 249 | 250 | /// 251 | /// Stops iterating items in the collection (similar to 'yield break' statement) 252 | /// 253 | /// Always throws this exception to stop the enumeration task 254 | public void Break() 255 | { 256 | SetComplete(); 257 | throw new AsyncEnumerationCanceledException(); 258 | } 259 | 260 | internal void SetComplete() 261 | { 262 | IsComplete = true; 263 | _moveNextCompleteTcs.TrySetResult(false); 264 | } 265 | 266 | internal void SetCanceled() 267 | { 268 | IsComplete = true; 269 | if (!CancellationToken.IsCancellationRequested) 270 | CancellationToken = CancellationTokenEx.Canceled; 271 | _resumeEnumerationTcs?.TrySetException(new AsyncEnumerationCanceledException()); 272 | _moveNextCompleteTcs?.TrySetCanceled(); 273 | } 274 | 275 | internal void SetFailed(Exception ex) 276 | { 277 | IsComplete = true; 278 | _moveNextCompleteTcs?.TrySetException(ex.GetBaseException()); 279 | } 280 | 281 | internal ValueTask OnMoveNext() 282 | { 283 | if (!IsComplete) 284 | { 285 | TaskCompletionSource.Reset(ref _moveNextCompleteTcs); 286 | _resumeEnumerationTcs?.TrySetResult(true); 287 | } 288 | 289 | return new ValueTask(_moveNextCompleteTcs.Task); 290 | } 291 | } 292 | 293 | /// 294 | /// Constructor 295 | /// 296 | /// A function that enumerates items in a collection asynchronously 297 | /// Optional action that gets invoked on Dispose() 298 | public AsyncEnumerator(Func enumerationFunction, Action onDispose = null) 299 | : base( 300 | enumerationFunction: NoStateAdapter.Enumerate, 301 | onDispose: onDispose == null ? null : NoStateAdapter.OnDispose, 302 | state: new NoStateAdapter 303 | { 304 | EnumerationFunction = enumerationFunction, 305 | DisposeAction = onDispose 306 | }) 307 | { 308 | } 309 | 310 | /// 311 | /// Internal implementation details 312 | /// 313 | [EditorBrowsable(EditorBrowsableState.Never)] 314 | public struct NoStateAdapter 315 | { 316 | internal Func EnumerationFunction; 317 | internal Action DisposeAction; 318 | 319 | internal static readonly Func Enumerate = EnumerateWithNoStateAdapter; 320 | internal static readonly Action OnDispose = OnDisposeAdapter; 321 | 322 | private static Task EnumerateWithNoStateAdapter(Yield yield, NoStateAdapter state) => state.EnumerationFunction(yield); 323 | 324 | private static void OnDisposeAdapter(NoStateAdapter state) => state.DisposeAction?.Invoke(); 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Unofficial C# Async Streams](banner.png) 2 | 3 | ## SUMMARY 4 | 5 | Makes asynchronous enumeration as easy as the synchronous counterpart. Such feature is also known as 'Async Streams' in upcoming C# 8.0. The library introduces familiar and easy to use syntax, `IAsyncEnumerable`, `IAsyncEnumerator`, `ForEachAsync()`, `ParallelForEachAsync()`, and other useful extension methods. 6 | 7 | 8 | ## PROBLEM SPACE 9 | 10 | Helps to (a) create an element provider, where producing an element can take a lot of time due to dependency on other asynchronous events (e.g. wait handles, network streams), and (b) a consumer that processes those element as soon as they are ready without blocking the thread (the processing is scheduled on a worker thread instead). [Bassam Alugili made a great explanation on Async Streams in an InfoQ article](https://www.infoq.com/articles/Async-Streams). 11 | 12 | 13 | ## INSTALLATION 14 | 15 | Visual Studio's [Package Manager Console](https://docs.microsoft.com/en-us/nuget/consume-packages/install-use-packages-powershell): 16 | ```powershell 17 | Install-Package AsyncEnumerator -ProjectName MyProject 18 | ``` 19 | 20 | [NET Core CLI](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x): 21 | ```shell 22 | dotnet add package AsyncEnumerator 23 | ``` 24 | 25 | Edit .csproj file: 26 | ```xml 27 | 28 | 29 | 30 | ``` 31 | 32 | 33 | ## EXAMPLE 1 (demonstrates usage only) 34 | 35 | ```csharp 36 | using Dasync.Collections; 37 | 38 | static IAsyncEnumerable ProduceAsyncNumbers(int start, int end) 39 | { 40 | return new AsyncEnumerable(async yield => { 41 | 42 | // Just to show that ReturnAsync can be used multiple times 43 | await yield.ReturnAsync(start); 44 | 45 | for (int number = start + 1; number <= end; number++) 46 | await yield.ReturnAsync(number); 47 | 48 | // You can break the enumeration loop with the following call: 49 | yield.Break(); 50 | 51 | // This won't be executed due to the loop break above 52 | await yield.ReturnAsync(12345); 53 | }); 54 | } 55 | 56 | // Just to compare with synchronous version of enumerator 57 | static IEnumerable ProduceNumbers(int start, int end) 58 | { 59 | yield return start; 60 | 61 | for (int number = start + 1; number <= end; number++) 62 | yield return number; 63 | 64 | yield break; 65 | 66 | yield return 12345; 67 | } 68 | 69 | static async Task ConsumeNumbersAsync() 70 | { 71 | var asyncEnumerableCollection = ProduceAsyncNumbers(start: 1, end: 10); 72 | int count == 0; 73 | await asyncEnumerableCollection.ForEachAsync(async number => { 74 | await Console.Out.WriteLineAsync($"{number}"); 75 | count++; 76 | if (count >= 5) 77 | { 78 | // You can break the ForEachAsync loop with the following call: 79 | ForEachAsync.Break(); 80 | } 81 | }); 82 | } 83 | 84 | // Just to compare with synchronous version of enumeration 85 | static void ConsumeNumbers() 86 | { 87 | var enumerableCollection = ProduceNumbers(start: 1, end: 10); 88 | foreach (var number in enumerableCollection) { 89 | Console.Out.WriteLine($"{number}"); 90 | } 91 | } 92 | ``` 93 | 94 | 95 | ## EXAMPLE 2 (LINQ-style extension methods) 96 | 97 | ```csharp 98 | using Dasync.Collections; 99 | 100 | IAsyncEnumerable ConvertGoodFoosToBars(IAsyncEnumerable items) 101 | { 102 | return items 103 | .Where(foo => foo.IsGood) 104 | .Select(foo => Bar.FromFoo(foo)); 105 | } 106 | ``` 107 | 108 | 109 | ## EXAMPLE 3 (async parallel for-each) 110 | 111 | ```csharp 112 | using Dasync.Collections; 113 | 114 | async Task> GetStringsAsync(IEnumerable uris, HttpClient httpClient, CancellationToken cancellationToken) 115 | { 116 | var result = new ConcurrentBag(); 117 | 118 | await uris.ParallelForEachAsync( 119 | async uri => 120 | { 121 | var str = await httpClient.GetStringAsync(uri, cancellationToken); 122 | result.Add(str); 123 | }, 124 | maxDegreeOfParallelism: 5, 125 | cancellationToken: cancellationToken); 126 | 127 | return result; 128 | } 129 | ``` 130 | 131 | 132 | ## [EXAMPLE 4 (Azure Cloud Table streaming)](https://gist.github.com/tyrotoxin/9f5ffb69cbcd9042f6f989104e4da0f6) 133 | 134 | 135 | ## [EXAMPLE 5 (Azure Cloud Queue streaming)](https://gist.github.com/tyrotoxin/c1f85a900bba9e5a4f2d570f67be2e37) 136 | 137 | 138 | ## WILL THIS MAKE MY APP FASTER? 139 | 140 | No and Yes. Just making everything `async` makes your app tiny little bit slower because it adds overhead in form of state machines and tasks. However, this will help you to better utilize worker threads in the app because you don't need to block them anymore by waiting on the next element to be produced - i.e. this will make your app better in general when it has such multiple enumerations running in parallel. The best fit for `IAsyncEnumerable` is a case when you read elements from a network stream, like HTTP + XML (as shown above; SOAP), or a database client implementation where result of a query is a set or rows. 141 | 142 | 143 | ## DIFFERENCES BETWEEN C# 8.0 AND EARLIER VERSIONS 144 | C# 8.0 and .NET Standard 2.1 introduce the native support for [Async Streams](https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/generate-consume-asynchronous-stream). However, if you still use an older version of C# and wish to upgrade, the changes should be straight-forward. 145 | 146 | Change an iterator from this: 147 | ```csharp 148 | using Dasync.Collections; 149 | IAsyncEnumerable AsyncIterator() => new AsyncEnumerable(async yield => 150 | { 151 | await yield.ReturnAsync(123); 152 | }); 153 | ``` 154 | to this: 155 | ```csharp 156 | using System.Collections.Generic; 157 | async IAsyncEnumerable AsyncIterator() 158 | { 159 | yield return 123; 160 | } 161 | ``` 162 | 163 | Change a consumer from this: 164 | ```csharp 165 | using Dasync.Collections; 166 | await asyncEnumerable.ForEachAsync(item => 167 | { 168 | ... 169 | }); 170 | ``` 171 | to this: 172 | ```csharp 173 | using System.Collections.Generic; 174 | await foreach (var item in asyncEnumerable) 175 | { 176 | ... 177 | } 178 | ``` 179 | 180 | 181 | ## REFERENCES 182 | 183 | GitHub: https://github.com/Dasync/AsyncEnumerable 184 | 185 | NuGet.org: https://www.nuget.org/packages/AsyncEnumerator/ 186 | 187 | License: https://github.com/Dasync/AsyncEnumerable/blob/master/LICENSE 188 | 189 | 190 | ## IMPLEMENTATION DETAILS 191 | 192 | __1: How to use this library?__ 193 | 194 | See examples above. The core code is in `System.Collections.Async` namespace. You can also find useful extension methods in `System.Collections` and `System.Collections.Generic` namespaces for `IEnumerable` and `IEnumerator` interfaces. 195 | 196 | 197 | __2: Using CancellationToken__ 198 | 199 | ```csharp 200 | IAsyncEnumerable ProduceNumbers() 201 | { 202 | return new AsyncEnumerable(async yield => { 203 | 204 | await FooAsync(yield.CancellationToken); 205 | }); 206 | } 207 | ``` 208 | 209 | __3: Always remember about ConfigureAwait(false)__ 210 | 211 | To avoid performance degradation and possible dead-locks in ASP.NET or WPF applications (or any `SynchronizationContext`-dependent environment), you should always put `ConfigureAwait(false)` in your `await` statements: 212 | 213 | ```csharp 214 | IAsyncEnumerable GetValues() 215 | { 216 | return new AsyncEnumerable(async yield => 217 | { 218 | await FooAsync().ConfigureAwait(false); 219 | 220 | // Yes, it's even needed for 'yield.ReturnAsync' 221 | await yield.ReturnAsync(123).ConfigureAwait(false); 222 | }); 223 | } 224 | ``` 225 | 226 | __4: Clean-up on incomplete enumeration__ 227 | 228 | Imagine such situation: 229 | 230 | ```csharp 231 | IAsyncEnumerable ReadValuesFromQueue() 232 | { 233 | return new AsyncEnumerable(async yield => 234 | { 235 | using (var queueClient = CreateQueueClient()) 236 | { 237 | while (true) 238 | { 239 | var message = queueClient.DequeueMessageAsync(); 240 | if (message == null) 241 | break; 242 | 243 | await yield.ReturnAsync(message.Value); 244 | } 245 | } 246 | }); 247 | } 248 | 249 | Task ReadFirstValueOrDefaultAsync() 250 | { 251 | return ReadValuesFromQueue().FirstOrDefaultAsync(); 252 | } 253 | ``` 254 | 255 | The `FirstOrDefaultAsync` method will try to read first value from the `IAsyncEnumerator`, and then will just dispose it. However, disposing `AsyncEnumerator` does not mean that the `queueClient` in the lambda function will be disposed automatically as well, because async methods are just state machines which need somehow to go to a particular state to do the clean-up. To provide such behavior, when you dispose an `AsyncEnumerator` before you reach the end of enumeration, it will tell to resume your async lambda function (at `await yield.ReturnAsync()`) with the `AsyncEnumerationCanceledException` (derives from `OperationCanceledException`). Having such exception in your lambda method will break normal flow of enumeration and will go to terminal state of the underlying state machine, what will do the clean-up, i.e. dispose the `queueClient` in this case. You don't need (and shouldn't) catch that exception type, because it's handled internally by `AsyncEnumerator`. The same exception is thrown when you call `yield.Break()`. 256 | 257 | There is another option to do the cleanup on `Dispose`: 258 | 259 | ```csharp 260 | IAsyncEnumerator GetQueueEnumerator() 261 | { 262 | var queueClient = CreateQueueClient(); 263 | 264 | return new AsyncEnumerable(async yield => 265 | { 266 | while (true) 267 | { 268 | var message = queueClient.DequeueMessageAsync(); 269 | if (message == null) 270 | break; 271 | 272 | await yield.ReturnAsync(message.Value); 273 | } 274 | }, 275 | onDispose: () => queueClient.Dispose()); 276 | } 277 | ``` 278 | 279 | __5: Why is GetAsyncEnumeratorAsync async?__ 280 | 281 | The `IAsyncEnumerable.GetAsyncEnumeratorAsync()` method is async and returns a `Task`, where the current implementation of `AsyncEnumerable` always runs that method synchronously and just returns an instance of `AsyncEnumerator`. Having interfaces allows you to do your own implementation, where classes mentioned above are just helpers. The initial idea was to be able to support database-like scenarios, where `GetAsyncEnumeratorAsync()` executes a query first (what internally returns a pointer), and the `MoveNextAsync()` enumerates through rows (by using that pointer). 282 | 283 | __6: Returning IAsyncEnumerable vs IAsyncEnumerator__ 284 | 285 | When you implement a method that returns an async-enumerable collection you have a choice to return either `IAsyncEnumerable` or `IAsyncEnumerator` - the constructors of the helper classes `AsyncEnumerable` and `AsyncEnumerator` are absolutely identical. Both interfaces have same set of useful extension methods, like `ForEachAsync`. 286 | 287 | When you create an 'enumerable', you create a factory that produces 'enumerators', i.e. you can enumerate through a collection many times. On the other hand, creating an 'enumerator' is needed when you can through a collection only once. 288 | 289 | Consider these 2 scenarios: 290 | 291 | ```csharp 292 | // You want to execute the same query against a database many times - you need an 'enumerable' 293 | IAsyncEnumerable GetItemsFromDatabase() 294 | { 295 | return new AsyncEnumerable(async yield => 296 | { 297 | using (var dbReader = DbContext.ExecuteQuery(...)) 298 | { 299 | while (true) 300 | { 301 | DbRow row = dbReader.ReadAsync(); 302 | if (row == null) 303 | break; 304 | await yield.ReturnAsync(row); 305 | } 306 | } 307 | }); 308 | } 309 | 310 | // Assume that you cannot seek in the stream - you need an 'enumerator' 311 | IAsyncEnumerator EnumerateBytesInStream(Stream stream) 312 | { 313 | return new AsyncEnumerator(async yield => 314 | { 315 | while (true) 316 | { 317 | int byte = await stream.ReadByteAsync(); 318 | if (byte < 0) 319 | break; 320 | await yield.ReturnAsync((byte)byte); 321 | } 322 | }); 323 | } 324 | ``` 325 | 326 | __7: Where is Reset or ResetAsync?__ 327 | 328 | The `Reset` method must not be on the `IEnumerator` interface, and should be considered as deprecated. Create a new enumerator instead. This is the reason why the 'oneTimeUse' flag was removed in version 2 of this library. 329 | 330 | __8: How can I do synchronous for-each enumeration through IAsyncEnumerable?__ 331 | 332 | You can use extension methods like `IAsyncEnumerable.ToEnumerable()` to use built-in `foreach` enumeration, BUT you should never do that! The general idea of this library is to avoid thread-blocking calls on worker threads, where converting an `IAsyncEnumerable` to `IEnumerable` will just defeat the whole purpose of this library. This is the reason why such synchronous extension methods are marked with `[Obsolete]` attribute. 333 | 334 | __9: What's the difference between ForEachAsync and ParallelForEachAsync?__ 335 | 336 | The `ForEachAsync` allows you to go through a collection and perform an action on every single item in sequential manner. On the other hand, `ParallelForEachAsync` allows you to run the action on multiple items at the same time where the sequential order of completion is not guaranteed. For the latter, the degree of the parallelism is controlled by the `maxDegreeOfParallelism` argument, however it does not guarantee to spin up the exact amount of threads, because it depends on the [thread pool size](https://msdn.microsoft.com/en-us/library/system.threading.threadpool.setmaxthreads(v=vs.110).aspx) and its occupancy at a moment of time. Such parallel approach is much better than trying to create a task for an action for every single item on the collection and then awaiting on all of them with `Task.WhenAll`, because it adds less overhead to the runtime, better with memory usage, and helps with throttling-sensitive scenarios. 337 | 338 | 339 | ## RELEASE NOTES 340 | 341 | 4.0.2: Bug-fix: Slow `Take` extension method. 342 | Add `AllAsync` and `AnyAsync` extension methods. 343 | Add support for SourceLink. 344 | 345 | 4.0.1: Explicitly add the DLL for .NET Framework 4.6.1 to be compatible with NET Standard 2.0. No functional changes. 346 | 347 | 4.0.0: Use interfaces from Microsoft.Bcl.AsyncInterfaces package in NET Standard 2.0. No functional changes. 348 | 349 | 3.1.0: Add support for NET Standard 2.1. 350 | Consolidate interface with Microsoft's implementation. 351 | 352 | 2.2.2: Bug-fix: IAsyncEnumerator.MoveNext must return False on Yield.Break instead of throwing OperationCanceledException. 353 | 354 | 2.2.0: New LINQ-style extension methods: SelectMany, Append, Prepend, OfType, Concat, Distinct, ToDictionaryAsync, ToLookupAsync, AggregateAsync. 355 | 356 | 2.1.1: Bug-fix: AsyncEnumerator.OnEnumerationComplete might throw a NullReferneceException when enumeration is canceled. 357 | Bug-fix: Batch extension method does not work at all - always throws InvalidOperationException. 358 | 359 | 2.1.0: New extension methods: Batch, UnionAll, Single, SingleOrDefault, DefaultIfEmpty, Cast. 360 | Bug-fix: AsyncEnumerator.MoveNextAsync() must not succeed after Dispose(). 361 | 362 | 2.0.1: Bug-fix: call onDispose when AsyncEnumerator is GC'ed but enumeration hasn't been started. 363 | Bug-fix: re-throw base exception instead of AggregateException in blocking synchronous methods. 364 | 365 | 2.0.0: Revise design of the library: same features, but slight paradigm shift and interface breaking changes. 366 | 367 | 1.5.0: Add support for .NET Standard, minor improvements. 368 | 369 | 1.4.2: Add finalizer to AsyncEnumerator and call Dispose in ForEachAsync and ParallelForEachAsync extension methods. 370 | 371 | 1.4.0: Add new generic type AsyncEnumeratorWithState for performance optimization. 372 | Now IAsyncEnumerator<T> is covariant. 373 | Add ForEachAsync, ParallelForeachAsync, and LINQ-style extension methods for IAsyncEnumerator. 374 | 375 | 1.3.0: Significantly improve performance of AsyncEnumerator by reducing thread switching and re-using instances of TaskCompletionSource. 376 | Add support for a state object that can be passed into AsyncEnumerable and AsyncEnumerator for performance optimization. 377 | Remove CancellationToken from Select/Take/Skip/Where extension methods - fix improper implementation. 378 | Move AsyncEnumerationCanceledException out of the generic AsyncEnumerator type. 379 | Change default behavior of the ToAsyncEnumerable extension method - now MoveNextAsync will run synchronously by default. 380 | 381 | 1.2.3: AsyncEnumerationCanceledException is thrown to the async enumeration function when the AsyncEnumerator is disposed before reaching the end of enumeration, what allows to do the clean-up. 382 | Fixed MoveNextAsync() that threw an exception sometimes only when you passed the end of enumeration. 383 | 384 | 1.2.2: Fix exception propagation in AsyncEnumerator. 385 | 386 | 1.2.1: New Linq-style extension methods in System.Collections.Async namespace. 387 | 388 | 1.2.0: Contract breaking changes in ParallelForEachAsync: introduce ParallelForEachException to unify error outcome of the loop. 389 | 390 | 1.1.0: Add ParallelForEachAsync extension methods for IEnumerable<T> and IAsyncEnumerable<T> in System.Collections.Async namespace. -------------------------------------------------------------------------------- /src/Extensions/ForEachAsyncExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Dasync.Collections 8 | { 9 | /// 10 | /// Enables asynchronous 'foreach' enumeration over an IAsyncEnumerable 11 | /// 12 | [EditorBrowsable(EditorBrowsableState.Never)] 13 | public static class ForEachAsyncExtensions 14 | { 15 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 16 | /// 17 | /// Enumerates over all elements in the collection asynchronously 18 | /// 19 | /// The collection of elements which can be enumerated asynchronously 20 | /// A synchronous action to perform for every single item in the collection 21 | /// A cancellation token to stop enumerating 22 | /// Returns a Task which does enumeration over elements in the collection 23 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Action action, CancellationToken cancellationToken = default) 24 | { 25 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 26 | try 27 | { 28 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 29 | { 30 | action(enumerator.Current); 31 | } 32 | } 33 | catch (ForEachAsyncBreakException) 34 | { 35 | } 36 | finally 37 | { 38 | await enumerator.DisposeAsync().ConfigureAwait(false); 39 | } 40 | } 41 | 42 | /// 43 | /// Enumerates over all elements in the collection asynchronously 44 | /// 45 | /// The collection of elements which can be enumerated asynchronously 46 | /// A synchronous action to perform for every single item in the collection 47 | /// Returns a Task which does enumeration over elements in the collection 48 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Action action) 49 | { 50 | try 51 | { 52 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 53 | { 54 | action(enumerator.Current); 55 | } 56 | } 57 | catch (ForEachAsyncBreakException) 58 | { 59 | } 60 | finally 61 | { 62 | await enumerator.DisposeAsync().ConfigureAwait(false); 63 | } 64 | } 65 | 66 | /// 67 | /// Enumerates over all elements in the collection asynchronously 68 | /// 69 | /// The collection of elements which can be enumerated asynchronously 70 | /// A synchronous action to perform for every single item in the collection, where the second argument is the index of an item 71 | /// A cancellation token to stop enumerating 72 | /// Returns a Task which does enumeration over elements in the collection 73 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Action action, CancellationToken cancellationToken = default) 74 | { 75 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 76 | try 77 | { 78 | long index = 0; 79 | 80 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 81 | { 82 | action(enumerator.Current, index); 83 | index++; 84 | } 85 | } 86 | catch (ForEachAsyncBreakException) 87 | { 88 | } 89 | finally 90 | { 91 | await enumerator.DisposeAsync().ConfigureAwait(false); 92 | } 93 | } 94 | 95 | /// 96 | /// Enumerates over all elements in the collection asynchronously 97 | /// 98 | /// The collection of elements which can be enumerated asynchronously 99 | /// A synchronous action to perform for every single item in the collection, where the second argument is the index of an item 100 | /// Returns a Task which does enumeration over elements in the collection 101 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Action action) 102 | { 103 | try 104 | { 105 | long index = 0; 106 | 107 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 108 | { 109 | action(enumerator.Current, index); 110 | index++; 111 | } 112 | } 113 | catch (ForEachAsyncBreakException) 114 | { 115 | } 116 | finally 117 | { 118 | await enumerator.DisposeAsync().ConfigureAwait(false); 119 | } 120 | } 121 | 122 | /// 123 | /// Enumerates over all elements in the collection asynchronously 124 | /// 125 | /// The collection of elements which can be enumerated asynchronously 126 | /// An asynchronous action to perform for every single item in the collection 127 | /// A cancellation token to stop enumerating 128 | /// Returns a Task which does enumeration over elements in the collection 129 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Func action, CancellationToken cancellationToken = default) 130 | { 131 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 132 | try 133 | { 134 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 135 | { 136 | await action(enumerator.Current).ConfigureAwait(false); 137 | } 138 | } 139 | catch (ForEachAsyncBreakException) 140 | { 141 | } 142 | finally 143 | { 144 | await enumerator.DisposeAsync().ConfigureAwait(false); 145 | } 146 | } 147 | 148 | /// 149 | /// Enumerates over all elements in the collection asynchronously 150 | /// 151 | /// The collection of elements which can be enumerated asynchronously 152 | /// An asynchronous action to perform for every single item in the collection 153 | /// Returns a Task which does enumeration over elements in the collection 154 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Func action) 155 | { 156 | try 157 | { 158 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 159 | { 160 | await action(enumerator.Current).ConfigureAwait(false); 161 | } 162 | } 163 | catch (ForEachAsyncBreakException) 164 | { 165 | } 166 | finally 167 | { 168 | await enumerator.DisposeAsync().ConfigureAwait(false); 169 | } 170 | } 171 | 172 | /// 173 | /// Enumerates over all elements in the collection asynchronously 174 | /// 175 | /// The collection of elements which can be enumerated asynchronously 176 | /// An asynchronous action to perform for every single item in the collection, where the second argument is the index of an item 177 | /// A cancellation token to stop enumerating 178 | /// Returns a Task which does enumeration over elements in the collection 179 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Func action, CancellationToken cancellationToken = default) 180 | { 181 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 182 | try 183 | { 184 | long index = 0; 185 | 186 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 187 | { 188 | await action(enumerator.Current, index).ConfigureAwait(false); 189 | index++; 190 | } 191 | } 192 | catch (ForEachAsyncBreakException) 193 | { 194 | } 195 | finally 196 | { 197 | await enumerator.DisposeAsync().ConfigureAwait(false); 198 | } 199 | } 200 | 201 | /// 202 | /// Enumerates over all elements in the collection asynchronously 203 | /// 204 | /// The collection of elements which can be enumerated asynchronously 205 | /// An asynchronous action to perform for every single item in the collection, where the second argument is the index of an item 206 | /// Returns a Task which does enumeration over elements in the collection 207 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Func action) 208 | { 209 | try 210 | { 211 | long index = 0; 212 | 213 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 214 | { 215 | await action(enumerator.Current, index).ConfigureAwait(false); 216 | index++; 217 | } 218 | } 219 | catch (ForEachAsyncBreakException) 220 | { 221 | } 222 | finally 223 | { 224 | await enumerator.DisposeAsync().ConfigureAwait(false); 225 | } 226 | } 227 | #endif 228 | 229 | /// 230 | /// Enumerates over all elements in the collection asynchronously 231 | /// 232 | /// The type of elements in the collection 233 | /// The collection of elements which can be enumerated asynchronously 234 | /// A synchronous action to perform for every single item in the collection 235 | /// A cancellation token to stop enumerating 236 | /// Returns a Task which does enumeration over elements in the collection 237 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Action action, CancellationToken cancellationToken = default) 238 | { 239 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 240 | try 241 | { 242 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 243 | { 244 | action(enumerator.Current); 245 | } 246 | } 247 | catch (ForEachAsyncBreakException) 248 | { 249 | } 250 | finally 251 | { 252 | await enumerator.DisposeAsync().ConfigureAwait(false); 253 | } 254 | } 255 | 256 | /// 257 | /// Enumerates over all elements in the collection asynchronously 258 | /// 259 | /// The type of elements in the collection 260 | /// The collection of elements which can be enumerated asynchronously 261 | /// A synchronous action to perform for every single item in the collection 262 | /// Returns a Task which does enumeration over elements in the collection 263 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Action action) 264 | { 265 | try 266 | { 267 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 268 | { 269 | action(enumerator.Current); 270 | } 271 | } 272 | catch (ForEachAsyncBreakException) 273 | { 274 | } 275 | finally 276 | { 277 | await enumerator.DisposeAsync().ConfigureAwait(false); 278 | } 279 | } 280 | 281 | /// 282 | /// Enumerates over all elements in the collection asynchronously 283 | /// 284 | /// The type of elements in the collection 285 | /// The collection of elements which can be enumerated asynchronously 286 | /// A synchronous action to perform for every single item in the collection, where the second argument is the index of an item 287 | /// A cancellation token to stop enumerating 288 | /// Returns a Task which does enumeration over elements in the collection 289 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Action action, CancellationToken cancellationToken = default) 290 | { 291 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 292 | try 293 | { 294 | long index = 0; 295 | 296 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 297 | { 298 | action(enumerator.Current, index); 299 | index++; 300 | } 301 | } 302 | catch (ForEachAsyncBreakException) 303 | { 304 | } 305 | finally 306 | { 307 | await enumerator.DisposeAsync().ConfigureAwait(false); 308 | } 309 | } 310 | 311 | /// 312 | /// Enumerates over all elements in the collection asynchronously 313 | /// 314 | /// The type of elements in the collection 315 | /// The collection of elements which can be enumerated asynchronously 316 | /// A synchronous action to perform for every single item in the collection, where the second argument is the index of an item 317 | /// Returns a Task which does enumeration over elements in the collection 318 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Action action) 319 | { 320 | try 321 | { 322 | long index = 0; 323 | 324 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 325 | { 326 | action(enumerator.Current, index); 327 | index++; 328 | } 329 | } 330 | catch (ForEachAsyncBreakException) 331 | { 332 | } 333 | finally 334 | { 335 | await enumerator.DisposeAsync().ConfigureAwait(false); 336 | } 337 | } 338 | 339 | /// 340 | /// Enumerates over all elements in the collection asynchronously 341 | /// 342 | /// The type of elements in the collection 343 | /// The collection of elements which can be enumerated asynchronously 344 | /// An asynchronous action to perform for every single item in the collection 345 | /// A cancellation token to stop enumerating 346 | /// Returns a Task which does enumeration over elements in the collection 347 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Func action, CancellationToken cancellationToken = default) 348 | { 349 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 350 | try 351 | { 352 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 353 | { 354 | await action(enumerator.Current).ConfigureAwait(false); 355 | } 356 | } 357 | catch (ForEachAsyncBreakException) 358 | { 359 | } 360 | finally 361 | { 362 | await enumerator.DisposeAsync().ConfigureAwait(false); 363 | } 364 | } 365 | 366 | /// 367 | /// Enumerates over all elements in the collection asynchronously 368 | /// 369 | /// The type of elements in the collection 370 | /// The collection of elements which can be enumerated asynchronously 371 | /// An asynchronous action to perform for every single item in the collection 372 | /// Returns a Task which does enumeration over elements in the collection 373 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Func action) 374 | { 375 | try 376 | { 377 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 378 | { 379 | await action(enumerator.Current).ConfigureAwait(false); 380 | } 381 | } 382 | catch (ForEachAsyncBreakException) 383 | { 384 | } 385 | finally 386 | { 387 | await enumerator.DisposeAsync().ConfigureAwait(false); 388 | } 389 | } 390 | 391 | /// 392 | /// Enumerates over all elements in the collection asynchronously 393 | /// 394 | /// The type of elements in the collection 395 | /// The collection of elements which can be enumerated asynchronously 396 | /// An asynchronous action to perform for every single item in the collection, where the second argument is the index of an item 397 | /// A cancellation token to stop enumerating 398 | /// Returns a Task which does enumeration over elements in the collection 399 | public static async Task ForEachAsync(this IAsyncEnumerable enumerable, Func action, CancellationToken cancellationToken = default) 400 | { 401 | var enumerator = enumerable.GetAsyncEnumerator(cancellationToken); 402 | try 403 | { 404 | long index = 0; 405 | 406 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 407 | { 408 | await action(enumerator.Current, index).ConfigureAwait(false); 409 | index++; 410 | } 411 | } 412 | catch (ForEachAsyncBreakException) 413 | { 414 | } 415 | finally 416 | { 417 | await enumerator.DisposeAsync().ConfigureAwait(false); 418 | } 419 | } 420 | 421 | /// 422 | /// Enumerates over all elements in the collection asynchronously 423 | /// 424 | /// The type of elements in the collection 425 | /// The collection of elements which can be enumerated asynchronously 426 | /// An asynchronous action to perform for every single item in the collection, where the second argument is the index of an item 427 | /// Returns a Task which does enumeration over elements in the collection 428 | public static async Task ForEachAsync(this IAsyncEnumerator enumerator, Func action) 429 | { 430 | try 431 | { 432 | long index = 0; 433 | 434 | while (await enumerator.MoveNextAsync().ConfigureAwait(false)) 435 | { 436 | await action(enumerator.Current, index).ConfigureAwait(false); 437 | index++; 438 | } 439 | } 440 | catch (ForEachAsyncBreakException) 441 | { 442 | } 443 | finally 444 | { 445 | await enumerator.DisposeAsync().ConfigureAwait(false); 446 | } 447 | } 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /Tests/EnumerableLinqStyleExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using System.Linq; 6 | using Dasync.Collections; 7 | 8 | namespace Tests 9 | { 10 | [TestFixture] 11 | public class EnumerableLinqStyleExtensionsTests 12 | { 13 | [Test] 14 | public async Task Select() 15 | { 16 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 17 | var actualResult = await collection.Select(x => x.ToString()).ToArrayAsync(); 18 | var expectedResult = new string[] { "1", "2", "3" }; 19 | Assert.AreEqual(expectedResult, actualResult); 20 | } 21 | 22 | [Test] 23 | public async Task SelectWithIndex() 24 | { 25 | var collection = new int[] { 1, 1, 1 }.ToAsyncEnumerable(); 26 | var actualResult = await collection.Select((x, i) => x + i).ToArrayAsync(); 27 | var expectedResult = new long[] { 1, 2, 3 }; 28 | Assert.AreEqual(expectedResult, actualResult); 29 | } 30 | 31 | [Test] 32 | public async Task First() 33 | { 34 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 35 | var actualResult = await collection.FirstAsync(); 36 | Assert.AreEqual(1, actualResult); 37 | } 38 | 39 | [Test] 40 | public void First_Empty() 41 | { 42 | var collection = AsyncEnumerable.Empty; 43 | Assert.ThrowsAsync(() => collection.FirstAsync()); 44 | } 45 | 46 | [Test] 47 | public async Task First_Predicate() 48 | { 49 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 50 | var actualResult = await collection.FirstAsync(x => x > 1); 51 | Assert.AreEqual(2, actualResult); 52 | } 53 | 54 | [Test] 55 | public void First_Predicate_Empty() 56 | { 57 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 58 | Assert.ThrowsAsync(() => collection.FirstAsync(x => x > 3)); 59 | } 60 | 61 | [Test] 62 | public async Task FirstOrDefault() 63 | { 64 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 65 | var actualResult = await collection.FirstAsync(); 66 | Assert.AreEqual(1, actualResult); 67 | } 68 | 69 | [Test] 70 | public async Task FirstOrDefault_Empty() 71 | { 72 | var collection = AsyncEnumerable.Empty; 73 | var actualResult = await collection.FirstOrDefaultAsync(); 74 | Assert.AreEqual(0, actualResult); 75 | } 76 | 77 | [Test] 78 | public async Task FirstOrDefault_Predicate() 79 | { 80 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 81 | var actualResult = await collection.FirstOrDefaultAsync(x => x > 1); 82 | Assert.AreEqual(2, actualResult); 83 | } 84 | 85 | [Test] 86 | public async Task FirstOrDefault_Predicate_Empty() 87 | { 88 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 89 | var actualResult = await collection.FirstOrDefaultAsync(x => x > 3); 90 | Assert.AreEqual(0, actualResult); 91 | } 92 | 93 | [Test] 94 | public async Task Take() 95 | { 96 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 97 | var actualResult = await collection.Take(2).ToArrayAsync(); 98 | var expectedResult = new int[] { 1, 2 }; 99 | Assert.AreEqual(expectedResult, actualResult); 100 | } 101 | 102 | [Test] 103 | public async Task Take_Zero() 104 | { 105 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 106 | var actualResult = await collection.Take(0).ToArrayAsync(); 107 | var expectedResult = new int[] { }; 108 | Assert.AreEqual(expectedResult, actualResult); 109 | } 110 | 111 | [Test] 112 | public async Task Take_More() 113 | { 114 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 115 | var actualResult = await collection.Take(int.MaxValue).ToArrayAsync(); 116 | var expectedResult = new int[] { 1, 2, 3 }; 117 | Assert.AreEqual(expectedResult, actualResult); 118 | } 119 | 120 | [Test] 121 | public async Task TakeWhile() 122 | { 123 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 124 | var actualResult = await collection.TakeWhile(x => x < 3).ToArrayAsync(); 125 | var expectedResult = new int[] { 1, 2 }; 126 | Assert.AreEqual(expectedResult, actualResult); 127 | } 128 | 129 | [Test] 130 | public async Task TakeWhile_None() 131 | { 132 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 133 | var actualResult = await collection.TakeWhile(x => x < 1).ToArrayAsync(); 134 | var expectedResult = new int[] { }; 135 | Assert.AreEqual(expectedResult, actualResult); 136 | } 137 | 138 | [Test] 139 | public async Task TakeWhile_All() 140 | { 141 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 142 | var actualResult = await collection.TakeWhile(x => x > 0).ToArrayAsync(); 143 | var expectedResult = new int[] { 1, 2, 3 }; 144 | Assert.AreEqual(expectedResult, actualResult); 145 | } 146 | 147 | [Test] 148 | public async Task Skip() 149 | { 150 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 151 | var actualResult = await collection.Skip(2).ToArrayAsync(); 152 | var expectedResult = new int[] { 3 }; 153 | Assert.AreEqual(expectedResult, actualResult); 154 | } 155 | 156 | [Test] 157 | public async Task Skip_Zero() 158 | { 159 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 160 | var actualResult = await collection.Skip(0).ToArrayAsync(); 161 | var expectedResult = new int[] { 1, 2, 3 }; 162 | Assert.AreEqual(expectedResult, actualResult); 163 | } 164 | 165 | [Test] 166 | public async Task Skip_More() 167 | { 168 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 169 | var actualResult = await collection.Skip(1000).ToArrayAsync(); 170 | var expectedResult = new int[] { }; 171 | Assert.AreEqual(expectedResult, actualResult); 172 | } 173 | 174 | [Test] 175 | public async Task SkipWhile() 176 | { 177 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 178 | var actualResult = await collection.SkipWhile(x => x < 3).ToArrayAsync(); 179 | var expectedResult = new int[] { 3 }; 180 | Assert.AreEqual(expectedResult, actualResult); 181 | } 182 | 183 | [Test] 184 | public async Task SkipWhile_None() 185 | { 186 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 187 | var actualResult = await collection.SkipWhile(x => x > 3).ToArrayAsync(); 188 | var expectedResult = new int[] { 1, 2, 3 }; 189 | Assert.AreEqual(expectedResult, actualResult); 190 | } 191 | 192 | [Test] 193 | public async Task SkipWhile_All() 194 | { 195 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 196 | var actualResult = await collection.SkipWhile(x => x > 0).ToArrayAsync(); 197 | var expectedResult = new int[] { }; 198 | Assert.AreEqual(expectedResult, actualResult); 199 | } 200 | 201 | [Test] 202 | public async Task Where() 203 | { 204 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 205 | var actualResult = await collection.Where(x => x != 2).ToArrayAsync(); 206 | var expectedResult = new int[] { 1, 3 }; 207 | Assert.AreEqual(expectedResult, actualResult); 208 | } 209 | 210 | [Test] 211 | public async Task Where_None() 212 | { 213 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 214 | var actualResult = await collection.Where(x => x > 3).ToArrayAsync(); 215 | var expectedResult = new int[] { }; 216 | Assert.AreEqual(expectedResult, actualResult); 217 | } 218 | 219 | [Test] 220 | public async Task Where_All() 221 | { 222 | var collection = new int[] { 1, 2, 3 }.ToAsyncEnumerable(); 223 | var actualResult = await collection.Where(x => x > 0).ToArrayAsync(); 224 | var expectedResult = new int[] { 1, 2, 3 }; 225 | Assert.AreEqual(expectedResult, actualResult); 226 | } 227 | 228 | [Test] 229 | public async Task WhereWithIndex() 230 | { 231 | var collection = new int[] { 1, 2, 1 }.ToAsyncEnumerable(); 232 | var actualResult = await collection.Where((x, i) => (x + i) != 3).ToArrayAsync(); 233 | var expectedResult = new int[] { 1 }; 234 | Assert.AreEqual(expectedResult, actualResult); 235 | } 236 | 237 | [Test] 238 | public async Task SelectMany_Async() 239 | { 240 | var collection1 = new int[] { 1, 2 }.ToAsyncEnumerable(); 241 | var collection2 = new int[0].ToAsyncEnumerable(); 242 | var collection3 = new int[] { 3, 4, 5 }.ToAsyncEnumerable(); 243 | var set = new[] { collection1, collection2, collection3 }.ToAsyncEnumerable(); 244 | var actualResult = await set.SelectMany(collection => collection).ToArrayAsync(); 245 | var expectedResult = new int[] { 1, 2, 3, 4, 5 }; 246 | Assert.AreEqual(expectedResult, actualResult); 247 | } 248 | 249 | [Test] 250 | public async Task SelectMany_Async_Transform() 251 | { 252 | var collection1 = new int[] { 1, 2 }.ToAsyncEnumerable(); 253 | var collection2 = new int[] { 3, 4, 5 }.ToAsyncEnumerable(); 254 | var set = new[] { collection1, collection2 }.ToAsyncEnumerable(); 255 | var actualResult = await set.SelectMany( 256 | collectionSelector: collection => collection, 257 | resultSelector: (collection, item) => item.ToString()) 258 | .ToArrayAsync(); 259 | var expectedResult = new [] { "1", "2", "3", "4", "5" }; 260 | Assert.AreEqual(expectedResult, actualResult); 261 | } 262 | 263 | [Test] 264 | public async Task SelectMany_Sync() 265 | { 266 | var collection1 = new int[] { 1, 2 }; 267 | var collection2 = new int[0]; 268 | var collection3 = new int[] { 3, 4, 5 }; 269 | var set = new[] { collection1, collection2, collection3 }.ToAsyncEnumerable(); 270 | var actualResult = await set.SelectMany(collection => collection).ToArrayAsync(); 271 | var expectedResult = new int[] { 1, 2, 3, 4, 5 }; 272 | Assert.AreEqual(expectedResult, actualResult); 273 | } 274 | 275 | [Test] 276 | public async Task SelectMany_Sync_Transform() 277 | { 278 | var collection1 = new int[] { 1, 2 }; 279 | var collection2 = new int[] { 3, 4, 5 }; 280 | var set = new[] { collection1, collection2 }.ToAsyncEnumerable(); 281 | var actualResult = await set.SelectMany( 282 | collectionSelector: collection => collection, 283 | resultSelector: (collection, item) => item.ToString()) 284 | .ToArrayAsync(); 285 | var expectedResult = new [] { "1", "2", "3", "4", "5" }; 286 | Assert.AreEqual(expectedResult, actualResult); 287 | } 288 | 289 | [Test] 290 | public async Task Append() 291 | { 292 | var collection = new int[] { 1, 2 }.ToAsyncEnumerable(); 293 | var extendedCollection = collection.Append(3); 294 | var actualResult = await extendedCollection.ToArrayAsync(); 295 | var expectedResult = new int[] { 1, 2, 3 }; 296 | Assert.AreEqual(expectedResult, actualResult); 297 | } 298 | 299 | [Test] 300 | public async Task Prepend() 301 | { 302 | var collection = new int[] { 1, 2 }.ToAsyncEnumerable(); 303 | var extendedCollection = collection.Prepend(0); 304 | var actualResult = await extendedCollection.ToArrayAsync(); 305 | var expectedResult = new int[] { 0, 1, 2 }; 306 | Assert.AreEqual(expectedResult, actualResult); 307 | } 308 | 309 | #if !NETSTANDARD2_1 && !NETSTANDARD2_0 && !NET461 310 | [Test] 311 | public async Task OfType() 312 | { 313 | var collection = new object[] { "a", 1, "b", Guid.NewGuid() }; 314 | var asyncCollection = (IAsyncEnumerable)collection.ToAsyncEnumerable(); 315 | 316 | var filteredStringCollection = asyncCollection.OfType(); 317 | var actualStringResult = await filteredStringCollection.ToArrayAsync(); 318 | var expectedStringResult = new [] { "a", "b" }; 319 | Assert.AreEqual(expectedStringResult, actualStringResult); 320 | 321 | var filteredIntegerCollection = asyncCollection.OfType(); 322 | var actualIntegerResult = await filteredIntegerCollection.ToArrayAsync(); 323 | var expectedIntegerResult = new[] { 1 }; 324 | Assert.AreEqual(expectedIntegerResult, actualIntegerResult); 325 | 326 | var filteredUriCollection = asyncCollection.OfType(); 327 | var actualUriResult = await filteredUriCollection.ToArrayAsync(); 328 | var expectedUriResult = new Uri[0]; 329 | Assert.AreEqual(expectedUriResult, actualUriResult); 330 | 331 | var filteredObjectCollection = asyncCollection.OfType(); 332 | var actualObjectResult = await filteredObjectCollection.ToArrayAsync(); 333 | var expectedObjectResult = collection; 334 | Assert.AreEqual(expectedObjectResult, actualObjectResult); 335 | } 336 | #endif 337 | 338 | [Test] 339 | public async Task Concat() 340 | { 341 | var collection1 = new int[] { 1 }.ToAsyncEnumerable(); 342 | var collection2 = new int[] { 2, 3 }.ToAsyncEnumerable(); 343 | var resultCollection = collection1.Concat(collection2); 344 | var actualResult = await resultCollection.ToArrayAsync(); 345 | var expectedResult = new int[] { 1, 2, 3 }; 346 | Assert.AreEqual(expectedResult, actualResult); 347 | } 348 | 349 | [Test] 350 | public async Task ToDictionary() 351 | { 352 | var collection = new(int key, string value)[] { (1, "a"), (2, "b") }; 353 | var asyncCollection = collection.ToAsyncEnumerable(); 354 | var actualDictionary = await asyncCollection.ToDictionaryAsync(x => x.key); 355 | Assert.IsNotNull(actualDictionary); 356 | Assert.AreEqual(actualDictionary[1], collection[0]); 357 | Assert.AreEqual(actualDictionary[2], collection[1]); 358 | } 359 | 360 | [Test] 361 | public async Task ToDictionary_ValueSelector() 362 | { 363 | var collection = new(int key, string value)[] { (1, "a"), (2, "b") }; 364 | var asyncCollection = collection.ToAsyncEnumerable(); 365 | var actualDictionary = await asyncCollection.ToDictionaryAsync(x => x.key, x => x.value); 366 | Assert.IsNotNull(actualDictionary); 367 | Assert.AreEqual(actualDictionary[1], "a"); 368 | Assert.AreEqual(actualDictionary[2], "b"); 369 | } 370 | 371 | [Test] 372 | public async Task ToDictionary_ValueSelector_WithComparer() 373 | { 374 | var collection = new(string key, int value)[] { ("a", 1), ("b", 2) }; 375 | var asyncCollection = collection.ToAsyncEnumerable(); 376 | var actualDictionary = await asyncCollection.ToDictionaryAsync(x => x.key, x => x.value, StringComparer.OrdinalIgnoreCase); 377 | Assert.IsNotNull(actualDictionary); 378 | Assert.AreEqual(actualDictionary["A"], 1); 379 | Assert.AreEqual(actualDictionary["B"], 2); 380 | } 381 | 382 | [Test] 383 | public async Task Distinct() 384 | { 385 | var collection = new [] { "a", "a", "A", "a" }.ToAsyncEnumerable(); 386 | var actualResult = await collection.Distinct().ToArrayAsync(); 387 | var expectedResult = new [] { "a", "A" }; 388 | Assert.AreEqual(expectedResult, actualResult); 389 | } 390 | 391 | [Test] 392 | public async Task Distinct_WithComparer() 393 | { 394 | var collection = new[] { "a", "a", "A", "a" }.ToAsyncEnumerable(); 395 | var actualResult = await collection.Distinct(StringComparer.OrdinalIgnoreCase).ToArrayAsync(); 396 | var expectedResult = new[] { "a" }; 397 | Assert.AreEqual(expectedResult, actualResult); 398 | } 399 | 400 | [Test] 401 | public void Aggregate_NoElements() 402 | { 403 | var collection = new int[0].ToAsyncEnumerable(); 404 | Assert.ThrowsAsync(() => collection.AggregateAsync((a, b) => a + b)); 405 | } 406 | 407 | [Test] 408 | public async Task Aggregate() 409 | { 410 | var collection = new [] { 1, 2, 3}.ToAsyncEnumerable(); 411 | var actualResult = await collection.AggregateAsync((a, b) => a + b); 412 | var expectedResult = 6; 413 | Assert.AreEqual(expectedResult, actualResult); 414 | } 415 | 416 | [Test] 417 | public async Task Aggregate_Seed() 418 | { 419 | var collection = new[] { 1, 2, 3 }.ToAsyncEnumerable(); 420 | var actualResult = await collection.AggregateAsync(-10, (a, b) => a + b); 421 | var expectedResult = -4; 422 | Assert.AreEqual(expectedResult, actualResult); 423 | } 424 | 425 | [Test] 426 | public async Task Aggregate_Seed_ResultSelector() 427 | { 428 | var collection = new[] { 1, 2, 3 }.ToAsyncEnumerable(); 429 | var actualResult = await collection.AggregateAsync(10, (a, b) => a + b, x => x.ToString()); 430 | var expectedResult = "16"; 431 | Assert.AreEqual(expectedResult, actualResult); 432 | } 433 | 434 | [Test] 435 | public async Task ToLookup() 436 | { 437 | var collection = new(int key, string value)[] { (1, "a"), (2, "b"), (1, "c") }.ToAsyncEnumerable(); 438 | var actualLookup = await collection.ToLookupAsync(x => x.key); 439 | Assert.IsNotNull(actualLookup); 440 | Assert.AreEqual(new[] { (1, "a"), (1, "c") }, actualLookup[1]); 441 | Assert.AreEqual(new[] { (2, "b") }, actualLookup[2]); 442 | } 443 | 444 | [Test] 445 | public async Task ToLookup_ValueSelector() 446 | { 447 | var collection = new(int key, string value)[] { (1, "a"), (2, "b"), (1, "c") }.ToAsyncEnumerable(); 448 | var actualLookup = await collection.ToLookupAsync(x => x.key, x => x.value); 449 | Assert.IsNotNull(actualLookup); 450 | Assert.AreEqual(new[] { "a", "c" }, actualLookup[1]); 451 | Assert.AreEqual(new[] { "b" }, actualLookup[2]); 452 | } 453 | 454 | [Test] 455 | public async Task ToLookup_ValueSelector_WithComparer() 456 | { 457 | var collection = new(string key, int value)[] { ("a", 1), ("b", 2), ("A", 3) }.ToAsyncEnumerable(); 458 | var actualLookup = await collection.ToLookupAsync(x => x.key, x => x.value, StringComparer.OrdinalIgnoreCase); 459 | Assert.IsNotNull(actualLookup); 460 | Assert.AreEqual(new[] { 1, 3 }, actualLookup["A"]); 461 | Assert.AreEqual(new[] { 2 }, actualLookup["b"]); 462 | } 463 | 464 | [Test] 465 | public async Task All_NoElements() 466 | { 467 | var collection = new bool[0].ToAsyncEnumerable(); 468 | var actualResult = await collection.AllAsync(x => x); 469 | Assert.IsTrue(actualResult); 470 | } 471 | 472 | [Test] 473 | public async Task All_False() 474 | { 475 | var collection = new[] {1, 2, 3}.ToAsyncEnumerable(); 476 | var actualResult = await collection.AllAsync(x => x > 2); 477 | Assert.IsFalse(actualResult); 478 | } 479 | 480 | [Test] 481 | public async Task All_True() 482 | { 483 | var collection = new[] {1, 2, 3}.ToAsyncEnumerable(); 484 | var actualResult = await collection.AllAsync(x => x > 0); 485 | Assert.IsTrue(actualResult); 486 | } 487 | 488 | [Test] 489 | public async Task Any_NoElements() 490 | { 491 | var collection = new bool[0].ToAsyncEnumerable(); 492 | var actualResult = await collection.AnyAsync(x => x); 493 | Assert.IsFalse(actualResult); 494 | } 495 | 496 | [Test] 497 | public async Task Any_True() 498 | { 499 | var collection = new[] {1, 2, 3}.ToAsyncEnumerable(); 500 | var actualResult = await collection.AnyAsync(x => x > 2); 501 | Assert.IsTrue(actualResult); 502 | } 503 | 504 | [Test] 505 | public async Task Any_False() 506 | { 507 | var collection = new[] {1, 2, 3}.ToAsyncEnumerable(); 508 | var actualResult = await collection.AnyAsync(x => x > 4); 509 | Assert.IsFalse(actualResult); 510 | } 511 | } 512 | } --------------------------------------------------------------------------------