async void: an exception crashes the process and the caller doesn't know when it finished.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | // ====================================================================== // Q: Find the bugs in this async code (written by an AI agent). // // - async void: an exception crashes the process and the caller doesn't know // when it finished. It must be async Task (Q85). // - Singleton with a scoped DbContext: captive dependency (Q5). Fix: // IDbContextFactory or a new scope. // - .Result inside the lambda: sync over async, thread pool starvation (Q120). // - A Dictionary written from multiple threads: a race, and it can get corrupted // internally (Q122). // - The same DbContext in parallel tasks: "A second operation was started on // this context instance" (Q63). // - Task.WhenAll without await: SaveChangesAsync runs before the tasks finish, // and the exceptions are lost. // - No CancellationToken and no limit: 5,000 SKUs = 5,000 concurrent calls to // the ERP. // - N+1: one query per SKU instead of a single Where(p => skus.Contains(p.Sku)) // (Q65). // - One failing SKU brings down the whole batch, and the log says "Synced" // without any numbers. // ====================================================================== // Original AI-generated code (buggy) // Registered as a singleton public class PriceSyncService(AppDbContext db, IErpClient erp, ILogger<PriceSyncService> log) { private readonly Dictionary<string, decimal> _last = new(); public async void SyncAll(List<string> skus) { var tasks = skus.Select(async sku => { var price = erp.GetPriceAsync(sku).Result; _last[sku] = price; var p = await db.Products.FirstAsync(x => x.Sku == sku); p.Price = price; }); Task.WhenAll(tasks); await db.SaveChangesAsync(); log.LogInformation("Synced"); } } // Fixed version: IDbContextFactory, bounded parallelism, one query, cancellation public sealed class PriceSyncService( IDbContextFactory<AppDbContext> dbFactory, IErpClient erp, ILogger<PriceSyncService> log) { public async Task SyncAllAsync(IReadOnlyCollection<string> skus, CancellationToken ct) { var prices = new ConcurrentDictionary<string, decimal>(); await Parallel.ForEachAsync(skus, new ParallelOptions { MaxDegreeOfParallelism = 4, CancellationToken = ct }, async (sku, token) => { try { prices[sku] = await erp.GetPriceAsync(sku, token); } catch (HttpRequestException ex) { log.LogWarning(ex, "Price for {Sku} failed", sku); } }); var found = prices.Keys.ToList(); await using var db = await dbFactory.CreateDbContextAsync(ct); var products = await db.Products.Where(p => found.Contains(p.Sku)).ToListAsync(ct); // 1 query foreach (var p in products) p.Price = prices[p.Sku]; await db.SaveChangesAsync(ct); log.LogInformation("Synced {Updated}/{Total} prices", products.Count, skus.Count); } } |