domingo, 30 de noviembre de 2025

Dapper: un micro ORM para .NET

En una anterior entrega vimos lo que era EntityFramework (EF), un ORM para .NET (C#). En está ocasión veremos una alternativa: Dapper.

Dapper es un micro ORM para .NET que:

  • Permite ejecutar consultas SQL directamente, pero mapeando resultados a objetos C#
  • Es extremadamente rápido y ligero. 
  • Compatible con múltiples bases de datos (SQL Server, MySQL/MariaDB, PostgreSQL, etc.). 
  • Ideal para proyectos donde el rendimiento es crítico y no se requiere toda la abstracción de EF.

Siguiendo el ejemplo del anterior post, crearemos una aplicación usando esta herramienta.

Requisitos:

  • Tener instalado dotnet
  • Contar con MariaDB.
  • Conocimientos básicos de C# y SQL.

Usaremos la estructura de la tabla persons:

CREATE OR REPLACE TABLE persons(
    personid INT,
    lastname VARCHAR(255),
    firstname VARCHAR(255),
    address VARCHAR(255),
    city VARCHAR(255)
);

1. Crearemos el proyecto y nos ubicaremos en el directorio creado:

$ dotnet new console -n DemoDapperMariaDB
$ cd DemoDapperMariaDB

2. Instalaremos los paquetes nugets necesarios:

$ dotnet add package Dapper
$ dotnet add package MySql.Data

3. En el directorio principal crearemos una carpeta llamada Model y dentro de este una clase llamada Person.cs , la cual tendrá este contenido:

namespace Model
{
    public class Person
    {
        public int PersonId { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string Address { get; set; }

        public string City { get; set; }

        public Person()
        {
            PersonId = 0;
            LastName = string.Empty;
            FirstName = string.Empty;
            Address = string.Empty;
            City = string.Empty;
        }

    }

}

4. Ahora crearemos el contexto de conexión a la base de datos:

DbContext.cs

using System.Data;
using MySql.Data.MySqlClient;

public class DbContext
{
    private readonly string _connectionString =
        "server=localhost;database=cursomariadb;user=root;password=secreta";

    public IDbConnection CreateConnection()
        => new MySqlConnection(_connectionString);
}

5. Modificaremos el programa principal Program.cs para incluir las operaciones CRUD:

using System;
using Dapper;
using System.Linq;

class Program
{
    static void Main()
    {
        var db = new DbContext();

        // INSERT
        using (var conn = db.CreateConnection())
        {
            var newPerson = new Person {
                PersonId = 4,
                FirstName = "Juan",
                LastName = "Pérez",
                Address = "Calle Falsa 123",
                City = "CDMX"
            };
            conn.Execute("INSERT INTO persons VALUES (@PersonId,@LastName,@FirstName,@Address,@City)", newPerson);
            Console.WriteLine("Insertado correctamente.");
        }

        // SELECT
        using (var conn = db.CreateConnection())
        {
            var persons = conn.Query<Person>("SELECT * FROM persons").ToList();
            Console.WriteLine("Listado de personas:");
            foreach (var p in persons)
                Console.WriteLine($"{p.PersonId} - {p.FirstName} {p.LastName} - {p.City}");
        }

        // UPDATE
        using (var conn = db.CreateConnection())
        {
            conn.Execute("UPDATE persons SET City=@City WHERE PersonId=@PersonId",
                new { City = "Guadalajara", PersonId = 4 });
            Console.WriteLine("Actualizado correctamente.");
        }

        // DELETE
        using (var conn = db.CreateConnection())
        {
            conn.Execute("DELETE FROM persons WHERE PersonId=@PersonId", new { PersonId = 4 });
            Console.WriteLine("Eliminado correctamente.");
        }
    }
}

Como se puede observar fue necesario la creación de consultas SQL nativas para realizar las operaciones CRUD.

6. Ejecutamos la aplicación:

$ dotnet run

Tabla comparativa Dapper vs EntityFramework

He aquí una tabla comparando ambas herramientas.

Aspecto Entity Framework Dapper
Tipo de ORM ORM completo Micro ORM
Abstracción Alta: genera modelos y consultas automáticamente Baja: requiere escribir SQL manual
Facilidad de uso Muy sencillo para CRUD básico, menos control sobre SQL Más control sobre SQL, pero requiere mayor conocimiento
Rendimiento Más lento en escenarios de alto volumen Muy rápido y eficiente
Flexibilidad Limitada por el modelo y LINQ Total: puedes usar cualquier consulta SQL
Aprendizaje Curva más suave para principiantes Curva más técnica, ideal para quienes dominan SQL
Casos de uso Aplicaciones con lógica compleja y necesidad de abstracción Microservicios, APIs rápidas, proyectos donde el rendimiento es crítico
Soporte de base de datos Amplio, con integración nativa Amplio, pero depende del proveedor ADO.NET
Mantenimiento Código más limpio y menos SQL explícito Código más explícito, pero más controlado

Conclusiones:

  • Dapper es más ligero que EF y ofrece control total sobre el SQL. 
  • Ideal para proyectos donde el rendimiento importa más que la abstracción. 
  • Aunque requiere escribir consultas manualmente, el código sigue siendo limpio y fácil de mantener.

Continuaremos con esta serie sobre .NET y C#.

Enlaces:

https://www.learndapper.com/
https://aspnetcoremaster.com/dotnet/introduccion-a-dapper.html
https://codemonkeyjunior.blogspot.com/2025/11/entity-framework-un-orm-para-net.html


Programando en C# no. 13: tipo Nullable

En C#, un tipo nullable (Nullable <T> o T?) permite que un tipo de valor (como int, bool, double) pueda contener además el valor null. El valor null se utiliza para representar una ausencia de valor. Sin embargo, los tipos de valor (como int, double, bool) no pueden ser null por defecto, siempre tienen un valor predeterminado.

Esto es útil cuando un valor puede ser opcional o no siempre está disponible.

Ejemplo: crear variables tipo int y bool con valor null.

int? edad = null;     // válido
bool? activo = true;  // válido

Aquí usamos la sintaxis corta T? para incluir null. La sintaxis larga sería como esto:

Nullable<int> numero1 = null;
Nullable<string> cadena = null;             

Un ejemplo más práctico sería como esto:

int? edad = null;

if (edad.HasValue) {
    Console.WriteLine("Edad: " + edad.Value);
} else {
    Console.WriteLine("Edad no especificada");
}         

Operadores útiles:

  • ?? → operador de coalescencia nula (valor por defecto si es null). 
  •  ?. → acceso seguro a miembros (C# 6+).

Ejemplo de uso:

int? valor = null;
int resultado = valor ?? -1; // devuelve -1 si valor es null    

Usando el método GetValueOrDefault() :

int? numero = null;
Console.WriteLine(numero.GetValueOrDefault()); // imprime 0  

Para los programadores Java esto se les hará familiar, pues existe una clase genérica llamada Optional que es similar a Nullable<T>. Miremos un ejemplo comparativo:

En Java:

Optional<String> usuario = Optional.ofNullable(null);
System.out.println(usuario.orElse("Invitado"));

En C#:

string? usuario = null;
Console.WriteLine(usuario ?? "Invitado");

Métodos principales de Nullable<T>

He aquí una tabla de los principales métodos:

Método/Propiedad Propósito Ejemplo Nota clave
HasValue Indica si contiene un valor if (x.HasValue) { ... } true si no es null
Value Devuelve el valor almacenado int v = x.Value; Lanza InvalidOperationException si es null
GetValueOrDefault() Devuelve el valor o el default del tipo int v = x.GetValueOrDefault(); Si es null, devuelve 0 para int, false para bool, etc.
GetValueOrDefault(defaultValue) Devuelve el valor o un valor por defecto especificado int v = x.GetValueOrDefault(-1); Útil para valores personalizados
ToString() Convierte el valor a cadena Console.WriteLine(x.ToString()); Devuelve "" si es null

Ahora comparemos la clase Optional de Java con Nullable<T> de C#.

Aspecto Java Optional<T> C# Nullable<T>
Propósito Evitar null en retornos y expresar ausencia de valor de forma explícita Permitir que tipos de valor (int, bool, etc.) puedan ser null
Sintaxis Optional<String> nombre = Optional.of("Carraro"); int? edad = null; (azúcar sintáctico de Nullable<int>)
Tipos soportados Cualquier tipo (referencia o valor) Solo tipos de valor (structs, int, double, bool, etc.)
Creación Optional.of(value) / Optional.ofNullable(value) / Optional.empty() new Nullable<int>(5) / int? x = null;
Acceso al valor get() (lanza excepción si vacío) .Value (lanza excepción si es null)
Verificación de presencia isPresent() / isEmpty() .HasValue
Valores por defecto orElse(default) / orElseGet(Supplier) .GetValueOrDefault() / .GetValueOrDefault(default)
Transformaciones map(Function) / flatMap(Function) / filter(Predicate) No tiene transformaciones funcionales integradas
Acciones condicionales ifPresent(Consumer) / ifPresentOrElse(...) Uso de HasValue + condicionales tradicionales
Excepciones orElseThrow() / orElseThrow(Supplier) .Value lanza InvalidOperationException si es null
Uso recomendado Como tipo de retorno en APIs y Streams para evitar null Para representar datos opcionales en BD, formularios, cálculos
Introducción Java 8 (2014) .NET 2.0 (2005)

Ejemplo. Crear un programa en C# que emplee los métodos Nullable en su sintaxis corta y larga

Program.cs

using System;

class Program
{
    static void Main()
    {
        // 1. empty()
        string? emptyOpt = null;
        Console.WriteLine("emptyOpt: " + (emptyOpt ?? "null"));

        // 2. of()
        string? opt = "Hello";
        Console.WriteLine("opt: " + opt);

        // 3. ofNullable()
        string? nullableOpt = null;
        Console.WriteLine("nullableOpt: " + (nullableOpt ?? "null"));

        // 4. isPresent()
        Console.WriteLine("opt is present: " + (opt != null));
        Console.WriteLine("emptyOpt is present: " + (emptyOpt != null));

        // 5. ifPresent()
        if (opt != null)
        {
            Console.WriteLine("Value in opt: " + opt);
        }

        // 6. isEmpty()
        Console.WriteLine("emptyOpt is empty: " + (emptyOpt == null));

        // 7. get()
        try
        {
            Console.WriteLine("Value from opt: " + opt);
            Console.WriteLine("Value from emptyOpt: " + emptyOpt!); // Forzamos acceso
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from emptyOpt.get(): " + e);
        }

        // 8. orElse()
        string value = opt ?? "Default Value";
        Console.WriteLine("orElse value: " + value);
        string defaultValue = emptyOpt ?? "Default Value";
        Console.WriteLine("orElse default value: " + defaultValue);

        // 9. orElseGet()
        string valueWithSupplier = opt ?? GetDefault();
        Console.WriteLine("orElseGet value: " + valueWithSupplier);
        string defaultValueWithSupplier = emptyOpt ?? GetDefault();
        Console.WriteLine("orElseGet default value: " + defaultValueWithSupplier);

        // 10. orElseThrow()
        try
        {
            string throwValue = emptyOpt ?? throw new ArgumentException("Value not present");
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from orElseThrow: " + e);
        }

        // 11. map()
        int? lengthOpt = opt?.Length;
        Console.WriteLine("Mapped lengthOpt: " + (lengthOpt.HasValue ? lengthOpt.Value.ToString() : "null"));

        // 12. flatMap()
        string? flatMapped = opt != null ? opt.ToUpper() : null;
        Console.WriteLine("FlatMapped opt: " + (flatMapped ?? "null"));

        // 13. filter()
        string? filtered = (opt != null && opt.Length > 3) ? opt : null;
        Console.WriteLine("Filtered opt: " + (filtered ?? "null"));
        string? emptyFiltered = (opt != null && opt.Length > 10) ? opt : null;
        Console.WriteLine("Empty Filtered opt: " + (emptyFiltered ?? "null"));

        // 14. stream() (Java 9+)
        // En C# usamos LINQ o simplemente comprobamos null
        if (opt != null)
        {
            Console.WriteLine(opt);
        }

        // 15. or() (Java 9+)
        string? anotherOpt = opt ?? "Another Value";
        Console.WriteLine("Or opt: " + anotherOpt);
        string? anotherEmptyOpt = emptyOpt ?? "Another Value";
        Console.WriteLine("Or emptyOpt: " + anotherEmptyOpt);

        // 16. equals()
        Console.WriteLine("opt equals anotherOpt: " + (opt == anotherOpt));
        Console.WriteLine("emptyOpt equals anotherEmptyOpt: " + (emptyOpt == anotherEmptyOpt));

        // 17. hashCode()
        Console.WriteLine("opt hashCode: " + (opt?.GetHashCode() ?? 0));
        Console.WriteLine("emptyOpt hashCode: " + (emptyOpt?.GetHashCode() ?? 0));

        // 18. toString()
        Console.WriteLine("opt toString: " + (opt ?? "null"));
        Console.WriteLine("emptyOpt toString: " + (emptyOpt ?? "null"));
    }

    static string GetDefault()
    {
        return "Default Value from Supplier";
    }
}

Ahora con su sintaxis larga:

Program.cs

using System;

class Program
{
    static void Main()
    {
        // 1. empty()
        Nullable<string> emptyOpt = null;
        Console.WriteLine("emptyOpt: " + (emptyOpt ?? "null"));

        // 2. of()
        Nullable<string> opt = "Hello";
        Console.WriteLine("opt: " + opt);

        // 3. ofNullable()
        Nullable<string> nullableOpt = null;
        Console.WriteLine("nullableOpt: " + (nullableOpt ?? "null"));

        // 4. isPresent()
        Console.WriteLine("opt is present: " + opt.HasValue);
        Console.WriteLine("emptyOpt is present: " + emptyOpt.HasValue);

        // 5. ifPresent()
        if (opt.HasValue)
        {
            Console.WriteLine("Value in opt: " + opt.Value);
        }

        // 6. isEmpty()
        Console.WriteLine("emptyOpt is empty: " + !emptyOpt.HasValue);

        // 7. get()
        try
        {
            Console.WriteLine("Value from opt: " + opt.Value);
            Console.WriteLine("Value from emptyOpt: " + emptyOpt.Value); // Lanza excepción
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from emptyOpt.get(): " + e);
        }

        // 8. orElse()
        string value = opt.HasValue ? opt.Value : "Default Value";
        Console.WriteLine("orElse value: " + value);
        string defaultValue = emptyOpt.HasValue ? emptyOpt.Value : "Default Value";
        Console.WriteLine("orElse default value: " + defaultValue);

        // 9. orElseGet()
        string valueWithSupplier = opt.HasValue ? opt.Value : GetDefault();
        Console.WriteLine("orElseGet value: " + valueWithSupplier);
        string defaultValueWithSupplier = emptyOpt.HasValue ? emptyOpt.Value : GetDefault();
        Console.WriteLine("orElseGet default value: " + defaultValueWithSupplier);

        // 10. orElseThrow()
        try
        {
            string throwValue = emptyOpt.HasValue ? emptyOpt.Value : throw new ArgumentException("Value not present");
        }
        catch (Exception e)
        {
            Console.WriteLine("Exception from orElseThrow: " + e);
        }

        // 11. map()
        Nullable<int> lengthOpt = opt.HasValue ? opt.Value.Length : (int?)null;
        Console.WriteLine("Mapped lengthOpt: " + (lengthOpt.HasValue ? lengthOpt.Value.ToString() : "null"));

        // 12. flatMap()
        Nullable<string> flatMapped = opt.HasValue ? opt.Value.ToUpper() : null;
        Console.WriteLine("FlatMapped opt: " + (flatMapped.HasValue ? flatMapped.Value : "null"));

        // 13. filter()
        Nullable<string> filtered = (opt.HasValue && opt.Value.Length > 3) ? opt.Value : null;
        Console.WriteLine("Filtered opt: " + (filtered.HasValue ? filtered.Value : "null"));
        Nullable<string> emptyFiltered = (opt.HasValue && opt.Value.Length > 10) ? opt.Value : null;
        Console.WriteLine("Empty Filtered opt: " + (emptyFiltered.HasValue ? emptyFiltered.Value : "null"));

        // 14. stream() (Java 9+)
        if (opt.HasValue)
        {
            Console.WriteLine(opt.Value);
        }

        // 15. or()
        Nullable<string> anotherOpt = opt.HasValue ? opt.Value : "Another Value";
        Console.WriteLine("Or opt: " + anotherOpt);
        Nullable<string> anotherEmptyOpt = emptyOpt.HasValue ? emptyOpt.Value : "Another Value";
        Console.WriteLine("Or emptyOpt: " + anotherEmptyOpt);

        // 16. equals()
        Console.WriteLine("opt equals anotherOpt: " + (opt.Equals(anotherOpt)));
        Console.WriteLine("emptyOpt equals anotherEmptyOpt: " + (emptyOpt.Equals(anotherEmptyOpt)));

        // 17. hashCode()
        Console.WriteLine("opt hashCode: " + (opt.HasValue ? opt.Value.GetHashCode() : 0));
        Console.WriteLine("emptyOpt hashCode: " + (emptyOpt.HasValue ? emptyOpt.Value.GetHashCode() : 0));

        // 18. toString()
        Console.WriteLine("opt toString: " + (opt.HasValue ? opt.Value : "null"));
        Console.WriteLine("emptyOpt toString: " + (emptyOpt.HasValue ? emptyOpt.Value : "null"));
    }

    static string GetDefault()
    {
        return "Default Value from Supplier";
    }
}

Concluyendo:

La utilidad de Nullable <T> es representar datos opcionales de manera segura y expresiva, evitando ambigüedades y errores comunes como el uso de valores ficticios o NullReferenceException. Es especialmente valioso en aplicaciones que interactúan con bases de datos, formularios y cálculos donde ciertos valores pueden estar ausentes.

Enlaces:

https://www.luisllamas.es/csharp-tipos-nullables/
https://comunidadcityjavamex.blogspot.com/2025/11/java-tip-24-conociendo-la-clase-optional.html

sábado, 29 de noviembre de 2025

LINQ: un lenguaje de consulta para C#

LINQ (Language Integrated Query) es una característica de C# que permite consultar y manipular datos directamente desde el lenguaje, usando una sintaxis coherente y expresiva. En lugar de escribir SQL o recorrer colecciones manualmente, LINQ te deja trabajar con objetos, colecciones, XML o bases de datos de forma declarativa.

Su ventaja primordial es: unificar la forma de consultar datos, sin importar el origen.

Se puede emplear de esta forma:

1. Sintaxis de consulta (query syntax, parecida a SQL):

var resultado = from p in db.Persons
                where p.City == "CDMX"
                orderby p.LastName
                select p;

2. Sintaxis de métodos (method syntax, con lambdas y métodos de extensión):

var resultado = db.Persons
                  .Where(p => p.City == "CDMX")
                  .OrderBy(p => p.LastName)
                  .ToList();

Como se puede observar ambas sintaxis, aunque diferentes, hacen lo mismo.

Usando LINQ en nuestro proyecto

En el post anterior vimos cómo crear una aplicación dotnet que intergra EntityFramework para trabajar con una base de datos de MariaDB. Usemos LINQ como herramienta para hacer SELECT, INSERT, UPDATE y DELETE de forma sencilla.

1. Hacer consultas como un SELECT:

using var db = new AppDbContext();

var personsInCDMX = db.Persons
                      .Where(p => p.City == "CDMX")
                      .OrderBy(p => p.LastName)
                      .ToList();

foreach (var p in personsInCDMX)
{
    Console.WriteLine($"{p.FirstName} {p.LastName} - {p.City}");
}

2. Insertar datos como un INSERT INTO:

db.Persons.Add(new Person {
    PersonId = 2,
    FirstName = "Ana",
    LastName = "García",
    Address = "Av. Reforma 456",
    City = "CDMX"
});
db.SaveChanges();

3. Modificar datos como un UPDATE:

var person = db.Persons.FirstOrDefault(p => p.PersonId == 2);
if (person != null)
{
    person.City = "Guadalajara";
    db.SaveChanges();
}

4. Remover datos como un DELETE:

var person = db.Persons.FirstOrDefault(p => p.PersonId == 2);
if (person != null)
{
    db.Persons.Remove(person);
    db.SaveChanges();
}

El código Program.cs quedaría de la siguiente manera:

using System;
using System.Linq;
using Model;

class Program
{
    static void Main()
    {
        using var db = new AppDbContext();

        // INSERT
        db.Persons.Add(new Person {
            PersonId = 2,
            FirstName = "Ana",
            LastName = "García",
            Address = "Av. Reforma 456",
            City = "CDMX"
            });
        db.SaveChanges();

        // SELECT
        var personsInCDMX = db.Persons
                      .Where(p => p.City == "CDMX")
                      .OrderBy(p => p.LastName)
                      .ToList();
        foreach (var p in personsInCDMX)
        {
            Console.WriteLine($"{p.FirstName} {p.LastName} - {p.City}");
        }
        
        // UPDATE
        var person = db.Persons.FirstOrDefault(p => p.PersonId == 2);
        if (person != null)
        {
            person.City = "Guadalajara";
            db.SaveChanges();
        }

        // DELETE
        if (person != null)
        {
            db.Persons.Remove(person);
            db.SaveChanges();
        }
        
    }
}

Y podríamos hacer más ejemplos.

Filtrar registros (Where):

// Personas que viven en CDMX
var enCDMX = db.Persons
               .Where(p => p.City == "CDMX")
               .ToList();

Seleccionar campos específicos:

// Solo nombres y apellidos
var nombres = db.Persons
                .Select(p => new { p.FirstName, p.LastName })
                .ToList();

Ordenar (ORDER BY):

// Ordenar por apellido
var ordenados = db.Persons
                  .OrderBy(p => p.LastName)
                  .ToList();

Combinar condiciones:

// Personas en CDMX o Guadalajara
var filtrados = db.Persons
                  .Where(p => p.City == "CDMX" || p.City == "Guadalajara")
                  .ToList();

Agrupar (GROUP BY):

// Agrupar personas por ciudad
var agrupados = db.Persons
                  .GroupBy(p => p.City)
                  .Select(g => new { Ciudad = g.Key, Total = g.Count() })
                  .ToList();

Unir tablas (JOIN):

var consulta = db.Persons
                 .Join(db.Orders,
                       p => p.PersonId,
                       o => o.PersonId,
                       (p, o) => new { p.FirstName, p.LastName, o.OrderDate })
                 .ToList();

Paginar (Skip y Take):

// Obtener los primeros 10 registros
var primeros10 = db.Persons
                   .Take(10)
                   .ToList();

// Saltar los primeros 10 y traer los siguientes 5
var pagina2 = db.Persons
                .Skip(10)
                .Take(5)
                .ToList();

Y otras cosas más.

Contar registros (Count):

// Total de personas en la tabla
int total = db.Persons.Count();

// Personas en CDMX
int enCDMX = db.Persons.Count(p => p.City == "CDMX");

Sumar valores (Sum):

// Suma de todas las edades
int sumaEdades = db.Persons.Sum(p => p.Age);

Obtener promedio (Average):

// Edad promedio
double promedioEdad = db.Persons.Average(p => p.Age);

Agrupación con agregados:

// Número de personas por ciudad
var personasPorCiudad = db.Persons
    .GroupBy(p => p.City)
    .Select(g => new {
        Ciudad = g.Key,
        Total = g.Count()
    })
    .ToList();

Búsquedas con Any/All:

// ¿Existe alguien en Monterrey?
bool existeMonterrey = db.Persons.Any(p => p.City == "Monterrey");

// ¿Todos viven en CDMX?
bool todosCDMX = db.Persons.All(p => p.City == "CDMX");

Proyección de cálculos:

// Proyectar nombre completo y longitud del apellido
var consulta = db.Persons
    .Select(p => new {
        NombreCompleto = p.FirstName + " " + p.LastName,
        LargoApellido = p.LastName.Length
    })
    .ToList();

LINQ es una herramienta que nos ayuda a realizar consultas de una manera sencilla.

Enlaces:

https://codemonkeyjunior.blogspot.com/2025/11/entity-framework-un-orm-para-net.html
https://learn.microsoft.com/es-es/dotnet/csharp/linq/
https://www.netmentor.es/entrada/linq-csharp

Dapper: un micro ORM para .NET

En una anterior entrega vimos lo que era EntityFramework (EF), un ORM para .NET ( C# ). En está ocasión veremos una alternativa: Dap...

Etiquetas

Archivo del blog