# Acceso a los datos en .NET: Comparación de las ORM y las estrategias de cartografía (Parte 1 - Núcleo del Marco de Entidad)

<!-- category -- .NET, EF Core, PostgreSQL, Performance, Database -->
<datetime class="hidden">2025-12-03T14:00</datetime>

Al crear aplicaciones .NET, una de las decisiones arquitectónicas más importantes que tomarás es cómo manejar el acceso a datos y el mapeo de objetos. El ecosistema .NET ofrece una rica variedad de enfoques, desde ORMs completos hasta la ejecución de SQL de metal desnudo. Cada enfoque viene con sus propias compensaciones en términos de rendimiento, productividad del desarrollador, seguridad de tipo y mantenimiento.

En esta guía integral de dos partes, exploraremos los patrones de acceso de datos más populares en .NET. Mientras usamos PostgreSQL con Npgsql en nuestros ejemplos (ya que eso es lo que potencia este blog), los conceptos, patrones y compensaciones se aplican igualmente a SQL Server, MySQL, SQLite y otras bases de datos relacionales. Los principios siguen siendo los mismos - sólo el dialecto SQL y algunas características específicas difieren.

**Parte 1 (este artículo)** se centra en Entity Framework Core, generación SQL y dificultades comunes.
**Parte 2** cubrirá Dapper, RAW ADO.NET, bibliotecas de mapeo de objetos y enfoques híbridos.

## Artículos relacionados

Si le interesan las implementaciones prácticas de EF Core, eche un vistazo a mis otros artículos:

- [Añadiendo marco de entidad para entradas de blog (Parte 1)](/blog/addingentityframeworkforblogpostspt1) - Configuración de EF Core desde cero
- [EF Migrations the right way](/blog/efmigrationstherightway) - Cómo manejar las migraciones correctamente en la producción
- [Búsqueda de texto completo (Pt 1)](/blog/textsearchingpt1) - Implementación de búsqueda de texto completo PostgreSQL con EF Core
- [CQRS moderno y Abastecimiento de Eventos](/blog/moderncqrsandeventsourcing) - Patrones arquitectónicos avanzados con EF Core

## Índice

## El espectro de los enfoques de acceso a los datos

El panorama de acceso a datos .NET se puede visualizar como un espectro:

```
Full Abstraction                                      Full Control
     ↓                                                      ↓
[EF Core] → [EF Core Raw SQL] → [Dapper] → [Npgsql ADO.NET]
```

A medida que se mueve de izquierda a derecha, obtiene rendimiento y control, pero pierde comodidad y características automáticas. Examinemos cada enfoque en detalle.

### Comparación de flujo de acceso a datos

He aquí una comparación visual de cómo cada enfoque maneja una consulta típica:

```mermaid
graph TB
    subgraph "EF Core Flow"
        A1[LINQ Query] -->|Compile| B1[Expression Tree]
        B1 -->|Translate| C1[SQL Query]
        C1 -->|Execute| D1[PostgreSQL]
        D1 -->|Results| E1[DbDataReader]
        E1 -->|Materialize| F1[Tracked Entities]
        F1 -->|Return| G1[Application]
    end

    subgraph "Dapper Flow"
        A2[SQL String] -->|Parameterize| B2[DbCommand]
        B2 -->|Execute| C2[PostgreSQL]
        C2 -->|Results| D2[DbDataReader]
        D2 -->|Map| E2[POCOs]
        E2 -->|Return| F2[Application]
    end

    subgraph "Raw Npgsql Flow"
        A3[SQL + Parameters] -->|Build Command| B3[NpgsqlCommand]
        B3 -->|Execute| C3[PostgreSQL]
        C3 -->|Results| D3[NpgsqlDataReader]
        D3 -->|Manual Mapping| E3[Objects]
        E3 -->|Return| F3[Application]
    end

    style A1 stroke:#2563eb,stroke-width:2px
    style B1 stroke:#2563eb,stroke-width:2px
    style C1 stroke:#2563eb,stroke-width:2px
    style D1 stroke:#2563eb,stroke-width:2px
    style E1 stroke:#2563eb,stroke-width:2px
    style F1 stroke:#2563eb,stroke-width:2px
    style G1 stroke:#2563eb,stroke-width:2px

    style A2 stroke:#059669,stroke-width:2px
    style B2 stroke:#059669,stroke-width:2px
    style C2 stroke:#059669,stroke-width:2px
    style D2 stroke:#059669,stroke-width:2px
    style E2 stroke:#059669,stroke-width:2px
    style F2 stroke:#059669,stroke-width:2px

    style A3 stroke:#dc2626,stroke-width:2px
    style B3 stroke:#dc2626,stroke-width:2px
    style C3 stroke:#dc2626,stroke-width:2px
    style D3 stroke:#dc2626,stroke-width:2px
    style E3 stroke:#dc2626,stroke-width:2px
    style F3 stroke:#dc2626,stroke-width:2px
```

### Desempeño vs. productividad del desarrollador

