.NET9 EFcore支持早期MSSQL数据库 ROW_NUMBER()分页

前言

NET程序员是很幸福的,MS在上个月发布了NET9.0RTM,带来了不少的新特性,但是呢,我们是不是还有很多同学软硬件都还没更上,比如,自己的电脑还在跑Win7,公司服务器还在跑MSSQL2005-2008的!

这不就引入了我们本文要探索的问题,因为MS早在EFcore3.1后就不再内置支持ROW_NUMBER()了,以至于需要兼容分页的代码都需要自行处理!

最近自己发的nuget包有个国外的程序员朋友提了一个Issue,以至于我马上行动起来

EFCore9中, 以前兼容的好好的ROW_NUMBER()代码,升级尝鲜后发现跑不起来了,这主要是因为新版本的EFcore做了很多破坏性更新,以至于我们不得不研究新的底层代码!

兼容实现

之前发布过一个nuget包,代码主要是基于以前程序员兼容EFCore7适配到EFCore8的兼容,代码也不多变化也不大,不过呢,升级到EFCore9后发现底层的API全变了,不得不重新再实现一遍!

以下是兼容EFCore9的代码部分:

csharp 复制代码
#if NET9_0_OR_GREATER

#pragma warning disable EF1001 // Internal EF Core API usage.

namespace Biwen.EFCore.UseRowNumberForPaging;

using Microsoft.EntityFrameworkCore.Query;
using System.Collections.Generic;
using System.Reflection;

public class SqlServer2008QueryTranslationPostprocessorFactory(
    QueryTranslationPostprocessorDependencies dependencies,
    RelationalQueryTranslationPostprocessorDependencies relationalDependencies) : IQueryTranslationPostprocessorFactory
{
    private readonly QueryTranslationPostprocessorDependencies _dependencies = dependencies;
    private readonly RelationalQueryTranslationPostprocessorDependencies _relationalDependencies = relationalDependencies;

    public virtual QueryTranslationPostprocessor Create(QueryCompilationContext queryCompilationContext)
        => new SqlServer2008QueryTranslationPostprocessor(
            _dependencies,
            _relationalDependencies,
            queryCompilationContext);

    public class SqlServer2008QueryTranslationPostprocessor(QueryTranslationPostprocessorDependencies dependencies, RelationalQueryTranslationPostprocessorDependencies relationalDependencies, QueryCompilationContext queryCompilationContext) :
        RelationalQueryTranslationPostprocessor(dependencies, relationalDependencies, (RelationalQueryCompilationContext)queryCompilationContext)
    {
        public override Expression Process(Expression query)
        {
            query = base.Process(query);
            query = new Offset2RowNumberConvertVisitor(query, RelationalDependencies.SqlExpressionFactory).Visit(query);
            return query;
        }
        internal class Offset2RowNumberConvertVisitor(
            Expression root,
            ISqlExpressionFactory sqlExpressionFactory) : ExpressionVisitor
        {
            private readonly Expression root = root;
            private readonly ISqlExpressionFactory sqlExpressionFactory = sqlExpressionFactory;
            private const string SubTableName = "subTbl";
            private const string RowColumnName = "_Row_";//下标避免数据表存在字段

            private const string _mp = "_projectionMapping";
            private static readonly FieldInfo ProjectionMapping = typeof(SelectExpression).GetField(_mp, BindingFlags.NonPublic | BindingFlags.Instance);

            protected override Expression VisitExtension(Expression node) => node switch
            {
                ShapedQueryExpression shapedQueryExpression => shapedQueryExpression.Update(Visit(shapedQueryExpression.QueryExpression), shapedQueryExpression.ShaperExpression),
                SelectExpression se => VisitSelect(se),
                _ => base.VisitExtension(node)
            };

            private SelectExpression VisitSelect(SelectExpression selectExpression)
            {
                var oldOffset = selectExpression.Offset;
                if (oldOffset == null)
                    return selectExpression;

                var oldLimit = selectExpression.Limit;
                var oldOrderings = selectExpression.Orderings;
                var newOrderings = oldOrderings switch
                {
                    { Count: > 0 } when oldLimit != null || selectExpression == root => oldOrderings.ToList(),
                    _ => []
                };

                var rowOrderings = oldOrderings.Any() switch
                {
                    true => oldOrderings,
                    false => [new OrderingExpression(new SqlFragmentExpression("(SELECT 1)"), true)]
                };

                var oldSelect = selectExpression;

                var rowNumberExpression = new RowNumberExpression([], rowOrderings, oldOffset.TypeMapping);
                // 创建子查询
                IList<ProjectionExpression> projections = [new ProjectionExpression(rowNumberExpression, RowColumnName),];

                var subquery = new SelectExpression(
                    SubTableName,
                    oldSelect.Tables,
                    oldSelect.Predicate,
                    oldSelect.GroupBy,
                    oldSelect.Having,
                    [.. oldSelect.Projection, .. projections],
                    oldSelect.IsDistinct,
                    [],//排序已经在rowNumber中了
                    null,
                    null,
                    null,
                    null);

                //构造新的条件:
                var and1 = sqlExpressionFactory.GreaterThan(
                    new ColumnExpression(RowColumnName, SubTableName, typeof(int), null, true),
                    oldOffset);
                var and2 = sqlExpressionFactory.LessThanOrEqual(
                    new ColumnExpression(RowColumnName, SubTableName, typeof(int), null, true),
                    sqlExpressionFactory.Add(oldOffset, oldLimit));

                var newPredicate = sqlExpressionFactory.AndAlso(and1, and2);

                //新的Projection:
                var newProjections = oldSelect.Projection.Select(e =>
                {
                    if (e is { Expression: ColumnExpression col })
                    {
                        var newCol = new ColumnExpression(col.Name, SubTableName, col.Type, col.TypeMapping, col.IsNullable);
                        return new ProjectionExpression(newCol, e.Alias);
                    }
                    return e;
                });

                // 创建新的 SelectExpression,将子查询作为来源
                var newSelect = new SelectExpression(
                    oldSelect.Alias,
                    [subquery],
                    newPredicate,
                    oldSelect.GroupBy,
                    oldSelect.Having,
                    [.. newProjections],
                    oldSelect.IsDistinct,
                    [],
                    null,
                    null,
                    null,
                    null);

                //使用反射替换_projectionMapping变量:
                ProjectionMapping.SetValue(newSelect, ProjectionMapping.GetValue(oldSelect));

                return newSelect;
            }
        }
    }
}

