RefactorMCP catalog

Convert Foreach to LINQ

Turns a foreach loop that builds a list, a total, a count or a flag from a sequence into a LINQ query in method syntax. The reverse of Convert LINQ to Foreach.

var names = new List<string>();
foreach (var user in users)
{
    if (user.IsActive)
    {
        names.Add(user.Name);
    }
}

becomes

var names = users.Where(user => user.IsActive).Select(user => user.Name).ToList();

Target

The loop, by a caret anywhere in it.

Precondition

  • The loop enumerates a generic sequence (an array or an IEnumerable<T>) into a single variable, without converting the elements to another type, and is not an await foreach.
  • The body reads, from the outside in:
  • an if without an else, whose condition becomes a Where;
  • a local declared from the current element, when the element is not used after it, which becomes a Select whose lambda takes the local's name;
  • and finally one accumulation:
  • list.Add(value) on a List<T> of the value's type, which becomes ToList or AddRange;
  • total += value on an int, long, float, double or decimal of the value's type, which becomes Sum;
  • count++, ++count or count += 1 on an int, which becomes Count;
  • found = true; break; on a local bool declared false just before the loop, which becomes Any;
  • return true; with return false; straight after the loop, which becomes a returned Any.
  • The expressions that move into lambdas do not assign, increment or await, and do not read the accumulator.

Transformation

  • When the statement just before the loop declares the accumulator with its starting value (an empty new List<T>(), zero or false), the query becomes that declaration's initializer and the declaration keeps its written type.
  • Otherwise the query is added to the accumulator: list.AddRange(query), total += query.Sum(...) or count += query.Count(...).
  • Each lambda's parameter takes the name of the loop variable or local it replaces. A Select of the current element itself is left out, as is the selector of a Sum of the elements themselves.
  • A final Where before Count or Any becomes their predicate.
  • using System.Linq; is added when the file does not already have it.

Preserved

  • Behaviour: LINQ runs each element through the whole query before the next, so conditions and projections run in the same order as in the loop, and Any stops at the first match as the loop's break or return did.
  • Comments above the accumulator's declaration and above the loop, which join above the query.

Limitations

  • Comments inside the loop body are not carried into the query.
  • Sum checks for overflow and += does not, so a total that overflowed and wrapped in the loop throws OverflowException in the query. Sum of float values adds them as double before rounding the result.
  • Bodies with else, continue guards, several accumulations, or a local whose element is still used after it are refused rather than converted with query syntax or anonymous types.
  • The refactoring changes one loop, so there are no references in other files to update.

Error codes

CodeMeaning
not-a-foreach-loopthe caret is not in a foreach loop
not-queryablethe loop enumerates a sequence that is not generic, which LINQ does not query without a cast
unsupported-loop-bodythe body is not a filter, projection and accumulation of the kinds above
side-effectsthe body does something besides the accumulation, or a moved expression assigns, increments or awaits
early-exitthe body leaves the loop early other than to report a match
accumulator-used-in-bodya condition or value reads the accumulator, which the query does not know until it ends

Cases

·

Diffs show each file from before to after. /*[*/ … /*]*/ marks a selection and /*^*/ a caret; the runner removes them before the refactoring runs.

#accumulator-used-in-body-rejected

Refuses a body whose condition reads the accumulator

refusal
TargetSample.cs, the /*^*/ caret
Refusesaccumulator-used-in-body; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Grow(int[] values)
4 {
5 var total = 0;
6 /*^*/foreach (var value in values)
7 {
8 if (value > total)
9 total += value;
10 }
11
12 return total;
13 }
14 }

#adds-to-existing-list

Adding to a list that already exists becomes AddRange

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
2+using System.Linq;
23
34 public class Sample
45 {
56 private readonly List<string> _names = new List<string>();
67
78 public void AddActive(List<User> users)
89 {
9− /*^*/foreach (var user in users)
10− {
11− if (user.IsActive)
12− {
13− _names.Add(user.Name);
14− }
15− }
10+ _names.AddRange(users.Where(user => user.IsActive).Select(user => user.Name));
1611 }
1712 }
User.csunchanged
11 public class User
22 {
33 public string Name { get; set; } = "";
44
55 public bool IsActive { get; set; }
66
77 public bool IsAdmin { get; set; }
88 }

