Back to "Gegevenstoegang in .NET: vergelijking van ORM's en Mapping Strategieën (deel 1 - Framework Core van de entiteit)"

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

Gegevenstoegang in .NET: vergelijking van ORM's en Mapping Strategieën (deel 1 - Framework Core van de entiteit)

Wednesday, 03 December 2025

Bij het bouwen van .NET toepassingen, een van de belangrijkste architectonische beslissingen die u zult maken is hoe om te gaan met data toegang en object mapping. Het .NET ecosysteem biedt een rijke verscheidenheid van benaderingen, van full-featured ORMs tot bare-metal SQL uitvoering. Elke aanpak wordt geleverd met zijn eigen trade-offs in termen van prestaties, ontwikkelaar productiviteit, type veiligheid, en onderhoud.

In deze uitgebreide twee-delige gids, zullen we de meest populaire data toegang patronen in .NET te verkennen. Terwijl we PostgreSQL met Npgsql gebruiken in onze voorbeelden (sinds dat is wat deze blog powers), de concepten, patronen, en trade-offs gelden gelijkelijk voor SQL Server, MySQL, SQLite, en andere relationele databases. De principes blijven hetzelfde - alleen de SQL dialect en een aantal specifieke kenmerken verschillen.

Deel 1 (dit artikel) richt zich op de kern van het entiteitskader, SQL-generatie en gemeenschappelijke valkuilen. Deel 2 zal betrekking hebben op Dapper, rauwe ADO.NET, object mapping bibliotheken, en hybride benaderingen.

Gerelateerde artikelen

Als je geïnteresseerd bent in praktische EF Core implementaties, bekijk dan mijn andere artikelen:

Inhoudstabel

Het spectrum van gegevenstoegangsbenaderingen

Het .NET data access landschap kan worden gevisualiseerd als een spectrum:

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

Als je van links naar rechts, krijg je prestaties en controle, maar verlies gemak en automatische functies. Laten we elke aanpak in detail te onderzoeken.

Vergelijking van gegevenstoegangsstroom

Hier is een visuele vergelijking van hoe elke aanpak omgaat met een typische query:

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

Prestaties versus Productiviteit van de Ontwikkelaar 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

Kern van het kader van de entiteit: de volledig kenmerkende ORM

Kern van het entiteitskader is Microsoft's vlaggenschip ORM, het verstrekken van een volledige abstractie over uw database. Het ondersteunt PostgreSQL via de Npgsql.EntityFrameworkCore.PostgreSQL provider.

Voor praktische begeleiding bij het opzetten van EF Core in uw project, zie mijn artikel over Het toevoegen van een entiteitskader voor blogberichten.

Belangrijkste kenmerken

  • Tracking wijzigen: Het automatisch bijhouden van entiteit verandert en genereert passende SQL
  • Migratie: Code-first schemabeheer en versiecontrole (zie EF Migraties De juiste weg)
  • LINQ-provider: Type-veilige queries met behulp van C#-taalconstructies
  • Lazy/Eager Laden: Flexibele laadstrategieën voor verwante entiteiten
  • Geavanceerde PostgreSQL functies: Full-text search (zie mijn artikel), JSON kolommen, arrays, range types
  • Onderceptoren en gebeurtenissen: Extensibiliteitspunten voor horizontale problemen

Voorbeeld: Basic CRUD met 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-kern met ruwe SQL

EF Core ondersteunt ook rauwe SQL queries als je meer controle nodig hebt:

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

Wanneer EF Core moet worden gebruikt

Gebruik EF Core wanneer:

  • Bouwen aan een nieuwe toepassing met veranderende schema-eisen
  • U heeft een sterke type- en compileer-tijd-queryvalidatie nodig
  • Migraties en schemaversies zijn belangrijk (zie mijn migratiegids)
  • Uw team werkt liever met objecten dan met SQL
  • Je gebruikt complexe domeinmodellen met relaties
  • Ontwikkelingssnelheid is kritischer dan ruwe prestaties
  • Je hebt cross-database draagbaarheid nodig (hoewel PostgreSQL-specifieke functies je insluiten)

