RefactorMCP catalog

Convert LINQ to Foreach

Turns a LINQ query of Where and Select ending in ToList, Sum, Count or Any into a foreach loop that builds the same result. The reverse of Convert Foreach to LINQ.

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

becomes

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

Target

The query, by a caret anywhere in the statement.

Precondition

  • The query is the whole initializer of a local declaration, or the whole expression of a return, in a block.
  • It is a chain of System.Linq.Enumerable calls on a source: any number of Where and Select, then one of ToList(), Sum(), Sum(selector), Count(), Count(predicate), Any() or Any(predicate).
  • Every argument is a lambda with one parameter and an expression body, and is not async.
  • A Sum is not of nullable values.
  • The names the loop declares (the loop variable and a local for each Select followed by another lambda) are not already declared in scope.

Transformation

  • The loop is foreach (var x in source), named after the first lambda's parameter, or after the singular of the source's name, or item.
  • Each Where becomes an if around the rest of the body. A Select followed by another lambda becomes a local named after that lambda's parameter; a Select straight before ToList or Sum() becomes the value added. Later lambdas' parameters are renamed to the loop's current variable.
  • The result accumulates in the declared local, keeping its written type. A var total, count or flag is declared with the result's type, since its starting value 0 or false would not say it.
  • ToList starts from new List<T>() and calls Add, adding using System.Collections.Generic; when the file does not have it.
  • Sum starts at 0 and adds with +=.
  • Count starts at 0 and increments.
  • Any starts at false and sets it to true and breaks on the first match.
  • A returned query accumulates in a new local, arguments.name or result, total or count, returned after the loop; a returned Any returns true from the loop and false after it.

Preserved

  • Behaviour: each element runs through the conditions and projections in the query's order, and Any stops at the first match.
  • Comments above the statement stay above the accumulator, and a comment ending its line stays on the declaration.

Limitations

  • Other operators (OrderBy, First, ToArray, query syntax and so on), method groups, and lambdas with a block body are refused.
  • A Select whose values are only counted by Count() or Any() is refused.
  • += does not check for overflow as Sum does, and adds float values as float where Sum adds them as double.
  • The refactoring changes one statement, so there are no references in other files to update.

Error codes

CodeMeaning
not-a-querythe statement at the caret is not a LINQ query
unsupported-querythe query uses an operator, argument or lambda the loop form does not cover
unsupported-statementthe query is part of a larger expression rather than assigned to a local or returned
name-conflicta name the loop would declare is already declared

Cases

·

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

#adds-collections-using

A list in a file without System.Collections.Generic adds the using

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
1+using System.Collections.Generic;
12 using System.Linq;
23
34 public class Sample
45 {
56 public int CountPositives(int[] numbers)
67 {
7− var positives = /*^*/numbers.Where(n => n > 0).ToList();
8+ var positives = new List<int>();
9+ foreach (var n in numbers)
10+ {
11+ if (n > 0)
12+ {
13+ positives.Add(n);
14+ }
15+ }
816 return positives.Count;
917 }
1018 }

#any-assigned

Any assigned to a local becomes a flag set on the first match before breaking out

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Linq;
33
44 public class Sample
55 {
66 public void Report(int[] values, int target)
77 {
8− var found = /*^*/values.Any(value => value == target);
8+ bool found = false;
9+ foreach (var value in values)
10+ {
11+ if (value == target)
12+ {
13+ found = true;
14+ break;
15+ }
16+ }
917
1018 Console.WriteLine(found);
1119 }
1220 }

#any-returned

A returned Any becomes a loop returning true on the first match, then return false

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
22 using System.Linq;
33
44 public class Sample
55 {
66 public bool HasAdmin(List<User> users)
77 {
88 // Admins can approve.
9− return /*^*/users.Any(user => user.IsAdmin);
9+ foreach (var user in users)
10+ {
11+ if (user.IsAdmin)
12+ {
13+ return true;
14+ }
15+ }
16+
17+ return false;
1018 }
1119 }
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 }

#block-lambda-rejected

Refuses a lambda with a block body

refusal
TargetSample.cs, the /*^*/ caret
Refusesunsupported-query; every file is left unchanged
Sample.csinput
1 using System.Collections.Generic;
2 using System.Linq;
3
4 public class Sample
5 {
6 public List<int> Positives(int[] values)
7 {
8 var positives = /*^*/values.Where(value => { return value > 0; }).ToList();
9 return positives;
10 }
11 }

#count

Count with a predicate becomes a counter incremented for each match

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Linq;
22
33 public class Sample
44 {
55 public int LongWords(string[] words)
66 {
7− int count = words./*^*/Count(word => word.Length > 3);
7+ int count = 0;
8+ foreach (var word in words)
9+ {
10+ if (word.Length > 3)
11+ {
12+ count++;
13+ }
14+ }
815
916 return count;
1017 }
1118 }

#explicitly-typed-list

An explicitly typed local keeps its type, and a Where alone adds the element itself

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 = numbers.Where(n => n > 0)./*^*/ToList();
8+ List<int> positives = new List<int>();
9+ foreach (var n in numbers)
10+ {
11+ if (n > 0)
12+ {
13+ positives.Add(n);
14+ }
15+ }
916 return positives;
1017 }
1118 }

#generic

A query over a sequence of a type parameter fills a list of that type

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System;
22 using System.Collections.Generic;
33 using System.Linq;
44
55 public class Sample
66 {
77 public List<T> Matching<T>(IEnumerable<T> items, Func<T, bool> predicate)
88 {
9− var result = /*^*/items.Where(item => predicate(item)).ToList();
9+ var result = new List<T>();
10+ foreach (var item in items)
11+ {
12+ if (predicate(item))
13+ {
14+ result.Add(item);
15+ }
16+ }
1017
1118 return result;
1219 }
1320 }