#adds-to-existing-total

A total that does not start at zero right before the loop is added to, and summing the elements themselves needs no selector

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
1+using System.Linq;
2+
13 public class Sample
24 {
35 public int Sum(int start, int[] values)
46 {
57 var total = start;
6− /*^*/foreach (var value in values)
7− {
8− total += value;
9− }
8+ total += values.Sum();
109
1110 return total;
1211 }
1312 }

#any-with-break

A loop that sets a flag and breaks on the first match becomes Any

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
2+using System.Linq;
23
34 public class Sample
45 {
56 public void Report(int[] values, int target)
67 {
7− var found = false;
8− /*^*/foreach (var value in values)
9− {
10− if (value == target)
11− {
12− found = true;
13− break;
14− }
15− }
8+ var found = values.Any(value => value == target);
169
1710 Console.WriteLine(found);
1811 }
1912 }

#any-with-return

A loop that returns true on the first match, followed by return false, becomes a return of Any

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
2+using System.Linq;
23
34 public class Sample
45 {
56 public bool HasAdmin(List<User> users)
67 {
78 // Admins can approve.
8− /*^*/foreach (var user in users)
9− {
10− if (user.IsAdmin)
11− {
12− return true;
13− }
14− }
15−
16− return false;
9+ return users.Any(user => user.IsAdmin);
1710 }
1811 }
User.csunchanged
11 public class User
22 {
33 public string Name { get; set; } = "";
44
55 public bool IsActive { get; set; }
66
77 public bool IsAdmin { get; set; }
88 }

#count

A loop that increments a counter for the matching elements becomes Count with a predicate

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
1+using System.Linq;
2+
13 public class Sample
24 {
35 public int LongWords(string[] words)
46 {
5− var count = 0;
6− /*^*/foreach (var word in words)
7− {
8− if (word.Length > 3)
9− count++;
10− }
7+ var count = words.Count(word => word.Length > 3);
118
129 return count;
1310 }
1411 }

#early-exit-rejected

Refuses a loop that stops early other than to find a match

refusal
TargetSample.cs, the /*^*/ caret
Refusesearly-exit; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int SumUntilNegative(int[] values)
4 {
5 var total = 0;
6 /*^*/foreach (var value in values)
7 {
8 if (value < 0)
9 break;
10 total += value;
11 }
12
13 return total;
14 }
15 }

#filter-and-project

A loop that adds a projection of the matching elements to a new list becomes Where, Select and ToList

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
2+using System.Linq;
23
34 public class Sample
45 {
56 public List<string> ActiveNames(List<User> users)
67 {
7− var names = new List<string>();
8− /*^*/foreach (var user in users)
9− {
10− if (user.IsActive)
11− {
12− names.Add(user.Name);
13− }
14− }
8+ var names = users.Where(user => user.IsActive).Select(user => user.Name).ToList();
159
1610 return names;
1711 }
1812 }
User.csunchanged
11 public class User
22 {
33 public string Name { get; set; } = "";
44
55 public bool IsActive { get; set; }
66
77 public bool IsAdmin { get; set; }
88 }

#filter-only

Adding the element itself needs no Select, an explicitly typed list keeps its type, and an existing using is not repeated

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
22 using System.Linq;
33
44 public class Sample
55 {
66 public List<int> Positives(int[] numbers)
77 {
8− List<int> positives = new List<int>();
9− /*^*/foreach (int n in numbers)
10− if (n > 0)
11− positives.Add(n);
8+ List<int> positives = numbers.Where(n => n > 0).ToList();
129 return positives;
1310 }
1411 }

#generic

A loop over a sequence of a type parameter becomes a query of that type

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
3+using System.Linq;
34
45 public class Sample
56 {
67 public List<T> Matching<T>(IEnumerable<T> items, Func<T, bool> predicate)
78 {
8− var result = new List<T>();
9− /*^*/foreach (var item in items)
10− {
11− if (predicate(item))
12− {
13− result.Add(item);
14− }
15− }
9+ var result = items.Where(item => predicate(item)).ToList();
1610
1711 return result;
1812 }
1913 }

