From 02b85ad4ce1fc35b7c1c5cb3c955ab8e29d3dd46 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:10:18 +0100 Subject: [PATCH] fix: keep examples compiling in CI Refs #341 --- .../DisposalExample.cs | 33 ++++++++++--------- .../Modules/BuildExampleProjectsModule.cs | 32 ++++++++++++++++++ .../Modules/PackProjectsModule.cs | 1 + EnumerableAsyncProcessor.Pipeline/Program.cs | 3 +- 4 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 EnumerableAsyncProcessor.Pipeline/Modules/BuildExampleProjectsModule.cs diff --git a/EnumerableAsyncProcessor.Example/DisposalExample.cs b/EnumerableAsyncProcessor.Example/DisposalExample.cs index 14ffe2b..c1769f4 100644 --- a/EnumerableAsyncProcessor.Example/DisposalExample.cs +++ b/EnumerableAsyncProcessor.Example/DisposalExample.cs @@ -1,7 +1,6 @@ #if NET6_0_OR_GREATER using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -29,19 +28,19 @@ public static async Task RunExamples() // Example 1: The problematic pattern from the issue Console.WriteLine("Example 1: PROBLEMATIC - No disposal (resource leak!)"); var results1 = await ProblematicPatternAsync(new[] { 1, 2, 3, 4, 5 }, CancellationToken.None); - Console.WriteLine($"Results: {string.Join(", ", results1.ToList())}"); + Console.WriteLine($"Results: {string.Join(", ", results1)}"); Console.WriteLine("⚠️ This pattern leaks resources because the processor is never disposed!\n"); // Example 2: Proper disposal with await using Console.WriteLine("Example 2: PROPER - Using await using for automatic disposal"); var results2 = await ProperPatternWithAwaitUsingAsync(new[] { 1, 2, 3, 4, 5 }, CancellationToken.None); - Console.WriteLine($"Results: {string.Join(", ", results2.ToList())}"); + Console.WriteLine($"Results: {string.Join(", ", results2)}"); Console.WriteLine("✅ Resources automatically cleaned up with await using\n"); // Example 3: Proper disposal with manual try-finally Console.WriteLine("Example 3: PROPER - Manual disposal with try-finally"); var results3 = await ProperPatternWithManualDisposalAsync(new[] { 1, 2, 3, 4, 5 }, CancellationToken.None); - Console.WriteLine($"Results: {string.Join(", ", results3.ToList())}"); + Console.WriteLine($"Results: {string.Join(", ", results3)}"); Console.WriteLine("✅ Resources manually cleaned up in finally block\n"); // Example 4: Using the convenience extension (no disposal needed) @@ -65,23 +64,27 @@ public static async Task RunExamples() /// This is the PROBLEMATIC pattern from the GitHub issue - it leaks resources! /// DO NOT USE THIS PATTERN in production code. /// - private static async Task> ProblematicPatternAsync(int[] input, CancellationToken token) + private static async Task> ProblematicPatternAsync(int[] input, CancellationToken token) { // ⚠️ PROBLEM: The processor is created but never disposed! var batchProcessor = input.SelectAsync(static v => TransformAsync(v), token).ProcessInParallel(); - - // This returns the async enumerable, but the processor that created it is never disposed - return batchProcessor.GetResultsAsyncEnumerable(); - + + var results = new List(); + await foreach (var result in batchProcessor.GetResultsAsyncEnumerable()) + { + results.Add(result); + } + // 🔥 RESOURCE LEAK: The processor goes out of scope without being disposed, // potentially leaving tasks running and resources uncleaned + return results; } /// /// PROPER pattern using await using for automatic disposal. /// This is the recommended approach. /// - private static async Task> ProperPatternWithAwaitUsingAsync(int[] input, CancellationToken token) + private static async Task> ProperPatternWithAwaitUsingAsync(int[] input, CancellationToken token) { // ✅ Create processor with await using for automatic disposal await using var processor = input.SelectAsync(static v => TransformAsync(v), token).ProcessInParallel(); @@ -93,17 +96,15 @@ private static async Task> ProperPatternWithAwaitUsingAsyn results.Add(result); } - // Return as async enumerable - return results.ToAsyncEnumerable(); - // ✅ Processor is automatically disposed here due to 'await using' + return results; } /// /// PROPER pattern using manual disposal with try-finally. /// Use this when you need more control over the disposal timing. /// - private static async Task> ProperPatternWithManualDisposalAsync(int[] input, CancellationToken token) + private static async Task> ProperPatternWithManualDisposalAsync(int[] input, CancellationToken token) { var processor = input.SelectAsync(static v => TransformAsync(v), token).ProcessInParallel(); @@ -116,7 +117,7 @@ private static async Task> ProperPatternWithManualDisposal results.Add(result); } - return results.ToAsyncEnumerable(); + return results; } finally { @@ -168,4 +169,4 @@ private static async IAsyncEnumerable GenerateAsyncEnumerable( } } } -#endif \ No newline at end of file +#endif diff --git a/EnumerableAsyncProcessor.Pipeline/Modules/BuildExampleProjectsModule.cs b/EnumerableAsyncProcessor.Pipeline/Modules/BuildExampleProjectsModule.cs new file mode 100644 index 0000000..6ea0c59 --- /dev/null +++ b/EnumerableAsyncProcessor.Pipeline/Modules/BuildExampleProjectsModule.cs @@ -0,0 +1,32 @@ +using ModularPipelines.Context; +using ModularPipelines.DotNet.Extensions; +using ModularPipelines.DotNet.Options; +using ModularPipelines.Git.Extensions; +using ModularPipelines.Models; +using ModularPipelines.Modules; + +namespace EnumerableAsyncProcessor.Pipeline.Modules; + +public class BuildExampleProjectsModule : Module> +{ + protected override async Task?> ExecuteAsync( + IModuleContext context, + CancellationToken cancellationToken) + { + var results = new List(); + + foreach (var exampleProjectFile in context + .Git().RootDirectory + .GetFiles(file => file.Path.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) + && file.Path.Contains("Example", StringComparison.OrdinalIgnoreCase))) + { + results.Add(await context.DotNet().Build(new DotNetBuildOptions + { + ProjectSolution = exampleProjectFile.Path, + Configuration = "Release", + }, cancellationToken: cancellationToken)); + } + + return results; + } +} diff --git a/EnumerableAsyncProcessor.Pipeline/Modules/PackProjectsModule.cs b/EnumerableAsyncProcessor.Pipeline/Modules/PackProjectsModule.cs index a6bf5fe..2777d1b 100644 --- a/EnumerableAsyncProcessor.Pipeline/Modules/PackProjectsModule.cs +++ b/EnumerableAsyncProcessor.Pipeline/Modules/PackProjectsModule.cs @@ -13,6 +13,7 @@ namespace EnumerableAsyncProcessor.Pipeline.Modules; [DependsOn] [DependsOn] [DependsOn] +[DependsOn] public class PackProjectsModule : Module> { protected override async Task?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken) diff --git a/EnumerableAsyncProcessor.Pipeline/Program.cs b/EnumerableAsyncProcessor.Pipeline/Program.cs index dfe0c87..3b28668 100644 --- a/EnumerableAsyncProcessor.Pipeline/Program.cs +++ b/EnumerableAsyncProcessor.Pipeline/Program.cs @@ -26,7 +26,8 @@ builder.AddModule(); } -builder.AddModule() +builder.AddModule() + .AddModule() .AddModule() .AddModule() .AddModule()