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
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>

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

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCore.UseRowNumberForPaging
namespace EntityFrameworkCore.UseRowNumberForPaging;

public class NotUseRowNumberDbContext : DbContext
{
public class NotUseRowNumberDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Blog> Blogs { get; set; }
public DbSet<Author> Authors { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=Blogging;Integrated Security=True");
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=Blogging;Integrated Security=True");
}

}
64 changes: 64 additions & 0 deletions EntityFrameworkCore.UseRowNumberForPaging.Test/SimpleTestCases.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Shouldly;
using Xunit;

namespace EntityFrameworkCore.UseRowNumberForPaging.Test;

public class SimpleTestCases
{
[Fact]
public void With_TrivialOk()
{
using (var dbContext = new UseRowNumberDbContext())
{
var rawSql = dbContext.Blogs.Where(i => i.BlogId > 1).Skip(0).Take(10).ToQueryString();
rawSql.ShouldContain("ROW_NUMBER");
}
}

[Fact]
public void Without_TrivialOk()
{
using (var dbContext = new NotUseRowNumberDbContext())
{
var rawSql = dbContext.Blogs.Where(i => i.BlogId > 1).Skip(0).Take(10).ToQueryString();
rawSql.ShouldContain("OFFSET");
rawSql.ShouldNotContain("ROW_NUMBER");
}
}

[Fact]
public void With_NoSkipClause_OrderDesc_NoRowNumber()
{
using var dbContext = new UseRowNumberDbContext();
var rawSql = dbContext.Blogs.Where(i => i.BlogId > 1).OrderByDescending(o => o.Rating).Take(20).ToQueryString();
rawSql.ShouldNotContain("ROW_NUMBER");
rawSql.ShouldContain("TOP");
rawSql.ShouldContain("ORDER BY");
}

[Fact]
public void With_OrderDesc_UsesRowNumber()
{
using var dbContext = new UseRowNumberDbContext();
var rawSql = dbContext.Blogs.Where(i => i.BlogId > 1).OrderByDescending(o => o.Rating).Skip(20).Take(20).ToQueryString();
rawSql.ShouldContain("ROW_NUMBER");
rawSql.ShouldContain("ORDER BY");
rawSql.ShouldContain("TOP");
}

[Fact]
public void With_Order_SplitQuery_UsesRowNumber()
{
using var dbContext = new UseRowNumberDbContext();
var rawSql = dbContext.Blogs.Include(b => b.Author).Where(i => i.BlogId > 1)
.OrderBy(a => a.Author.ContributingSince)
.OrderByDescending(o => o.Rating)
.Skip(30).Take(15)
.AsSplitQuery().ToQueryString();
rawSql.ShouldContain("ROW_NUMBER");
rawSql.ShouldContain("ORDER BY");
rawSql.ShouldContain("TOP");
}
}
32 changes: 0 additions & 32 deletions EntityFrameworkCore.UseRowNumberForPaging.Test/UnitTest1.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace EntityFrameworkCore.UseRowNumberForPaging
{
public class UseRowNumberDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
namespace EntityFrameworkCore.UseRowNumberForPaging;

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=Blogging;Integrated Security=True", i => i.UseRowNumberForPaging());
}
}
public class UseRowNumberDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Author> Authors { get; set; }

public class Blog
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
public int BlogId { get; set; }
public string Url { get; set; }
public int Rating { get; set; }
optionsBuilder.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=Blogging;Integrated Security=True", i => i.UseRowNumberForPaging());
}
}

public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public int Rating { get; set; }
public virtual Author Author { get; set; }
}
public class Author
{
public int AuthorId { get; set; }
public string Name { get; set; }
public DateOnly ContributingSince { get; set; }
public virtual List<Blog> Blogs { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<Version>0.5</Version>
<TargetFrameworks>net8.0;net9.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 8.0/7.0/6.0/5.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 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>
<DebugType>embedded</DebugType>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup Condition="$(TargetFramework) != 'net9.0'">
<!-- only warn about lower-priority security issues for latest target framework -->
<NoWarn>$(NoWarn);NU1901;NU1902;</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" />
<ItemGroup Condition="'$(TargetFramework)' == 'net9.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<None Include="..\LICENSE">
<Pack>True</Pack>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#if !NET9_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;

namespace EntityFrameworkCore.UseRowNumberForPaging;

internal class Offset2RowNumberConvertVisitor : ExpressionVisitor
{
private static readonly MethodInfo GenerateOuterColumnAccessor;
private static readonly Type TableReferenceExpressionType;

static Offset2RowNumberConvertVisitor()
{
var method = typeof(SelectExpression).GetMethod("GenerateOuterColumn", BindingFlags.NonPublic | BindingFlags.Instance);
if (!typeof(ColumnExpression).IsAssignableFrom(method?.ReturnType))
{
throw new InvalidOperationException("SelectExpression.GenerateOuterColum() was not found");
}

TableReferenceExpressionType = method.GetParameters().First().ParameterType;
GenerateOuterColumnAccessor = method;
}

private readonly Expression root;
private readonly ISqlExpressionFactory sqlExpressionFactory;

public Offset2RowNumberConvertVisitor(Expression root, ISqlExpressionFactory sqlExpressionFactory)
{
this.root = root;
this.sqlExpressionFactory = sqlExpressionFactory;
}

protected override Expression VisitExtension(Expression node)
{
if (node is ShapedQueryExpression shapedQueryExpression)
{
return shapedQueryExpression.Update(Visit(shapedQueryExpression.QueryExpression), Visit(shapedQueryExpression.ShaperExpression));
}
if (node is SelectExpression se)
{
return VisitSelect(se);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rwing specifically this line is incorrect

}
return base.VisitExtension(node);
}

private Expression VisitSelect(SelectExpression selectExpression)
{
var oldOffset = selectExpression.Offset;
if (oldOffset == null)
return selectExpression;
var oldLimit = selectExpression.Limit;
var oldOrderings = selectExpression.Orderings;
var newOrderings = oldOrderings.Count > 0 && (oldLimit != null || selectExpression == root)
? oldOrderings.ToList()
: new List<OrderingExpression>();
// Change SelectExpression
selectExpression = selectExpression.Update(projections: selectExpression.Projection.ToList(),
tables: selectExpression.Tables.ToList(),
predicate: selectExpression.Predicate,
groupBy: selectExpression.GroupBy.ToList(),
having: selectExpression.Having,
orderings: newOrderings,
limit: null,
offset: null);
var rowOrderings = oldOrderings.Count != 0 ? oldOrderings
: new[] { new OrderingExpression(new SqlFragmentExpression("(SELECT 1)"), true) };

selectExpression.PushdownIntoSubquery();

var subQuery = (SelectExpression)selectExpression.Tables[0];
var projection = new RowNumberExpression(Array.Empty<SqlExpression>(), rowOrderings, oldOffset.TypeMapping);
var left = GenerateOuterColumnAccessor.Invoke(subQuery
, new object[]
{
Activator.CreateInstance(TableReferenceExpressionType, new object[] { subQuery,subQuery.Alias! })!,
projection,
"row",
true
}) as ColumnExpression;
selectExpression.ApplyPredicate(sqlExpressionFactory.GreaterThan(left!, oldOffset));

if (oldLimit != null)
{
if (oldOrderings.Count == 0)
{
selectExpression.ApplyPredicate(sqlExpressionFactory.LessThanOrEqual(left, sqlExpressionFactory.Add(oldOffset, oldLimit)));
}
else
{
selectExpression.ApplyLimit(oldLimit);
}
}
return selectExpression;
}
}
#endif
Loading