Accès aux données dans .NET: Comparaison des ORM et stratégies de cartographie (Parte 1 M- Cadre d'entités CoreM SK5 (Français (French))

Accès aux données dans .NET: Comparaison des ORM et stratégies de cartographie (Parte 1 M- Cadre d'entités CoreM SK5

Wednesday, 03 December 2025

//

24 minute read

Lorsqu’il s’agit de construire des applications .NET,, l’une des décisions architecturales les plus importantes que vous prendrez est celle de gérer l’accès aux données et la cartographie d’objetsM SK3 L’écosystème .NET offre une grande variété d’approchesSSK5 allant du traitement complet des ORM à l’exécution brute des SQL métalliques.

Dans ce guide complet de deux parties, - et ,, nous étudierons les modèles d'accès aux données les plus populaires dans |. |NET | . | Alors que nous utilisons PostgreSQL avec Npgsql dans nos exemples ( | car c'est ce qui permet à ce blog de fonctionner |

Partie 1 Concentration sur le noyau du cadre d’entités, la génération de SQL , et les pièges communs. Partie 2 Ce programme comprendra les bibliothèques de cartographie d’objets Dapper, ADO raw.NETM SK2 , et les approches hybridesMSC4

Articles connexes

Si vous êtes 'intéressé par les mises en œuvres pratiques de l’EF Core, lisez mes autres articlesM SK2

Table des matières

Le spectre des approches d'accès aux données

Le paysage d’accès aux données .NET peut être visualisé comme un spectre.

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

À mesure que vous vous déplacez de gauche à droite, vous accroîtrez la performance et le contrôle, mais vous perdez la commodité et les fonctionnalités automatiquesM SK1 Laissez' examiner chaque approche en détailMSC3

Comparaison des flux d'accès aux données

Voici une comparaison visuelle de la façon dont chaque approche gère une demande typique.

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

Performance vs Developer Productivity Trade-off

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

Cadre de l'entité Core: Le volet intégral-ORM caractérisé

Cadre de l'entité est Microsoft's flagship ORM, fournissant une abstraction complète sur votre base de donnéesM SK2 Il appuie PostgreSQL par le biais du Npgsql.EntityFrameworkCore.PostgreSQL fournisseur.

Pour des conseils pratiques sur la mise en place d’un EF Core dans votre projet,, consultez mon article sur Ajouter un cadre d'entités pour les articles de blog.

Faits clés

  • Changer la surveillance: Suivre automatiquement les changements de l'entité et génère la SQL appropriée
  • Migrations: CodeM SK1première gestion du schéma et contrôle de la version (voir Migrations de l'EF La bonne voie)
  • Fournisseur de LINQ: Type-questions sécuritaires à l'aide de constructions linguistiques CM SK2
  • Lazy/Impatient de charger: Stratégies de chargement souples pour les entités apparentées
  • Fonctions avancées de PostgreSQLRecherche de texte (voir mon article), colonnes JSON tableaux , types de gammes
  • Intercepteurs et événements: Points d'étendue pour les préoccupations de coupure croiséesM SK1

Exemple: CRUD de base avec EF Core

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();
    }
}

EF Core avec SQL brut

EF Core appuie également les demandes de SQL brutes lorsque vous avez besoin d’un plus grand contrôle:

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;
}

Quand utiliser EF Core

✅ Utiliser le noyau EF lorsque

  • Établissement d'une nouvelle application avec les exigences du schéma en évolution
  • Vous avez besoin d'un type fort et de la validation des demandes en temps réel.
  • Les migrations et la version du schéma sont importantes (voir my migration guide)
  • Votre équipe préfère travailler avec des objets plutôt que SQL
  • Vous utilisez des modèles de domaine complexes avec relations
  • La vitesse de développement est plus importante que la performance brute
  • Vous avez besoin de trans- portabilité de la base de données ( bien que PostgreSQL - fonctions spécifiques vous attachent à l’interface

❌ Évitez l’EF Core lorsqueM SK1

  • La performance maximale est cruciale (highM SK1throughput APIs , batch processing)
  • Vous avez des demandes de SQL complexes,hand-tuned
  • Vos demandes ne permettent pas de cartographier bien les graphes des objets
  • Vous avez besoin de contrôle fine-grained sur chaque déclaration SQL
  • L’utilisation de la mémoire est une contrainte critique (change tracking overhead)
  • Vous travaillez avec des schémas généraux qui ne mappent pas les conventions.

EF Core SQL Generation: Comprendre ce qui est exécuté

L’un des aspects les plus importants d’utiliser EF Core efficacement est de comprendre ce qu’il génère en SQL.

Voir SQL généré

// 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();
    }
}

