HLQ005: Avoid use of Single(), SingleOrDefault(), SingleAsync() and SingleOrDefaultAsync() operations

July 17, 2023 ยท View on GitHub

Cause

Methods Single(), SingleOrDefault(), SingleAsync() or SingleOrDefaultAsync() are used to get the first element of a collection.

Severity

Warning

Rule description

The methods Single(), SingleOrDefault(), First() and FirstOrDefault() (plus the async counterparts) are typically used get the first element of a LINQ query. Verifying if the result of the query contains a single item requires applying the query to the souce items until a second item is found or, until the end of the collection. This can be a very expensive operation.

If the query returns more than one item and it's not expected, there is either a problem with the query of in the source collection. Verifying these conditions at runtime is not a good practice.

Benchmarks

Comparing the performance of Single() and First() for the best case scenario, where the first element of the collection satisfies the query conditions, and the worst case, where only the last element of the collection does not satisfy the query conditions.

Source: https://github.com/NetFabric/NetFabric.Hyperlinq.Analyzer/blob/master/NetFabric.Hyperlinq.Analyzer.Benchmarks/HLQ005_AvoidSingleAnalyzer.cs


BenchmarkDotNet v0.13.6, Windows 10 (10.0.19045.3269/22H2/2022Update)
Intel Core i7-7567U CPU 3.50GHz (Kaby Lake), 1 CPU, 4 logical and 2 physical cores
.NET SDK 8.0.100-preview.5.23303.2
  [Host] : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
  .NET 6 : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
  .NET 7 : .NET 7.0.8 (7.0.823.31807), X64 RyuJIT AVX2
  .NET 8 : .NET 8.0.0 (8.0.23.28008), X64 RyuJIT AVX2


MethodJobRuntimeCategoriesCountMeanErrorStdDevMedianRatioRatioSDGen0Code SizeAllocatedAlloc Ratio
BestCase_Single.NET 6.NET 6.0BestCase100620.48 ns10.790 ns13.646 ns616.74 nsbaseline****0.0153547 B32 B****
BestCase_First.NET 6.NET 6.0BestCase10017.92 ns0.197 ns0.242 ns17.94 ns34.66x faster0.72x0.0153456 B32 B1.00x more
BestCase_Single.NET 7.NET 7.0BestCase100647.87 ns7.507 ns5.861 ns649.43 nsbaseline0.0153510 B32 B
BestCase_First.NET 7.NET 7.0BestCase10019.52 ns0.371 ns0.329 ns19.51 ns33.19x faster0.69x0.0153430 B32 B1.00x more
BestCase_Single.NET 8.NET 8.0BestCase100287.77 ns3.058 ns2.554 ns287.55 nsbaseline0.01531,036 B32 B
BestCase_First.NET 8.NET 8.0BestCase10013.98 ns0.145 ns0.129 ns13.97 ns20.57x faster0.20x0.0153659 B32 B1.00x more
BestCase_Single.NET 6.NET 6.0BestCase1000059,386.15 ns428.718 ns401.023 ns59,392.95 nsbaseline****-547 B32 B****
BestCase_First.NET 6.NET 6.0BestCase1000018.41 ns0.272 ns0.213 ns18.34 ns3,225.926x faster49.88x0.0153456 B32 B1.00x more
BestCase_Single.NET 7.NET 7.0BestCase1000059,037.31 ns333.439 ns260.327 ns59,069.24 nsbaseline-510 B32 B
BestCase_First.NET 7.NET 7.0BestCase1000020.71 ns0.955 ns2.710 ns19.22 ns2,894.540x faster284.74x0.0153430 B32 B1.00x more
BestCase_Single.NET 8.NET 8.0BestCase1000031,992.80 ns280.590 ns219.066 ns31,943.08 nsbaseline-883 B32 B
BestCase_First.NET 8.NET 8.0BestCase1000013.83 ns0.308 ns0.400 ns13.75 ns2,332.087x faster61.31x0.0153660 B32 B1.00x more
WorstCase_Single.NET 6.NET 6.0WorstCase100603.35 ns9.153 ns7.643 ns601.27 nsbaseline****0.0153547 B32 B****
WorstCase_First.NET 6.NET 6.0WorstCase100620.98 ns6.142 ns5.129 ns620.11 ns1.03x slower0.02x0.0153456 B32 B1.00x more
WorstCase_Single.NET 7.NET 7.0WorstCase100644.13 ns5.286 ns5.875 ns642.98 nsbaseline0.0153510 B32 B
WorstCase_First.NET 7.NET 7.0WorstCase100646.30 ns10.952 ns8.550 ns644.88 ns1.00x slower0.02x0.0153430 B32 B1.00x more
WorstCase_Single.NET 8.NET 8.0WorstCase100287.32 ns2.771 ns2.457 ns286.77 nsbaseline0.0153826 B32 B
WorstCase_First.NET 8.NET 8.0WorstCase100307.45 ns3.885 ns4.914 ns306.72 ns1.07x slower0.02x0.0153639 B32 B1.00x more
WorstCase_Single.NET 6.NET 6.0WorstCase1000057,425.36 ns1,099.017 ns1,079.382 ns57,099.79 nsbaseline****-547 B32 B****
WorstCase_First.NET 6.NET 6.0WorstCase1000060,242.67 ns1,124.441 ns1,998.693 ns59,291.72 ns1.05x slower0.04x-456 B32 B1.00x more
WorstCase_Single.NET 7.NET 7.0WorstCase1000061,896.58 ns647.584 ns636.014 ns61,721.18 nsbaseline-510 B32 B
WorstCase_First.NET 7.NET 7.0WorstCase1000062,517.62 ns945.663 ns1,776.185 ns61,873.28 ns1.02x slower0.04x-430 B32 B1.00x more
WorstCase_Single.NET 8.NET 8.0WorstCase1000026,906.21 ns331.350 ns309.945 ns26,867.91 nsbaseline-765 B32 B
WorstCase_First.NET 8.NET 8.0WorstCase1000030,013.80 ns593.585 ns1,252.072 ns29,412.80 ns1.12x slower0.05x-594 B32 B1.00x more

How to fix violations

Use First(), FirstOrDefault(), FirstAsync() or FirstOrDefaultAsync() methods to get the first element of a query.

When to suppress warnings

Suppress when unit testing, or validating data, and want to guarantee that the collection does not contain duplicates.

Example of a violation

public static Employee GetEmployee(this IEnumerable<Employee> employees, int employeeId)
    => employees.SingleOrDefault(employee => employee.Id == employeeId);

Example of how to fix

public static Employee GetEmployee(this IEnumerable<Employee> employees, int employeeId)
    => employees.FirstOrDefault(employee => employee.Id == employeeId);