Evitar el error sql parameter contained by another SqlParameterCollection / Otro SqlParameterCollection ya contiene SqlParameter.

Este error se suele producir cuando trabajamos por ejemplo con Stored Procedures con parámetros.

 

El error nos aparece así en el Visual Studio:

Error

Esto sucede porque cuando asignamos un SqlParameter a dos SqlParameterCollection, por ejemplo de dos SqlCommand diferentes, se produce una asociación con esta SqlParameterCollection, impidiendo que podamos asignar el mismo SqlParameter a otra SqlParameterCollection.

 

Este error no lo solucionaremos con la clausula Using, sino con un Clear() de la SqlparameterCollection, justo después de haberlo utilizado.

Lo voy a ilustrar con un ejemplo que nos permitirá reproducir el error.

Supongamos que tenemos un programa que llama dos veces a un procedimiento almacenado en una clase que tenemos desarrollada y que escribe el resultado en la consola.

El programa Main, que llama dos veces al procedimiento almacenado para que se produzca el error sería el siguiente:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Data;

namespace UsingCommand
{
    class Program
    {
        static void Main(string[] args)
        {
            CoreCommands comandos = new CoreCommands();

            List<SqlParameter> parametros = new List<SqlParameter>();

            parametros.Add(new SqlParameter("EmployeeID","1"));

            comandos.ExecuteDataReader_SP_Reader("GetEmployeebyID", parametros);

            comandos.ExecuteDataReader_SP_Reader("GetEmployeebyID", parametros);

            Console.ReadLine();
        }
    }
}

 

En la segunda llamada de    comandos.ExecuteDataReader_SP_Reader("GetEmployeebyID", parametros);  se produciría el error

 

Ahora os pongo la clase a la que se invoca desde MAIN con un método ExecuteDataReader_SP_Reader que provocaría el error:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace UsingCommand
{
    class CoreCommands
    {

        public void VisualizarDataReader(SqlDataReader reader)
        {
                while (reader.Read())
            {
                 for (int x=0; x<reader.FieldCount;x++){
                     Console.WriteLine(reader.GetValue(x).ToString());
                }
                }
        }

public void  ExecuteDataReader_SP_Reader(string SPname, List<SqlParameter> parametros)
       {
           using (SqlConnection conexion = new SqlConnection())
           {
               conexion.ConnectionString = "Data Source=.\MSSQL2008STD;Initial Catalog=AdventureWorks;Integrated Security=SSPI;";
               conexion.Open();

               using (SqlCommand comando = new SqlCommand())
               {
                   comando.Connection = conexion;
                   comando.CommandType = System.Data.CommandType.StoredProcedure;
                   comando.CommandText = SPname;
                   comando.Parameters.Clear();

                   foreach (SqlParameter parametro in parametros)
                   {
                       comando.Parameters.Add(parametro);
                   }

                   SqlDataReader lector = comando.ExecuteReader();
                   VisualizarDataReader(lector);

               }
           }

       }

}

}

Esta clase, con su función DataTable ExecuteTABLE_SP_Reader(string SPname, List<SqlParameter> parametros) provocaría un error, pese a estar tanto el objeto conexion como el command bajo la claúsula “using”.

También hemos puesto el Clear(), pero para que funcione debemos ponerlo después de utilizar el comando.

Para que no se produzca el error, la función ExecuteTABLE_SP_Reader debería hacer el Clear() después de haber utilizado el SqlCommand:

public void  ExecuteDataReader_SP_Reader(string SPname, List<SqlParameter> parametros)
       {
           using (SqlConnection conexion = new SqlConnection())
           {
               conexion.ConnectionString = "Data Source=.\MSSQL2008STD;Initial Catalog=AdventureWorks;Integrated Security=SSPI;";
               conexion.Open();

               using (SqlCommand comando = new SqlCommand())
               {
                   comando.Connection = conexion;
                   comando.CommandType = System.Data.CommandType.StoredProcedure;
                   comando.CommandText = SPname;

                   foreach (SqlParameter parametro in parametros)
                   {
                       comando.Parameters.Add(parametro);
                   }

                   SqlDataReader lector = comando.ExecuteReader();
                   VisualizarDataReader(lector);
                   comando.Parameters.Clear();
               }
           }

       }