#keeps-comments

Comments above and after the statement stay with the code that replaces it

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Linq;
22
33 public class Sample
44 {
55 public int LongWords(string[] words)
66 {
77 // Words over three letters.
8− var count = /*^*/words.Count(word => word.Length > 3); // not trimmed
8+ int count = 0; // not trimmed
9+ foreach (var word in words)
10+ {
11+ if (word.Length > 3)
12+ {
13+ count++;
14+ }
15+ }
916
1017 return count;
1118 }
1219 }

#name-conflict-rejected

Refuses a Select whose result would be named like the loop variable

refusal
TargetSample.cs, the /*^*/ caret
Refusesname-conflict; every file is left unchanged
Sample.csinput
1 using System.Collections.Generic;
2 using System.Linq;
3
4 public class Sample
5 {
6 public List<string> Trimmed(string[] words)
7 {
8 var trimmed = /*^*/words.Select(word => word.Trim()).Where(word => word.Length > 0).ToList();
9 return trimmed;
10 }
11 }

#not-a-query-rejected

Refuses a statement that is not a LINQ query

refusal
TargetSample.cs, the /*^*/ caret
Refusesnot-a-query; every file is left unchanged
Sample.csinput
1 public class Sample
2 {
3 public int Twice(int value)
4 {
5 var doubled = /*^*/value * 2;
6 return doubled;
7 }
8 }

#nullable-context

In a nullable context a count of null elements becomes a loop

success
TargetSample.cs, the /*^*/ caret
Projectnullable enable
Sample.csmodified
11 using System.Collections.Generic;
22 using System.Linq;
33
44 public class Sample
55 {
66 public int CountMissing(List<string?> labels)
77 {
8− var missing = /*^*/labels.Count(label => label is null);
8+ int missing = 0;
9+ foreach (var label in labels)
10+ {
11+ if (label is null)
12+ {
13+ missing++;
14+ }
15+ }
916
1017 return missing;
1118 }
1219 }

#returned-list

A returned ToList fills a local named result, which is then returned

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> Doubled(IEnumerable<int> values)
77 {
8− return /*^*/values.Select(value => value * 2).ToList();
8+ var result = new List<int>();
9+ foreach (var value in values)
10+ {
11+ result.Add(value * 2);
12+ }
13+
14+ return result;
915 }
1016 }

#select-then-where

A Select followed by more operators becomes a local named by the next lambda's parameter

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> Lengths(IEnumerable<string> words)
77 {
8− var lengths = /*^*/words.Select(word => word.Trim()).Where(trimmed => trimmed.Length > 0).Select(trimmed => trimmed.Length).ToList();
8+ var lengths = new List<int>();
9+ foreach (var word in words)
10+ {
11+ var trimmed = word.Trim();
12+ if (trimmed.Length > 0)
13+ {
14+ lengths.Add(trimmed.Length);
15+ }
16+ }
917
1018 return lengths;
1119 }
1220 }

#sum

Where and Sum become a total starting at zero, written with the sum's type, and later lambdas use the loop variable

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Collections.Generic;
22 using System.Linq;
33
44 public class Sample
55 {
66 public decimal Paid(IEnumerable<Order> orders)
77 {
8− var total = /*^*/orders.Where(order => order.IsPaid).Sum(o => o.Amount);
8+ decimal total = 0;
9+ foreach (var order in orders)
10+ {
11+ if (order.IsPaid)
12+ {
13+ total += order.Amount;
14+ }
15+ }
916
1017 return total;
1118 }
1219 }
Order.csunchanged
11 public class Order
22 {
33 public decimal Amount { get; set; }
44
55 public bool IsPaid { get; set; }
66 }

#sum-without-selector

Sum of the elements themselves names the loop variable after the collection

success
TargetSample.cs, the /*^*/ caret
Sample.csmodified
11 using System.Linq;
22
33 public class Sample
44 {
55 public int Total(int[] values)
66 {
7− var total = /*^*/values.Sum();
7+ int total = 0;
8+ foreach (var value in values)
9+ {
10+ total += value;
11+ }
812 return total;
913 }
1014 }

#unsupported-operator-rejected

Refuses an operator the loop form does not cover

refusal
TargetSample.cs, the /*^*/ caret
Refusesunsupported-query mentioning “OrderBy”; every file is left unchanged
Sample.csinput
1 using System.Collections.Generic;
2 using System.Linq;
3
4 public class Sample
5 {
6 public List<int> Sorted(int[] values)
7 {
8 var sorted = /*^*/values.OrderBy(value => value).ToList();
9 return sorted;
10 }
11 }

#unsupported-statement-rejected

Refuses a query used inside a larger expression

refusal
TargetSample.cs, the /*^*/ caret
Refusesunsupported-statement; every file is left unchanged
Sample.csinput
1 using System;
2 using System.Linq;
3
4 public class Sample
5 {
6 public void Report(int[] values)
7 {
8 Console.WriteLine(/*^*/values.Count(value => value > 0));
9 }
10 }

#where-select-to-list

Where, Select and ToList assigned to a local become a new list filled by a loop

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<string> ActiveNames(List<User> users)
77 {
8− var names = /*^*/users.Where(user => user.IsActive).Select(user => user.Name).ToList();
8+ var names = new List<string>();
9+ foreach (var user in users)
10+ {
11+ if (user.IsActive)
12+ {
13+ names.Add(user.Name);
14+ }
15+ }
916
1017 return names;
1118 }
1219 }
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 }