SqlParameter 已经包含在另一个 SqlParameter 集合中-使用(){}会欺骗吗?

如下所示使用 using() {}(sic)块,并假设 cmd1不超出第一个 using() {}块的范围,那么为什么第二个块会抛出带有消息的异常

SqlParameter 已经包含在另一个 SqlParameter 集合中

这是否意味着资源和/或句柄(包括参数(SqlParameterCollection))附加到 cmd1时,不释放它在块的末尾销毁?

using (var conn = new SqlConnection("Data Source=.;Initial Catalog=Test;Integrated Security=True"))
{
var parameters = new SqlParameter[] { new SqlParameter("@ProductId", SqlDbType.Int ) };


using(var cmd1 = new SqlCommand("SELECT ProductName FROM Products WHERE ProductId = @ProductId"))
{
foreach (var parameter in parameters)
{
cmd1.Parameters.Add(parameter);
}
// cmd1.Parameters.Clear(); // uncomment to save your skin!
}


using (var cmd2 = new SqlCommand("SELECT Review FROM ProductReviews WHERE ProductId = @ProductId"))
{
foreach (var parameter in parameters)
{
cmd2.Parameters.Add(parameter);
}
}
}

注意: 执行 cmd1.Parameter。Clear ()就在第一个 使用(){}块的最后一个大括号之前,这样可以避免异常(以及可能的尴尬)。

如果需要复制,可以使用以下脚本创建对象:

CREATE TABLE Products
(
ProductId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
ProductName nvarchar(32) NOT NULL
)
GO


CREATE TABLE ProductReviews
(
ReviewId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
ProductId int NOT NULL,
Review nvarchar(128) NOT NULL
)
GO
116358 次浏览

I suspect that SqlParameter "knows" which command it's part of, and that that information isn't cleared when the command is disposed, but is cleared when you call command.Parameters.Clear().

Personally I think I'd avoid reusing the objects in the first place, but it's up to you :)

Using blocks do not ensure that an object is "destroyed", simply that the Dispose() method is called. What that actually does is up to the specific implementation and in this case it clearly does not empty the collection. The idea is to ensure that unmanaged resources that would not be cleaned up by the garbage collector are correctly disposed. As the Parameters collection is not an unmanaged resource it is not entirely suprising it is not cleared by the dispose method.

using defines a scope, and does the automatic call of Dispose() for which we love it.

A reference falling out of scope will not make the object itself "disappear" if another object has a reference to it, which in this case will be the case for parameters having a reference to cmd1.

Adding cmd.Parameters.Clear(); after execution should be fine.

I encountered this exception because I had failed to instantiate a parameter object. I thought it was complaining about two procedures having parameters with the same name. It was complaining about the same parameter being added twice.

            Dim aParm As New SqlParameter()
aParm.ParameterName = "NAR_ID" : aParm.Value = hfCurrentNAR_ID.Value
m_daNetworkAccess.UpdateCommand.Parameters.Add(aParm)
aParm = New SqlParameter
Dim tbxDriveFile As TextBox = gvNetworkFileAccess.Rows(index).FindControl("tbxDriveFolderFile")
aParm.ParameterName = "DriveFolderFile" : aParm.Value = tbxDriveFile.Text
m_daNetworkAccess.UpdateCommand.Parameters.Add(aParm)
**aParm = New SqlParameter()**  <--This line was missing.
Dim aDDL As DropDownList = gvNetworkFileAccess.Rows(index).FindControl("ddlFileAccess")
aParm.ParameterName = "AccessGranted" : aParm.Value = aDDL.Text
**m_daNetworkAccess.UpdateCommand.Parameters.Add(aParm)** <-- The error occurred here.

I faced this particular error because I was using the same SqlParameter objects as part of a SqlParameter collection for calling a procedure multiple times. The reason for this error IMHO is that the SqlParameter objects are associated to a particular SqlParameter Collection and you can't use the same SqlParameter objects to create a new SqlParameter collection.

So, instead of this:

var param1 = new SqlParameter{ DbType = DbType.String, ParameterName = param1,Direction = ParameterDirection.Input , Value = "" };
var param2 = new SqlParameter{ DbType = DbType.Int64, ParameterName = param2, Direction = ParameterDirection.Input , Value = 100};


SqlParameter[] sqlParameter1 = new[] { param1, param2 };


ExecuteProc(sp_name, sqlParameter1);


/*ERROR :
SqlParameter[] sqlParameter2 = new[] { param1, param2 };
ExecuteProc(sp_name, sqlParameter2);
*/

Do this:

var param3 = new SqlParameter{ DbType = DbType.String, ParameterName = param1, Direction = ParameterDirection.Input , Value = param1.Value };
var param4 = new SqlParameter{ DbType = DbType.Int64, ParameterName = param2, Direction = ParameterDirection.Input , Value = param2.Value};


SqlParameter[] sqlParameter3 = new[] { param3, param4 };


ExecuteProc(sp_name, sqlParameter3);

Issue
I was executing a SQL Server stored procedure from C# when I encountered this issue:

Exception message [The SqlParameter is already contained by another SqlParameterCollection.]

Cause
I was passing 3 parameters to my stored procedure. I added the