Exemple: Demande simple

C# LINQM SK1

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

SQL ( généré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

Notez comment EF Core 8+ génère un SQL propre, efficace avec une paramétrisation appropriéeM SK2 EF Core 10 continue cette tendance avec encore plus d’améliorations.

Exemple: Join with Include (Before EF Core 5)

C# CodeM SK1

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

Ancien SQL (EF Core 3.1 - Explosion Cartesienne):

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"

Il s’agit d’une Produit Cartesian - s’il y a 10 commentairesM SK2 dans un poste, cette ligne est répétée 10 fois!

Exemple: Demandes de partage (EF Core 5+)

C# Code avec une question de division:

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

Generated SQL (Multiple Queries):

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

Cela élimine le produit Cartesian et est souvent beaucoup plus rapidement pour les collections!

Exemple: Filtered Include (EF Core 5+)

C# CodeM SK1

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

SQL généré:

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"

Exemple: Demandes de renseignements sur la colonne JSON (EF Core 7+)

C# CodeM SK1

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

SQL généré:

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

EF Core 7+ peut traduire l'accès aux propriétés de JSON à des exploitants PostgreSQL JSON!

Exemple: Mise à jour en vrac M SK1EF Core 7+ ExecuteUpdate)

Voie ancienne (Inefficace):

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!

New Way (EF Core 7+):

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

Generated SQL (Query unique!):

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

Il s’agit d’un de masse amélioration - une déclaration SQL au lieu de N!

Exemple: Supprimer en vrac (EF Core 7+)

Voie ancienne:

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

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

Nouvelle voie:

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

SQL généré:

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

Exemple: Aggrégation complexe

C# CodeM SK1

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

Generated SQL (EF Core 8+/10):

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"

PostgreSQL Full-Suite de texte

Pour une recherche plus approfondie dans le texte -text search, voir mon article sur Mettre en œuvre une recherche de texte intégrale- avec EF Core.

C# CodeM SK1

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

SQL généré:

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

⚠️ Avertissements et débouchés fondamentaux d'EF critiques

1. Changer les fuites de la mémoire de suivi

Le problème:

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

Pourquoi le problème est-il?

  • Les entités suivies demeurent en mémoire pendant toute la durée de vie du DbContext
  • Le détecteur de changement maintient les références, pour prévenir la collecte des ordures
  • Long-contextes vivants (eM SK2g., singletonsMSC4 = fuite de mémoire
  • En ASP, le contexte .NET Core, est défini par défaut.
  • Mais si vous cachez des entités suivies, vous serez en difficulté.

La solution:

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. Proxy Generation and Lazy Loading Dangers

🚨 AVERTISSEMENT CRITIQUE : N'UTILISEZ PAS LES PROXÉES EF CORE S'IL Y A DES ENTITÉS

Proxies chargés faux + caching = RETOUR DE MEMORY GUARANTED

Si vous cachez des instances DbContext ou des entités de cache chargées avec les proxies activées, vous OUVERTURES mémoire fuite. Le mécanisme de proxy maintient les références au contexte Db, empêche la collecte des ordures . Il s'agit d'une des erreurs les plus courantes et dangereuses dans les applications EF CoreM SK3

Règle de la poignée: Inclure toujours les collections explicitement avec .Include(). Utiliser des proxies seulement si vous comprenez pleinement les compromis et que vous n’avez jamais entreposé des entités de proxye.

Problème 1: The N+1 Nightmare de la demande

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

Que se passe-t-il:

  1. La première demande charge tous les postes
  2. Pour Chaque posteAccès , Category déclenche une demande de base de données
  3. Pour Chaque posteAccès , Comments déclenche une autre demande
  4. Si vous avez 100 postes,, vous venez d'exécuter Demandes de renseignements 201!

Problème 2: Proxy + Caching = fuite de mémoire

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

Pourquoi cela est catastrophique:

  • Les entités proxyes conservent une référence à leur DbContext
  • L'article 5 de la Loi sur l'immigration et le statut des réfugiés DbContext maintient une référence à toutes les entités suivies
  • Votre cache empêche maintenant la collecte de déchets dans l'ensemble du graphique d'objet.
  • Chaque fois que vous accéderez à une propriété de navigation,, elle peut déclencher des demandes en utilisant contexte cached old,
  • La mémoire augmente sans limite à mesure que vous chargez plus de données.
  • Vous ' finirez par manquer de mémoire ou d’exhausseurs de connexion

La solution: doit être explicite

// ✅ 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;
    }
}

Quand les proxies peuvent être acceptables (Pour comprendre les compromis):

Les proxies à chargement lent pourraient être acceptables uniquement lorsque:

  1. ✅ Vous avez Short-lived contextes scoped (eM SK1g., par demande HTTPMSC3
  2. ✅ Vous Aucune fois Les entités de cache
  3. ✅ VousM SK1Vous êtes en règle avec la performance de la demande N+1
  4. ✅ VousM SK1êtes en prototype et optimisez ultérieurement
  5. ✅ Votre équipe comprend pleinement les implications

Mais même alors, explicite Include() est presque toujours la meilleure option car:

  • Il fait charger les données explicite et évidente
  • Il s’agit d’un système de gestion des ressources humaines qui a été mis en place dans le cadre duquel les ministères et organismes fédéraux, provinciaux et territoriaux participent à l’élaboration des politiques. Il est plus facile d'optimiser (Vous pouvez voir ce que ' est chargée
  • Il s'agit Prévient les demandes accidentelles de N+1
  • Il fonctionne correctement avec le caching et les contextes longs-lived
  • Il s’agit de approche recommandée par l'équipe EF Core

3. DbContexte Questions de vie

Pour de plus amples détails sur la gestion du DbContext lifetime in production, voir mon article sur Migrations de l'EF La bonne voie.

Le problème:

// ❌ 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);
    }
}

Pourquoi ce n’est pas le cas?

  • DbContext est non thread-safe
  • Les demandes concomitantes causeront une corruption des données
  • Change tracker augmente indéfiniment
  • Épuisement du bassin de connexion
  • Supprimer les données provenant du cache

La solution:

// ✅ 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. Involontaire inclut les propriétés de navigation

Le problème:

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

// ✅ 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. Async vs Sync Mixing

Le problème:

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

// ✅ 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);
}

Core EF 10: Quels sont les changements nouveaux et importants de '?

Avec EF Core 10 release alongside .NET 10, il y a plusieurs changements importants à prendre en considération lors de la mise à niveau Changements apportés à l’EF Core 10.

Exigences en matière de temps d'exécution

EF Core 10 requiert .NET 10. Il ne fonctionnera pas sur .NET | | 8, | net10.0 avant la mise à niveau.

Demande-Changes liés à la rupture

1. Traduction de la collecte paramétrisée (Modifié par défaut)

