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

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

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:

- [Het toevoegen van een entiteitskader voor blogberichten (deel 1)](/blog/addingentityframeworkforblogpostspt1) - Het opzetten van EF Core vanaf nul
- [EF Migraties De juiste weg](/blog/efmigrationstherightway) - Hoe om te gaan met migraties goed in de productie
- [Volledige tekst zoeken (Pt 1)](/blog/textsearchingpt1) - Het uitvoeren van PostgreSQL full-text zoeken met EF Core
- [Moderne CQRS en Event Sourcing](/blog/moderncqrsandeventsourcing) - Geavanceerde architectonische patronen met EF Core

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

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

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

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

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

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

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

### Prestaties versus Productiviteit van de Ontwikkelaar trade-off

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

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

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

[Kern van het entiteitskader](https://learn.microsoft.com/en-us/ef/core/) is Microsoft's vlaggenschip ORM, het verstrekken van een volledige abstractie over uw database. Het ondersteunt PostgreSQL via de [Npgsql.EntityFrameworkCore.PostgreSQL](https://www.npgsql.org/efcore/) provider.

Voor praktische begeleiding bij het opzetten van EF Core in uw project, zie mijn artikel over [Het toevoegen van een entiteitskader voor blogberichten](/blog/addingentityframeworkforblogpostspt1).

### 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](/blog/efmigrationstherightway))
- **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](/blog/textsearchingpt1)), JSON kolommen, arrays, range types
- **Onderceptoren en gebeurtenissen**: Extensibiliteitspunten voor horizontale problemen

### Voorbeeld: Basic CRUD met EF Core

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

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

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

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

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

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

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

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

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

### EF-kern met ruwe SQL

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

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

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

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

    await _context.Database.OpenConnectionAsync();

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

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

    return results;
}
```

### 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](/blog/efmigrationstherightway))
- 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

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

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

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

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

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

### Voorbeeld: Eenvoudige zoekopdracht

**C# LINQ:**

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

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

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

Merk op hoe EF Core 8+ schone, efficiënte SQL genereert met de juiste parameterisatie. [EF-kern 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) zet deze trend voort met nog meer verbeteringen.

### Voorbeeld: Samenvoegen met Include (Voor EF Core 5)

**C# Code:**

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

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

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

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

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

**Gegenereerde SQL (Multiple Queries):**

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

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

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

### Voorbeeld: Gefilterd Include (EF Core 5+)

**C# Code:**

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

**Gegenereerde SQL:**

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

### Voorbeeld: JSON Column Queries (EF Core 7+)

**C# Code:**

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

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

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

**Gegenereerde SQL:**

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

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

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

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

**Nieuwe manier (EF-kern 7+):**

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

**Generated SQL (Single Query!):**

```sql
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:**

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

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

**Nieuwe manier:**

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

**Gegenereerde SQL:**

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

### Voorbeeld: Complexe aggregatie

**C# Code:**

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

**Gegenereerde SQL (EF-kern 8+/10):**

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

### 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](/blog/textsearchingpt1).

**C# Code:**

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

**Gegenereerde SQL:**

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

## Kritieke EF Core Waarschuwingen en Pitfalls

### 1. Het volgen van geheugenlekken wijzigen

**Het probleem:**

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

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

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

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

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

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

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

### 2. 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**

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

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

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

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

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

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

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

        _context = new BlogDbContext(optionsBuilder.Options);
    }

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

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

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

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

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

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

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

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

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

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

**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](/blog/efmigrationstherightway).

**Het probleem:**

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

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

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

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

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

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

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

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

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

### 4. Onbedoelde Inclusief in navigatie-eigenschappen

**Het probleem:**

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

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

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

await _context.SaveChangesAsync();

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

**De oplossing:**

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

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

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

### 5. Async vs Sync Mixing

**Het probleem:**

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

    return post;
}

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

**De oplossing:**

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

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

## EF Core 10: Wat is nieuw en brekende veranderingen

Met [EF-kern 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew) 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](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes).

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

```csharp
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):**

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

**EF-kern 10 (parameters):**

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

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

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

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

#### 2. 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):**

```csharp
// 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:**

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

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

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

```csharp
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](https://github.com/borisdj/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](/blog/textsearchingpt1)) 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

- [Kerndocumentatie van het entiteitskader](https://learn.microsoft.com/en-us/ef/core/)
- [Npgsql Entity Framework Core Provider](https://www.npgsql.org/efcore/)
- [EF Core Performance](https://learn.microsoft.com/en-us/ef/core/performance/)
- [Wat is er nieuw in EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/whatsnew)
- [Breaking Changes in EF Core 10](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes)
- [Wat is er nieuw in EF Core 8](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew)
- [PostgreSQL-documentatie](https://www.postgresql.org/docs/)

**Gerelateerde artikelen op dit blog:**

- [Het toevoegen van een entiteitskader voor blogberichten](/blog/addingentityframeworkforblogpostspt1)
- [EF Migraties De juiste weg](/blog/efmigrationstherightway)
- [Volledige tekst zoeken met EF-kern](/blog/textsearchingpt1)
- [Moderne CQRS en Event Sourcing](/blog/moderncqrsandeventsourcing)

---


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