Skip to content
Open
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
49 changes: 49 additions & 0 deletions EntityFrameworkCore.UseRowNumberForPaging.Test/EfSqlHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.SqlServer.TransactSql.ScriptDom;

namespace EntityFrameworkCore.UseRowNumberForPaging.Test;

public static class EfSqlHelpers
{
private const string SplitQueryMarker =
"This LINQ query is being executed in split-query mode";

public static string StripEfSplitQueryComment(string sql)
{
if (string.IsNullOrEmpty(sql)) return sql;

var idx = sql.IndexOf(SplitQueryMarker, StringComparison.Ordinal);
if (idx < 0) return sql;

return sql.Substring(0, idx).TrimEnd();
}

public static void ValidateSql(string sql, string compatibilityLevel = "100")
{
TSqlParser parser = compatibilityLevel switch
{
"100" => new TSql100Parser(initialQuotedIdentifiers: true),
"110" => new TSql110Parser(initialQuotedIdentifiers: true),
"120" => new TSql120Parser(initialQuotedIdentifiers: true),
"130" => new TSql130Parser(initialQuotedIdentifiers: true),
"140" => new TSql140Parser(initialQuotedIdentifiers: true),
"150" => new TSql150Parser(initialQuotedIdentifiers: true),
_ => throw new ArgumentException($"Unsupported compatibility level: {compatibilityLevel}")
};
IList<ParseError> errors;

using var reader = new StringReader(sql);
parser.Parse(reader, out errors);

if (errors != null && errors.Count > 0)
{
var message = string.Join("\n", errors.Select(e =>
$"Line {e.Line}, Col {e.Column}: {e.Message}"));

throw new Exception("Invalid T-SQL:\n" + message);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>

<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Microsoft.SqlServer.TransactSql.ScriptDom" Version="170.128.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public class NotUseRowNumberDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<Category> Categories { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Shouldly;
Expand All @@ -12,8 +13,10 @@ public void With_TrivialOk()
{
using (var dbContext = new UseRowNumberDbContext())
{
var rawSql = dbContext.Blogs.Where(i => i.BlogId > 1).Skip(0).Take(10).ToQueryString();
var rawSql = dbContext.Blogs.Where(i => i.BlogId > 1).OrderBy(i => i.BlogId).Skip(0).Take(10).ToQueryString();
rawSql.ShouldContain("ROW_NUMBER");

EfSqlHelpers.ValidateSql(rawSql);
}
}

Expand All @@ -25,6 +28,8 @@ public void Without_TrivialOk()
var rawSql = dbContext.Blogs.Where(i => i.BlogId > 1).Skip(0).Take(10).ToQueryString();
rawSql.ShouldContain("OFFSET");
rawSql.ShouldNotContain("ROW_NUMBER");

EfSqlHelpers.ValidateSql(rawSql, "150");
}
}

Expand All @@ -36,6 +41,8 @@ public void With_NoSkipClause_OrderDesc_NoRowNumber()
rawSql.ShouldNotContain("ROW_NUMBER");
rawSql.ShouldContain("TOP");
rawSql.ShouldContain("ORDER BY");

EfSqlHelpers.ValidateSql(rawSql);
}

[Fact]
Expand All @@ -46,6 +53,8 @@ public void With_OrderDesc_UsesRowNumber()
rawSql.ShouldContain("ROW_NUMBER");
rawSql.ShouldContain("ORDER BY");
rawSql.ShouldContain("TOP");

EfSqlHelpers.ValidateSql(rawSql);
}

[Fact]
Expand All @@ -57,8 +66,61 @@ public void With_Order_SplitQuery_UsesRowNumber()
.OrderByDescending(o => o.Rating)
.Skip(30).Take(15)
.AsSplitQuery().ToQueryString();

rawSql = EfSqlHelpers.StripEfSplitQueryComment(rawSql);

rawSql.ShouldContain("ROW_NUMBER");
rawSql.ShouldContain("ORDER BY");
rawSql.ShouldContain("TOP");
rawSql.ShouldNotContain("OFFSET");

EfSqlHelpers.ValidateSql(rawSql);
}

[Fact]
public void With_MultipleIncludes_UsesRowNumber_NoOffset()
{
using var dbContext = new UseRowNumberDbContext();
var rawSql = dbContext.Blogs
.Include(b => b.Author)
.Include(b => b.Category)
.Where(i => i.BlogId > 1)
.OrderBy(a => a.Author.ContributingSince)
.OrderByDescending(o => o.Rating)
.Skip(30).Take(15)
.ToQueryString();

rawSql.ShouldContain("ROW_NUMBER");
rawSql.ShouldContain("ORDER BY");
rawSql.ShouldContain("TOP");
rawSql.ShouldNotContain("OFFSET");

EfSqlHelpers.ValidateSql(rawSql);
}

