ado.net

ADO.NET: Buenas prácticas en el acceso a datos.

ADO.NET es un conjunto de clases que exponen servicios de acceso a datos para programadores de .NET Framework. […]
Constituye una parte integral de .NET Framework y proporciona acceso a datos relacionales, XML y de aplicaciones.[…]

Las clases que nos brinda ADO.NET representan todos los conceptos básicos y necesarios para acceder a cualquier orígen de datos y realizar cualquier tipo de operación: Conexión, Transacción, Comando, Parametro, Set de datos, Tabla, y más…

Es importante que conozcamos qué es y cómo funciona ADO.NET porque siempre lo vamos a estar utilizando al acceder a datos, ya sea de manera directa mediante el uso de sus clases o de manera indirecta al utilizar alguna herramienta de mapeo de objetos a datos (ORM) o librería de terceros, las cuales internamente harán uso de ADO.NET.

Es muy importante leer la documentación existente en el Sitio oficial y siempre es muy útil seguir las novedades en el Blog oficial.

La cantidad de artículos y videos no oficiales que existen con tutoriales del estilo “Cómo conectarse a SQL Server desde C#” o “Accediendo a datos con .NET”, es incontable y crece día a día. Pero, lamentablemente, es común encontrar en ellos errores y/o malas prácticas que afectan la seguridad y/o performance de la aplicación final. Es por eso que este artículo intento destacar las mejores prácticas que podemos seguir y también mencionar los principales errores que debemos evitar, basado en mis propias experiencias y en libros/documentación oficial.

Cadenas de conexión

Para poder conectarnos a un orígen de datos, vamos a necesitar una cadena de conexión o Connection String. Es altamente probable que una cadena de conexión cambie, por ej.: porque cambia el ambiente en el cual se está ejecutando la aplicación (desarrollo, testing, producción, etc.) o porque cambiaron las credenciales de acceso a una BD.

Es por esto que debemos evitar definir las cadenas de conexión dentro del código (hard-coded), ya que para cambiarlas vamos a tener que: modificar el código, compilar e instalar la nueva versión de la aplicación. Lo recomendable entonces, es definirlas en algún medio en el cual puedan ser modificadas de manera fácil, rápida y sin afectar la aplicación.

Siguiendo esta recomendación, el medio recomendado para las cadenas de conexión es el archivo de configuración de la aplicación (web.config o app.config, según el tipo de aplicación). Allí contamos con la sección dedicada al respecto <connectionStrings>.

Modo ineficaz:

string connectionString = "Server=.;Database=Ejemplo;Integrated Security=True";
using (var conn = new SqlConnection(connectionString))
{
    //  ...
}

Modo eficiente:

<connectionStrings>
  <add name="EjemploBD" 
        connectionString="Server=.;Database=Ejemplo;Integrated Security=True" 
        providerName="System.Data.SqlClient" />
</connectionStrings>
	

string connectionString = ConfigurationManager.ConnectionStrings["EjemploBD"].ConnectionString;
using (var conn = new SqlConnection(connectionString))
{
    // ...
}

Opcionalmente, también contamos con la funcionalidad de encriptar todas las cadenas de conexión del archivo de configuración para una mayor seguridad. Mas info al respecto acá.

En una sección posterior de este artículo también veremos una interesante utilidad que le podemos dar al dato providerName

NO usar “conexiones estáticas”

En muchas oportunidades encontré proyectos que usan una variable estática para guardar el objeto de conexión a la BD. Probablemente la intención de esto es evitar abrir nuevas conexiones para cada operación que se desea realizar, ya que es un proceso costoso (tanto en tiempo como recursos).

Guardar la conexión en una variable estática implica que ésta será única para toda la aplicación. Lo cual sería completamente aceptable si la aplicación utilizara un único hilo de ejecución y fuera usado por un único usuario, sin ningún tipo de concurrencia. Aunque difícilmente una aplicación tendrá dichas características…

Pongamos como ejemplo una aplicación web que usa una “conexión estática”: cada usuario que acceda al sitio será atendido usando un nuevo hilo de ejecución del proceso que ejecuta la aplicación, imaginemos que un primer usuario accederá a una página que utilizará la conexión para obtener una lista de registros a mostrar, y ahora imaginemos que un segundo usuario accede en ese preciso instante a otra página de la aplicación la cual también intentará utilizar la conexión para obtener otra lista de registros a mostrar, pero lamentablemente se encontrará con que la conexión no está disponible y verá un “hermoso” mensaje de error en pantalla… Sólo por mencionar uno de los tantos escenarios de error posibles.

En definitiva: “conexión estática” = muy mala práctica.

Entonces, ¿cómo abrir una conexión sin problemas y de la manera mas performante? La respuesta es simple:

  1. Crear una conexión.
  2. Abrir la conexión.
  3. Usar la conexión para realizar las operaciones necesarias.
  4. Cerrar la conexión.
  5. Liberar los recursos.

Pero, ¿no es acaso costoso abrir una nueva conexión? Si. Pero ADO.NET emplea un proceso llamado Connection Pooling o agrupación de conexiones para resolver este problema. Para explicarlo de manera resumida y muy simplificada, se encarga de llevar una lista de las últimas conexiones abiertas y cada vez que se intente abrir una nueva conexión busca en dicha lista alguna similar para reutilizarla. Todo esto sucede mágicamente bajo nuestras narices, y por lo tanto podemos despreocuparnos del tema.

Pueden ver mas info al respecto acá.

Liberar los recursos luego de utilizarlos

Este aspecto no es algo propio del ADO.NET, sino más bien de todo el Framework .NET y que siempre debemos tenerlo en cuenta en el desarrollo de nuestras aplicaciones.

