ASP.NET MVC + LINQ 异常

5
我有一个ASP.NET MVC应用程序,99.9%的时间都很好用。但是偶尔会出现严重问题,我想知道有没有人能够解释一下可能出了什么问题。
这个Web应用程序使用Linq2SQL,在执行以下一组指令后在控制器中崩溃:
const int pageSize = 5;
var allHeadings = artRepository.FindAllVisibleHeadings();
var paginatedHeadings = new PaginatedList<Article>(allHeadings, id ?? 0, pageSize);

allHeadings包含所有文章可见标题的IQueryable列表,而PaginatedList则负责从这个非常长的列表中取出适当的一块。具体如下:

public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize)
{
    PageIndex = pageIndex;
    PageSize = pageSize;
    TotalCount = source.Count();
    TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);

    this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
}

在统计数据库中所有可见文章的source.Count()行上程序出错了。有趣的是,当我多次重新加载页面时,会出现两种不同类型的异常:

第一种异常:序列包含多个元素

at System.Data.Linq.SqlClient.SqlProvider.Execute(Expression query, QueryInfo queryInfo, IObjectReaderFactory factory, Object[] parentArgs, Object[] userArgs, ICompiledSubQuery[] subQueries, Object lastResult)
at System.Data.Linq.SqlClient.SqlProvider.ExecuteAll(Expression query, QueryInfo[] queryInfos, IObjectReaderFactory factory, Object[] userArguments, ICompiledSubQuery[] subQueries)
at System.Data.Linq.SqlClient.SqlProvider.System.Data.Linq.Provider.IProvider.Execute(Expression query)
at System.Data.Linq.DataQuery`1.System.Linq.IQueryProvider.Execute[S](Expression expression)
at System.Linq.Queryable.Count[TSource](IQueryable`1 source)
at KoscierzynaInfo.Helpers.PaginatedList`1..ctor(IQueryable`1 source, Int32 pageIndex, Int32 pageSize) in C:\Users\mr\Documents\Visual Studio 2008\Projects\KoscierzynaInfo\KoscierzynaInfo\Helpers\PaginatedList.cs:line 20
at KoscierzynaInfo.Controllers.HomeController.Index(Nullable`1 id) in C:\Users\mr\Documents\Visual Studio 2008\Projects\KoscierzynaInfo\KoscierzynaInfo\Controllers\HomeController.cs:line 63
at lambda_method(ExecutionScope , ControllerBase , Object[] )
at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters)
at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters)
at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters)
at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClassa.<InvokeActionMethodWithFilters>b__7()
at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)
at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClassa.<>c__DisplayClassc.<InvokeActionMethodWithFilters>b__9()
at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodWithFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor, IDictionary`2 parameters)
at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)
at System.Web.Mvc.Controller.ExecuteCore()
at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext)
at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext)
at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext)
at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext)
at System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext httpContext)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

第二种类型:数组索引超出范围

at System.Data.SqlClient.SqlDataReader.ReadColumnHeader(Int32 i)
at System.Data.SqlClient.SqlDataReader.IsDBNull(Int32 i)
at Read_Article(ObjectMaterializer`1 )
at System.Data.Linq.SqlClient.ObjectReaderCompiler.ObjectReader`2.MoveNext()
at System.Collections.Generic.List`1.InsertRange(Int32 index, IEnumerable`1 collection)
at KoscierzynaInfo.Helpers.PaginatedList`1..ctor(IQueryable`1 source, Int32 pageIndex, Int32 pageSize) in C:\Users\mr\Documents\Visual Studio 2008\Projects\KoscierzynaInfo\KoscierzynaInfo\Helpers\PaginatedList.cs:line 20
at KoscierzynaInfo.Controllers.HomeController.Index(Nullable`1 id) in C:\Users\mr\Documents\Visual Studio 2008\Projects\KoscierzynaInfo\KoscierzynaInfo\Controllers\HomeController.cs:line 63
at lambda_method(ExecutionScope , ControllerBase , Object[] )
at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters)
at System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary`2 parameters)
at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary`2 parameters)
at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClassa.<InvokeActionMethodWithFilters>b__7()
at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func`1 continuation)
at System.Web.Mvc.ControllerActionInvoker.<>c__DisplayClassa.<>c__DisplayClassc.<InvokeActionMethodWithFilters>b__9()
at System.Web.Mvc.ControllerActionInvoker.InvokeActionMethodWithFilters(ControllerContext controllerContext, IList`1 filters, ActionDescriptor actionDescriptor, IDictionary`2 parameters)
at System.Web.Mvc.ControllerActionInvoker.InvokeAction(ControllerContext controllerContext, String actionName)
at System.Web.Mvc.Controller.ExecuteCore()
at System.Web.Mvc.ControllerBase.Execute(RequestContext requestContext)
at System.Web.Mvc.ControllerBase.System.Web.Mvc.IController.Execute(RequestContext requestContext)
at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContextBase httpContext)
at System.Web.Mvc.MvcHandler.ProcessRequest(HttpContext httpContext)
at System.Web.Mvc.MvcHandler.System.Web.IHttpHandler.ProcessRequest(HttpContext httpContext)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

