From cf324c5760f1aff79fab104c8868a2c52d7e4425 Mon Sep 17 00:00:00 2001 From: Chris Seils Date: Sat, 29 Nov 2025 13:36:47 +1000 Subject: [PATCH 1/6] add support for 10.0 --- .../EntityFrameworkCore.UseRowNumberForPaging.Test.csproj | 2 +- .../EntityFrameworkCore.UseRowNumberForPaging.csproj | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj b/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj index 4ace74d..8183134 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj +++ b/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj @@ -1,7 +1,7 @@ - net8.0;net9.0 + net8.0;net9.0;net10.0 false diff --git a/EntityFrameworkCore.UseRowNumberForPaging/EntityFrameworkCore.UseRowNumberForPaging.csproj b/EntityFrameworkCore.UseRowNumberForPaging/EntityFrameworkCore.UseRowNumberForPaging.csproj index 696879a..9e85a9b 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging/EntityFrameworkCore.UseRowNumberForPaging.csproj +++ b/EntityFrameworkCore.UseRowNumberForPaging/EntityFrameworkCore.UseRowNumberForPaging.csproj @@ -1,13 +1,13 @@  - net8.0;net9.0 + net8.0;net9.0;net10.0 0.7 Rwing true https://github.com/Rwing/EntityFrameworkCore.UseRowNumberForPaging https://github.com/Rwing/EntityFrameworkCore.UseRowNumberForPaging - 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. + 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. LICENSE true true @@ -31,6 +31,10 @@ + + + + True From 49eef3195686664e1dc39d19783b303b97dfaa74 Mon Sep 17 00:00:00 2001 From: Chris Seils Date: Sat, 29 Nov 2025 17:25:18 +1000 Subject: [PATCH 2/6] Fixed subquery issue in .net 10 --- .../Offset2RowNumberConvertVisitor.net9.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs b/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs index b5bda52..0f39ccf 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs +++ b/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs @@ -30,7 +30,12 @@ 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) { +#if NET10_0_OR_GREATER + // still visit children to catch offsets in nested subqueries + return (SelectExpression)base.VisitExtension(selectExpression); +#else return selectExpression; +#endif } var isRootQuery = selectExpression == root; @@ -41,6 +46,7 @@ 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, tables: new(selectExpression.Tables), From dbb0836db1fb43afba51ff61952e312b0a832346 Mon Sep 17 00:00:00 2001 From: Chris Seils Date: Sun, 30 Nov 2025 12:55:38 +1000 Subject: [PATCH 3/6] Added more tests --- ...workCore.UseRowNumberForPaging.Test.csproj | 1 + .../NotUseRowNumberDbContext.cs | 1 + .../SimpleTestCases.cs | 64 ++++++++++++++++++- .../UseRowNumberDbContext.cs | 9 +++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj b/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj index 8183134..f661cfe 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj +++ b/EntityFrameworkCore.UseRowNumberForPaging.Test/EntityFrameworkCore.UseRowNumberForPaging.Test.csproj @@ -8,6 +8,7 @@ + diff --git a/EntityFrameworkCore.UseRowNumberForPaging.Test/NotUseRowNumberDbContext.cs b/EntityFrameworkCore.UseRowNumberForPaging.Test/NotUseRowNumberDbContext.cs index 4f85b22..c9235d4 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging.Test/NotUseRowNumberDbContext.cs +++ b/EntityFrameworkCore.UseRowNumberForPaging.Test/NotUseRowNumberDbContext.cs @@ -6,6 +6,7 @@ public class NotUseRowNumberDbContext : DbContext { public DbSet Blogs { get; set; } public DbSet Authors { get; set; } + public DbSet Categories { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/EntityFrameworkCore.UseRowNumberForPaging.Test/SimpleTestCases.cs b/EntityFrameworkCore.UseRowNumberForPaging.Test/SimpleTestCases.cs index cbdd883..f90abea 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging.Test/SimpleTestCases.cs +++ b/EntityFrameworkCore.UseRowNumberForPaging.Test/SimpleTestCases.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Microsoft.EntityFrameworkCore; using Shouldly; @@ -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); } } @@ -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"); } } @@ -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] @@ -46,6 +53,8 @@ public void With_OrderDesc_UsesRowNumber() rawSql.ShouldContain("ROW_NUMBER"); rawSql.ShouldContain("ORDER BY"); rawSql.ShouldContain("TOP"); + + EfSqlHelpers.ValidateSql(rawSql); } [Fact] @@ -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); } } diff --git a/EntityFrameworkCore.UseRowNumberForPaging.Test/UseRowNumberDbContext.cs b/EntityFrameworkCore.UseRowNumberForPaging.Test/UseRowNumberDbContext.cs index f73874c..b6f2241 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging.Test/UseRowNumberDbContext.cs +++ b/EntityFrameworkCore.UseRowNumberForPaging.Test/UseRowNumberDbContext.cs @@ -8,6 +8,7 @@ public class UseRowNumberDbContext : DbContext { public DbSet Blogs { get; set; } public DbSet Authors { get; set; } + public DbSet Categories { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -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 { @@ -30,3 +32,10 @@ public class Author public DateOnly ContributingSince { get; set; } public virtual List Blogs { get; set; } } + +public class Category +{ + public int CategoryId { get; set; } + public string Name { get; set; } + public virtual List Blogs { get; set; } +} \ No newline at end of file From d00f9f562ff5b5da5bb5d2f48ff936ad7c54075a Mon Sep 17 00:00:00 2001 From: Chris Seils Date: Sun, 30 Nov 2025 12:56:12 +1000 Subject: [PATCH 4/6] Ensure subqueries are visited for .net8 9 and 10 --- .../Offset2RowNumberConvertVisitor.net8.cs | 2 +- .../Offset2RowNumberConvertVisitor.net9.cs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net8.cs b/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net8.cs index e733652..1345e31 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net8.cs +++ b/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net8.cs @@ -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) diff --git a/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs b/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs index 0f39ccf..bbe0c15 100644 --- a/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs +++ b/EntityFrameworkCore.UseRowNumberForPaging/Offset2RowNumberConvertVisitor.net9.cs @@ -30,12 +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) { -#if NET10_0_OR_GREATER // still visit children to catch offsets in nested subqueries return (SelectExpression)base.VisitExtension(selectExpression); -#else - return selectExpression; -#endif } var isRootQuery = selectExpression == root; @@ -48,7 +44,7 @@ private SelectExpression VisitSelect(SelectExpression selectExpression) // 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), From 50e774f9899b4fc96c637ea5d21d235c8ea9edce Mon Sep 17 00:00:00 2001 From: Chris Seils Date: Sun, 30 Nov 2025 12:56:30 +1000 Subject: [PATCH 5/6] Added EfSqlHelpers to validate t-sql --- .../EfSqlHelpers.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 EntityFrameworkCore.UseRowNumberForPaging.Test/EfSqlHelpers.cs diff --git a/EntityFrameworkCore.UseRowNumberForPaging.Test/EfSqlHelpers.cs b/EntityFrameworkCore.UseRowNumberForPaging.Test/EfSqlHelpers.cs new file mode 100644 index 0000000..4291aa8 --- /dev/null +++ b/EntityFrameworkCore.UseRowNumberForPaging.Test/EfSqlHelpers.cs @@ -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 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); + } + } +} \ No newline at end of file From 30a408c9c7a6f4cea38035c1053bd25a9956a8ce Mon Sep 17 00:00:00 2001 From: Chris Seils Date: Sun, 30 Nov 2025 12:58:52 +1000 Subject: [PATCH 6/6] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b78dc3..f708f35 100644 --- a/README.md +++ b/README.md @@ -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.