Skip to content

PagedList 带 SqlParameter 的分页查询时发生 ArgumentException 异常

🏷️ Entity Framework

使用 EntityFramework 6.2.0 + PagedList1.17.0.0)来实现自定义查询的分页时,如果传入了 SqlParameter 参数,查询时会报 System.ArgumentException 异常:

另一个 SqlParameterCollection 中已包含 SqlParameter

具体的 StackTrace 如下:

txt
在 System.Data.SqlClient.SqlParameterCollection.Validate(Int32 index, Object value)
在 System.Data.SqlClient.SqlParameterCollection.AddRange(Array values)
在 System.Data.Entity.Core.Objects.ObjectContext.CreateStoreCommand(String commandText, Object[] parameters)
在 System.Data.Entity.Core.Objects.ObjectContext.ExecuteStoreQueryInternal[TElement](String commandText, String entitySetName, ExecutionOptions executionOptions, Object[] parameters)
在 System.Data.Entity.Core.Objects.ObjectContext.<>c__DisplayClass69`1.<ExecuteStoreQueryReliably>b__68()
在 System.Data.Entity.Core.Objects.ObjectContext.ExecuteInTransaction[T](Func`1 func, IDbExecutionStrategy executionStrategy, Boolean startLocalTransaction, Boolean releaseConnectionOnSuccess)
在 System.Data.Entity.Core.Objects.ObjectContext.<>c__DisplayClass69`1.<ExecuteStoreQueryReliably>b__67()
在 System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute[TResult](Func`1 operation)
在 System.Data.Entity.Core.Objects.ObjectContext.ExecuteStoreQueryReliably[TElement](String commandText, String entitySetName, ExecutionOptions executionOptions, Object[] parameters)
在 System.Data.Entity.Core.Objects.ObjectContext.ExecuteStoreQuery[TElement](String commandText, ExecutionOptions executionOptions, Object[] parameters)
在 System.Data.Entity.Internal.InternalContext.<>c__DisplayClass14`1.<ExecuteSqlQuery>b__13()
在 System.Data.Entity.Internal.LazyEnumerator`1.MoveNext()
在 System.Linq.Enumerable.<SkipIterator>d__31`1.MoveNext()
在 System.Linq.Enumerable.<TakeIterator>d__25`1.MoveNext()
在 System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
在 System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
在 PagedList.PagedList`1..ctor(IQueryable`1 superset, Int32 pageNumber, Int32 pageSize)
在 PagedList.PagedList`1..ctor(IEnumerable`1 superset, Int32 pageNumber, Int32 pageSize)
在 PagedList.PagedListExtensions.ToPagedList[T](IEnumerable`1 superset, Int32 pageNumber, Int32 pageSize)

示例代码:

csharp
return db.Database
    .SqlQuery<ScheduleViewModel>(sql, sqlParams.ToArray())
    .ToPagedList(searchParameter.Page, searchParameter.PageSize);

根据错误消息可以判断出是 sqlParams 被重复使用了导致的。很显然 ToPagedList 方法中应该访问了两次数据库,一次是获取查询结果总件数,一次是获取当前页的查询结果。从上面的 StackTrace 可以看出调用了 TakeSkip 方法。

Stack Overflow中给出了解决这个异常的方案,不过可惜数据库访问是封装在 ToPagedList 方法里的,即使改成下面的方式也还是会报错。

csharp
return db.Database
    .SqlQuery<ScheduleViewModel>(sql, sqlParams.Select(p => ((ICloneable)p).Clone()).ToArray())
    .ToPagedList(searchParameter.Page, searchParameter.PageSize);

GitHub 上看了下,这个项目已经不再维护,最近的一次更新已经是 5 年前了。

如果不想更换翻页的包的话,可以暂时使用如下两种方式来规避这个异常。

案 1

改为使用 LINQ 的方式添加查询条件,以避免使用 SqlParameter 传参。
使用这种方案代码的改动可能会比较大。

csharp
return db.Database
    .SqlQuery<ScheduleViewModel>(sql)
    .Where(m => m.Title.IndexOf(searchParameter.SearchKey) >= 0)
    .ToPagedList(searchParameter.Page, searchParameter.PageSize);

案 2

如果带参数的查询结果集比较小,可以先调用 ToList 方法获得所有的结果数据,然后再调用 ToPagedList 方法。

注意:确定查询结果集比较小的情况下才使用这种方案。

csharp
return db.Database
    .SqlQuery<ScheduleViewModel>(sql, sqlParams.ToArray())
    .ToList()
    .ToPagedList(searchParameter.Page, searchParameter.PageSize);

2020-5-3 追记

下面是最终实现分页处理的代码。其中调用 Count() 方法是第一次访问数据库,ToList() 方法是第二次。异常就是发生在这第二次访问的时候。

这么看来,PagedList 包和查询的参数是完全解耦的关系,貌似无法通过修改 PagedList 包来修复这个异常。也许只能寄希望于 Entity Framework 的更新来修复了。

csharp
public PagedList(IQueryable<T> superset, int pageNumber, int pageSize)
{
    if (pageNumber < 1)
        throw new ArgumentOutOfRangeException("pageNumber", pageNumber, "PageNumber cannot be below 1.");
    if (pageSize < 1)
        throw new ArgumentOutOfRangeException("pageSize", pageSize, "PageSize cannot be less than 1.");

    // set source to blank list if superset is null to prevent exceptions
    TotalItemCount = superset == null ? 0 : superset.Count();
    PageSize = pageSize;
    PageNumber = pageNumber;
    PageCount = TotalItemCount > 0
                ? (int)Math.Ceiling(TotalItemCount / (double)PageSize)
                : 0;
    HasPreviousPage = PageNumber > 1;
    HasNextPage = PageNumber < PageCount;
    IsFirstPage = PageNumber == 1;
    IsLastPage = PageNumber >= PageCount;
    FirstItemOnPage = (PageNumber - 1) * PageSize + 1;
    var numberOfLastItemOnPage = FirstItemOnPage + PageSize - 1;
    LastItemOnPage = numberOfLastItemOnPage > TotalItemCount
                    ? TotalItemCount
                    : numberOfLastItemOnPage;

    // add items to internal list
    if (superset != null && TotalItemCount > 0)
        Subset.AddRange(pageNumber == 1
            ? superset.Skip(0).Take(pageSize).ToList()
            : superset.Skip((pageNumber - 1) * pageSize).Take(pageSize).ToList()
        );
}