Analyzer rule
LC050: OrderBy before Distinct is discarded
LC050 flags EF Core queries that sort before Distinct(), where SQL DISTINCT silently drops the ORDER BY, and moves the sort after Distinct().
- Default severity
- Warning
- Code fix
- Available
- Category
- Correctness
Change the severity in .editorconfig:
[*.cs]
dotnet_diagnostic.LC050.severity = error
LC050: OrderBy before Distinct is discarded
In Plain Terms
You line people up by height, then tell them to remove duplicates by forming a crowd. Whatever order they had is gone. If you wanted them sorted, you have to line them up after the crowd forms.
Goal
Detect EF Core queries that call OrderBy / OrderByDescending and then Distinct() with nothing in between that needs the order.
The Problem
SQL DISTINCT does not preserve row order. When an EF Core query sorts and then calls Distinct(), EF Core removes the ORDER BY from the generated SQL. The query runs without an error, and the results come back in whatever order the database picks. That order often looks sorted in development and changes with data volume, indexes, or query plans in production.
// Violation: the ORDER BY is dropped, so the names are not sorted.
var names = await db.Customers
.OrderBy(c => c.Name)
.Select(c => c.Name)
.Distinct()
.ToListAsync();
The Fix
Sort after Distinct():
var names = await db.Customers
.Select(c => c.Name)
.Distinct()
.OrderBy(name => name)
.ToListAsync();
When the query keeps whole rows, move the sort chain as it is:
// Before
var orders = db.Orders.OrderByDescending(o => o.PlacedAt).ThenBy(o => o.Id).Distinct();
// After
var orders = db.Orders.Distinct().OrderByDescending(o => o.PlacedAt).ThenBy(o => o.Id);
If the sort key is not part of the projection, you cannot sort the distinct values by it directly. Group instead, for example GroupBy(o => o.Customer.Name).OrderBy(g => g.Min(o => o.PlacedAt)).Select(g => g.Key).
Analyzer Logic
ID: LC050
Category: Correctness
Severity: Warning
- Start at a parameterless
Queryable.Distinct()(fluent, static, or after query syntax). - Walk back through operators that neither need nor keep the order:
ThenBy/ThenByDescending,Where,Select, and the EF Core pass-throughsAsNoTracking,AsNoTrackingWithIdentityResolution,AsTracking,TagWith,TagWithCallSite,IgnoreQueryFilters,IgnoreAutoIncludes,Include,ThenInclude,AsSplitQuery, andAsSingleQuery. - Report when the walk reaches
OrderByorOrderByDescending. The diagnostic sits onDistinctand names the discarded sort.
When it stays quiet (non-goals)
Skip,Take, or any other operator between the sort andDistinct(). Those keep the sort meaningful because it decides which rows reachDistinct().Distinct(comparer), which EF Core cannot translate anyway.- LINQ to Objects (
List<T>.OrderBy(...).Distinct()) andAsQueryable()over in-memory data, whereDistinctkeeps first-seen order. GroupBy,Join,SelectMany, and other shape-changing operators between the sort andDistinct().- Sorted queries stored in a local and made distinct later. Only fluent chains are analyzed.
Code Fix
The fixer handles two shapes and reports everything else without a fix:
- A sort chain directly before
Distinct():q.OrderBy(k).ThenBy(k2).Distinct()becomesq.Distinct().OrderBy(k).ThenBy(k2). - A single sort whose key is exactly the projected value:
q.OrderBy(o => o.Name).Select(x => x.Name).Distinct()becomesq.Select(x => x.Name).Distinct().OrderBy(x => x).
After the fix the expression is an IOrderedQueryable<T>. When the query initializes a var local that is later reassigned, the new type would break that assignment, so no fix is offered there. Query-syntax orderby clauses and chains with Where between the sort and Distinct() are also reported without a fix.
Test Cases
Violations
db.Orders.OrderBy(o => o.PlacedAt).Distinct();
db.Orders.OrderBy(o => o.Total).Where(o => o.Total > 0).Select(o => o.Total).Distinct();
(from c in db.Customers orderby c.Name select c.Name).Distinct();
Valid
db.Customers.Select(c => c.Name).Distinct().OrderBy(n => n);
db.Orders.OrderBy(o => o.PlacedAt).Take(10).Distinct();
cachedOrders.OrderBy(o => o.PlacedAt).Distinct();