EF Core 10 modifie comment Contains() avec in-collections de mémoire est traduite en SQL. PrécédemmentM SK2 EF Core utilisé OpenJson() (SQL ServerM SK1 ou similaire. Maintenant il est par défaut pour tableaux de paramètres qui offrent un meilleur caching du plan de demandes.

Incidences: Vous pouvez voir différents types de SQL générés pour les demandes comme :

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 (OpenJsonM SK2

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

EF Core 10 (Arrayes de paramètres):

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 vous éprouvez des régressions de rendement, retourner à l'ancien comportementM SK1

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

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

2. ExecuteUpdateAsync Signature Change

L'article 5 de la Loi sur l'immigration et le statut des réfugiés ExecuteUpdateAsync Signature a été modifiée pour supporter non-expression lambdas. Ceci est plus souple mais brise le code qui a construit des arbres d'expression de façon programmatique:

Autre mode (EF Core 7-9):

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

Nouveaux éléments du noyau EF 10 - NonM SK2expression 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. Nom de la colonne du type complexe

EF Core 10 modifie la façon dont les colonnes de type complexe sont désignées pour prévenir la corrosion des données

EF Core 9:

NestedComplex_Property

EF Core 10:

OuterComplex_NestedComplex_Property

Impact des migrations: Si vous avez des tableaux existants comportant des types complexes, vous devez peut-être renommer les colonnes ou configurer les noms explicites de celles-ci.

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

SQL Server / Changements spécifiques à Azure SQL

Type de données JSON par défaut

Pour la base de données SQL d'Azure ou pour le serveur SQL 2025 ( niveau de compatibilité 170+), Core EF JSON le type de données au lieu NVARCHAR(MAX).

Pour s'exclure (si vous avez besoin d'une compatibilité à l'arrière-plan

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

Mise à niveau de la liste de vérification

Lors de la mise à niveau de l’EF Core 8/9 au EF Core ≥10:

  1. ✅ Mise à jour du cadre cible net10.0
  2. ✅ Mise à jour de tous Microsoft.EntityFrameworkCore.* paquets à 10.x
  3. ✅ Mise à jour Npgsql.EntityFrameworkCore.PostgreSQL à 10.x
  4. ✅ Examiner les demandes en utilisant Contains() avec des collections pour les changements de performance
  5. ✅ Tester tout code qui est construit de façon programmatique ExecuteUpdateAsync expressions
  6. ✅ Vérifier les noms de colonnes du type complexe si l'on utilise des types complexes nichés
  7. ✅ Examiner l’utilisation de la colonne JSON du serveur SQL si vous utilisez Azure SQL/SQL Server 2025

Quoi de neuf (HighlightsM SK2

  • Amélioration de la traduction LINQ: Une meilleure génération de SQL pour les demandes complexes
  • Non-expression lambdas dans ExecuteUpdateAsync: Plus de souplesse dans les mises à jour en vrac
  • Une meilleure manipulation des paramètres: Amélioration du caching des plans de demande
  • Amélioration de l'appui JSON: Type de JSON natif sur le serveur SQL 2025
  • Amélioration du rendement: Matérialisation plus rapide et suivi de changements

Caractéristiques du rendement

  • Rendement de la demande: 20-50% frais généraux comparés à Dapper pour les demandes simples
  • Utilisation de la mémoire: Plus élevé en raison du changement de suivi et de la génération des proxyes
  • Première demande: Sous-coordination et cache de la demande
  • Demandes subséquentes: Plus rapide en raison du cache de demande compilé
  • Insérer/Mises à jour: Le suivi automatique des changements ajoute des frais généraux
  • Opérations en vrac: Faible rendement avec les méthodes par défaut (considérer EFCore.BulkExtensions ou ExecuteUpdate/ExecuteDelete dans EF Core 7+)

Best Practices for EF Core

Lignes directrices générales

  1. Utiliser AsNoTracking() pour lire-seulement les demandes afin de réduire les frais généraux de mémoire
  2. Éviter les demandes de N+1 Usage - Include() ou diviser les demandes de façon appropriée
  3. Utiliser des demandes compilées pour les modèles de demande répétés
  4. Examiner AsSplitQuery() pour le complexe inclut l'évitement des produits Cartesians
  5. Utiliser le battage pour plusieurs insertions/updates
  6. Projet à l'intention des DTO réduire rapidement le transfert de données et l'utilisation des mémoires
  7. Leverage ExecuteUpdate/ExecuteDelete (EF Core 7+) pour les opérations en vrac
  8. Always log and review generated SQL dans le cadre du développement

Lorsque vous travaillez avec PostgreSQL

  1. Utiliser la recherche complète-texte (mon guide) au lieu de LIKE Questions
  2. Leverage des colonnes JSONB pour les données sans schéma souple-
  3. Utiliser les types d'array pour les recouvrements effectués à l'intérieur des entités
  4. Configurer la mise en commun des connexions pour votre charge de travail
  5. Indiquer votre tsvector Les colonnes avec les indices GIN
  6. Utiliser les types de portée pour les intervalles de date/temps

Arrivée dans la partie 2

Dans l’article suivant, nous ' allons explorer:

  • Dapper: Le point d’intérêt microM SK1ORM
  • Raw ADO.NET/Npgsql: Performance et contrôle maximaux
  • Bibliothèques de cartographie des objets: Mapster vs AutoMapper
  • Approches hybrides: Combinaison du noyau EF et de l'acier Dapper
  • Indicateurs de rendement: RealM SK1comparations mondiales
  • Matrice de décision: Choix de l’outil approprié pour votre scénario

Références et lectures complémentaires

Articles connexes sur ce blog:


Dans la partie 2,, nous ' nous plongons dans le Dapper, raw NpgsqlM SK3 et étudierons comment combiner plusieurs approches pour optimiser la performance et l’entretien.

Finding related posts...
logo

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