Utilización de Data Providers (DbProviderFactories)

Alguna vez se nos ha planteado el caso de hacer un programa que soporte varias bases de datos. Para ello podemos utilizar los Dataproviders de ADO.NET tal y como explico en este POST. 

 

  La jerarquía de clases provista por .net permite la utilización de proveedores de datos específicos como SqlClient para la conexión a SQL Server.

    Por encima de este provider específico existe toda una jeraquía de interfaces y clases abstractas que permiten a los fabricantes o incluso a nosotros mismos la extensión del modelo ADO.

    Es conveniente la utilización del provider específico de cada fabricante, así podemos utilizar por ejemplo:

                             System.Data.SqlClient

                             System.Data.OleDb

                             System.Data.Odbc

                             System.Data.OracleClient.

            

           Podemos ver por ejemplo el interface IDbConnection y la implementación en SqlConnection de System.Data.SqlClient

IDBConnection

sqlconnection

Esta libertad bien construida en ADO.NET nos permite por ejemplo realizar una aplicación que sea multi base de datos, es decir, que podamos preparar una capa de conexión, que en función de las necesidades por ejemplo pueda desplegarse para utilizar Oracle o Sql Server, Access. Teniendo en cuenta claro los temas específicos de cada base de datos o proveedor.

Los proveedores disponibles los encontramos en el fichero machine.config, el cual se ubica en la carpeta del framework, en mi caso C:WindowsMicrosoft.NETFrameworkv2.0.50727CONFIG

<system.data>
    <DbProviderFactories>

        <add name="Odbc Data Provider" invariant="System.Data.Odbc" description=".Net Framework Data Provider for Odbc" type="System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>

        <add name="OleDb Data Provider" invariant="System.Data.OleDb" description=".Net Framework Data Provider for OleDb" type="System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>

        <add name="OracleClient Data Provider" invariant="System.Data.OracleClient" description=".Net Framework Data Provider for Oracle" type="System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>

        <add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>

        <add name="Microsoft SQL Server Compact Data Provider" invariant="System.Data.SqlServerCe.3.5" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"/>

</DbProviderFactories>

</system.data>

En el siguiente Ejemplo podemos ver la diferencia en utilizar un proveedor de forma específica (SqlClient para SQL Server) y de utilizar las clases DbProviderFactories del espacio de nombres System.Data.Common para recuperar el mismo proveedor.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Data.SqlClient;

namespace DataProviders
{
    class Program
    {
        static void Main(string[] args)
        {
            DbProviderFactory factoria = DbProviderFactories.GetFactory("System.Data.SqlClient");

            DbConnection conexion = factoria.CreateConnection();
            conexion.ConnectionString ="Data Source=.\MSSQL2008STD;Initial Catalog=AdventureWorks;Integrated Security = SSPI";
            conexion.Open();
            Console.WriteLine("conexión creada");
            conexion.Close();

            SqlConnection conexionSql = new SqlConnection();
            conexionSql.ConnectionString="Data Source=.\MSSQL2008STD;Initial Catalog=AdventureWorks;Integrated Security = SSPI";
            conexion.Open();
            Console.WriteLine("conexión creada");
            conexion.Close();

        }
    }
}

 

Podríamos por ejemplo tener un escenario en que hacemos una aplicación pueda distribuirse con dos modalidades, una básica con Access o una Enterprise con Sql Server. Y además dejar preparado nuestro código para la futura incorporación de un nuevo tipo de base de datos.

Vamos a ver por ejemplo como haríamos si quisiéramos que nuestro código funcionara para Access o Sql Server (Lo siento no tengo Oracle instalado, jejeje)

Podemos utilizar la conocida AdventureWorks, en mi caso la importo desde desde Sql a Access para poder hacer el programa.

Para simplificar el ejemplo, usaremos una propiedad en la clase de Datos que servirá para indicar con un valor entero si queremos conectarnos a SQL Server o Access.

En la misma clase construiremos el método conectar, que en función del tipo de conexión establecerá la cadena de conexión necesaria y el Dataprovider.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;

namespace DataProviders
{

    class DataproviderEjemplo
    {
         int conecctionType;

public int ConecctionType
{
  get { return conecctionType; }
  set { conecctionType = value; }
}

public DbConnection Conectar()
{
    String cadenaConexion = "";
    String proveedor = "";
   switch (conecctionType){
       case 0:
           cadenaConexion = "Data Source=.\MSSQL2008STD;Initial Catalog=AdventureWorks;Integrated Security = SSPI";
           proveedor = "System.Data.SqlClient";
           break;
       case 1:
           cadenaConexion = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Users\javier\Documents\AdventureWorksAccess.accdb;Persist Security Info=False;";
           proveedor = "System.Data.OleDb";
           break;
   }

   DbProviderFactory factoria = DbProviderFactories.GetFactory(proveedor);
   DbConnection conexion = factoria.CreateConnection();
   conexion.ConnectionString = cadenaConexion;
   conexion.Open();
   return conexion;

}
    }
}

Un programa que utilice esta clase podría ser el siguiente, simplemente se intenta ilustrar cómo conecto a access o sql en función del valor establecido en la propiedad connectiontype comentada arriba.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;

namespace DataProviders
{
    class Program
    {
        static void Main(string[] args)
        {
            DataproviderEjemplo _obj = new DataproviderEjemplo();
             DbConnection conexion;
            //conectamos con sql server
            _obj.ConecctionType=0;
            conexion = _obj.Conectar();

            //conectamos con Access
            _obj.ConecctionType = 1;
            conexion = _obj.Conectar();

        }
    }
}

 

Para finalizar vamos a hacer un método que devuelva un datatable y que haga una consulta a una tabla (Employee). Se ejecutará sobre Access o SqlServer en función del valor del ConnectionType que hayamos asignado.

Aprovechando que el nombre de las tablas en access no pueden contener un “.”, pues en la función que devuelve el datatable evaluaremos si es sql o access para ejecutar una consulta sql u otra. Esto nos puede ser util para el caso de que la sentencia sql cambie de una base de datos a otra.

La clase incluyendo la función que consulta la tabla sería la siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Data;

namespace DataProviders
{

    class DataproviderEjemplo
    {
         int conecctionType;

public int ConecctionType
{
  get { return conecctionType; }
  set { conecctionType = value; }
}

public DbConnection Conectar()
{
    String cadenaConexion = "";
    String proveedor = "";
   switch (conecctionType){
       case 0:
           cadenaConexion = "Data Source=.\MSSQL2008STD;Initial Catalog=AdventureWorks;Integrated Security = SSPI";
           proveedor = "System.Data.SqlClient";
           break;
       case 1:
           cadenaConexion = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\Users\javier\Documents\AdventureWorksAccess.accdb;Persist Security Info=False;";
           proveedor = "System.Data.OleDb";
           break;
   }

   DbProviderFactory factoria = DbProviderFactories.GetFactory(proveedor);
   DbConnection conexion = factoria.CreateConnection();
   conexion.ConnectionString = cadenaConexion;
   conexion.Open();
   return conexion;

}
public DataTable GetReader(DbConnection conexion)
{

    DataTable tabla = new DataTable();
    DbCommand micommand = conexion.CreateCommand();
    switch (conecctionType)
    {
        case 0:
            micommand.CommandText = "select * from HumanResources.Employee";
            break;
        case 1:
            micommand.CommandText = "select * from HumanResources_Employee";
            break;
    }
    micommand.CommandType = System.Data.CommandType.Text;
    DbDataReader reader = micommand.ExecuteReader();
    tabla.Load(reader);
    return tabla;
}

    }
}

