From ecd83a1785583dff1b6b755a1750f71e6e1d2f34 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 11 Oct 2026 12:42:55 +0200 Subject: [PATCH] Add IAsyncEnumerableWithYield sample on .NET 10 Moves the "IAsyncEnumerable with yield in C#" sample from the standalone async-enumerable-yield-csharp repository into async-csharp/IAsyncEnumerableWithYield. - Three console programs on net10.0 with top-level statements, one per step of the article: Task>, synchronous yield return, and async IAsyncEnumerable consumed with await foreach. - Web API on minimal hosting (one Program.cs, no Startup class, no Swashbuckle): ValuesController, a minimal API endpoint, and an EF Core ItemsController that streams rows with AsNoTracking().AsAsyncEnumerable() over SQLite. - MSTest tests: both endpoints deliver their first item before the last, the EF Core query runs no command until it is enumerated, and the items endpoint returns every row. --- .../IAsyncEnumerableWithYield/.gitignore | 4 + .../IAsyncEnumerableWithYield.sln | 90 +++++++++++++++++++ .../IAsyncEnumerableWithYield.csproj | 10 +++ .../IAsyncEnumerableWithYield/Program.cs | 17 ++++ .../Controllers/ItemsController.cs | 18 ++++ .../Controllers/ValuesController.cs | 21 +++++ .../IAsyncEnumerableWithYieldApi/Data/Item.cs | 5 ++ .../Data/ItemContext.cs | 6 ++ .../IAsyncEnumerableWithYieldApi.csproj | 13 +++ .../IAsyncEnumerableWithYieldApi/Program.cs | 39 ++++++++ .../Properties/launchSettings.json | 15 ++++ .../appsettings.Development.json | 8 ++ .../appsettings.json | 12 +++ .../SyncYield/Program.cs | 17 ++++ .../SyncYield/SyncYield.csproj | 10 +++ .../TaskOfIEnumerable/Program.cs | 20 +++++ .../TaskOfIEnumerable.csproj | 10 +++ .../Tests/AsAsyncEnumerableUnitTest.cs | 53 +++++++++++ .../Tests/StreamingUnitTest.cs | 83 +++++++++++++++++ .../Tests/Tests.csproj | 26 ++++++ 20 files changed, 477 insertions(+) create mode 100644 async-csharp/IAsyncEnumerableWithYield/.gitignore create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.sln create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.csproj create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/Program.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ItemsController.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ValuesController.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/Item.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/ItemContext.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/IAsyncEnumerableWithYieldApi.csproj create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Program.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Properties/launchSettings.json create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.Development.json create mode 100644 async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.json create mode 100644 async-csharp/IAsyncEnumerableWithYield/SyncYield/Program.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/SyncYield/SyncYield.csproj create mode 100644 async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/Program.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/TaskOfIEnumerable.csproj create mode 100644 async-csharp/IAsyncEnumerableWithYield/Tests/AsAsyncEnumerableUnitTest.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/Tests/StreamingUnitTest.cs create mode 100644 async-csharp/IAsyncEnumerableWithYield/Tests/Tests.csproj diff --git a/async-csharp/IAsyncEnumerableWithYield/.gitignore b/async-csharp/IAsyncEnumerableWithYield/.gitignore new file mode 100644 index 0000000000..ec260fd053 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/.gitignore @@ -0,0 +1,4 @@ +# SQLite database the API creates on first run +*.db +*.db-shm +*.db-wal diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.sln b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.sln new file mode 100644 index 0000000000..2bc483d455 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.sln @@ -0,0 +1,90 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskOfIEnumerable", "TaskOfIEnumerable\TaskOfIEnumerable.csproj", "{CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncYield", "SyncYield\SyncYield.csproj", "{29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IAsyncEnumerableWithYield", "IAsyncEnumerableWithYield\IAsyncEnumerableWithYield.csproj", "{296E143D-0CCD-48CA-9054-5E918F8B5462}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IAsyncEnumerableWithYieldApi", "IAsyncEnumerableWithYieldApi\IAsyncEnumerableWithYieldApi.csproj", "{01A66C19-2011-41A9-876A-E02A45431465}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{32DFB249-9EA5-4E7C-8BBA-E01853778BE7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Debug|x64.Build.0 = Debug|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Debug|x86.Build.0 = Debug|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Release|Any CPU.Build.0 = Release|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Release|x64.ActiveCfg = Release|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Release|x64.Build.0 = Release|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Release|x86.ActiveCfg = Release|Any CPU + {CEF7D2E3-0B02-4E40-9084-C29DCCA519CD}.Release|x86.Build.0 = Release|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Debug|x64.ActiveCfg = Debug|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Debug|x64.Build.0 = Debug|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Debug|x86.ActiveCfg = Debug|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Debug|x86.Build.0 = Debug|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Release|Any CPU.Build.0 = Release|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Release|x64.ActiveCfg = Release|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Release|x64.Build.0 = Release|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Release|x86.ActiveCfg = Release|Any CPU + {29FC1E6D-1BB0-4AE1-879B-B97A2BC0AD44}.Release|x86.Build.0 = Release|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Debug|Any CPU.Build.0 = Debug|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Debug|x64.ActiveCfg = Debug|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Debug|x64.Build.0 = Debug|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Debug|x86.ActiveCfg = Debug|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Debug|x86.Build.0 = Debug|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Release|Any CPU.ActiveCfg = Release|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Release|Any CPU.Build.0 = Release|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Release|x64.ActiveCfg = Release|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Release|x64.Build.0 = Release|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Release|x86.ActiveCfg = Release|Any CPU + {296E143D-0CCD-48CA-9054-5E918F8B5462}.Release|x86.Build.0 = Release|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Debug|x64.ActiveCfg = Debug|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Debug|x64.Build.0 = Debug|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Debug|x86.ActiveCfg = Debug|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Debug|x86.Build.0 = Debug|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Release|Any CPU.Build.0 = Release|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Release|x64.ActiveCfg = Release|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Release|x64.Build.0 = Release|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Release|x86.ActiveCfg = Release|Any CPU + {01A66C19-2011-41A9-876A-E02A45431465}.Release|x86.Build.0 = Release|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Debug|x64.Build.0 = Debug|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Debug|x86.Build.0 = Debug|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Release|Any CPU.Build.0 = Release|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Release|x64.ActiveCfg = Release|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Release|x64.Build.0 = Release|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Release|x86.ActiveCfg = Release|Any CPU + {32DFB249-9EA5-4E7C-8BBA-E01853778BE7}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.csproj b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.csproj new file mode 100644 index 0000000000..dfb40caafc --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/Program.cs b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/Program.cs new file mode 100644 index 0000000000..cf738af6d4 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYield/Program.cs @@ -0,0 +1,17 @@ +Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: Start"); + +await foreach (var item in FetchItems()) +{ + Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {item}"); +} + +Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: End"); + +static async IAsyncEnumerable FetchItems() +{ + for (int i = 1; i <= 10; i++) + { + await Task.Delay(1000); + yield return i; + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ItemsController.cs b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ItemsController.cs new file mode 100644 index 0000000000..1d1b8c08e8 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ItemsController.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +[Route("api/[controller]")] +[ApiController] +public class ItemsController : ControllerBase +{ + private readonly ItemContext _context; + + public ItemsController(ItemContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + [HttpGet] + public IAsyncEnumerable GetItems() => + _context.Items.AsNoTracking().AsAsyncEnumerable(); +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ValuesController.cs b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ValuesController.cs new file mode 100644 index 0000000000..1228caf3e7 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Controllers/ValuesController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; + +[Route("api/[controller]")] +[ApiController] +public class ValuesController : ControllerBase +{ + [HttpGet] + public IAsyncEnumerable Get() + { + return FetchItems(); + } + + static async IAsyncEnumerable FetchItems() + { + for (int i = 1; i <= 10; i++) + { + await Task.Delay(1000); + yield return i; + } + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/Item.cs b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/Item.cs new file mode 100644 index 0000000000..6a9851c863 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/Item.cs @@ -0,0 +1,5 @@ +public class Item +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/ItemContext.cs b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/ItemContext.cs new file mode 100644 index 0000000000..1ca706d470 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Data/ItemContext.cs @@ -0,0 +1,6 @@ +using Microsoft.EntityFrameworkCore; + +public class ItemContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Items => Set(); +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/IAsyncEnumerableWithYieldApi.csproj b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/IAsyncEnumerableWithYieldApi.csproj new file mode 100644 index 0000000000..81acd49e9c --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/IAsyncEnumerableWithYieldApi.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Program.cs b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Program.cs new file mode 100644 index 0000000000..4bc756cb34 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Program.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("Items"))); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + + if (!context.Items.Any()) + { + context.Items.AddRange( + new Item { Name = "Keyboard" }, + new Item { Name = "Mouse" }, + new Item { Name = "Monitor" }); + context.SaveChanges(); + } +} + +app.MapControllers(); + +app.MapGet("/items", () => FetchItems()); + +app.Run(); + +static async IAsyncEnumerable FetchItems() +{ + for (var i = 1; i <= 10; i++) + { + await Task.Delay(1000); + yield return i; + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Properties/launchSettings.json b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Properties/launchSettings.json new file mode 100644 index 0000000000..03999a45cf --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "api/values", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.Development.json b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.json b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.json new file mode 100644 index 0000000000..be75002d98 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/IAsyncEnumerableWithYieldApi/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "Items": "Data Source=items.db" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/async-csharp/IAsyncEnumerableWithYield/SyncYield/Program.cs b/async-csharp/IAsyncEnumerableWithYield/SyncYield/Program.cs new file mode 100644 index 0000000000..7d12714d2b --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/SyncYield/Program.cs @@ -0,0 +1,17 @@ +Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: Start"); + +foreach (var item in FetchItems()) +{ + Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {item}"); +} + +Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: End"); + +static IEnumerable FetchItems() +{ + for (int i = 1; i <= 10; i++) + { + Thread.Sleep(1000); + yield return i; + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/SyncYield/SyncYield.csproj b/async-csharp/IAsyncEnumerableWithYield/SyncYield/SyncYield.csproj new file mode 100644 index 0000000000..dfb40caafc --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/SyncYield/SyncYield.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/Program.cs b/async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/Program.cs new file mode 100644 index 0000000000..dd64f3e3ba --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/Program.cs @@ -0,0 +1,20 @@ +Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: Start"); + +foreach (var item in await FetchItems()) +{ + Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: {item}"); +} + +Console.WriteLine($"{DateTime.Now.ToLongTimeString()}: End"); + +static async Task> FetchItems() +{ + List Items = new List(); + for (int i = 1; i <= 10; i++) + { + await Task.Delay(1000); + Items.Add(i); + } + + return Items; +} diff --git a/async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/TaskOfIEnumerable.csproj b/async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/TaskOfIEnumerable.csproj new file mode 100644 index 0000000000..dfb40caafc --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/TaskOfIEnumerable/TaskOfIEnumerable.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/async-csharp/IAsyncEnumerableWithYield/Tests/AsAsyncEnumerableUnitTest.cs b/async-csharp/IAsyncEnumerableWithYield/Tests/AsAsyncEnumerableUnitTest.cs new file mode 100644 index 0000000000..e1c7ac8481 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/Tests/AsAsyncEnumerableUnitTest.cs @@ -0,0 +1,53 @@ +using System.Data.Common; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace Tests; + +[TestClass] +public class AsAsyncEnumerableUnitTest +{ + [TestMethod] + public async Task GivenItemsQuery_WhenAsAsyncEnumerableIsCalled_ThenNoCommandRunsUntilEnumeration() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + + var counter = new ReaderCounter(); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .AddInterceptors(counter) + .Options; + + await using var context = new ItemContext(options); + await context.Database.EnsureCreatedAsync(); + context.Items.AddRange(new Item { Name = "Keyboard" }, new Item { Name = "Mouse" }); + await context.SaveChangesAsync(); + counter.Readers = 0; + + var items = context.Items.AsNoTracking().AsAsyncEnumerable(); + await using var enumerator = items.GetAsyncEnumerator(); + + Assert.AreEqual(0, counter.Readers); + + Assert.IsTrue(await enumerator.MoveNextAsync()); + Assert.AreEqual(1, counter.Readers); + Assert.AreEqual("Keyboard", enumerator.Current.Name); + } + + private sealed class ReaderCounter : DbCommandInterceptor + { + public int Readers; + + public override ValueTask> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult result, + CancellationToken cancellationToken = default) + { + Readers++; + return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); + } + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/Tests/StreamingUnitTest.cs b/async-csharp/IAsyncEnumerableWithYield/Tests/StreamingUnitTest.cs new file mode 100644 index 0000000000..ee1a7a0c18 --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/Tests/StreamingUnitTest.cs @@ -0,0 +1,83 @@ +using System.Diagnostics; +using System.Text; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Data.Sqlite; + +namespace Tests; + +[TestClass] +public class StreamingUnitTest +{ + private static readonly string DatabasePath = + Path.Combine(Path.GetTempPath(), $"items-{Guid.NewGuid():N}.db"); + + private static WebApplicationFactory _factory = null!; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + _factory = new WebApplicationFactory() + .WithWebHostBuilder(builder => + builder.UseSetting("ConnectionStrings:Items", $"Data Source={DatabasePath}")); + } + + [ClassCleanup] + public static void ClassCleanup() + { + _factory.Dispose(); + SqliteConnection.ClearAllPools(); + File.Delete(DatabasePath); + } + + [TestMethod] + public async Task GivenValuesController_WhenGetIsCalled_ThenFirstItemArrivesBeforeTheLast() + { + await AssertStreamsTenItems("/api/values"); + } + + [TestMethod] + public async Task GivenMinimalApiEndpoint_WhenItemsAreRequested_ThenFirstItemArrivesBeforeTheLast() + { + await AssertStreamsTenItems("/items"); + } + + [TestMethod] + public async Task GivenItemsController_WhenGetItemsIsCalled_ThenEveryRowIsReturned() + { + using var client = _factory.CreateClient(); + + var body = await client.GetStringAsync("/api/items"); + + Assert.AreEqual( + """[{"id":1,"name":"Keyboard"},{"id":2,"name":"Mouse"},{"id":3,"name":"Monitor"}]""", + body); + } + + private static async Task AssertStreamsTenItems(string url) + { + using var client = _factory.CreateClient(); + var stopwatch = Stopwatch.StartNew(); + + using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + await using var stream = await response.Content.ReadAsStreamAsync(); + + var received = new StringBuilder(); + var buffer = new byte[64]; + while (!received.ToString().StartsWith("[1")) + { + var read = await stream.ReadAsync(buffer); + Assert.AreNotEqual(0, read, "The response ended before the first item arrived."); + received.Append(Encoding.UTF8.GetString(buffer, 0, read)); + } + + var firstItemAfter = stopwatch.Elapsed; + + using var reader = new StreamReader(stream); + received.Append(await reader.ReadToEndAsync()); + var lastItemAfter = stopwatch.Elapsed; + + Assert.AreEqual("[1,2,3,4,5,6,7,8,9,10]", received.ToString()); + Assert.IsLessThan(TimeSpan.FromSeconds(5), firstItemAfter); + Assert.IsGreaterThanOrEqualTo(TimeSpan.FromSeconds(9), lastItemAfter); + } +} diff --git a/async-csharp/IAsyncEnumerableWithYield/Tests/Tests.csproj b/async-csharp/IAsyncEnumerableWithYield/Tests/Tests.csproj new file mode 100644 index 0000000000..3eb168c8ad --- /dev/null +++ b/async-csharp/IAsyncEnumerableWithYield/Tests/Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + +