O SqlParameter já está contido por outro SqlParameterCollection - using () {} trapaceia?

87

Ao usar os using() {}blocos (sic) conforme mostrado abaixo, e assumindo que cmd1não está além do escopo do primeiro using() {}bloco, por que o segundo bloco deve lançar uma exceção com a mensagem

O SqlParameter já está contido por outro SqlParameterCollection

Isso significa que os recursos e / ou identificadores - incluindo os parâmetros ( SqlParameterCollection) - anexados cmd1não são liberados quando são destruídos no final do bloco?

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);
        }
    }
}

NOTA: Fazer cmd1.Parameters.Clear () logo antes da última chave do primeiro bloco using () {} salvará você da exceção (e possível constrangimento).

Se precisar reproduzir, você pode usar os seguintes scripts para criar os objetos:

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
John Gathogo
fonte
Também estou vendo isso, mas a correção não funcionou. Frustrante. E estou usando apenas um único objeto cmd, não reutilizado. Ele está envolvido em um loop de repetição assíncrona, então é provavelmente a mesma causa raiz, mas não evitada da mesma maneira.
Ed Williams

Respostas:

111

Suspeito que SqlParameter"sabe" de qual comando ele faz parte e que essa informação não é apagada quando o comando é descartado, mas é apagada quando você chama command.Parameters.Clear().

Pessoalmente, acho que evitaria reutilizar os objetos em primeiro lugar, mas depende de você :)

Jon Skeet
fonte
2
Obrigado. Suspeitei que fosse esse o caso. Isso também significaria que o SqlParameter está se associando a um objeto descartado, o que não tenho certeza se é uma coisa boa
John Gathogo,
@JohnGathogo: Bem, está associado a um objeto que é descartado após a associação ser formada. Não é ideal, certamente.
Jon Skeet de
11
Uma nota para os outros. Tive que realizar o Clearantes de sair do primeiro usingbloco. Fazer isso ao entrar no meu 2º usingbloco ainda gerou esse erro.
Snekse de
@JonSkeet é idiota ter que recriar o mesmo conjunto de parâmetros, apenas para fazer outra consulta. parece um acoplamento forte
simbionte
9

O uso de blocos não garante que um objeto seja "destruído", apenas que o Dispose()método é chamado. O que isso realmente faz depende da implementação específica e, neste caso, claramente não esvazia a coleção. A ideia é garantir que os recursos não gerenciados que não seriam limpos pelo coletor de lixo sejam descartados corretamente. Como a coleção Parameters não é um recurso não gerenciado, não é totalmente surpreendente, pois não é limpa pelo método dispose.

Ben Robinson
fonte
7

Adicionando cmd.Parameters.Clear (); após a execução deve estar bem.

Nish
fonte
3

usingdefine um escopo e faz a chamada automática de Dispose()que gostamos.

Uma referência fora do escopo não fará com que o próprio objeto "desapareça" se outro objeto tiver uma referência a ele, o que, neste caso, será o caso para parameterster uma referência cmd1.

Jon Hanna
fonte
2

Também tenho o mesmo problema Obrigado @Jon, com base no que dei o exemplo.

Quando chamei a função abaixo na qual passou 2 vezes o mesmo parâmetro sql. Na primeira chamada ao banco de dados, ele foi chamado corretamente, mas na segunda vez, deu-se o erro acima.

    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;
    }

Esta é a função DAL comum

       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;
        }
Ajay2707
fonte
2

Eu enfrentei esse erro específico porque estava usando os mesmos objetos SqlParameter como parte de uma coleção SqlParameter para chamar um procedimento várias vezes. A razão para esse erro IMHO é que os objetos SqlParameter estão associados a uma coleção SqlParameter específica e você não pode usar os mesmos objetos SqlParameter para criar uma nova coleção SqlParameter.

Então, em vez disso:

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);
*/ 

Faça isso:

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);
SaCh
fonte
0

Encontrei essa exceção porque não consegui instanciar um objeto de parâmetro. Achei que fosse reclamar de dois procedimentos que tinham parâmetros com o mesmo nome. Ele estava reclamando sobre o mesmo parâmetro sendo adicionado duas vezes.

            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.
Jon Boy
fonte
0

Problema
Eu estava executando um procedimento armazenado do SQL Server em C # quando encontrei este problema:

Mensagem de exceção [O SqlParameter já está contido por outro SqlParameterCollection.]

Porque
eu estava passando 3 parâmetros para meu procedimento armazenado. Eu adicionei o

param = command.CreateParameter();

apenas uma vez. Eu deveria ter adicionado esta linha para cada parâmetro, isso significa 3 vezes no total.

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);

Solução
Adicionar a seguinte linha de código para cada parâmetro

param = command.CreateParameter();
Goldfish
fonte
0

É assim que eu fiz!

        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));
        }
KrazKjn
fonte
0

Se você estiver usando EntityFramework

Eu também tive essa mesma exceção. No meu caso, eu estava chamando o SQL por meio de um EntityFramework DBContext. A seguir está meu código e como corrigi-lo.

Código Quebrado

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
}

Observação: a chamada acima para SqlQuery<T>()realmente retorna um DbRawSqlQuery<T>, que implementaIEnumerable

Por que chamar .Count () lança a exceção?

Não iniciei o SQL Profiler para confirmar, mas suspeito que .Count()esteja disparando outra chamada para o SQL Server e, internamente, está reutilizando o mesmoSQLCommand objeto e tentando adicionar novamente os parâmetros duplicados.

Solução / Código de Trabalho

Eu adicionei um contador dentro do meu foreach, para que eu pudesse manter uma contagem de linhas sem ter que chamar.Count()

int rowCount = 0;

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

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

Depois

Meu projeto provavelmente está usando uma versão antiga do EF. A versão mais recente pode ter corrigido esse bug interno limpando os parâmetros ou eliminando oSqlCommand objeto.

Ou talvez haja instruções explícitas que dizem aos desenvolvedores para não chamarem .Count()após a iteração de a DbRawSqlQuery, e estou codificando errado.

Walter Stabosz
fonte