Back to "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)"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

.NET Database EF Core Performance PostgreSQL

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)

Wednesday, 03 December 2025

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:

Í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:

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

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

Marco básico de las entidades 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 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.

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)
  • 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), 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

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:

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)
  • 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

// 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:

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

Generado SQL (EF Core 8+):

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 continúa esta tendencia con aún más mejoras.

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

Código C#:

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):

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:

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

Generado SQL (Pruebas múltiples):

-- 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#:

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

Generado 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#:

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:

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):

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):

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

Generado SQL (¡Una consulta individual!):

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:

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:

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

Generado SQL:

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

Ejemplo: Agregación compleja

Código C#:

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):

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.

Código C#:

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:

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:

// ❌ 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:

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

// ❌ 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

// ❌ 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

// ✅ 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.

El problema:

// ❌ 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:

// ✅ 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:

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:

// ✅ 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:

// ❌ 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:

// ✅ 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 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.

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:

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):

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):

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:

// 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):

// 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:

// 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:

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):

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 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) 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

Artículos relacionados en este blog:


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.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.