#pragma warning restore EF1001 // Internal EF Core API usage.
#endif

最后

实现上逻辑还是一致的,反正都是将Offset转换为ROW_NUMBER()子查询中,取行号数据

只是代码实现区别有一些,以前的EFCore底层代码很多已经不在可用比如直接使用PushdownIntoSubquery()会报错,GenerateOuterColumn()内部的扩展方法发生了破坏性更新导致不能再使用等!

如果你的程序需要升级到NET9并还在使用早期数据库的话,可以引用我实现的代码部分,或者直接引用我发布的Nuget包

代码我放在了,任何问题欢迎Issue https://github.com/vipwan/Biwen.EFCore.UseRowNumberForPaging

相关推荐
gAlAxy...9 小时前
Spring 从 0 → 1 保姆级笔记:IOC、DI、多配置、Bean 生命周期一次讲透
数据库·sqlserver
南棱笑笑生4 天前
20251031在Ubuntu24.04.2中文版下使用diff命令查找两份SDK的差异并删除同样的部分以便比对awk
数据库·sqlserver
vortex56 天前
在 Windows 系统中安装 Oracle、SQL Server(MSSQL)和 MySQL
windows·oracle·sqlserver
薛家明9 天前
easy-query暴打efcore(包括其他所有orm),隐式Group看我如何在子查询做到极致的性能天花板
java·orm·efcore·easy-query·entityframeworkcore·dotnetcore
周杰伦fans11 天前
Navicat - 连接 mysql 、 sqlserver 数据库 步骤与问题解决
数据库·mysql·sqlserver
满昕欢喜17 天前
SQL Server从入门到项目实践(超值版)读书笔记 28
数据库·sql·sqlserver
TiAmo zhang18 天前
SQL Server 2019实验 │ 设计数据库的完整性
数据库·sqlserver
安冬的码畜日常19 天前
【JUnit实战3_02】第二章:探索 JUnit 的核心功能(一)
数据库·junit·sqlserver
TiAmo zhang19 天前
SQL Server 2019实验 │ 管理SQL Server的安全性
数据库·sqlserver
FIavor.20 天前
程序包org.junit.jupiter.api不存在这怎么办
数据库·junit·sqlserver