#keeps-comments

Comments above the declaration and above the loop stay above the query

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
2+using System.Linq;
23
34 public class Sample
45 {
56 public List<string> LongWords(string[] words)
67 {
78 // Collect the long words.
8− var longWords = new List<string>();
9−
109 // Only words over five letters.
11− /*^*/foreach (var word in words)
12− {
13− if (word.Length > 5)
14− {
15− longWords.Add(word);
16− }
17− }
10+ var longWords = words.Where(word => word.Length > 5).ToList();
1811
1912 return longWords;
2013 }
2114 }

#local-in-body

A local computed from the element becomes a Select, and the rest of the query uses its name

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
2+using System.Linq;
23
34 public class Sample
45 {
56 public List<int> Lengths(IEnumerable<string> words)
67 {
7− var lengths = new List<int>();
8− /*^*/foreach (var word in words)
9− {
10− var trimmed = word.Trim();
11− if (trimmed.Length > 0)
12− {
13− lengths.Add(trimmed.Length);
14− }
15− }
8+ var lengths = words.Select(word => word.Trim()).Where(trimmed => trimmed.Length > 0).Select(trimmed => trimmed.Length).ToList();
169
1710 return lengths;
1811 }
1912 }

#not-a-foreach-loop-rejected

Refuses a caret that is not in a foreach loop

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-a-foreach-loop; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Sum(int[] values)
4 {
5 var total = 0;
6 /*^*/for (int i = 0; i < values.Length; i++)
7 {
8 total += values[i];
9 }
10
11 return total;
12 }
13 }

#not-queryable-rejected

Refuses a non-generic collection, which LINQ does not query without a cast

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-queryable; every file is left unchanged
Sample.csinput
1 using System.Collections;
2
3 public class Sample
4 {
5 public int CountNonNull(ArrayList items)
6 {
7 var count = 0;
8 /*^*/foreach (object item in items)
9 {
10 if (item != null)
11 count++;
12 }
13
14 return count;
15 }
16 }

#nullable-context

In a nullable context a count of null elements becomes Count

success
TargetSample.cs, the /*^*/ caret
Projectnullable enable
Sample.csmodified
11 using System.Collections.Generic;
2+using System.Linq;
23
34 public class Sample
45 {
56 public int CountMissing(List<string?> labels)
67 {
7− var missing = 0;
8− /*^*/foreach (var label in labels)
9− {
10− if (label is null)
11− {
12− missing++;
13− }
14− }
8+ var missing = labels.Count(label => label is null);
159
1610 return missing;
1711 }
1812 }

#side-effects-rejected

Refuses a body that does more than accumulate

refusal
TargetSample.cs, the /*^*/ caret
Refusesside-effects; every file is left unchanged
Sample.csinput
1 using System;
2
3 public class Sample
4 {
5 public int Sum(int[] values)
6 {
7 var total = 0;
8 /*^*/foreach (var value in values)
9 {
10 Console.WriteLine(value);
11 total += value;
12 }
13
14 return total;
15 }
16 }

#sum

A loop that adds a value of the matching elements to a total starting at zero becomes Where and Sum

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
2+using System.Linq;
23
34 public class Order
45 {
56 public decimal Amount { get; set; }
67
78 public bool IsPaid { get; set; }
89 }
910
1011 public class Sample
1112 {
1213 public decimal Paid(IEnumerable<Order> orders)
1314 {
14− decimal total = 0;
15− /*^*/foreach (var order in orders)
16− {
17− if (order.IsPaid)
18− {
19− total += order.Amount;
20− }
21− }
15+ decimal total = orders.Where(order => order.IsPaid).Sum(order => order.Amount);
2216
2317 return total;
2418 }
2519 }

#unsupported-loop-body-rejected

Refuses a body that is not a filter, projection or aggregation

refusal
TargetSample.cs, the /*^*/ caret
Refusesunsupported-loop-body; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Product(int[] values)
4 {
5 var product = 1;
6 /*^*/foreach (var value in values)
7 {
8 product *= value;
9 }
10
11 return product;
12 }
13 }