Y por último el programa que la utiliza puede ser el siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Common;
using System.Data;

namespace DataProviders
{
    class Program
    {
        static void Main(string[] args)
        {
            DataproviderEjemplo _obj = new DataproviderEjemplo();
             DbConnection conexion;
             DataTable tabla;

            //conectamos con sql server o access cambiando el vaor 0 a 1
            _obj.ConecctionType=1;
            conexion = _obj.Conectar();
            tabla=_obj.GetReader(conexion);
            foreach(DataRow row in tabla.Rows) {
                foreach (DataColumn col in tabla.Columns)
                {
                    Console.WriteLine(row[col].ToString());
                }
            }

            conexion.Close();

        }
    }
}

Proteger una cadena de conexión con RSA por código

Si no nos interesa que nuestra cadena de conexión a una base de datos esté visible en nuestro fichero config, bien sea un app.config o un fichero config de Web, podemos escribir un código que lo encripte mediante protección RSA.

Para ello y partiendo de un fichero de configuración como el siguiente:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="MainConnection" connectionString="Data Source=.MSSQL2008STD
         ;Initial Catalog=AdventureWorks; Integrated Security=SSPI"/>
  </connectionStrings>
</configuration>

Podemos escribir el código necesario para encriptarlo.

El código sería el siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.SqlClient;

namespace ProtectConnection
{
    class Program
    {
        static void Main(string[] args)
        {
            //Primero abrimos el fichero de configuración
            System.Configuration.Configuration config =
            ConfigurationManager.OpenExeConfiguration(
             ConfigurationUserLevel.None);

            // No es necesario pero probamos a Conectarnos para probar la cadena de conexión que hemos creado…
            String cadenaconexion = ConfigurationManager.ConnectionStrings["MainConnection"].ConnectionString;
            SqlConnection miconexion = new SqlConnection();
            miconexion.ConnectionString = cadenaconexion;
            miconexion.Open();
            Console.WriteLine("Conexion Creada");
            miconexion.Close();

            //A continuación cargamos la sección de cadenas de conexión.
            ConfigurationSection cadenasdeconexion =
                config.GetSection("connectionStrings");

            //si no está encriptada la encriptamos.
            if (!cadenasdeconexion.SectionInformation.IsProtected)
            {
                Console.WriteLine("Encriptando fichero");
                cadenasdeconexion.SectionInformation.ProtectSection
                    ("RsaProtectedConfigurationProvider");
                cadenasdeconexion.SectionInformation.ForceSave = true;
                config.Save(ConfigurationSaveMode.Full);
            }
            Console.ReadLine();
        }
    }
}

 

Si observamos en el directorio una vez generado el .exe, veremos que si abrimos nuestro fichero de configuración, está sin encriptar:

conexion0

Si ejecutamos una vez el programa veremos como se conecta a la base de datos y luego encripta la conexión:

conexion1

Si lo volvemos a ejecutar, veremos que ya no lo encripta y que vuelve a desencriptarlo automáticamente sin escribir código adicional.

conexion2