```mermaid
graph LR
    A[High Productivity<br/>Low Performance] --> B[EF Core<br/>Full Tracking]
    B --> C[EF Core<br/>No Tracking]
    C --> D[EF Core<br/>Raw SQL]
    D --> E[Dapper]
    E --> F[Raw Npgsql]
    F --> G[Low Productivity<br/>High Performance]

    style A stroke:#2563eb,stroke-width:2px
    style B stroke:#2563eb,stroke-width:2px
    style C stroke:#3b82f6,stroke-width:2px
    style D stroke:#059669,stroke-width:2px
    style E stroke:#059669,stroke-width:2px
    style F stroke:#dc2626,stroke-width:2px
    style G stroke:#dc2626,stroke-width:2px
```

## Entity Framework Core: The Full-Featured ORM

[Marco básico de las entidades](https://learn.microsoft.com/en-us/ef/core/) es el buque insignia de Microsoft ORM , proporcionando una abstracción completa sobre su base de datos . Es compatible con PostgreSQL a través de la [Npgsql.EntityFrameworkCore.PostreSQL](https://www.npgsql.org/efcore/) proveedor.

Para obtener orientación práctica sobre la creación de EF Core en su proyecto, consulte mi artículo sobre [Añadiendo marco de entidad para entradas de blog](/blog/addingentityframeworkforblogpostspt1).

### Características principales

- **Cambiar el seguimiento**: Automáticamente el seguimiento de la entidad cambia y genera SQL apropiado
- **Migraciones**: Gestión de esquemas y control de versiones primero en código (ver [EF Migrations the right way](/blog/efmigrationstherightway))
- **LINQ Provider**: Consultas de tipo seguro usando constructos de lenguaje C#
- **Loading/Eager**: Estrategias de carga flexibles para entidades relacionadas
- **Características avanzadas de PostgreSQL**: Búsqueda de texto completo ([ver mi artículo](/blog/textsearchingpt1)), columnas JSON, arrays, tipos de rango
- **Interceptores y eventos**: Puntos de extensibilidad para las preocupaciones transversales

### Ejemplo: CRUD básico con núcleo de EF

```csharp
public class BlogDbContext : DbContext
{
    public DbSet<BlogPost> BlogPosts { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseNpgsql("Host=localhost;Database=blog;Username=postgres;Password=secret");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // PostgreSQL-specific: Full-text search
        modelBuilder.Entity<BlogPost>()
            .HasGeneratedTsVectorColumn(
                p => p.SearchVector,
                "english",
                p => new { p.Title, p.Content })
            .HasIndex(p => p.SearchVector)
            .HasMethod("GIN");

        // PostgreSQL array type
        modelBuilder.Entity<BlogPost>()
            .Property(p => p.Tags)
            .HasPostgresArrayConversion(
                tag => tag.ToLowerInvariant(),
                tag => tag);
    }
}

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public string[] Tags { get; set; }
    public NpgsqlTsVector SearchVector { get; set; }
    public List<Comment> Comments { get; set; }
    public DateTime PublishedDate { get; set; }
}

// Usage
public class BlogService
{
    private readonly BlogDbContext _context;

    public async Task<List<BlogPost>> GetRecentPostsAsync(int count)
    {
        return await _context.BlogPosts
            .Include(p => p.Comments)
            .OrderByDescending(p => p.PublishedDate)
            .Take(count)
            .ToListAsync();
    }

    public async Task<List<BlogPost>> SearchPostsAsync(string searchTerm)
    {
        return await _context.BlogPosts
            .Where(p => p.SearchVector.Matches(EF.Functions.ToTsQuery("english", searchTerm)))
            .ToListAsync();
    }

    public async Task AddPostAsync(BlogPost post)
    {
        _context.BlogPosts.Add(post);
        await _context.SaveChangesAsync();
    }
}
```

### Núcleo de EF con SQL en bruto

EF Core también soporta consultas SQL sin procesar cuando necesita más control:

```csharp
public async Task<List<BlogPost>> GetPostsByComplexCriteriaAsync()
{
    var searchTerm = "postgresql";

    return await _context.BlogPosts
        .FromSqlInterpolated($@"
            SELECT * FROM ""BlogPosts""
            WHERE ""SearchVector"" @@ to_tsquery('english', {searchTerm})
            AND array_length(""Tags"", 1) > 3
            ORDER BY ts_rank(""SearchVector"", to_tsquery('english', {searchTerm})) DESC
        ")
        .ToListAsync();
}

// Or with DbDataReader for maximum control
public async Task<List<PostStatistics>> GetPostStatisticsAsync()
{
    using var command = _context.Database.GetDbConnection().CreateCommand();
    command.CommandText = @"
        SELECT
            DATE_TRUNC('month', ""PublishedDate"") as Month,
            COUNT(*) as PostCount,
            AVG(ARRAY_LENGTH(""Tags"", 1)) as AvgTags
        FROM ""BlogPosts""
        GROUP BY DATE_TRUNC('month', ""PublishedDate"")
        ORDER BY Month DESC";

    await _context.Database.OpenConnectionAsync();

    var results = new List<PostStatistics>();
    using var reader = await command.ExecuteReaderAsync();

    while (await reader.ReadAsync())
    {
        results.Add(new PostStatistics
        {
            Month = reader.GetDateTime(0),
            PostCount = reader.GetInt32(1),
            AverageTags = reader.GetDouble(2)
        });
    }

    return results;
}
```

### Cuándo usar el núcleo de la FE

**• Usar el núcleo de EF cuando:**

- Creación de una nueva aplicación con requisitos de esquema en evolución
- Necesita una fuerte validación de la consulta de tipo y tiempo de compilación
- Las migraciones y la versión de esquemas son importantes (ver [mi guía de migración](/blog/efmigrationstherightway))
- Su equipo prefiere trabajar con objetos sobre SQL
- Estás usando modelos de dominio complejos con relaciones
- La velocidad de desarrollo es más crítica que el rendimiento bruto
- Necesita portabilidad de base de datos cruzada (aunque las características específicas de PostgreSQL lo bloquean)

**• Evitar el núcleo de EF cuando:**

- El máximo rendimiento es crítico ( API de alto rendimiento, procesamiento por lotes)
- Tiene consultas SQL complejas y afinadas a mano
- Sus consultas no mapean bien a gráficos de objetos
- Usted necesita control de grano fino sobre cada instrucción SQL
- El uso de memoria es una restricción crítica (cambiar los gastos generales de seguimiento)
- Usted está trabajando con esquemas heredados que no mapean a convenciones

## EF Core SQL Generation: Entendiendo lo que se ejecuta

Uno de los aspectos más importantes del uso eficaz de EF Core es entender qué SQL genera. EF Core ha mejorado significativamente la generación de SQL a lo largo de los años, pero es fundamental verificar las consultas que se envían a PostgreSQL.

### Viendo SQL generado

```csharp
// Enable sensitive data logging and detailed errors (development only!)
optionsBuilder
    .UseNpgsql(connectionString)
    .EnableSensitiveDataLogging()
    .EnableDetailedErrors()
    .LogTo(Console.WriteLine, LogLevel.Information);

// Or use logging to see SQL
public class BlogService
{
    private readonly BlogDbContext _context;
    private readonly ILogger<BlogService> _logger;

    public async Task<List<BlogPost>> GetPostsAsync()
    {
        var query = _context.BlogPosts
            .Where(p => p.PublishedDate > DateTime.UtcNow.AddDays(-30))
            .OrderByDescending(p => p.PublishedDate);

        // View the SQL before execution
        var sql = query.ToQueryString();
        _logger.LogInformation("Executing query: {Sql}", sql);

        return await query.ToListAsync();
    }
}
```

### Ejemplo: Consulta sencilla

**C# LINQ:**

```csharp
var recentPosts = await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .OrderByDescending(p => p.PublishedDate)
    .Take(10)
    .ToListAsync();
```

**Generado SQL ([EF Core 8+](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)):**

```sql
SELECT b."Id", b."Title", b."Content", b."CategoryId", b."PublishedDate"
FROM "BlogPosts" AS b
WHERE b."CategoryId" = @__categoryId_0
ORDER BY b."PublishedDate" DESC
LIMIT @__p_1
```

Observe cómo EF Core 8+ genera SQL limpio y eficiente con una parametrización adecuada. [EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) continúa esta tendencia con aún más mejoras.

### Ejemplo: Únete a Incluir (Antes de EF Core 5)

**Código C#:**

```csharp
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .ToListAsync();
```

**Antiguo SQL ( Core 3.1 de la FE - Explosión cartesiana):**

```sql
SELECT b."Id", b."Title", c."Id", c."Name", cm."Id", cm."Content"
FROM "BlogPosts" AS b
LEFT JOIN "Categories" AS c ON b."CategoryId" = c."Id"
LEFT JOIN "Comments" AS cm ON b."Id" = cm."BlogPostId"
ORDER BY b."Id", c."Id"
```

Esto crea un **Producto cartesiano** - si un post tiene 10 comentarios, esa fila se repite 10 veces!

### Ejemplo: Consultas divididas (Base 5+ de la FE)

**Código C# con la consulta de Split:**

```csharp
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .AsSplitQuery()  // ← This is the key!
    .ToListAsync();
```

**Generado SQL (Pruebas múltiples):**

```sql
-- Query 1: Get posts and categories
SELECT b."Id", b."Title", b."Content", c."Id", c."Name"
FROM "BlogPosts" AS b
LEFT JOIN "Categories" AS c ON b."CategoryId" = c."Id"

-- Query 2: Get comments for those posts
SELECT cm."Id", cm."Content", cm."BlogPostId"
FROM "Comments" AS cm
INNER JOIN (
    SELECT b."Id"
    FROM "BlogPosts" AS b
) AS t ON cm."BlogPostId" = t."Id"
ORDER BY t."Id"
```

Esto elimina el producto cartesiano y es a menudo **Mucho más rápido.** ¡Para las colecciones!

### Ejemplo: Incluir filtrado (Base 5+ de la FE)

**Código C#:**

```csharp
var posts = await _context.BlogPosts
    .Include(p => p.Comments.Where(c => c.IsApproved))
    .ToListAsync();
```

**Generado SQL:**

```sql
SELECT b."Id", b."Title", b."Content", t."Id", t."Content", t."IsApproved"
FROM "BlogPosts" AS b
LEFT JOIN (
    SELECT c."Id", c."Content", c."IsApproved", c."BlogPostId"
    FROM "Comments" AS c
    WHERE c."IsApproved" = TRUE
) AS t ON b."Id" = t."BlogPostId"
ORDER BY b."Id"
```

### Ejemplo: Consultas sobre columnas de JSON (Base 7+ de la FE)

**Código C#:**

```csharp
public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public PostMetadata Metadata { get; set; }  // Stored as JSONB
}

public class PostMetadata
{
    public bool IsFeatured { get; set; }
    public int ViewCount { get; set; }
    public List<string> RelatedTags { get; set; }
}

// Query JSON properties
var featuredPosts = await _context.BlogPosts
    .Where(p => p.Metadata.IsFeatured)
    .ToListAsync();
```

**Generado SQL:**

```sql
SELECT b."Id", b."Title", b."Metadata"
FROM "BlogPosts" AS b
WHERE b."Metadata" ->> 'IsFeatured' = 'true'
```

¡EF Core 7+ puede traducir el acceso de propiedad JSON a los operadores de PostgreSQL JSON!

### Ejemplo: Actualización a granel (EF Core 7+ Ejecutar actualización)

**Camino antiguo (ineficiente):**

```csharp
var posts = await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ToListAsync();

foreach (var post in posts)
{
    post.IsArchived = true;
}

await _context.SaveChangesAsync();  // Generates N UPDATE statements!
```

**Nueva forma (subrúbrica 7+ de la FE):**

```csharp
await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsArchived, true));
```

**Generado SQL (¡Una consulta individual!):**

```sql
UPDATE "BlogPosts" AS b
SET "IsArchived" = TRUE
WHERE b."CategoryId" = 5
```

Esto es un **masiva** mejora - una declaración SQL en lugar de N!

### Ejemplo: Borrar a granel (Base 7+ de la FE)

**Camino antiguo:**

```csharp
var oldPosts = await _context.BlogPosts
    .Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
    .ToListAsync();

_context.BlogPosts.RemoveRange(oldPosts);
await _context.SaveChangesAsync();  // N DELETE statements
```

**Nueva manera:**

```csharp
await _context.BlogPosts
    .Where(p => p.PublishedDate < DateTime.UtcNow.AddYears(-5))
    .ExecuteDeleteAsync();
```

**Generado SQL:**

```sql
DELETE FROM "BlogPosts" AS b
WHERE b."PublishedDate" < @__p_0
```

### Ejemplo: Agregación compleja

**Código C#:**

```csharp
var categoryStats = await _context.Categories
    .Select(c => new CategoryStats
    {
        CategoryName = c.Name,
        PostCount = c.BlogPosts.Count(),
        LatestPostDate = c.BlogPosts.Max(p => p.PublishedDate),
        AverageComments = c.BlogPosts.Average(p => p.Comments.Count)
    })
    .ToListAsync();
```

**SQL generado ( Core 8+/10 de la FE):**

```sql
SELECT c."Name" AS "CategoryName",
       COUNT(*)::int AS "PostCount",
       MAX(b."PublishedDate") AS "LatestPostDate",
       COALESCE(AVG((
           SELECT COUNT(*)::int
           FROM "Comments" AS c0
           WHERE b."Id" = c0."BlogPostId"
       ))::double precision, 0.0) AS "AverageComments"
FROM "Categories" AS c
LEFT JOIN "BlogPosts" AS b ON c."Id" = b."CategoryId"
GROUP BY c."Id", c."Name"
```

### Búsqueda de texto completo de PostgreSQL

Para una inmersión más profunda en la búsqueda de texto completo, vea mi artículo en [implementar búsqueda de texto completo con EF Core](/blog/textsearchingpt1).

**Código C#:**

```csharp
var searchResults = await _context.BlogPosts
    .Where(p => p.SearchVector.Matches(EF.Functions.ToTsQuery("english", "postgresql & performance")))
    .OrderByDescending(p => p.SearchVector.Rank(EF.Functions.ToTsQuery("english", "postgresql & performance")))
    .Take(20)
    .ToListAsync();
```

**Generado SQL:**

```sql
SELECT b."Id", b."Title", b."Content", b."SearchVector"
FROM "BlogPosts" AS b
WHERE b."SearchVector" @@ to_tsquery('english', @__searchTerm_0)
ORDER BY ts_rank(b."SearchVector", to_tsquery('english', @__searchTerm_0)) DESC
LIMIT 20
```

## Advertencias críticas de núcleo de EF y fallas

### 1. Cambio de seguimiento de fugas de memoria

**El problema:**

```csharp
// ❌ DANGER: This can cause memory leaks!
public class PostCache
{
    private readonly BlogDbContext _context;
    private List<BlogPost> _cachedPosts;

    public PostCache(BlogDbContext context)
    {
        _context = context;
    }

    public async Task LoadCacheAsync()
    {
        // These entities are now tracked by the context
        _cachedPosts = await _context.BlogPosts.ToListAsync();

        // The DbContext holds references to these entities FOREVER
        // They can never be garbage collected while the context lives!
    }
}
```

**Por qué es un problema:**

- Las entidades rastreadas permanecen en la memoria durante toda la vida del `DbContext`
- El rastreador de cambios mantiene referencias, evitando la recolección de basura
- Contextos de larga vida (por ejemplo, singletons) = fuga de memoria
- En ASP.NET Core, el contexto está visionado por defecto (¡bueno!)
- Pero si cacheas entidades rastreadas, estás en problemas.

**La solución:**

```csharp
public async Task LoadCacheAsync()
{
    // ✅ Use AsNoTracking() for read-only queries
    _cachedPosts = await _context.BlogPosts
        .AsNoTracking()
        .ToListAsync();

    // Or detach entities after loading
    var posts = await _context.BlogPosts.ToListAsync();
    foreach (var post in posts)
    {
        _context.Entry(post).State = EntityState.Detached;
    }
    _cachedPosts = posts;
}
```

### 2. Generación de proxy y peligros de carga perezosos

> **ADVERTENCIA CRÍTICA: NO USAR PROXIMACIONES CORRESPONDIENTES DE EF SI CACHEA LAS ENTIDADES**
> 
> Proximos de carga perezosos + caché = **PLAZO DE MEMORIA GARANTIZADO**
> 
> Si cachea instancias de DbContext o entidades de caché cargadas con proxys habilitados, usted **Will.** memoria de fuga. El mecanismo proxy mantiene referencias al DbContext, evitando la recolección de basura. Este es uno de los errores más comunes y peligrosos en las aplicaciones EF Core.
> 
> **Regla general**:Incluya siempre las colecciones explícitamente con `.Include()`. Sólo use proxys si usted entiende completamente las compensaciones y nunca, nunca cache entidades proxy.

**Problema 1: La pesadilla de la consulta N+1**

```csharp
// ❌ Enable lazy loading
optionsBuilder
    .UseNpgsql(connectionString)
    .UseLazyLoadingProxies();  // Convenient but dangerous!

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public virtual Category Category { get; set; }  // Virtual = proxy
    public virtual List<Comment> Comments { get; set; }
}

// Somewhere in your code
var posts = await _context.BlogPosts.ToListAsync();

foreach (var post in posts)
{
    Console.WriteLine(post.Category.Name);  // N+1 query here!
    Console.WriteLine(post.Comments.Count);  // Another N+1 query!
}
```

**¿Qué sucede?**

1. La primera consulta carga todos los mensajes
2. Por **cada puesto**, acceso `Category` activa una consulta de base de datos
3. Por **cada puesto**, acceso `Comments` activa otra consulta
4. Si usted tiene 100 puestos, acaba de ejecutar **201 consultas**!

**Problema 2: Proxy + Caché = fuga de memoria**

```csharp
// ❌ CATASTROPHIC: Lazy loading proxies + caching
public class BlogPostCache
{
    private static List<BlogPost> _cachedPosts;
    private readonly BlogDbContext _context;

    public BlogPostCache()
    {
        var optionsBuilder = new DbContextOptionsBuilder<BlogDbContext>();
        optionsBuilder
            .UseNpgsql(connectionString)
            .UseLazyLoadingProxies();  // ⚠️ DANGER!

        _context = new BlogDbContext(optionsBuilder.Options);
    }

    public async Task<List<BlogPost>> GetCachedPostsAsync()
    {
        if (_cachedPosts == null)
        {
            // ❌ These proxy entities hold references to _context
            _cachedPosts = await _context.BlogPosts.ToListAsync();
        }
        return _cachedPosts;
    }
}
```

**Por qué esto es catastrófico:**

- Las entidades proxy mantienen una referencia a sus `DbContext`
- Los `DbContext` mantiene una referencia a todas las entidades de seguimiento
- Su caché ahora evita que todo el gráfico de objetos sea recogido en la basura
- Cada vez que accede a una propiedad de navegación, puede desencadenar consultas utilizando el **contexto antiguo, en caché**
- La memoria crece sin límites a medida que se cargan más datos
- Con el tiempo se quedará sin memoria o piscinas de conexión de escape

**La solución: Ser explícito**

```csharp
// ✅ NEVER use lazy loading proxies - always be explicit
optionsBuilder
    .UseNpgsql(connectionString);
    // NO .UseLazyLoadingProxies()!

public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public Category Category { get; set; }  // NOT virtual
    public List<Comment> Comments { get; set; }  // NOT virtual
}

// ✅ Explicit eager loading - you control what's loaded
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .ToListAsync();

// ✅ Or use split queries for better performance
var posts = await _context.BlogPosts
    .Include(p => p.Category)
    .Include(p => p.Comments)
    .AsSplitQuery()
    .ToListAsync();

// ✅ Or use projection to DTOs (best for caching)
var posts = await _context.BlogPosts
    .Select(p => new PostDto
    {
        Title = p.Title,
        CategoryName = p.Category.Name,
        CommentCount = p.Comments.Count
    })
    .ToListAsync();

// ✅ If you MUST cache, use AsNoTracking and no proxies
public class SafeBlogPostCache
{
    private static List<BlogPost> _cachedPosts;
    private readonly IDbContextFactory<BlogDbContext> _contextFactory;

    public async Task<List<BlogPost>> GetCachedPostsAsync()
    {
        if (_cachedPosts == null)
        {
            using var context = await _contextFactory.CreateDbContextAsync();

            _cachedPosts = await context.BlogPosts
                .Include(p => p.Category)
                .Include(p => p.Comments)
                .AsNoTracking()  // Critical for caching!
                .ToListAsync();
        }
        return _cachedPosts;
    }
}
```

**Cuando las ventajas pueden ser aceptables (entienda las compensaciones):**

Los proxies de carga perezosos pueden ser aceptables SÓLO cuando:

1. # Tienes # **de corta duración** contextos voluminosos (por ejemplo, por solicitud HTTP)
2. # Tú # **nunca** entidades de caché
3. • Estás bien con el rendimiento de la consulta N+1
4. Estás prototipando y optimizarás más tarde.
5. • Su equipo entiende perfectamente las implicaciones

Pero incluso entonces, explícito `Include()` es casi siempre la mejor opción porque:

- Hace que la carga de datos **explícita y obvia**
- Es **más fácil de optimizar** (Puedes ver lo que se está cargando)
- Lo siento. **evita consultas accidentales N+1**
- Funciona correctamente con caché y contextos de larga vida
- Es la **enfoque recomendado** por el equipo del EF Core

### 3. Cuestiones de por vida de DbContext

Para más detalles sobre la gestión de la vida útil de DbContext en la producción, consulte mi artículo sobre [EF Migrations the right way](/blog/efmigrationstherightway).

**El problema:**

```csharp
// ❌ NEVER do this - singleton DbContext
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<BlogDbContext>();  // WRONG!
}

// ❌ Also wrong - storing context in static field
public static class DataAccess
{
    private static BlogDbContext _context = new BlogDbContext();

    public static async Task<BlogPost> GetPostAsync(int id)
    {
        return await _context.BlogPosts.FindAsync(id);
    }
}
```

**Por qué está mal:**

- `DbContext` es **no es seguro para el hilo**
- Las solicitudes simultáneas causarán corrupción de datos
- El rastreador de cambios crece indefinidamente
- Agotamiento de la piscina de conexión
- Datos rancios de la caché

**La solución:**

```csharp
// ✅ Use scoped lifetime (default in ASP.NET Core)
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<BlogDbContext>(options =>
        options.UseNpgsql(connectionString));
}

// ✅ Or use DbContext factory for background services
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<BlogDbContext>(options =>
        options.UseNpgsql(connectionString));
}

public class BlogBackgroundService
{
    private readonly IDbContextFactory<BlogDbContext> _contextFactory;

    public async Task ProcessPostsAsync()
    {
        // Create a new context for this operation
        using var context = await _contextFactory.CreateDbContextAsync();

        var posts = await context.BlogPosts.ToListAsync();
        // Process posts...
    }
}
```

### 4. Incluyes no intencionales en las propiedades de navegación

**El problema:**

```csharp
public class BlogPost
{
    public int Id { get; set; }
    public string Title { get; set; }
    public List<Comment> Comments { get; set; }
}

// You query one post...
var post = await _context.BlogPosts.FirstAsync();

// Add a new comment
var newComment = new Comment { Content = "Great post!" };
post.Comments.Add(newComment);

await _context.SaveChangesAsync();

// ❌ EF Core saves the comment, BUT...
// If Comments wasn't loaded, you just lost all existing comments!
// The collection is empty, so EF thinks there are no other comments
```

**La solución:**

```csharp
// ✅ Always load navigation properties before modifying
var post = await _context.BlogPosts
    .Include(p => p.Comments)
    .FirstAsync(p => p.Id == postId);

post.Comments.Add(newComment);
await _context.SaveChangesAsync();

// Or add directly to the DbSet
_context.Comments.Add(new Comment
{
    BlogPostId = postId,
    Content = "Great post!"
});
await _context.SaveChangesAsync();
```

### 5. Mezcla de Sync vs Sync

**El problema:**

```csharp
// ❌ Mixing sync and async - deadlock risk!
public async Task<BlogPost> GetPostAsync(int id)
{
    var post = _context.BlogPosts
        .Where(p => p.Id == id)
        .FirstOrDefault();  // Sync method in async context!

    return post;
}

// ❌ Even worse - blocking async code
public BlogPost GetPost(int id)
{
    return _context.BlogPosts
        .FirstOrDefaultAsync(p => p.Id == id)
        .Result;  // DEADLOCK RISK!
}
```

**La solución:**

```csharp
// ✅ Use async all the way
public async Task<BlogPost> GetPostAsync(int id)
{
    return await _context.BlogPosts
        .FirstOrDefaultAsync(p => p.Id == id);
}

// ✅ Or use sync all the way (not recommended for ASP.NET Core)
public BlogPost GetPost(int id)
{
    return _context.BlogPosts
        .FirstOrDefault(p => p.Id == id);
}
```

## EF Core 10: Qué hay de nuevo y cómo romper los cambios

Con [EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) publicado junto con .NET 10, hay varios cambios importantes a tener en cuenta al actualizar. Para la lista completa, consulte [Rompiendo los cambios en el núcleo 10 de la FE](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes).

### Requisitos de tiempo de ejecución

**EF Core 10 requiere .NET 10**. No se ejecutará en .NET 8, .NET 9, o .NET Framework. Este es el cambio más significativo - asegurar los objetivos de su proyecto `net10.0` antes de la actualización.

### Cambios de ruptura relacionados con la consulta

#### 1. Traducción de la colección parametrizada (Cambió por defecto)

El núcleo 10 de la FE cambia cómo `Contains()` con colecciones en memoria se traduce a SQL. Anteriormente, EF Core utilizado `OpenJson()` (SQL Server) o similar. Ahora es por defecto a **arrays de parámetros** que proporcionan un mejor almacenamiento en caché del plan de consulta.

**Impacto**: Puede ver diferentes SQL generados para consultas como:

```csharp
var ids = new List<int> { 1, 2, 3, 4, 5 };
var posts = await _context.BlogPosts
    .Where(p => ids.Contains(p.Id))
    .ToListAsync();
```

**EF Core 8/9 (OpenJson):**

```sql
SELECT b."Id", b."Title"
FROM "BlogPosts" AS b
WHERE b."Id" IN (SELECT value FROM OPENJSON(@__ids_0))
```

**EF Core 10 (Rayas paramétricas):**

```sql
SELECT b."Id", b."Title"
FROM "BlogPosts" AS b
WHERE b."Id" = ANY(@__ids_0)  -- PostgreSQL
-- Or: WHERE b."Id" IN (@__ids_0_0, @__ids_0_1, @__ids_0_2, ...) -- SQL Server
```

**Si experimenta regresiones de rendimiento**, volver a la vieja conducta:

```csharp
// SQL Server
optionsBuilder.UseSqlServer(connectionString,
    o => o.UseParameterizedCollectionMode(ParameterTranslationMode.Constant));

// PostgreSQL - generally parameter arrays work well, but you can opt out if needed
```

#### 2. EjecuteUpdateAsync Signature Change

Los `ExecuteUpdateAsync` la firma ha cambiado para apoyar no-expresión lambdas. Esto es más flexible, pero **rompe el código que construyó los árboles de expresión programáticamente**:

**Vejez antigua (Base Básica de la FE 7-9):**

```csharp
// This still works
await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(p => p.IsArchived, true));
```

**Nuevo en EF Core 10 - No-expresión lambdas:**

```csharp
// Now you can include custom logic!
await _context.BlogPosts
    .Where(p => p.CategoryId == 5)
    .ExecuteUpdateAsync(setters =>
    {
        setters.SetProperty(p => p.IsArchived, true);
        setters.SetProperty(p => p.UpdatedAt, DateTime.UtcNow);
        // Can now include conditional logic, loops, etc.
    });
```

#### 3. Nombre de columna de tipo complejo

EF Core 10 cambia cómo se nombran las columnas de tipo complejo anidadas para prevenir la corrupción de datos:

**EF Core 9:**

```
NestedComplex_Property
```

**EF Core 10:**

```
OuterComplex_NestedComplex_Property
```

**Repercusiones en la migración**: Si tiene tablas existentes con tipos complejos, puede necesitar renombrar columnas o configurar nombres explícitos de columnas:

```csharp
modelBuilder.Entity<Order>()
    .ComplexProperty(o => o.ShippingAddress)
    .Property(a => a.Street)
    .HasColumnName("ShippingAddress_Street"); // Explicit name
```

### SQL Server / Azure Cambios específicos SQL

#### Tipo de datos JSON predeterminado

Para Azure SQL Database o SQL Server 2025 (nivel de compatibilidad 170+), EF Core 10 por defecto al nuevo nativo `JSON` tipo de datos en lugar de `NVARCHAR(MAX)`.

**Optar por no participar** (si necesita compatibilidad hacia atrás):

```csharp
optionsBuilder.UseAzureSql(connectionString,
    o => o.UseCompatibilityLevel(160)); // Use old NVARCHAR behavior
```

### Actualizando lista de verificación

Al pasar de la EF Core 8/9 a la EF Core 10:

1. • Actualizar el marco de objetivos a `net10.0`
2. Actualizar todo `Microsoft.EntityFrameworkCore.*` paquetes a 10.x
3. Actualizar `Npgsql.EntityFrameworkCore.PostgreSQL` a 10.x
4. • Revisar las consultas utilizando `Contains()` con colecciones para cambios de rendimiento
5. • Probar cualquier código que se construye programáticamente `ExecuteUpdateAsync` expresiones
6. • Comprobar los nombres de columnas de tipo complejo si se utilizan tipos complejos anidados
7. • Revisar el uso de la columna SQL Server JSON si se utiliza Azure SQL/SQL Server 2025

### ¿Qué hay de nuevo?

- **Traducción mejorada de LINQ**: Mejor generación SQL para consultas complejas
- **No-expresión lambdas en EjecutarUpdateAsync**: Más flexibilidad en las actualizaciones a granel
- **Mejor manejo de parámetros**: Caché mejorado del plan de consulta
- **Apoyo JSON mejorado**: Tipo JSON nativo en SQL Server 2025
- **Mejoras del rendimiento**: Más rápido materialización y seguimiento de cambios

## Características del rendimiento

- **Ejecución de la consulta**: 20-50% de gastos generales en comparación con Dapper para consultas simples
- **Uso de memoria**: Más alto debido al seguimiento de cambios y generación de proxy
- **Primera consulta**: Lenta (pregunta de compilación y caché)
- **Consultas posteriores**: Más rápido debido a la caché de consulta compilada
- **Inserta/Actualiza**: Seguimiento automático de cambios añade gastos generales
- **Operaciones a granel**: Desempeño deficiente con métodos por defecto (considerar [EFCore.Extensiones de Bulk](https://github.com/borisdj/EFCore.BulkExtensions) o Ejecutar actualización/ejecutar supresión en el núcleo de EF 7+)

## Mejores prácticas para el núcleo de la FE

### Directrices generales

1. **Usar AsNoTracking()** para consultas de sólo lectura para reducir los gastos de memoria
2. **Evitar consultas N+1** - uso `Include()` o bien dividir adecuadamente las consultas
3. **Usar consultas compiladas** para patrones de consulta repetidos
4. **Considere AsSplitQuery()** para complejos incluye para evitar productos cartesianos
5. **Usar lotes** para múltiples inserciones/actualizaciones
6. **Proyecto a las OFD** temprana para reducir la transferencia de datos y el uso de memoria
7. **Aprovechar Ejecutar Actualizar/EjecutarBorrar** (Núcleo 7+ de la FE) para operaciones a granel
8. **Siempre registrar y revisar generado SQL** en el desarrollo

### Cuando se trabaja con PostgreSQL

1. **Usar la búsqueda de texto completo** características ([mi guía](/blog/textsearchingpt1)) en lugar de `LIKE` consultas
2. **Aproveche las columnas JSONB** para datos flexibles sin esquema
3. **Usar tipos de array** para las colecciones dentro de las entidades
4. **Configurar la conexión de conexión** correctamente para su carga de trabajo
5. **Indexa tu `tsvector` columnas** con índices GIN
6. **Usar tipos de rango** para los intervalos de fecha/hora

## Próximamente en la Parte 2

En el siguiente artículo, exploraremos:

- **Dapper**: El punto dulce micro-ORM
- **ADO.NET/Npgsql en bruto**: Máximo rendimiento y control
- **Bibliotecas de mapeo de objetos**: Mapster vs AutoMapper
- **Enfoques híbridos**: Combinando el núcleo de EF y el Dapper (patrón CQRS)
- **Parámetros de rendimiento**: Comparaciones del mundo real
- **Matriz de decisiones**: Elegir la herramienta adecuada para su escenario

## Referencias y lectura adicional

- [Documentación básica del marco de entidades](https://learn.microsoft.com/en-us/ef/core/)
- [Npgsql Entity Framework Core Provider](https://www.npgsql.org/efcore/)
- [Rendimiento básico de la FE](https://learn.microsoft.com/en-us/ef/core/performance/)
- [¿Qué hay de nuevo en EF Core 10?](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew)
- [Rompiendo los cambios en el núcleo 10 de la FE](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes)
- [Qué hay de nuevo en EF Core 8](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)
- [Documentación PostgreSQL](https://www.postgresql.org/docs/)

**Artículos relacionados en este blog:**

- [Añadiendo marco de entidad para entradas de blog](/blog/addingentityframeworkforblogpostspt1)
- [EF Migrations the right way](/blog/efmigrationstherightway)
- [Búsqueda de texto completo con EF Core](/blog/textsearchingpt1)
- [CQRS moderno y Abastecimiento de Eventos](/blog/moderncqrsandeventsourcing)

---


*En la Parte 2, nos sumergiremos en Dapper, crudo Npgsql, y exploraremos cómo combinar múltiples enfoques para un rendimiento óptimo y la mantenibilidad.*