Haciendo un brevísimo y simplificado resúmen podemos decir lo siguiente:
Existen objetos que internamente utilizan ciertos recursos que deben ser liberados luego de ser utilizados (entre ellos, las conexiones a bases de datos). Estos objetos implementan la interfaz IDisposable, que publica un único método Dispose, el cual se encarga de liberar dichos recursos. Quien utiliza estos objetos tiene la responsabilidad de llamar siempre al método Dispose una vez que haya finalizado con su utilización. No liberar estos recursos puede deteriorar la performance de la aplicación, y en algunos casos extremos incluso puede causar la caída del servidor (al agotar el procesamiento/memoria). La manera más conveniente de asegurar la llamada al método Dispose de un objeto es mediante el uso del bloque using. Este bloque permite declarar cualquier objeto que implemente la interfaz Dispose, dentro del cual estará disponible para ser utilizado dicho objeto, y se encargará de llamar automáticamente al método Dispose cuando se finalice la ejecución de todas las instrucciones que contiene, incluso si se produce un error.

Pueden encontrar más info acá.

Volviendo a ADO.NET, debemos asegurarnos de liberar todos los recursos de los objetos luego de utilizarlos. Podemos determinar fácilmente cuales son estos objetos verificando si implementan la interfaz IDisposable.

Modo ineficaz:

var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["EjemploBD"].ConnectionString);
conn.Open();
            
var command = new SqlCommand("SELECT * FROM Usuarios", conn);
var reader = command.ExecuteReader();
while (reader.Read())
{
    // ...
}

//  Si se produce un error antes de llegar a esta línea, "reader" no se cierra ni libera correctamente sus recursos
reader.Close();
reader.Dispose();

//  Si se produce un error antes de llegar a esta línea, "command" no libera correctamente sus recursos
command.Dispose();

//  Si se produce un error antes de llegar a esta línea, "conn" no se cierra ni libera correctamente sus recursos
conn.Close();
conn.Dispose();

Modo eficiente:

//  Este bloque using cierra y libera automáticamente los recursos utilizados por "conn"
using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["EjemploBD"].ConnectionString))
{
    conn.Open();

    //  Este bloque using libera automáticamente los recursos utilizados por "command"
    using (var command = new SqlCommand("SELECT * FROM Usuarios", conn))
                
    //  Este bloque using cierra y libera automáticamente los recursos utilizados por "reader"
    using (var reader = command.ExecuteReader()) 
    {
        while (reader.Read())
        {
            // ...
        }
    }
}

Uso de parámetros

En la mayoría de los casos, definimos la consulta o comando a ejecutar de manera dinámica, en base a datos ingresados/seleccionados por el usuario o según las reglas de negocio definidas. En estos casos, hay quienes directamente definen en una cadena de texto la consulta, concatenando directamente los datos ingresados por el usuario formateando (cuando corresponda) textos, números, fechas, etc.

Por ejemplo:

string cmdTexto = String.Format("SELECT * FROM Usuarios WHERE Login = '{0}'", login);

using (var command = new SqlCommand(cmdTexto, conn))
{
    //  ...
}

Esta metodología tiene muchas falencias, pero las dos mas importantes a destacar son:

  1. Presenta un grave riesgo de seguridad, frente a la técnica SQL Injection.
  2. Necesita una “conversión manual” de tipos de datos al armar la consulta SQL. Lo cual es “trabajoso” y muy propenso a error.

La manera mas eficiente y segura de especificar los parámetros a utilizar en una consulta es mediante el uso del objeto ADO.NET que representa un parámetro. Podemos crearlo indicando solamente su nombre y su valor, para finalmente agregarlo a la lista de parámetros del comando. De esta manera, tanto la consulta SQL como la lista de parámetros serán enviados al orígen de datos sin que nosotros tengamos que hacer ninguna verificación de seguridad ni conversión de datos alguna.

Modo ineficaz e inseguro:

var cmdTexto = String.Format(
    "INSERT INTO Usuarios (Login, EstaActivo, FechaCreacion) VALUES ('{0}', {1}, '{2})",
    login, estaActivo ? 1 : 0, fechaCreacion.ToString("yyyy-MM-dd HH:mm:ss"));

using (var command = new SqlCommand(cmdTexto, conn))
{
    int filasAfectadas = command.ExecuteNonQuery();
}

Modo eficiente:

var cmdTexto = "INSERT INTO Usuarios (Login, EstaActivo, FechaCreacion) VALUES (@Login, @EstaActivo, @FechaCreacion)";

using (var command = new SqlCommand(cmdTexto, conn))
{
    command.Parameters.Add(new SqlParameter("@Login", login));
    command.Parameters.Add(new SqlParameter("@EstaActivo", estaActivo));
    command.Parameters.Add(new SqlParameter("@FechaCreacion", fechaCreacion));

    int filasAfectadas = command.ExecuteNonQuery();
}

Manejo de transacciones

Las clases ADO.NET tienen las clases y métodos necesarios para gestionar transacciones. Uno debe utilizar el método BeginTransaction en el objeto de la conexión, el cual inicia una transacción y devuelve un nuevo objeto que la representa. Este objeto debe ser asignado en los comandos que se desean ejecutar dentro del contexto de dicha transacción, y finalmente, debe ser aplicada (commit) o deshechada (rollback).

Sin embargo, existe una manera más sencilla e incluso más potente de manejar transacciones, mediante el uso de una clase del Framework llamada System.Transactions.TransactionScope. Para poder utilizar esta clase será necesario agregar una referencia a la librería System.Transactions del Framework.

En su uso más básico, simplemente basta definir un bloque en el cual se crea una instancia de la clase TransactionScope, y automáticamente todas las operaciones siguientes se ejecutarán dentro del contexto de una transacción. Todo esto se gestionará de manera automática y transparente para nosotros. Lo único que debemos hacer es llamar al método Complete antes de finalizar el bloque para aplicar todas las operaciones realizadas, de lo contrario se desharán todos los cambios.

También cuenta con la capacidad de decidir si es necesario crear una transacción distribuída, en caso de haber realizado operaciones que afecten a más de un orígen de datos, y lo hace también de manera automática. Esto se hace sólo si es estrictamente necesario, eficientizando al máximo el procesamiento.

Pueden encontrar más info acá.

Un ejemplo:

