Skip to content

BRO1150: Pass the predicate to the LINQ call instead of calling Where first

source.Where(p).Count() becomes source.Count(p), and the same for Any, LongCount, First, FirstOrDefault, Last, LastOrDefault, Single and SingleOrDefault. Not a StyleCop rule: Sonar's S2971 reports this pattern (among other LINQ simplifications), and StyleBro also fixes it.

Example

Before

using System.Collections.Generic;
using System.Linq;

public static class Stock
{
    public static bool AnyMissing(List<int> counts) => counts.Where(c => c == 0).Any();

    public static int Low(List<int> counts) =>
        counts
            .Where(c => c < 5)
            .Count();
}

After

using System.Collections.Generic;
using System.Linq;

public static class Stock
{
    public static bool AnyMissing(List<int> counts) => counts.Any(c => c == 0);

    public static int Low(List<int> counts) =>
        counts
            .Count(c => c < 5);
}

Why

Every one of these methods takes the predicate itself. The Where call adds a step to read (and an iterator to allocate) without saying anything the call after it can't.

How the fix works

Where is renamed to the call after it, and that call (.Count()) goes; the predicate and the line breaks before it stay, so a call chain split over lines keeps its layout. A Where inside the predicate is fixed in the same run.

Lists and arrays when Sonar wants their own methods

Where Sonar's S6605 ("Exists" instead of "Any") or S6602 ("Find" instead of "FirstOrDefault") is on, the fix writes the collection's own method instead of the LINQ call Sonar would report next:

Before S6605/S6602 off S6605/S6602 on
list.Where(p).Any() list.Any(p) list.Exists(p)
list.Where(p).FirstOrDefault() list.FirstOrDefault(p) list.Find(p)
array.Where(p).Any() array.Any(p) Array.Exists(array, p)
array.Where(p).FirstOrDefault() array.FirstOrDefault(p) Array.Find(array, p)

"On" means a severity set for the Sonar id (dotnet_diagnostic.S6605.severity in .editorconfig, a global config or a ruleset), or, when none is set, the default of the SonarAnalyzer.CSharp package in the project: on before version 10 (9.x), off from 10 on. The package's version reaches StyleBro through its build targets (build_property.StyleBroSonar); an analyzer referenced without them only sees configured severities. Where the collection's method wouldn't bind (a Func<T, bool> variable isn't a Predicate<T>, Array without using System;), the LINQ call is written as usual.

Not reported

  • Methods other than System.Linq's Enumerable and Queryable (a type's own Where or Count), and calls where the version with the predicate would bind to something else (an instance method Any(Func<T, bool>) on the source, the index overload Where((x, i) => ...)).
  • The two calls from different classes (Queryable.Where then Enumerable.Count).
  • A call with arguments (FirstOrDefault(defaultValue)), explicit type arguments, ?., a ref argument.
  • Expression trees (Expression<Func<...>> lambdas, query clauses over IQueryable): a query provider would see another tree.
  • A comment or directive in the text that goes.

Compared with other analyzers

Sonar's S2971 reports this (on Where) and more (Select(x => x as T) instead of OfType<T>(), ...); it has no fix. Roslynator's RCS1077 covers it too, with a fix.

Also fixes Sonar's S2971 when the SonarAnalyzer.CSharp package is in the project: the fix is registered for S2971 too (the Where name, where Sonar reports it) and applies at the places this rule would fix, so dotnet format clears Sonar's warnings there even with this rule off. Where this rule skips the code (see Not reported), no fix is offered and Sonar's warning stays. Sonar's other S2971 cases (OfType<T>(), the Count property, ...) aren't fixed.

Compared with StyleCop

StyleCop has no such rule. stylebro-migrate leaves it off.