Code review: finding the bugs in AI-generated C# async code (async void, .Result, DbContext races)

C# Async & Multithreading November 13, 2026 csharp

async void: an exception crashes the process and the caller doesn't know when it finished.

csharp
 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);
    }
}