using (var scope = new TransactionScope())
{
    using (var conn1 = new System.Data.SqlClient.SqlConnection(connString))
    {
        conn1.Open();

        using (var cmd1 = conn1.CreateCommand())
        {
            cmd1.CommandText = commandText1;
            cmd1.ExecuteNonQuery();
        }
    }

    using (var conn2 = new System.Data.SqlClient.SqlConnection(connString))
    {
        conn2.Open();

        using (var cmd2 = conn2.CreateCommand())
        {
            cmd2.CommandText = commandText2;
            cmd2.ExecuteNonQuery();
        }
    }

    scope.Complete();
}

Uso de herramientas de mapeo de objetos a datos (ORM’s)

En mi opinión personal, una de las mejores alternativas es utilizar algún ORM que se encargue de resolver el acceso a datos, permitiéndonos enfocar pura y exclusivamente en la función principal de nuestra aplicación, brindando así valor agregado constantemente.

Estas herramientas también suelen ser la mejor opción ya que, al ser precisamente su principal función resolver el acceso a datos, lograrán una gran madurez y performance que probablemente nos tomaría a nosotros mucho tiempo.

EntityFramework es el ORM que recomiendo usar en las aplicaciones .NET.

Pueden encontrar más info acá.

Anexo: Acceso a datos único para cualquier tipo de BD.

Generalmente, al crear nuestras aplicaciones ya decidimos cual será el tipo de BD que usaremos: SQL Server, MySQL, Oracle, etc. Y podemos crear nuestra lógica de acceso a datos utilizando las clases ADO.NET correspondientes.

Sin embargo, existen aplicaciones que usan más de un tipo de BD. O quizás exista una alta probabilidad de migrar a otro tipo en alguna etapa avanzada del desarrollo.

Gracias a los patrones de diseño aplicados al diseñar las clases de ADO.NET, es muy fácil crear lógica de acceso a datos reutilizable para cualquier tipo de BD. Para ello es necesario hacer uso de las clases bases, de las cuales todos los Providers o proveedores de ADO.NET deben heredar.

Un ejemplo de cómo sería ese código sería algo así:

var dbFactory = DbProviderFactories.GetFactory(providerName);
if (dbFactory == null)
    throw new ArgumentException("providerName");

using (var conn = dbFactory.CreateConnection())
{
    conn.ConnectionString = connectionString;
    conn.Open();

    using (var cmd = conn.CreateCommand())
    {
        cmd.CommandText = cmdText;

        //  Se pueden agregar parámetros
        var p1 = cmd.CreateParameter();
        p1.ParameterName = "@Parametro1";
        p1.Value = new DateTime(2014, 2, 24);
        cmd.Parameters.Add(p1);

        //  Ejecutar comando con la operación necesaria
        int filasAfectadas = cmd.ExecuteNonQuery();

        object valor = cmd.ExecuteScalar();

        //  Abrir un DataReader para recorrer la lista de registros obtenidos por el comando
        using (var reader = cmd.ExecuteReader())
        {
            while (reader.Read())
            {
                //  hacer lo necesario con cada registro
            }
            reader.Close();
        }
    }
}

En el código pueden ver las variables:

  • providerName. Nombre del proveedor ADO.NET a utilizar. Ejemplos:
    "System.Data.SqlClient"
    "System.Data.Odbc"
  • connectionString. Cadena de conexión a utilizar. Ejemplos:
    "Server=(localhost);Database=Sistema;Integrated Security=True"
    "Server=(localhost);Database=Sistema;User ID=sa;Password=*****"
  • cmdText. Texto de la sentencia SQL a ejecutar. Ejemplos:
    "SELECT [UsuarioId], [NombreUsuario], [EstaActivo] FROM [Usuarios]"
    "UPDATE [Usuarios] SET [EstaActivo] = @EstaActivo WHERE [UsuarioId] = @UsuarioId"

Este era el uso interesante del dato providerName que les había mencionado anteriormente.

Pueden encontrar el código completo de acceso a datos genérico acá.

organizando-carpetas-proyectos

Organizando el código de las Aplicaciones .NET

La creación de cada nueva aplicación/sistema nos enfrenta a la misma situación: Proyectos vacíos (o casi vacíos) a los cuales debemos ir agregándoles archivos de manera incremental con el nuevo código. Aunque algunos opten por tener ciertos templates sobre los cuales iniciar, la situación es relativamente la misma.

organizando-solucion-vacia

Dejando de lado aplicaciones pequeñas o de corto uso, las cuales probablemente vayan a contener poco código cuya mantenibilidad no es una preocupación, es fácil ver cómo el número de archivos en cada Proyecto aumenta rápida y preocupantemente…

A lo largo de los años fui intentando resolver estos problemas realizando pequeños cambios en la organización del código. La idea de este artículo es compartir algunos de los que considero mas significativos y que me han sido de gran ayuda.

Cabe aclarar que no van a encontrar recetas mágicas, ni trucos increíbles… al contrario, en su mayoría son cosas muy simples, básicas y fáciles de realizar pero si resultan muy útiles.

Diseño en capas

Existe muchas maneras de diseñar aplicaciones en capas, todas tienen sus pros y contras. No voy a abordar este tema de manera completa, ya que en sí mismo merece un artículo completo (o incluso más de uno). Pero si quiero destacar la importancia de utilizar alguna de las estrategias de diseño en capas, ya que considero que es la manera más importante de lograr una buena organización. Y no solo a nivel “archivos” sino más bien a nivel “conceptual/lógico”.

Nombres de Proyectos

Respetar ciertas reglas para darle nombres a los Proyectos. Cada nombre esta compuesto por varias partes separadas por un punto, donde la primera parte siempre corresponde al nombre de la aplicación y las partes siguientes siguen un criterio de organización según su propósito.

organizando-nomenclatura-proyectos

Usamos entonces lo que se conoce como nombres completamente calificados que, además de darnos un poco más de entendimiento acerca del propósito del Proyecto, evita ambigüedades o colisiones de nombres con otros.

Carpetas en la Solución

Organizar los Proyectos que comparten un mismo o similar propósito mediante carpetas.

organizando-carpetas-proyectos

La utilidad de esto crece a medida que van apareciendo nuevos Proyectos que comparten los mismos propósitos. Por ejemplo, si aparecen nuevos sitios web, estos se incorporarán en la carpeta de Presentación; o si aparecen nuevos tipos de acceso a datos se incorporarán en la carpeta Datos.

