1、"ArgumentException The SqlParameter with ParameterName '@EntryID' is already contained by another SqlParameterCollection."
2、"ArgumentException The SqlParameter with ParameterName '@ItemCount' is already contained by another SqlParameterCollection."
这个Bug我在.Text中的Bug 文章中已经讨论过,但当时并没有找出问题的真正原因,文章中的解决方法也没有解决问题。最后,在韩磊的指点下才消除了这个Bug。在这篇文章中, 我谈谈自己的一些心得。
当出现上述两个异常时, 我首先想到的是找出异常抛出的位置。根据异常的内容,异常应该发生在数库访问代码中, 也就是SQLHelper.cs, .Text中所有的数据库访问都是通过SqlHelper。但在SqlHelper中并没有对异常进行捕获, 只有一处try...catch语句, 所做也只是在catch中关闭SqlConnection。为了发现异常在哪抛出的,需要增加捕获异常的代码,根据异常内容,可以判断出是在执行ExecuteReader过程中出现的异常。我就在ExecuteReader(SqlConnection connection, SqlTransaction transaction, CommandType commandType, string commandText, SqlParameter[] commandParameters, SqlConnectionOwnership connectionOwnership)中增加了try...catch代码,在catch中通过Logger.LogManager.CreateExceptionLog(e,"ExecuteReader Exception");将异常写入日志。在这里我犯一个错误:.Text中的日志是存在数据库中,CreateExceptionLog是要向数据库写入日志信息,但在catch中, 数据库访问已经出现了异常,再进行数据库写入操作,只会继续抛出异常。我这样寻找异常发生的位置显然是徒劳无获的,而且增加了新的异常,更不利于问题的解决。
对于这两个异常,我自然而然认为问题出在SqlHelper中,所以我要在SqlHelper捕获到异常发生的位置。“.Text中的Bug ”文章中想通过在finally中cmd.Parameters.Clear();来解决问题,可是异常仍然存在。所以我决定首先要捕获发生异常的位置,我发现捕获异常代码的问题后,改成了将.Text的日志写入xml文件,这样在发生数据库操作异常,也能将异常写入日志。经过这样的更改,我终于找到了异常发生的位置,异常发生在SqlHelper.AttachParameters中,在循环执行command.Parameters.Add(p);时产生了异常。
我开始仔细分析command.Parameters.Add(p);,实际就是SqlParameter.Add(SqlParameter value),用Reflector查看了一下其中的代码,没什么收获。这时我开始怀疑是多线程并发执行command.Parameters.Add(p)引起的问题。可command并不是共享资源,在每次执行ExecuteReader时,command都是一个新的实例(SqlCommand cmd = new SqlCommand();).不应该存在同步问题。
我百思不得其解,于是与韩磊交流了这个问题,开始他也没想到解决的方法。后来,突然他问我是不是只有“@EntryID”与“@ItemCount”会出现错误,其他的存储过程参数没有出现?根据日志,我的回答是“是”。然后,他告诉问题出在.Text的SqlDataProvider中,他以前遇到过并且解决了这个问题,只不过一时忘记了。
问题出在SqlDataProvider中的两个私有静态成员DefaultEntryQueryParameter、DefaultEntryParameters,类型都是SqlParameter[]。它们在SqlDataProviderr 的构造函数中被初始化:
DefaultEntryQueryParameters = BuildDefaultEntryQueryParameters();
DefaultEntryParameters = BuildDefaultEntryParameters();
解决方法就是去掉构造函数中的初如化,在每处调用DefaultEntryQueryParameters或DefaultEntryParameters的地方,重新初始化它们,也就是使它们指向新的SqlParameter[]实例。更改代码最简单的方法就是将这两个私有成员改成属性:














既然异常是在执行command.Parameters.Add(p);产生的,那我们要首先分析一下这里为什么会抛出异常?
用Reflector要查看一下SqlParameterCollection.Add的代码:








继续看看AddWithoutEvents的代码:







这里的value.Parent = this;应该引起我们的注意,参数value的Parent属性在SqlParameterCollection.Add
中被改变,这就使SqlParameter value与SqlParameterCollection关联起来,一个SqlParameter value只能
同时属于一个SqlParameterCollection。那我们再看看Validate(-1, value):







































从上面的代码就可以看出异常是如何产生的,如果value被另外一个SqlParameterCollection使用(this != value.Parent),就会引发异常。
那为什么出现SqlParameterCollection使用同一个SqlParameter的情况?
罪魁祸首就是两个私有静态成员DefaultEntryQueryParameter、DefaultEntryParameters,私有静态成员被类的所有实例共享。在SqlDataProvider的不同实例的生命周期中, 都共享这两个静态成员。当SqlDataProvider的多个实例同时执行command.Parameters.Add(p)操作时,如果都用到DefaultEntryQueryParameter或DefaultEntryParameters,就会引发异常"...is already contained by another SqlParameterCollection."
解决这个问题的方法除了前面的每次调用DefaultEntryQueryParameter或
DefaultEntryParameters,重新创建SqlParameter[],也可以将DefaultEntryQueryParameter与DefaultEntryParameters变成非静态私有成员,但这种在\方法在多线程的情况下,也会出现同样的问题。最安全的方法就是每次使用SqlParameter,都重新创建SqlParameter的实例。
这个bug一直存在.Text中,那为什么现在才发现?而且有很多.Text的网站为什么没有发现这个Bug?因为这个Bug只会出
现在ExecuteReader中,所以即使发生异常,对系统没什么影响,只要重新刷新一下就行了。而且这个异常只会出现在SqlDataProvider的多个实例同时执行command.Parameters.Add(p)操作时,同时发生的概率与网站的访问量有关。以前博客园很少出现这个异常,最近因为博客园访问量变大,同时执行command.Parameters.Add(p)的概率变高了,所以异常出现的次数也变多了。
从这个Bug中,我们应该吸取两个教训:
1、慎用私有静态成员。
2、安全地使用SqlParameter,每次使用,每次新建。
非常感谢韩磊在解决这个问题中给予指点。