[Fact]
public void With_MultipleIncludes_SplitQuery_UsesRowNumber_NoOffset()
{
using var dbContext = new UseRowNumberDbContext();

var rawSql = dbContext.Blogs
.Include(b => b.Author)
.Include(b => b.Category)
.Where(b => b.BlogId > 1)
.OrderBy(a => a.Author.ContributingSince)
.OrderByDescending(b => b.Rating)
.Skip(30)
.Take(15)
.AsSplitQuery()
.ToQueryString();

rawSql = EfSqlHelpers.StripEfSplitQueryComment(rawSql);

rawSql.ShouldContain("ROW_NUMBER");
rawSql.ShouldNotContain("OFFSET");
rawSql.ShouldContain("ORDER BY");
rawSql.ShouldContain("TOP");

EfSqlHelpers.ValidateSql(rawSql);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class UseRowNumberDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Author> Authors { get; set; }
public DbSet<Category> Categories { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
Expand All @@ -22,6 +23,7 @@ public class Blog
public string Url { get; set; }
public int Rating { get; set; }
public virtual Author Author { get; set; }
public virtual Category Category { get; set; }
}
public class Author
{
Expand All @@ -30,3 +32,10 @@ public class Author
public DateOnly ContributingSince { get; set; }
public virtual List<Blog> Blogs { get; set; }
}

public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public virtual List<Blog> Blogs { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<Version>0.7</Version>
<Authors>Rwing</Authors>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<RepositoryUrl>https://github.com/Rwing/EntityFrameworkCore.UseRowNumberForPaging</RepositoryUrl>
<PackageProjectUrl>https://github.com/Rwing/EntityFrameworkCore.UseRowNumberForPaging</PackageProjectUrl>
<Description>Bring back support for UseRowNumberForPaging in EntityFrameworkCore 9.0/8.0. Use a ROW_NUMBER() in queries instead of OFFSET/FETCH. This method is backwards-compatible to SQL Server 2005.</Description>
<Description>Bring back support for UseRowNumberForPaging in EntityFrameworkCore 10.0/9.0/8.0. Use a ROW_NUMBER() in queries instead of OFFSET/FETCH. This method is backwards-compatible to SQL Server 2005.</Description>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
Expand All @@ -31,6 +31,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<None Include="..\LICENSE">
<Pack>True</Pack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ private Expression VisitSelect(SelectExpression selectExpression)
{
var oldOffset = selectExpression.Offset;
if (oldOffset == null)
return selectExpression;
return base.VisitExtension(selectExpression);
var oldLimit = selectExpression.Limit;
var oldOrderings = selectExpression.Orderings;
var newOrderings = oldOrderings.Count > 0 && (oldLimit != null || selectExpression == root)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ private SelectExpression VisitSelect(SelectExpression selectExpression)
// if we have no offset, we do not need to use ROW_NUMBER for offset calculations
if (selectExpression.Offset == null)
{
return selectExpression;
// still visit children to catch offsets in nested subqueries
return (SelectExpression)base.VisitExtension(selectExpression);
}
var isRootQuery = selectExpression == root;

Expand All @@ -41,8 +42,9 @@ private SelectExpression VisitSelect(SelectExpression selectExpression)

// remove offset and limit by creating new select expression from old one
// we can't use SelectExpression.Update because that breaks PushDownIntoSubquery
#pragma warning disable EF1001
var enhancedSelect = new SelectExpression(
alias: null,
alias: selectExpression.Alias,
tables: new(selectExpression.Tables),
predicate: selectExpression.Predicate,
groupBy: new(selectExpression.GroupBy),
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[main-nuget]: https://www.nuget.org/packages/EntityFrameworkCore.UseRowNumberForPaging/
[main-nuget-badge]: https://img.shields.io/nuget/v/EntityFrameworkCore.UseRowNumberForPaging.svg?style=flat-square&label=nuget

Bring back support for [UseRowNumberForPaging](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.infrastructure.sqlserverdbcontextoptionsbuilder.userownumberforpaging?view=efcore-3.0) in EntityFrameworkCore 9.0/8.0/7.0/6.0/5.0
Bring back support for [UseRowNumberForPaging](https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.infrastructure.sqlserverdbcontextoptionsbuilder.userownumberforpaging?view=efcore-3.0) in EntityFrameworkCore 10.0/9.0/8.0/7.0/6.0/5.0

If you are using EntityFrameworkCore 5.0 please use version 0.2.
If you are using EntityFrameworkCore 6.0/7.0 please use version 0.5.
Expand Down