Carpetas en los Proyectos / Espacios de nombres (Namespaces)

Organizar los archivos dentro de cada Proyecto en carpetas respetando criterios de agrupación conceptual o lógica.

En las primeras etapas de desarrollo, los Proyectos suelen contener pocos archivos, pero con el paso del tiempo el número de archivos crece significativamente. El problema es que, generalmente, esos archivos se agregan sin aplicar ningún tipo de organización, como se muestra a continuación:

organizando-namespaces-1

Y se puede poner peor… Pero creo que ya se entiende el punto.

Aplicando el mismo criterio de clasificación y agrupamiento de los Proyectos, podemos organizar mucho mejor nuestros archivos dentro de cada Proyecto, mediante el uso de carpetas. Esto también se traducirá en la definición automática de espacios de nombres (namespaces) para las clases, interfaces, etc. declaradas en cada archivo.

Siguiendo esta pauta podemos lograr el siguiente resultado:

organizando-namespaces-2

Vemos que no solo organizamos mejor los archivos, sino que también mejoramos el nombre completamente calificado de cada clase, interfaz, etc. y la experiencia al codificar con el Intellisense:

organizando-namespaces-3
organizando-namespaces-4

Un archivo para un fin

Usar un archivo para que contenga el código de una única clase, interfaz, enumeración, etc.

Esto generalmente se suele respetar, pero también he visto muchos casos en los que se definen varias cosas en un único archivo de código. En definitiva, el resultado final al compilar el proyecto, será siempre el mismo. Pero se trata mas bien de una cuestión de organización y limpieza.

Muchas veces uno puede verse tentado a usar un único archivo para definir varias cosas que se encuentran relacionadas, para no tener que crear tantos archivos y/o para encontrarlas a todas fácilmente en un único lugar. Pero cuando uno debe trabajar en un equipo de trabajo, es común que suceda que dos miembros del equipo necesiten modificar clases que resultan estar en un mismo archivo, y en ese contexto pueden surgir varios conflictos. O también suele suceder que no todos pueden encontrar fácilmente la ubicación del código de una determinada clase en los archivos. El utilizar un archivo para un único fin, resuelve este tipo de problemas.

Utilizando una buena estrategia de carpetas en los Proyectos, el número de archivos deja de ser un problema grave.

Uso de regiones dentro de clases

Usar regiones para organizar mejor el código dentro de una clase. Estas permiten definir bloques o secciones que pueden ser colapsadas o expandidas, para mejorar la legibilidad.

Personalmente, me resulta bastante útil utilizarlas para definir las secciones de: constantes, atributos, constructores, propiedades y métodos. Incluso en algunos casos me ha servido agregar regiones anidadas según los modificadores de acceso, por ej.: dentro de la región Métodos, tener una región para aquellos que son públicos, otra para internos y otra para privados.

organizando-regiones

Si tienen más pautas de este estilo, no duden en compartirlas en los comentarios!

multiples-archivos

Subir múltiples archivos en ASP.NET 4.5

ASP.NET Web Forms nos ofrece un control que nos permite subir archivos. Este control es nuestro conocido, viejo y querido FileUpload.

Su uso es simple:

  • Para el usuario: se muestra un botón que abre una ventana que permite seleccionar un archivo de nuestro equipo (o red).
  • Para el desarrollador: luego de realizar un PostBack, se tendrá acceso al archivo seleccionado por el usuario mediante el uso de propiedades y métodos del FileUpload.

Como dicen:

Lo simple y bueno, es doblemente bueno.

Pero… como es costumbre, siempre necesitamos más! En este caso, más archivos.

Es muy común que en algunos escenarios necesitemos subir varios archivos a la vez, y ahi es donde recurrimos a diferentes opciones:

  • Controles/librerías desarrolladas por otros (comerciales/gratuitas).
  • Agregar controles FileUpload dinámicamente en nuestra página conforme se necesitan. Esto sería, en tiempo de ejecución.
  • Otras…

Afortunadamente, la versión 4.5 del Framework nos trajo un feature poco mencionado pero realmente útil, y es justamente la incorporación del atributo AllowMultiple en el control FileUpload. Este atributo nos permite hacer uso de la nueva funcionalidad disponible en HTML5 para seleccionar varios archivos a la vez.

Es importante prestar atención a la frase

[…] funcionalidad disponible en HTML5 […]

ya que si el browser NO soporta HTML5, el usuario sólo podrá seleccionar un archivo a la vez.

Su uso es tan simple como al que estamos acostumbrados:

  • Para el usuario: se muestra un botón que abre una ventana que permite seleccionar varios archivos de nuestro equipo (o red).
  • Para el desarrollador: luego de realizar un PostBack, se tendrá acceso a el/los archivo/s seleccionado/s por el usuario mediante el uso de la propiedad PostedFiles del control.

Ejemplo

ASPX

<asp:FileUpload runat="server" ID="fuArchivos" AllowMultiple="true" />
<asp:Button runat="server" ID="btnSubir" Text="Subir" OnClick="btnSubir_Click" />