我想到的唯一解决方法是重新启动IIS或回收应用程序池。你们有没有遇到过这个问题?它是从哪里来的?是否有任何解决办法?

我将应用程序从IIS7 + SQL Server 2005移动到另一台服务器上,使用IIS6 + SQLServer 2008,希望这能解决问题,但不幸的是今天又出现了这个问题,这让我相信这个问题并不是真正依赖于系统/数据库。


3
artRepository.FindAllVisibleHeadings() 返回的是什么类型的IQueryable?您正在添加哪种类型的查询?我相信它是来自那个角落。 - Maarten
2
source.Count() 是第一次实际执行 IQueryable 的代码。因此,在您的示例代码中,我们看不到执行代码。错误可能出现在 artRepository.FindAllVisibleHeadings() 方法中。 - Wouter Simons
3
强调Maarten和Wouter所说的,错误几乎肯定在FindAllVisibleHeadings()中。请将该代码添加到问题中。 - Bobson
各位,虽然你们对顺序是正确的,但我认为错误不在于FindAllVisibleHeadings()。原因是大多数情况下没有错误,有时会出现错误1,有时会出现错误2。这意味着它是一种竞争条件,与线程和平台内部(调用.NET方法)有关,而不是代码本身的问题。如果问题更加一致,我会说你们肯定是对的,但从我所看到的情况来看,情况并非如此。不幸的是。这可能是一些环境问题,比如IIS中的池大小,而不是Michal代码固有的问题。 - Display Name
蓝月亮,你看见我独自站着,心中没有梦想,没有属于自己的爱。糟糕,现在我脑海里一直哼唱这首歌! - Doctor Jones
显示剩余2条评论
6个回答

5
我猜测您可能正在使用与请求级别或单例级别相关联的 DataContext 类。如果是这样,我建议您将范围保留在方法级别。
请参阅奇怪的 LINQ 异常(请查看答案的评论)。

这也是我的猜测。在Google上搜索“DataContextFactory”,你会找到一个类的代码,可以用来管理DataContext的生命周期。我个人建议使用Request范围而不是方法范围。 - smartcaveman
我赞成这个。您正在将源作为方法参数接收,我怀疑您正在“缓存” DataContext。这是不应该的!事实上,推荐的做法是每次实例化一个新的 DataContext,连接管理由框架在后台完成,您不必担心它。关于此问题有很多关于此的问题在 SO 上,MSDN 也这样说(略微含蓄地)。DataContext 的 MSDN - NothingsImpossible

2

由于您使用了 IQueryable<T> 作为数据源,这意味着只有在调用 Count() 方法时才会将数据加载到 source 参数中。

当在包含多个元素的集合上调用 Single() 方法时,您可能会遇到第一个错误:

var data = Enumerable.Range(1, 10).Single();

基于以上,我可能会说在你的代码某处调用了Single(),且在大多数情况下集合只有一个元素,但当它有更多元素时,就会出现错误。

对于第二个错误:查看堆栈跟踪的顶部,您会看到

