Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ https://nuget.org/packages/EfOrderBy/

- **Automatic ordering**: Queries without explicit `OrderBy` automatically use configured default ordering
- **Include() support**: Nested collections in `.Include()` expressions are automatically ordered
- **Projection support**: Collection navigations loaded by a `.Select()` projection are automatically ordered
- **Deterministic paging**: Ordering is applied before `Skip`/`Take`/`First`, so pages and single results are stable
- **Inheritance support**: Ordering configured on a base entity type is automatically inherited by derived types (TPH)
- **Fluent configuration**: Configure default ordering using the familiar EF Core fluent API
Expand Down Expand Up @@ -132,6 +133,34 @@ var departments = await context.Departments
<!-- endSnippet -->


## Projection Support

A collection navigation read inside a `Select` projection is loaded by a subquery of its own, and is ordered the same way as one in an `Include`:

<!-- snippet: ProjectionSupport -->
<a id='snippet-ProjectionSupport'></a>
```cs
// Departments ordered by DisplayOrder
// Employees ordered by HireDate, then Salary descending
var departments = await context.Departments
.Select(_ => new
{
_.Name,
Employees = _.Employees
.Where(employee => employee.Salary > 0)
.Select(employee => employee.Name)
.ToList()
})
.ToListAsync();
```
<sup><a href='/src/Tests/Snippets.cs#L155-L170' title='Snippet source file'>snippet source</a> | <a href='#snippet-ProjectionSupport' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The ordering goes after any `Where` filters and before the operators that project the entity away or choose which rows come back: `Select`, `Skip`, `Take`, `First`, `FirstOrDefault`, `Last`, `LastOrDefault`, `ElementAt`, `ElementAtOrDefault`, `ToList`, `ToArray` and `AsEnumerable`. A navigation bound into the result as it is, such as `new { _.Name, _.Employees }`, is ordered too.

A collection whose order cannot reach the result is left alone: one that is only aggregated, with `Count`, `Any`, `Sum` and the like, or one composed with an operator EF Core drops ordering beneath, such as `Distinct`. Explicit ordering inside the projection is kept, and is checked by [redundant ordering detection](#detect-redundant-ordering) the same way as ordering inside an `Include`.


## Inheritance Support

When using TPH (Table Per Hierarchy) inheritance, ordering configured on a base entity type is automatically inherited by derived types. This eliminates the need to duplicate `.OrderBy()` on every derived type.
Expand Down Expand Up @@ -182,7 +211,7 @@ public class InheritanceDbContext : DbContext
public DbSet<DerivedEntityB> DerivedEntitiesB => Set<DerivedEntityB>();
}
```
<sup><a href='/src/Tests/Snippets.cs#L228-L273' title='Snippet source file'>snippet source</a> | <a href='#snippet-InheritanceOrdering' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Tests/Snippets.cs#L250-L295' title='Snippet source file'>snippet source</a> | <a href='#snippet-InheritanceOrdering' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Behavior:
Expand All @@ -205,7 +234,7 @@ builder.Entity<Product>()
.ThenBy(_ => _.Name)
.ThenByDescending(_ => _.Price);
```
<sup><a href='/src/Tests/Snippets.cs#L153-L160' title='Snippet source file'>snippet source</a> | <a href='#snippet-MultiColumnOrdering' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Tests/Snippets.cs#L175-L182' title='Snippet source file'>snippet source</a> | <a href='#snippet-MultiColumnOrdering' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down Expand Up @@ -548,7 +577,7 @@ public class AppDbContext : DbContext
public DbSet<Employee> Employees => Set<Employee>();
}
```
<sup><a href='/src/Tests/Snippets.cs#L164-L207' title='Snippet source file'>snippet source</a> | <a href='#snippet-CompleteExample' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Tests/Snippets.cs#L186-L229' title='Snippet source file'>snippet source</a> | <a href='#snippet-CompleteExample' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<Version>1.3.0</Version>
<Version>1.4.0</Version>
<AssemblyVersion>1.0.0</AssemblyVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
Expand Down
1 change: 1 addition & 0 deletions src/EfOrderBy/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
global using System.Collections.Concurrent;
global using System.Diagnostics.CodeAnalysis;
global using System.Linq.Expressions;
global using EfOrderBy;
global using Microsoft.EntityFrameworkCore;
Expand Down
16 changes: 11 additions & 5 deletions src/EfOrderBy/Interceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,24 @@ public Expression QueryCompilationStarting(Expression query, QueryExpressionEven
var detectRedundantOrdering = extension?.ThrowOnRedundantOrderBy ??
OrderBySettings.ThrowOnRedundantOrderBy;

// Analyze the query for ordering and includes in a single pass
var (hasOrdering, hasInclude) = QueryAnalyzer.Analyze(query);
// Analyze the query for ordering, includes and projections in a single pass
var (hasOrdering, hasInclude, hasSelect) = QueryAnalyzer.Analyze(query);

var queryWithOrderedIncludes = query;

// Ordering nested collections rewrites the whole tree, so only do it when there is an
// Include to rewrite. Neither flag above can change as a result, since the rewrite only
// touches the inside of Include lambdas.
// Include or a projection to rewrite. None of the flags above can change as a result, since
// the rewrites only touch the inside of Include and Select lambdas.
if (hasInclude)
{
var visitor = new IncludeOrderingApplicator(model, detectRedundantOrdering);
queryWithOrderedIncludes = visitor.Visit(query);
queryWithOrderedIncludes = visitor.Visit(queryWithOrderedIncludes);
}

if (hasSelect)
{
var visitor = new ProjectionOrderingApplicator(model, detectRedundantOrdering);
queryWithOrderedIncludes = visitor.Visit(queryWithOrderedIncludes);
}

if (hasOrdering)
Expand Down
Loading
Loading