Vermijd EF-kern wanneer:

  • Maximale prestaties zijn cruciaal (high-throughput API's, batch processing)
  • U heeft complexe, met de hand afgestemde SQL-queries
  • Uw vragen niet goed in kaart brengen om grafieken object
  • Je hebt fijnkorrelige controle nodig over elke SQL statement.
  • Geheugengebruik is een kritische beperking (verandering tracking overhead)
  • Je werkt met legacy schema's die niet in kaart brengen naar conventies.

EF Core SQL Generation: Begrijpen wat wordt uitgevoerd

Een van de belangrijkste aspecten van het effectief gebruik van EF Core is begrijpen wat SQL het genereert. EF Core heeft aanzienlijk verbeterd SQL generatie door de jaren heen, maar het is cruciaal om te controleren of de vragen worden verzonden naar PostgreSQL.

Bekijken Generated SQL

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

Voorbeeld: Eenvoudige zoekopdracht

C# LINQ:

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

Gegenereerd SQL (EF-kern 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

Merk op hoe EF Core 8+ schone, efficiënte SQL genereert met de juiste parameterisatie. EF-kern 10 zet deze trend voort met nog meer verbeteringen.

Voorbeeld: Samenvoegen met Include (Voor EF Core 5)

C# Code:

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

Oude SQL (EF-kern 3.1 - Cartesiaanse explosie):

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"

Dit creëert een Cartesiaans product - als een bericht 10 reacties heeft, wordt die rij 10 keer herhaald!

Voorbeeld: Splits zoekopdrachten (EF-kern 5+)

C# Code met Split Query:

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

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

Dit elimineert het Cartesiaanse product en is vaak veel sneller voor collecties!

Voorbeeld: Gefilterd Include (EF Core 5+)

C# Code:

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

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

Voorbeeld: JSON Column Queries (EF Core 7+)

C# Code:

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

Gegenereerde SQL:

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

EF Core 7+ kan JSON eigendomstoegang vertalen naar PostgreSQL JSON operators!

Voorbeeld: Bulk Update (EF Core 7+ ExecuteUpdate)

Oude weg (inefficiënt):

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!

Nieuwe manier (EF-kern 7+):

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

Generated SQL (Single Query!):

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

Dit is een massaal verbetering - één SQL statement in plaats van N!

Voorbeeld: Bulk Delete (EF Core 7+)

Oude manier:

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

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

Nieuwe manier:

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

Gegenereerde SQL:

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

Voorbeeld: Complexe aggregatie

C# Code:

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

Gegenereerde SQL (EF-kern 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 volledige tekst zoeken

Voor een diepere duik in full-text search, zie mijn artikel over het uitvoeren van full-text search met EF Core.

C# Code:

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

Gegenereerde 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

Kritieke EF Core Waarschuwingen en Pitfalls

1. Het volgen van geheugenlekken wijzigen

Het probleem:

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

Waarom het een probleem is:

  • Getraceerde entiteiten blijven in het geheugen voor de levensduur van de DbContext
  • De change tracker behoudt referenties, het voorkomen van vuilnisverzameling
  • Langlevende contexten (bv. singletons) = geheugenlek
  • In ASP.NET Core wordt de context standaard bekeken (goed!)
  • Maar als je entiteiten opspoort, zit je in de problemen.

De oplossing:

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 Generatie en Lazy Loading Gevaren

CRITISCHE WAARSCHUWING: GEBRUIK EF CORE PROXIES NIET ALS U CACHEN

Lazy loading proxies + caching = GEGARANTIED MEMORY LEAK

Als u cache DbContext instanties of cache entiteiten geladen met proxies ingeschakeld, u ZAL Het proxy-mechanisme behoudt verwijzingen naar de DbContext, waardoor vuilnisverzameling wordt voorkomen. Dit is een van de meest voorkomende en gevaarlijke fouten in EF Core-toepassingen.

Vuistregel: Altijd collecties expliciet opnemen met .Include(). Gebruik alleen proxies als je volledig begrijpt de tradeoffs en nooit, ooit cache proxy entiteiten.

Probleem 1: De N+1 Query Nachtmerrie

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

Wat er gebeurt:

  1. Eerste query laadt alle berichten
  2. Voor elke post, toegang tot Category activeert een database query
  3. Voor elke post, toegang tot Comments triggers een andere query
  4. Als je 100 berichten hebt, heb je net uitgevoerd 201 vragen!

Probleem 2: Proxy + Caching = Geheugenlek

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

Waarom dit catastrofaal is:

  • Proxy-entiteiten behouden een verwijzing naar hun DbContext
  • De DbContext behoudt een verwijzing naar alle tracked entiteiten
  • Uw cache voorkomt nu dat de gehele objectgrafiek wordt verzameld
  • Elke keer als u toegang tot een navigatie-eigenschap, kan het leiden tot vragen met behulp van de oude, gecachede context
  • Geheugen groeit ongebonden als u meer gegevens laadt
  • Je zult uiteindelijk zonder geheugen of uitlaatverbinding zwembaden

De oplossing: Be Explicit

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

Wanneer Proxies aanvaardbaar zou kunnen zijn (Begrijp de tradeoffs):

Luie laadproxies kunnen alleen aanvaardbaar zijn wanneer:

  1. Je hebt kortlevend scoped contexts (bv. per HTTP-verzoek)
  2. Jij nooit cache-entiteiten
  3. Je bent oké met N+1 query prestaties
  4. Je bent prototyping en zal later optimaliseren
  5. Uw team begrijpt de implicaties volledig.

Maar zelfs dan, expliciet Include() is bijna altijd de betere keuze omdat:

  • Het maakt data laden expliciet en duidelijk
  • Het is makkelijker te optimaliseren (Je kunt zien wat er geladen wordt)
  • Het voorkomt toevallige N+1 queries
  • Het werkt correct met caching en langlevende contexten
  • Het is de aanbevolen aanpak door het EF-kernteam

3. DbContext Lifetime kwesties

Voor meer informatie over het beheer van DbContext lifetime in de productie, zie mijn artikel over EF Migraties De juiste weg.

Het probleem:

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

Waarom het verkeerd is:

  • DbContext is niet thread-safe
  • Gelijktijdige verzoeken zullen leiden tot gegevenscorruptie
  • Veranderen tracker groeit voor onbepaalde tijd
  • Verbindingspool uitputting
  • Stamgegevens van cache

De oplossing:

// ✅ 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. Onbedoelde Inclusief in navigatie-eigenschappen

Het probleem:

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

De oplossing:

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

Het probleem:

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

De oplossing:

// ✅ 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: Wat is nieuw en brekende veranderingen

Met EF-kern 10 vrijgegeven naast .NET 10, zijn er verschillende belangrijke veranderingen om bewust te zijn van bij het upgraden. Voor de volledige lijst, zie Veranderingen in EF Core 10 breken.

Vereisten inzake runtime

EF Core 10 vereist .NET 10. Het zal niet draaien op .NET 8, .NET 9, of .NET Framework. Dit is de belangrijkste verandering - zorg ervoor dat uw project doelstellingen net10.0 vóór het upgraden.

Opvragen-verwante breaking wijzigingen

1. Genormaliseerde verzameling Vertaling (Standaard gewijzigd)

EF Core 10 verandert hoe Contains() met in-geheugen collecties wordt vertaald naar SQL. Eerder, EF Core gebruikt OpenJson() (SQL Server) of vergelijkbaar. Nu is het standaard om parameterarrays die betere query plan caching bieden.

Gevolgen: U ziet mogelijk verschillende SQL gegenereerd voor vragen als:

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

EF-kern 8/9 (OpenJson):

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

EF-kern 10 (parameters):

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

Als u prestatieregressies ervaart, keer terug naar het oude gedrag:

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

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

2. UitvoerenUpdateAsync Handtekening Wijzigen

De ExecuteUpdateAsync De ondertekening is gewijzigd om niet-expressie lambda's te ondersteunen. Dit is flexibeler, maar breekt code die expressiebomen programmatisch bouwde:

Oude weg (EF-kern 7-9):

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

Nieuw in EF Core 10 - Non-expressie 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. Complexe type kolomnaam

EF Core 10 verandert hoe geneste complexe type kolommen worden genoemd om data corruptie te voorkomen:

EF-kern 9:

NestedComplex_Property

EF-kern 10:

OuterComplex_NestedComplex_Property

Migratie-impact: Als je bestaande tabellen met complexe types hebt, moet je misschien kolommen hernoemen of expliciete kolomnamen configureren:

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

SQL-server / Azure SQL-specifieke wijzigingen

JSON Data Type Standaard

Voor Azure SQL Database of SQL Server 2025 (compatibiliteitsniveau 170+), standaard EF Core 10 naar de nieuwe native JSON gegevenstype in plaats van NVARCHAR(MAX).

Afmelden (als u achteruit compatibiliteit nodig heeft):

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

Checklist upgraden

Bij upgraden van EF Core 8/9 naar EF Core 10:

  1. Bijwerken van het doelkader aan net10.0
  2. Alles bijwerken@info:whatsthis Microsoft.EntityFrameworkCore.* pakketten tot 10.x
  3. Update Npgsql.EntityFrameworkCore.PostgreSQL tot 10.x
  4. Beoordeling queries met behulp van Contains() met collecties voor prestatieveranderingen
  5. Test elke code die programmatisch opbouwt ExecuteUpdateAsync expressies
  6. Controleer complexe kolomnamen als u nested complexe types gebruikt
  7. Beoordeling SQL Server JSON kolomgebruik als u Azure SQL/SQL Server 2025 gebruikt

Wat is er nieuw (Highlights)

  • Verbeterde LINQ-vertaling: Betere SQL generatie voor complexe vragen
  • Niet-expressie lambda's in UitvoerenUpdateAsync: Meer flexibiliteit in bulk updates
  • Betere parameterafhandeling: Verbeterde query plan caching
  • Verbeterde JSON-ondersteuning: Inheems JSON type op SQL Server 2025
  • Prestatieverbeteringen: Snellere materialisatie en change tracking

Prestatiekenmerken

  • Query Performance (Query Performance): 20-50% overhead in vergelijking met Dapper voor eenvoudige vragen
  • Geheugengebruik: Hoger als gevolg van het veranderen van tracking en proxy generatie
  • Eerste zoekopdracht: Langzaam (query compilatie en caching)
  • Latere zoekopdrachten: Sneller als gevolg van gecompileerde query cache
  • Invoegen/bijwerken: Automatische change tracking voegt overhead
  • Bulkbewerkingen: Slechte prestaties met standaardmethoden (zie EFCore.BulkExtensions of UitvoerenUpdate/UitvoerenVerwijderen in EF Core 7+)

Beste praktijken voor EF-kern

Algemene richtsnoeren

  1. Gebruik AsNoTracking() voor alleen-lezen vragen om geheugen overhead te verminderen
  2. Vermijd N+1 queries - gebruik Include() of split-query's op de juiste manier
  3. Gecompileerde queries gebruiken voor herhaalde zoekpatronen
  4. Overweeg AsSplitQuery() voor complex omvat om Cartesiaanse producten te vermijden
  5. batching gebruiken voor meerdere invoegsels/updates
  6. Project naar DTO's vroeg om gegevensoverdracht en geheugengebruik te verminderen
  7. Leverage UitvoerenUpdate/ExecuteDelete (EF-kern 7+) voor bulktransacties
  8. Altijd loggen en beoordelen gegenereerde SQL in ontwikkeling

Bij het werken met PostgreSQL

  1. Zoeken naar volledige tekst gebruiken kenmerken (mijn gids) in plaats van LIKE queries
  2. Kolommen "JSONB" en "JSONB" voor flexibele schema-loze gegevens
  3. arraytypes gebruiken voor collecties binnen entiteiten
  4. Verbindingspooling configureren goed voor uw werklast
  5. Indexeer uw tsvector Kolommen met GIN-indexen
  6. Soorten bereik gebruiken voor datum/tijdbereiken

Komt boven in deel 2

In het volgende artikel, zullen we verkennen:

  • Dapper: De micro-ORM-sweetvlek
  • Raw ADO.NET/Npgsql: Maximale prestaties en controle
  • Object Mapping Bibliotheken: Mapster vs AutoMapper
  • Hybride naderingen: Het combineren van EF Core en Dapper (CQRS patroon)
  • Prestatiebenchmarks: Vergelijkingen in de reële wereld
  • Decision Matrix: Het kiezen van de juiste tool voor uw scenario

Referenties en verdere lezing

Gerelateerde artikelen op dit blog:


In deel 2 duiken we in Dapper, rauwe Npgsql, en verkennen we hoe we meerdere benaderingen kunnen combineren voor optimale prestaties en duurzaamheid.

logo

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