Una vez ejecutado el programa, el fichero pasaría a tener el siguiente contenido:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings configProtectionProvider="RsaProtectedConfigurationProvider">
    <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
      xmlns="http://www.w3.org/2001/04/xmlenc#">
      <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
        <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
          <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
          <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
            <KeyName>Rsa Key</KeyName>
          </KeyInfo>
          <CipherData>
            <CipherValue>q3+U3GzGciObvpgVZgjWRGMpeNYXsgpQEKFHP6q+SG3EfG54ZhcK69ocFpnqnhZZE3eZaLE0KUZBecRWeZLbtz6zI+zqg2x1er7t4DeZn5LODslURYlmNelTrR4AmXwj+ZlMMiwTb0kFjTrzlrgCe0kpo346QtxV/4shW2c4Zw0=</CipherValue>
          </CipherData>
        </EncryptedKey>
      </KeyInfo>
      <CipherData>
        <CipherValue>I9uyNHeclHjakBYoa2Y53MGFYHKJAka+GI3Lr8lYREDh7wEDyGd54ijwOLuO7BEckpIB92bHEulvP0HsKFfYUEvuP3WTBwgsbKrwNR7fJ3iHKmYf/n770Ms0f5WByFlBKki9EXO+/bmBNCBKxy8ZKMqC/mtYU5M3QtX3vjJqzRzf+gL8/Ix33neAob3qFIn554Ho+wLKbsLpSBuYD2FVLUmCfN6NdXtO6ElfB6CEIrw9ayIhvd9qzRhxHIGhfIotpuD+gu0/YtdHKLSD6/wH4vwNsM8YmSF+17AX8+oRIYl3aZbQHk0olIyq0BqRsx1vTgAPZW9z5RZC1RD0lc2zeII/NcLFup09Z5k8jbVRFYcOnUKnC0FnWJXa6TlikpUYxkuRQIR+wsQDyBbu7oHUKBRe/249DNUP/GXKwxiwYO/T132w/rjGEiESVMx+/QkrEG7KsQZpuiPgDNyvtxKdlQOzsQFKJtGlR7D6l3Az+0pfB9mUprLeJP0JIYH8pN4UtY0cWUGeR6hc+PmljhSQ6U3njC8eASJL0HdNGTcB7+0JphDchDn8VmNM5846dY18qNE30n2ZAHGW96CCTv+UhA==</CipherValue>
      </CipherData>
    </EncryptedData>
  </connectionStrings>
</configuration>

Crear una Conexión con SqlConnectionStringBuilder

 

SqlConnectionStringBuilder es una clase que nos permite crear conexiones ADO.net sin correr riesgos al introducir los valores.

Esta clase nos permite a través de sus propiedades configurar una conexión ADO.

Algunas de sus propiedades más importantes son:

ApplicaitonName: Establece u obtiene el nombre de la aplicación asociada con el connection String

AsynchronousProcessing: Establece u obtiene el valor que indica si la conexión permite procesamiento asíncrono.

AttachDBFilename: Establece u obtiene el valor del datafile primario (.mdf).

BrowsableConnectionString: Establece u obtiene un valor que indica si la conexión es visible en el diseñador de Visual Studio.

ConnectionString: Establece u obtiene el valor completo de la cadena de conexión, asociada al DBConnectionStringBuilder.

ConnectTimeout: Establece u obtiene el timeout para la conexión.

DataSource: Establece u obtiene el nombre o la dirección IP  de la instancia SQL Server a la que se conecta.

Encrypt: Establece u obtiene un valor boolean que indica cuando SQL Server utiliza SSL encryption para todos los datos que se envían entre el cliente y el servidor.

InitalGatalog: Establece u obtiene el nombre de la base de datos asociada con la conexión.

Integrated Security: Establece u obtiene el valor que indica el tipo de conexión. True-> para la seguridad integrada de windows, y False-> para indicar usuario y contraseña.

MaxPoolSize: Obtiene o Establece el máximo número de conexiones en el connection pool.

MinPoolSize: Obtiene o establece el mínimo número de conexiones para el connection pool.

MultipleActiveResultsets: Obtiene o establece mediante un valor boolean el uso de MARS.

PacketSize: Obtiene o establece el número en bytes del paquete de red de intercambio de datos con SQL Server.

Password: Establece u obtiene la contraseña para la cuenta de SQL Server asociada a la conexión.

Pooling: Establece u obtiene un valor boolano que indica si se utiilza pooling.

UserId: El usuario de SQL Server asociado a la conexión.

 

Un sencillo ejemplo del uso de esta clase es el siguiente:

 

public void Conectar()
{
    SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
    builder.DataSource = "Server\MSSQL2008STD";
    builder.InitialCatalog="VideoGames";
    builder.IntegratedSecurity = true;
}