Realizar consultas a SQL Server desde .NET Core usando ADO.NET y mapear el resultado a una clase

El objetivo de este artículo es poder crear una clase genérica para poder mapear el resultado de una consulta realizada con ADO.NET a una clase con la misma estructura de la tabla.

¿Que es ADO.NET?

ADO.NET es una tecnología de acceso a datos de Microsoft que proporciona acceso programático a fuentes de datos relacionales, incluido SQL Server. Es una de las formas más antiguas y confiables de conectarse a SQL Server desde .NET Core. A través de ADO.NET, los desarrolladores pueden utilizar objetos como SqlConnection, SqlCommand y SqlDataReader para interactuar con la base de datos. Aunque es potente y flexible, el código puede resultar extenso y propenso a errores debido a la manipulación manual de conexiones y comandos.

Ejemplo de una consulta básica

Supongamos que queremos consultar una tabla llamada MiTabla, con la siguiente estructura.

CREATE TABLE [dbo].[MiTabla] (
    [Id]            INT            IDENTITY (1, 1) NOT NULL,
    [Nombre]        NVARCHAR (50)  NOT NULL,
    [Descripcion]   NVARCHAR (500) NOT NULL,
    [FechaCreacion] DATETIME       NOT NULL,
    CONSTRAINT [PK_MiTabla] PRIMARY KEY CLUSTERED ([Id] ASC)
);

Podemos listar el contenido de esa tabla usando el siguinte código.

using System.Data.SqlClient;

class Program
{
    static void Main()
    {
        SqlConnectionStringBuilder sqlConnectionString = new SqlConnectionStringBuilder
        {
            DataSource = "TuServidor",
            InitialCatalog = "TuBase",
            UserID = "TuUsuario",
            Password = "TuPass",
            TrustServerCertificate = true
        };

        using (SqlConnection connection = new SqlConnection(sqlConnectionString.ConnectionString))
        {
            string query = "SELECT * FROM MiTabla";
            SqlCommand command = new SqlCommand(query, connection);
            
            connection.Open();
            SqlDataReader reader = command.ExecuteReader();

            while (reader.Read()) 
            {
                Console.WriteLine("Id: {0}", reader.GetInt32(0));
                Console.WriteLine("Nombre: {0}", reader.GetString(1));
                Console.WriteLine("Descripción: {0}", reader.GetString(2));
                Console.WriteLine("Fecha: {0}", reader.GetDateTime(3));
            }
            
            reader.Close();
        }
    }
}

En ese ejemplo básico estamos mostrando el contenido de la tabla por pantalla, ahora supongamos que queremos mapear el contenido de la tabla a una clase. Podemos crear una clase con la misma estructura de la tabla.

public class MiTabla
{
    public int Id { get; set; }
    public string Nombre { get; set; } = string.Empty;
    public string Descripcion { get; set; } = string.Empty;
    public DateTime FechaCreacion { get; set; }
}

Y ahora modificamos el código que hicimos inicialmente para que en vez de mostrar el resultado por pantalla nos cree una lista del tipo MiTabla con los datos.

La clase queda como se ve a continuación:

using System.Data.SqlClient;

class Program
{
    static void Main()
    {
        SqlConnectionStringBuilder sqlConnectionString = new SqlConnectionStringBuilder
        {
            DataSource = "TuServidor",
            InitialCatalog = "TuBase",
            UserID = "TuUsuario",
            Password = "TuPass",
            TrustServerCertificate = true
        };

        using (SqlConnection connection = new SqlConnection(sqlConnectionString.ConnectionString))
        {
            string query = "SELECT * FROM MiTabla";
            SqlCommand command = new SqlCommand(query, connection);
            
            connection.Open();
            SqlDataReader reader = command.ExecuteReader();

            List<MiTabla> resultado = new List<MiTabla>();

            while (reader.Read()) 
            {
                resultado.Add(new MiTabla {
                    Id = reader.GetInt32(0),
                    Nombre = reader.GetString(1),
                    Descripcion = reader.GetString(2),
                    FechaCreacion = reader.GetDateTime(3)
                });
            }
            
            reader.Close();
        }
    }
}

Como se puede ver en el código, se creo una lista del tipo MiTabla llamada resultado y se asigno los campos manualmente a cada una de las propiedades de la clase.

Esto parece ser bastante sencillo y facil de hacer, por lo cual no representaría un problema a la hora de querer consultar los datos de nuestra base.

Ahora, supongamos que tenemos que realizar una trabajo en el cual necesitemos trabajar con bases que tengan cientos de tablas, y que a su vez esas tablas tengan cientos de campos. Si lo pensamos se volvería bastante tedioso tener que estar mapeando manualmente cada vez que tengamos que realizar una consulta a nuestra base.

Creando una clase genérica para realizar el mapeo

Lo que podemos hacer es crear una clase genérica que sea una extensión de la clase SqlDataReader con un método que sea el encargado de realizar el mapeo de los datos a nuestra clase.

Esto podemos hacerlo facilmente utilizando Reflection, con lo cual vamos a poder recorrer las propiedades de nuestro objeto genérico y así poder asignar los valores correspondientes.

Nuestra clase genérica queda de la siguiente forma

using System.Reflection;

public static class SqlDataReaderExtension {
    public static List<T> MapearData<T>(this SqlDataReader dataReader)
    {
        List<string> propiedades = typeof(T).GetProperties().Select(p => p.Name).ToList();

        List<T> registros = new List<T>();
        while (dataReader.Read())
        {
            T registro = (T)Activator.CreateInstance(typeof(T));
            for (int i = 0; i < dataReader.FieldCount; i++)
            {
                if (propiedades.Contains(dataReader.GetName(i)))
                {
                    PropertyInfo propiedad = registro.GetType().GetProperty(dataReader.GetName(i), BindingFlags.Public | BindingFlags.Instance);
                    if(propiedad != null && propiedad.CanWrite) {
                        propiedad.SetValue(registro, dataReader.IsDBNull(i) ? null : dataReader.GetValue(i), null);
                    }
                }
            }
            registros.Add(registro);
        }
        return registros;
    }
}

Ahora utilizando esta nueva clase podemos reescribir nuestro proceso de consulta básico y mapear el resultado de la consulta a nuestra clase MiTabla. El nuevo proceso queda de la siguiente manera.

using System.Data.SqlClient;
using System.Reflection;

class Program
{
    static void Main()
    {
        SqlConnectionStringBuilder sqlConnectionString = new SqlConnectionStringBuilder
        {
            DataSource = "TuServidor",
            InitialCatalog = "TuBase",
            UserID = "TuUsuario",
            Password = "TuPass",
            TrustServerCertificate = true
        };

        using (SqlConnection connection = new SqlConnection(sqlConnectionString.ConnectionString))
        {
            string query = "SELECT * FROM MiTabla";
            SqlCommand command = new SqlCommand(query, connection);
            
            connection.Open();
            SqlDataReader reader = command.ExecuteReader();

            List<MiTabla> resultado = reader.MapearData<MiTabla>();

            foreach (MiTabla registro in resultado)
            {
               Console.WriteLine(registro.Nombre + " - " + registro.Descripcion);
            }
            
            reader.Close();
        }
    }
}

Y así es como queda nuestro nuevo código utilizando nuestra clase genérica, esto podremos aplicarlo a cualquier tabla de nuestra base o a resultado de vistas o procedimientos almacenados.

Leave a Reply