PagedList 带 SqlParameter 的分页查询时发生 ArgumentException 异常
使用 EntityFramework 6.2.0 + PagedList(1.17.0.0)来实现自定义查询的分页时,如果传入了 SqlParameter
参数,查询时会报 System.ArgumentException 异常:
另一个
SqlParameterCollection
中已包含SqlParameter
。
具体的 StackTrace 如下:
在 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)
示例代码:
return db.Database
.SqlQuery<ScheduleViewModel>(sql, sqlParams.ToArray())
.ToPagedList(searchParameter.Page, searchParameter.PageSize);
根据错误消息可以判断出是 sqlParams 被重复使用了导致的。很显然 ToPagedList 方法中应该访问了两次数据库,一次是获取查询结果总件数,一次是获取当前页的查询结果。从上面的 StackTrace 可以看出调用了 Take 和 Skip 方法。
Stack Overflow中给出了解决这个异常的方案,不过可惜数据库访问是封装在 ToPagedList 方法里的,即使改成下面的方式也还是会报错。
return db.Database
.SqlQuery<ScheduleViewModel>(sql, sqlParams.Select(p => ((ICloneable)p).Clone()).ToArray())
.ToPagedList(searchParameter.Page, searchParameter.PageSize);
去 GitHub 上看了下,这个项目已经不再维护,最近的一次更新已经是 5 年前了。
如果不想更换翻页的包的话,可以暂时使用如下两种方式来规避这个异常。
案 1
改为使用 LINQ 的方式添加查询条件,以避免使用 SqlParameter
传参。
使用这种方案代码的改动可能会比较大。
return db.Database
.SqlQuery<ScheduleViewModel>(sql)
.Where(m => m.Title.IndexOf(searchParameter.SearchKey) >= 0)
.ToPagedList(searchParameter.Page, searchParameter.PageSize);
案 2
如果带参数的查询结果集比较小,可以先调用 ToList 方法获得所有的结果数据,然后再调用 ToPagedList 方法。
注意:确定查询结果集比较小的情况下才使用这种方案。
return db.Database
.SqlQuery<ScheduleViewModel>(sql, sqlParams.ToArray())
.ToList()
.ToPagedList(searchParameter.Page, searchParameter.PageSize);
2020-5-3 追记
下面是最终实现分页处理的代码。其中调用 Count() 方法是第一次访问数据库,ToList() 方法是第二次。异常就是发生在这第二次访问的时候。
这么看来,PagedList 包和查询的参数是完全解耦的关系,貌似无法通过修改 PagedList 包来修复这个异常。也许只能寄希望于 Entity Framework 的更新来修复了。
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()
);
}