C#
Parallel.ForEach
ASP.NET
concurrency
programming tips

C Adding context to Parallel.ForEach in ASP.NET

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When developers ask how to "add context" to Parallel.ForEach in ASP.NET, they usually mean one of two things: passing shared request data into each iteration, or trying to access request-specific objects such as HttpContext. The safe pattern is to copy the data you need into an explicit context object before the loop and avoid relying on ambient ASP.NET request state inside parallel workers.

Do Not Depend On Request Ambient State

Parallel.ForEach runs work on thread-pool threads. In ASP.NET, request-scoped state such as HttpContext.Current is not something you should casually rely on inside those parallel iterations.

Even if it seems to work in one environment, it creates fragile code because the loop body is no longer a simple function of its inputs. It depends on hidden request infrastructure instead.

The better design is:

  1. capture what you need from the request up front
  2. package it into an immutable object
  3. pass that object into the loop body

Pass An Explicit Context Object

Example:

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Threading.Tasks;
5
6public record ProcessingContext(string UserId, string CorrelationId);
7
8public class Worker
9{
10    public IEnumerable<string> Process(
11        IEnumerable<int> items,
12        ProcessingContext context)
13    {
14        var results = new ConcurrentBag<string>();
15
16        Parallel.ForEach(items, item =>
17        {
18            var message = $"User={context.UserId}, Correlation={context.CorrelationId}, Item={item}";
19            results.Add(message);
20        });
21
22        return results;
23    }
24}

The context is explicit, immutable, and safe to share across threads because it contains only read-only data.

That is usually all you need.

Use Thread-Local State When Each Worker Needs Its Own Scratch Data

Parallel.ForEach also supports local state for each worker. This is useful when every thread needs its own accumulator or expensive helper object.

csharp
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Threading.Tasks;
5
6var totals = new ConcurrentBag<int>();
7var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
8
9Parallel.ForEach(
10    numbers,
11    () => 0,
12    (number, loopState, localTotal) =>
13    {
14        localTotal += number;
15        return localTotal;
16    },
17    localTotal => totals.Add(localTotal)
18);
19
20foreach (var total in totals)
21{
22    Console.WriteLine(total);
23}

This pattern is good for thread-local aggregation, but it is not a replacement for ASP.NET request context. It is just a way to manage per-worker state efficiently.

Prefer Async I/O For Request Work

There is an even bigger ASP.NET-specific point: Parallel.ForEach is usually appropriate for CPU-bound work, not I/O-bound request fan-out.

If your loop body is making HTTP calls, database queries, or file access, async code is usually the better model. Spinning up parallel blocking work inside an ASP.NET request can waste threads and reduce scalability.

In those cases, prefer:

  • 'Task.WhenAll'
  • async database APIs
  • async HTTP clients

Reserve Parallel.ForEach for real parallel CPU work such as transformations, hashing, or independent calculations.

Limit Parallelism Deliberately

If you do use Parallel.ForEach in a web application, control the concurrency instead of assuming "more threads is better."

csharp
1var options = new ParallelOptions
2{
3    MaxDegreeOfParallelism = Environment.ProcessorCount
4};
5
6Parallel.ForEach(items, options, item =>
7{
8    DoCpuBoundWork(item);
9});

That keeps the loop from competing too aggressively with the rest of the application.

Thread Safety Still Matters

Passing context safely does not make shared mutable objects safe. If multiple iterations write to the same collection or service, use thread-safe structures or redesign the flow.

Good shared choices include:

  • immutable data
  • 'ConcurrentDictionary'
  • 'ConcurrentBag'
  • per-thread local state with a merge step

Bad choices include mutable lists and request-scoped objects being modified from many iterations at once.

Common Pitfalls

The biggest mistake is reading HttpContext or other ambient request state directly inside the loop body. Capture only the values you need before the parallel section starts.

Another mistake is using Parallel.ForEach for I/O-heavy request work. In ASP.NET, async I/O scales better than blocking parallel loops for that scenario.

People also forget that shared collections are not magically safe just because the context object is immutable. Shared writes still need thread-safe handling.

Finally, do not assume maximum parallelism should be unlimited. In web apps, aggressive parallel fan-out can hurt overall throughput.

Summary

  • In ASP.NET, pass explicit immutable context data into Parallel.ForEach instead of depending on ambient request state.
  • Use thread-local loop state only for per-worker scratch data or aggregation.
  • 'Parallel.ForEach is best for CPU-bound work, not request-time I/O fan-out.'
  • Control concurrency with ParallelOptions and use thread-safe result collection.
  • Safe context passing does not remove the need for normal thread-safety discipline.

Course illustration
Course illustration

All Rights Reserved.