SqlDataReader.ReadColumnHeader(Int32 i)

这是抛出ArgumentOutOfRangeException的方法。这可能表明您的数据库模型与数据库架构或查询输出不同步,即您期望读取N列,但实际上只有N-K列。


我在运行这段代码时遇到了问题:http://pastebin.com/nuEmuizi而且,有时候代码会在随机时间间隔内给出两个错误信息。但更奇怪的是,有时候即使计数没有抛出错误,也会返回无效的计数值!这非常像某种数据库问题或sqldatareader中的问题。 - Jan Johansen

1

我曾经遇到过错误一的情况,那是当我尝试设置单个对象时,返回了多个对象。

if (source == null)
{
  source = new List<T>().AsQueryable();
}

你尝试过在你的分页器中包含类似这样的内容吗?


我会尝试一下,看看是否有帮助。最糟糕的是这个错误时有时无,而且非常罕见。不过还是谢谢你的提示! - Michal Rogozinski

0
重要的是,即使在执行 Count() 时发生异常,失败的代码也不在该方法中,而是在构建 IQueryable 的 LINQ 表达式中。这就是 LINQ 的奇妙之处,“当枚举其结果时才执行 LINQ select 语句”。
可枚举(Queryable 继承自 Enumerable)在以下情况下被枚举(列表不详尽):Count、Foreach、ToList、ToArray、ToDictionary、First 等。当我们需要结果中的元素或元素数量时。
更重要的是,只要存储了 LINQ 查询的结果而没有将其转换为列表或数组,“选择语句每次枚举都会被执行”。
话虽如此,这里有两件重要的事情:
  1. 失败的代码在 FindAllVisibleHeadings 方法中,因为这是您的 LINQ 查询构建的地方。如果没有看到代码,很难知道具体在哪里,但考虑到您收到的错误消息,您应该寻找 Single 或 SingleOrDefault 语句。

  2. 在多个位置枚举 IQueryable 存在明显的代码异味(Count 和 Skip / Take)。

基本的解决方案是在使用前将您的IQueryable转换为列表。

public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize)
{
    var sourceList = source.ToList();
    PageIndex = pageIndex;
    PageSize = pageSize;
    TotalCount = sourceList.Count();
    TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);

    this.AddRange(sourceList.Skip(PageIndex * PageSize).Take(PageSize));
}

但仅仅这样做可能并不能解决您的问题。

你必须思考,在你构建FindAllVisibleHeadings方法中的LINQ查询和使用它的时刻之间,你的数据可能发生任何事情。

FindAllVisibleHeadings返回一个List而不是IQueryable可能是个好主意。但我们必须先看看里面的代码才能知道。


0

看起来你要连续两次枚举相同的源。Count将对数据库运行select *并返回计数,然后Take将对数据库运行另一个查询。最好先在内存集合中获取IEnumerable的结果,并首先在其上运行.Length,然后使用Take在IEnumerable上获取所需的分页结果。如果使用Take()、SingleOrDefault()或FirstOrDefault(),可以检查长度是否为零、一或多。


Count将执行SELECT COUNT(*)而不是在任何体面的LINQ to SQL提供程序中执行SELECT *。 - Guillaume86
抱歉,我打错了,我是指 Count(*)... 是我的错。 - DRobertE
1
好的,总之,在分页之前获取所有数据对我来说似乎是一个不好的想法。Take和Skip也将被翻译成SQL,这样你就可以避免在内存中加载整个表,随着数据库的增长,你的方法会变得越来越慢。使用Count然后Take/Skip将能够很好地扩展。 - Guillaume86

0

你的第二个异常不是在Count()方法中发生的,而是在AddRange()方法中发生的。这很可能是在PageSize超过跳过后剩余项目数量时发生的。需要注意的是,即使你已经缩小了代码并进行了检查,在运行AddRange之前,计数也可能不准确。

我猜测你必须循环应用程序池的原因是因为你正在重复使用DataContext,就像Mark所推测的那样。我建议将DataContext的重复使用至少减少到请求级别。这样,单个错误就不会导致整个应用程序崩溃。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接