param = command.CreateParameter();

only once altogether. I should have added this line for each parameter, it means 3 times altogether.

DbCommand command = CreateCommand(ct.SourceServer, ct.SourceInstance, ct.SourceDatabase);
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "[ETL].[pGenerateScriptToCreateIndex]";


DbParameter param = command.CreateParameter();
param.ParameterName = "@IndexTypeID";
param.DbType = DbType.Int16;
param.Value = 1;
command.Parameters.Add(param);


param = command.CreateParameter(); --This is the line I was missing
param.ParameterName = "@SchemaName";
param.DbType = DbType.String;
param.Value = ct.SourceSchema;
command.Parameters.Add(param);


param = command.CreateParameter(); --This is the line I was missing
param.ParameterName = "@TableName";
param.DbType = DbType.String;
param.Value = ct.SourceDataObjectName;
command.Parameters.Add(param);


dt = ExecuteSelectCommand(command);

Solution
Adding the following line of code for each parameter

param = command.CreateParameter();

I have Also got the same issue Thanks @Jon, based on that I gave example.

When I called the below function in which 2 times same sqlparameter passed. In the first database call, it was called properly, but in the second time, it was give the above error.

    public Claim GetClaim(long ClaimId)
{
string command = "SELECT * FROM tblClaim "
+ " WHERE RecordStatus = 1 and ClaimId = @ClaimId and ClientId =@ClientId";
List<SqlParameter> objLSP_Proc = new List<SqlParameter>(){
new SqlParameter("@ClientId", SessionModel.ClientId),
new SqlParameter("@ClaimId", ClaimId)
};


DataTable dt = GetDataTable(command, objLSP_Proc);
if (dt.Rows.Count == 0)
{
return null;
}


List<Claim> list = TableToList(dt);


command = "SELECT * FROM tblClaimAttachment WHERE RecordStatus = 1 and ClaimId = @ClaimId and ClientId =@ClientId";


DataTable dt = GetDataTable(command, objLSP_Proc); //gives error here, after add `sqlComm.Parameters.Clear();` in GetDataTable (below) function, the error resolved.




retClaim.Attachments = new ClaimAttachs().SelectMany(command, objLSP_Proc);
return retClaim;
}

This is the common DAL function

       public DataTable GetDataTable(string strSql, List<SqlParameter> parameters)
{
DataTable dt = new DataTable();
try
{
using (SqlConnection connection = this.GetConnection())
{
SqlCommand sqlComm = new SqlCommand(strSql, connection);


if (parameters != null && parameters.Count > 0)
{
sqlComm.Parameters.AddRange(parameters.ToArray());
}


using (SqlDataAdapter da = new SqlDataAdapter())
{
da.SelectCommand = sqlComm;
da.Fill(dt);
}
sqlComm.Parameters.Clear(); //this added and error resolved
}
}
catch (Exception ex)
{
throw;
}
return dt;
}

This is how I have done it!

        ILease lease = (ILease)_SqlParameterCollection.InitializeLifetimeService();
if (lease.CurrentState == LeaseState.Initial)
{
lease.InitialLeaseTime = TimeSpan.FromMinutes(5);
lease.SponsorshipTimeout = TimeSpan.FromMinutes(2);
lease.RenewOnCallTime = TimeSpan.FromMinutes(2);
lease.Renew(new TimeSpan(0, 5, 0));
}

If you're using EntityFramework

I also had this same exception. In my case, I was calling SQL via a EntityFramework DBContext. The following is my code, and how I fixed it.

Broken Code

string sql = "UserReport @userID, @startDate, @endDate";


var sqlParams = new Object[]
{
new SqlParameter { ParameterName= "@userID", Value = p.UserID, SqlDbType = SqlDbType.Int, IsNullable = true }
,new SqlParameter { ParameterName= "@startDate", Value = p.StartDate, SqlDbType = SqlDbType.DateTime, IsNullable = true }
,new SqlParameter { ParameterName= "@endDate", Value = p.EndDate, SqlDbType = SqlDbType.DateTime, IsNullable = true }
};


IEnumerable<T> rows = ctx.Database.SqlQuery<T>(sql,parameters);


foreach(var row in rows) {
// do something
}


// the following call to .Count() is what triggers the exception
if (rows.Count() == 0) {
// tell user there are no rows
}

Note: the above call to SqlQuery<T>() actually returns a DbRawSqlQuery<T>, which implements IEnumerable

Why does calling .Count() throw the exception?

I haven't fired up SQL Profiler to confirm, but I suspect that .Count() is triggering another call to SQL Server, and internally it is reusing the same SQLCommand object and trying to re-add the duplicate parameters.

Solution / Working Code

I added a counter inside my foreach, so that I could keep a row count without having to call .Count()

int rowCount = 0;


foreach(var row in rows) {
rowCount++
// do something
}


if (rowCount == 0) {
// tell user there are no rows
}

Afterthough

My project is probably using an old version of EF. The newer version may have fixed this internal bug by clearing the parameters, or disposing of the SqlCommand object.

Or maybe, there are explicit instructions that tell developers not to call .Count() after iterating a DbRawSqlQuery, and I'm coding it wrong.