Code-Behind (C#)

protected void btnSubir_Click(object sender, EventArgs e)
{
	foreach (var archivo in fuArchivos.PostedFiles)
	{
		// Tenemos acceso al archivo para realizar lo que necesitemos
	}
}	

Leer información del archivo

var informacionDelArchivo = String.Format("{0} es de tipo {1} y tiene un tamaño de {2} KB.", archivo.FileName, archivo.ContentType, archivo.ContentLength / 1024);

Guardar en el disco del servidor (o en red)

var pathCarpetaDestino = System.IO.Path.Combine(Server.MapPath("~/"), "archivos-subidos");
var carpetaDestino = new System.IO.DirectoryInfo(pathCarpetaDestino);
if (!carpetaDestino.Exists)
	carpetaDestino.Create();

var nombreArchivo = System.IO.Path.GetFileName(archivo.FileName);
var pathArchivoDestino = System.IO.Path.Combine(pathCarpetaDestino, nombreArchivo);
archivo.SaveAs(pathArchivoDestino);

Leer contenido del archivo como texto

using (var reader = new System.IO.StreamReader(archivo.InputStream))
{
	var contenidoTexto = reader.ReadToEnd();
}

Leer bytes del archivo

var len = archivo.ContentLength;
var bytes = new byte[len];
archivo.InputStream.Read(bytes, 0, len);

BONUS

Una funcionalidad adicional que sólo algunos browsers nos dan, es la capacidad de arrastrar el/los archivo/s que queremos seleccionar directamente dentro del control.

Metadata BD

Obtener metadata de una BD mediante ADO.NET

En ADO.NET tenemos una funcionalidad muy interesante que no está muy “publicitada”, y es la funcionalidad que nos permite obtener el esquema o metadata de una BD, es decir, la definición de la estructura de una BD (tablas, vistas, stored procedures, etc).

Básicamente, existe un método llamado GetSchema que está definido en la clase DbConnection que nos devolverá toda (o caaaaasi toda) la información de la BD que deseemos.

Es importante mencionar que al ser un método definido en la clase DbConnection (que es la clase base para las conexiones en ADO.NET) todos los proveedores (SQL Server, Oracle, MySQL, etc) DEBEN implementarlo. Con lo cual, podemos asumir que tenemos esta funcionalidad disponible independientemente del tipo de BD que estemos utilizando. Aunque también es posible que alguno de los proveedores haya decidido no implementar esa funcionalidad, en ese caso, se arrojará una Excepción cuando intentemos utilizarla.

El uso de esta funcionalidad es tan sencillo como se muestra en el siguiente método:

public DataTable ObtenerDatos(string connectionString, 
	string nombreColeccion, string[] restricciones)
{
	using (var sqlConnection = new SqlConnection(connectionString))
	{
		sqlConnection.Open();

		try
		{
			return sqlConnection.GetSchema(nombreColeccion, restricciones);
		}
		finally
		{
			sqlConnection.Close();
		}
	}
}

Donde nombreColeccion corresponde al nombre del tipo de información que estamos intentando obtener (tablas, columnas, vistas, indices, stored procedures, etc), y restricciones es una lista de valores que se utilizarán para filtrar la búsqueda dentro de la colección especificada.

Con respecto a las colecciones, es importante saber lo siguiente:

  • Cada proveedor soporta un subconjunto de las colecciones de la lista (pueden ser todas, o solo algunas).
  • En el caso de SQL Server, por ejemplo, existe un objeto que enumera las colecciones soportadas llamado System.Data.SqlClient.SqlClientMetaDataCollectionNames.

Con respecto al parámetro de restricciones, es importante saber lo siguiente:

  • Es opcional.
  • Cada colección tiene una lista disponible de restricciones. Para ver las listas completas pueden entrar acá.
  • El parámetro de restricciones debe ser una lista con los valores por los cuales queremos realizar el filtro, y dichos valores deben estar en las posiciones que les corresponden según la lista de restricciones (enumerada en el punto anterior).

Hice una aplicación web como ejemplo del uso de esta funcionalidad para BD de SQL Server. Consta de dos páginas:

  1. Se ingresa una Connection String y una colección. Se muestran todos los datos de dicha colección.
  2. Se ingresa una Connection String, un esquema (opcional) y un nombre de tabla. Se muestran todas las columnas de la tabla.

Metadata BD - ColeccionesMetadata BD - Tablas

Pueden ver el código acá.

Para ver la documentación completa de esta funcionalidad provista por Microsoft pueden entrar acá.

upgrade-cs-2

Actualizar versión del SDK de Windows Azure para .NET en proyecto CloudService

Si trabajás con Windows Azure seguramente ya estás acostumbrado a tener que actualizar SDK’s, herramientas y librerías para mantenerte actualizado con las últimas novedades. Y realmente, después de varias actualizaciones el asunto puede volverse bastante “sucio”. Yo actualmente tengo instaladas 3 versiones distintas del SDK de Windows Azure para .NET:

upgrade-cs-4

Recientemente, tuve que realizar unas modificaciones en una aplicación que contenía un proyecto de tipo CloudService. Después de haber hecho los cambios, ejecuté el CloudService para hacer el Debug y me saltó el siguiente mensaje de error:

upgrade-cs-2

El enorme grado de detalle del mensaje de error me forzó a tener que abrir la ventana de Output para tratar de entender cual era realmente el problema y por suerte ahí encontré un poco mas de información:

Windows Azure Tools: Error: The installed Windows Azure Emulator does not support the role binaries. Please install the latest Windows Azure Emulator and try again.
Windows Azure Tools: Warning: The role binaries in your package are using an older version of the Windows Azure SDK. Please consider upgrading.
Windows Azure Tools: The system is missing a prerequisite to execute the service. Please see the release notes.

En ese momento me di cuenta de cual era el problema: instalé una nueva versión del SDK de Windows Azure y el proyecto que estaba intentando abrir había sido creado usando una versión anterior. Excelente! Pero… ¿cómo lo corrijo?

Me acordaba que en ocasiones anteriores me había pasado algo similar: había intentado abrir una solución que contenía un CloudService y me apareció un mensaje de advertencia indicándome que no tenía instalado el SDK de Windows Azure y me había dado las opciones de bajarlo o de cambiar la versión del SDK del proyecto. Por suerte pude conseguir una captura del mensaje:

upgrade-cs-1

Aquella vez tuve suerte y seleccionando la opción para bajar el SDK, solucioné el problema al instante. Pero ahora, la historia es diferente, ya no es una advertencia al abrir la solución sino que es un mensaje de error al intentar hacer el Debug del proyecto, y sin ninguna ayuda de como arreglarlo… ¿Por qué esta vez es diferente?

Resulta ser que la diferencia está en que ésta vez, la versión del SDK que utiliza el CloudService sí está instalada, pero existe una versión más actualizada instalada, y al parecer la nueva versión reemplazó “algo” de la versión anterior dejándola inutilizable.

Afortunadamente, la manera de solucionar este problema resultó ser bastante simple:

  1. Abrir normalmente la solución que contiene el/los proyecto/s de tipo CloudService.
  2. Clic derecho en el/los proyecto/s de tipo CloudService y seleccionar la opción Properties.
  3. Clic en la pestaña Application. Allí debería de aparecer un mensaje como el de la imagen que muestro debajo.
  4. Clic en el botón “Upgrade…”. El VS realizará una actualización del proyecto (en algunos casos aparecerá un Wizard para realizar la actualización, preguntando si se desea hacer un backup, etc).
  5. Listo!

upgrade-cs-3

Después de la actualización, el mensaje que aparecía en la pestaña Application debería haber desaparecido y debería aparecer simplemente el nombre de la versión del SDK de Windows Azure. Y obviamente, el proyecto debería compilarse y ejecutarse sin ningún error.

NOTA: Igualmente, recomiendo verificar que la actualización no haya roto nada en nuestro proyecto y/o solución.

mantener-posicion-1

Mantener la posición luego de PostBack en una página ASP.NET WebForms

En ocasiones hacemos páginas en las cuales hay tanto contenido que quien las visita se ve obligado a moverse verticalmente usando el scroll para poder visualizar todo. Esto es completamente normal y aceptable cuando se trata de algún artículo o texto de lectura, pero cuando se trata de alguna página de ingreso de datos o que necesita algún tipo de interacción con el usuario, ese tipo de navegación no es muy “amigable”.

Cuando creamos páginas de este tipo en una aplicación ASP.NET WebForms, tenemos que tener en cuenta que cada vez que el usuario haga clic en algun botón o interactúe con algún otro control que dispare un evento, la página realizará un PostBack. El problema en estos casos, es que si el usuario se encontraba en la parte inferior del contenido de la página, habiendo hecho scroll hacia abajo, luego del PostBack la página volverá a la posición superior. Claramente, es una experiencia muy disruptiva y en algunos casos incluso hará que el usuario no entienda lo que pasó.

Podríamos tener por ejemplo la siguiente página:

mantener-posicion-1
mantener-posicion-2
mantener-posicion-3

Afortunadamente, existe una manera de evitar este comportamiento. Básicamente, lo que hacemos es indicarle a las páginas que luego de realizar un PostBack deben volver a la posición en la cual se encontraban anteriormente. Y existen diferentes maneras de hacerlo.

Para toda la aplicación

Si queremos establecer este comportamiento para todas las páginas de nuestra aplicación, debemos agregar la siguiente línea en el archivo web.config:

<configuration>
	...
	<system.web>
		...
		<pages maintainScrollPositionOnPostBack="true">
		...
	</system.web>
	...
</configuration>
</pages>

Aunque suene tentador activar este comportamiento de esta manera en todas nuestras aplicaciones web, por razones de simplicidad, tenemos que entender sus implicaciones. Esta funcionalidad utiliza un par de campos hidden para guardar las coordenadas de la posición y realiza una serie de operaciones mediante JavaScript SIEMPRE antes y luego de cada PostBack, en el primer caso para obtener los valores de posición actual y guardarlos en los campos, y en el segundo caso para leer la posición de los campos y mover la página hasta dicha posición.

Para una única página

Tenemos dos maneras de activar este comportamiento en una página. La primera es estableciendo una propiedad en la directiva de la página (ASPX):

<%@ Page ... MaintainScrollPositionOnPostback="true" %>

Y la segunda es en el Code-Behind, estableciendo la propiedad MaintainScrollPositionOnPostback de la página en el método Page_Load:

protected void Page_Load(object sender, EventArgs e)
{
	...
	MaintainScrollPositionOnPostBack = true;
	...
}

excel

Trabajando con Excel en .NET

Los archivos Excel son claramente uno de los formatos mas utilizados para la recopilación e intercambio de datos. Incluso, no es raro encontrar empresas pequeñas (y algunas no tan pequeñas) que utilizan uno o más archivos Excel como su sistema informático completo para la gestión de stock, precios, pedidos, etc. Todo esto principalmente gracias a su facilidad de uso y gran potencia para la ejecución de cálculos, capacidades de ordenamiento y filtro, creación de gráficos, etc.

Gracias a su gran popularidad, siempre ha sido una funcionalidad muy solicitada por los usuarios de variados sistemas informáticos, la capacidad de leer y crear archivos Excel.

Las alternativas (no tan recomendadas)

Para los que utilizamos .NET, existen y han existido muchas maneras de lograr trabajar con los archivos Excel, entre ellas las que más he visto (y usado) son:

  • Referenciando las DLL’s propias de Microsoft Excel.
  • Usando las clases de acceso a datos de .NET de OLE DB y ODBC.
  • Creando archivos de texto planos con ciertos formatos (tablas HTML, CSV) y guardándolos con la extensión de los archivos Excel.

Todos estos métodos tienen sus pros, contras, capacidades y limitaciones. Voy a intentar una breve reseña de cada uno:

Referenciando las DLL’s propias de Microsoft Excel

Utilizar las DLL’s propias de Microsoft Excel nos permiten trabajar respetando nuestra programación orientada a objetos, brindándonos un set de objetos que expone todas las capacidades de los archivos Excel y con el mismo lenguaje con el que trabajamos en la aplicación Microsoft Excel. Este enfoque tiene varios inconvenientes, entre ellos los más fuertes que se me ocurren son que para poder utilizar estas DLL’s necesitamos tener instalado el Microsoft Excel en el servidor en donde nuestra aplicación se esté ejecutando, necesitamos trabajar con archivos Excel persistentes en disco, y quizás el más peligroso es el hecho de que al usar estos objetos, debajo de nuestras narices, se están utilizando componentes de Microsoft Excel instalados en el servidor (como por ejemplo, se abren procesos del tipo excel.exe). Ésto último puede provocar problemas muy serios en la performance del servidor y, si es que no se liberan correctamente los recursos, se puede terminar con 200 procesos abiertos haciendo que el servidor finalmente “muera”.

Usando las clases de acceso a datos de .NET de OLE DB y ODBC

Con este enfoque se considera al archivo de Excel como si fuera una BD relacional. Primero necesitamos utilizar una ConnectionString para acceder al archivo, la cual va a variar dependiendo de muchos factores, entre ellos: la versión del archivo Excel, si en la primera fila se encuentran los encabezados de los datos de cada hoja, si se desea inferir el tipo de dato de una determinada columna a partir de los valores encontrados en las primeras filas, etc.

En mi opinión completamente personal, el principal problema que tiene este enfoque es la confusión que supone considerar un archivo Excel como una BD relacional, lo cual se encuentra muy alejado de la realidad. Además del hecho de que no se permite un acceso multi-usuario, ni existen tablas ni integridad referencial, y mucho menos podemos esperar que las operaciones que realicemos sean transaccionales; el tener que utilizar sentencias SQL para acceder/modificar/eliminar el contenido de una hoja me resulta extremadamente anti-natural. Y obviamente, este enfoque es útil y sencillo de usar sólo cuando el contenido del Excel se reduce a 1 o más hojas con datos tabulados. Si pensamos en un escenario donde tenemos que utilizar fórmulas, crear gráficos o alterar el formato visual, ya quedamos fuera de juego.

A esto también le sumamos la restricción de que el archivo se debe encontrar persistente en disco y, por otro lado, el hecho de que en algunos casos necesitaremos que el servidor tenga instalado un determinado motor de procesamiento para funcionar (esto va a depender de la manera en la cual se establece la ConnectionString), el cual quizás no sea posible instalar y/o sea complicado de hacer.

Creando archivos de texto planos con extensión de Excel

Este enfoque es uno de los más livianos y fáciles de utilizar con respecto a los que mencioné anteriormente. No necesitamos tener un archivo persistente en disco ni necesitamos ningún componente instalado. Simplemente, generamos texto plano con un determinado formato y lo guardamos (o renderizamos en una página web) con el tipo y extensión de un archivo Excel. Pero obviamente debemos saber que el resultado final NO es un archivo Excel per se, más bien podríamos decir que es un archivo de texto disfrazado.

Las limitaciones obvias de este enfoque es que no tenemos acceso a toda la verdadera potencia de los archivos Excel y que la experiencia de desarrollo no será para nada amigable, ya que se tratará solamente de interpretar y manipular cadenas de texto.

Una luz al final del túnel

Afortunadamente para todos nosotros, a partir de la versión 2007 del paquete Office, Microsoft implementó un nuevo formato para todos sus archivos (Word, Excel, PowerPoint, etc) llamado Office Open XML. Este formato fué creado por Microsoft y básicamente se trata de que cada archivo es realmente un archivo ZIP que posee una estructura de carpetas, archivos XML y algunos otros (pocos) archivos de otros tipos, los cuales definen el contenido completo del documento. Los invito a buscar cualquier documento de este tipo y le cambien su extensión a .zip y posteriormente lo abran, para que puedan verlo con sus propios ojos. Pueden encontrar más info acá.

La implementación de este nuevo formato hizo que podamos tener acceso a TODO el contenido de cualquiera de estos documentos, sólo contando con las capacidades de manejo de archivos .zip e interpretación de XML. Obviamente no tardó mucho tiempo para que aparecieran librerías que nos permitieran trabajar con estos archivos muy fácilmente (salvando así nuestra sanidad mental).

La propuesta recomendada: EPPlus

Justamente, EPPlus es una librería de .NET que nos da acceso a TODO el potencial de los archivos Excel de una manera extremadamente simple, 100% orientado a objetos, sin restricciones de uso de archivos persistentes en disco, performante y completamente gratuita. ¿Qué más se puede pedir?

Para incorporar esta librería a nuestros proyectos sólo necesitamos bajar los binarios desde su sitio y referenciarlos, o simplemente agregamos el NuGet Package a nuestro proyecto desde el mismo Visual Studio.

En cuanto a cómo usar esta librería, se podría decir que lo más importante que debemos saber es: Excel. Si entendemos claramente los conceptos que lo conforman y sus capacidades, el uso de la librería será completamente intuitivo. Si aún así, necesitamos documentación, en la sección de descargas de su sitio encontraremos un archivo de ayuda completo y una solución VS llena de ejemplos de todo tipo.

Les dejo a continuación una lista de lo que soporta (a día de hoy) esta librería:

  • Rangos y Celdas
  • Formatos y estilos (Bordes, Colores, Rellenos, Fuentes, Números, Alineación)
  • Gráficos
  • Imágenes
  • Formas
  • Comentarios
  • Tablas
  • Protección
  • Encripción
  • Tablas Pivot
  • Validación de datos
  • Formatos condicionales
  • VBA
  • Y mucho mas…

Les recomiendo que descarguen la solución VS de ejemplos para ver la gran potencia de esta librería en más de 20 casos que van desde lo más básico hasta cosas avanzadas realmente interesantes.

Revisando el uso básico y creando un lector

Para tener acceso a un archivo Excel usando la librería EPPlus, necesitamos el siguiente bloque básico:

//	Podemos usar también un MemoryStream y cargar en él los bytes del archivo 
//	(en caso de que el archivo no exista en disco)
using (var stream = System.IO.File.OpenRead(@"Archivo.xlsx"))
{
	using (var package = new OfficeOpenXml.ExcelPackage(stream))
	{
		//  Ya tenemos acceso completo al archivo Excel!
		//	De acá en más, sólo resta revisar las propiedades y métodos existentes de "package" 
		//	y todo el grafo de objetos relacionados para lograr lo que necesitemos.
		
		//	Sólo por poner algunos ejemplos muy básicos.

		//  Libro
		var workbook = package.Workbook;

		//  Hojas
		var hojas = workbook.Worksheets;

		//  Hoja usuario
		var hojaUsuarios = workbook.Worksheets["Usuarios"];

		//  Celda A2
		var celdaA2 = hojaUsuarios.Cells["A2"];
		celdaA2 = hojaUsuarios.Cells[2, 1];

		//  Rango A2:D10
		var rangoA2D10 = hojaUsuarios.Cells["A2:D10"];
		rangoA2D10 = hojaUsuarios.Cells[2, 1, 10, 4];
	}
}

Sólo para demostrar lo útil que nos resulta esta librería (y usando sólo una mínima parte de ella), podemos crear un lector básico de archivos Excel de manera que nos permita fácilmente leer de un Excel una serie de registros, distribuidos en distintas hojas, para luego hacer con ellos lo que necesitemos. Como comentaba al principio, esta es una funcionalidad muy requerida en una gran mayoría de los sistemas, por ejemplo: para importar información, realizar cargas masivas, etc.

El código completo está disponible acá.

En el código tenemos un proyecto de tipo librería que contiene la funcionalidad que estamos intentando crear, y un proyecto de tipo consola para hacer usar nuestra librería.

Tenemos 3 clases que nos permitirán representar y encapsular los conceptos principales que vamos a usar:

  • ExcelBase. Clase abstracta que representa un archivo Excel.
  • ExcelHojaBase. Clase abstracta que representa una hoja del archivo Excel.
  • ExcelRegistroBase. Clase abstracta que representa un registro en una hoja del archivo Excel.

La definición e implementación de dichas clases es bastante autodescriptiva, aunque también incluí comentarios.

La idea básicamente es que para cada archivo Excel que tengamos que leer, creemos una clase que herede de ExcelBase que lo represente. Lo mismo para cada hoja que nos interese, heredando de ExcelHojaBase. Y finalmente para los registros de dichas hojas, heredando de ExcelRegistroBase.

Al heredar de dichas clases, vamos a tener que implementar aquellas propiedades y métodos abstractos que existen en dichas clases, y sólo nos restará agregar una mínima cantidad de código adicional. Para entenderlo mejor, vamos a tomar las clases que representan el archivo Excel de Usuarios y Roles, la hoja Usuarios y sus registros.

En la clase del archivo Excel debemos:

  1. Definir las hojas que se desean utilizar, como propiedades.
  2. Implementar el método abstracto ObtenerHojas, inicializando las hojas y devolviendo una lista con todas ellas.
    1. public class ExcelUsuariosYRoles : ExcelBase
      {
      	#region Propiedades
      
      	public HojaUsuarios HojaUsuarios { get; private set; }
      	public HojaRoles HojaRoles { get; private set; }
      	public HojaUsuariosRoles HojaUsuariosRoles { get; private set; }
      	
      	#endregion Propiedades
      	
      	#region Metodos
      
      	#region Protegidos
      
      	protected override IEnumerable<IExcelHoja> ObtenerHojas(ExcelWorkbook workbook)
      	{
      		HojaUsuarios = new HojaUsuarios(workbook);
      		HojaRoles = new HojaRoles(workbook);
      		HojaUsuariosRoles = new HojaUsuariosRoles(workbook);
      
      		return new List<IExcelHoja> { HojaUsuarios, HojaRoles, HojaUsuariosRoles };
      	}
      
      	#endregion Protegidos
      
      	#endregion Metodos
      }
      

      Para las hojas debemos:

      1. Definir un constructor que reciba como parámetro el workbook y usar internamente el constructor de la clase base, el cual nos pide especificar el nombre de la hoja, la lista de encabezados, la fila de los encabezados y la fila en la cual empiezan los registros.
      2. Implementar el método abstracto InstanciarRegistro, donde creamos una nueva instancia del registro a partir de la fila especificada.
      public class HojaUsuarios : ExcelHojaBase<RegistroUsuario>
      {
      	public HojaUsuarios(ExcelWorkbook workbook)
      		: base(workbook,
      			nombre: "Usuarios", 
      			encabezados: new string[] { "Nombre de usuario", "Nombre completo", "Fecha de nacimiento", "Categoria", "Esta activo", "Fecha de bloqueo", "Comentarios" },
      			filaEncabezados: 2, 
      			filaPrimerRegistro: 3)
      	{ }
      
      	protected override RegistroUsuario InstanciarRegistro(int fila)
      	{
      		return new RegistroUsuario(Worksheet, fila);
      	}
      }
      

      Y en el caso de los registros debemos:

      1. Definir un constructor que llame al constructor de la clase base.
      2. Definir la lista de propiedades del registro que se desean utilizar con sus tipos correspondientes. Es importante, que todas las propiedades permitan null.
      3. Dentro del constructor, obtener los valores y asignarlos a las propiedades definidas usando los métodos existentes en la clase base del registro. Estos métodos, además de obtener los valores, nos permiten validar que tengan el tipo correcto y que los datos requeridos existan.
      4. Implementar la propiedad abstracta EsRegistroVacio, la cual debe verificar que TODAS las propiedades sean null. Esta propiedad se utilizará para saber cuando se llegó al último registro de cada hoja.
      public class RegistroUsuario : ExcelRegistroBase
      {
      	public RegistroUsuario(ExcelWorksheet hoja, int fila)
      		: base(hoja, fila)
      	{
      		NombreDeUsuario = ObtenerTexto(1, "Nombre de usuario");
      		NombreCompleto = ObtenerTexto(2, "Nombre completo");
      		FechaNacimiento = ObtenerValor<DateTime>(3, "Fecha de nacimiento");
      		Categoria = ObtenerEnum<UsuarioCategoria>(4, "Categoría");
      		EstaActivo = ObtenerValor<bool>(5, "Está activo");
      		FechaDeBloqueo = ObtenerValor<DateTime>(6, "Fecha de bloqueo", esRequerido: false);
      		Comentarios = ObtenerTexto(7, "Comentarios", esRequerido: false);
      	}
      
      	public string NombreDeUsuario { get; set; }
      	public string NombreCompleto { get; set; }
      	public DateTime? FechaNacimiento { get; set; }
      	public UsuarioCategoria? Categoria { get; set; }
      	public bool? EstaActivo { get; set; }
      	public DateTime? FechaDeBloqueo { get; set; }
      	public string Comentarios { get; set; }
      
      	public override bool EsRegistroVacio
      	{
      		get
      		{
      			return
      				string.IsNullOrWhiteSpace(NombreDeUsuario) &&
      				string.IsNullOrWhiteSpace(NombreCompleto) &&
      				!FechaNacimiento.HasValue &&
      				!Categoria.HasValue &&
      				!EstaActivo.HasValue &&
      				!FechaDeBloqueo.HasValue &&
      				string.IsNullOrWhiteSpace(Comentarios);
      		}
      	}
      }
      

      Cualquier duda, consulta o propuesta de mejora será bienvenida.