Back to "Datenzugriff in der Analyse von ORMs und Mapping-Strategien"

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

Datenzugriff in der Analyse von ORMs und Mapping-Strategien

Wednesday, 03 December 2025

Beim Bau von Anwendungen für .-NET – eine der wichtigsten architektonischen Entscheidungen, die Sie treffen, ist der Umgang mit Datenzugriff und Objektmapping..- Das Ecosystem von .-NET bietet eine Vielzahl an Ansätzen, von vollwertigen ORMs bis hin zu bloßer Ausführung von --Metall SQL Jeder Ansatz kommt mit seinem eigenen Trade – --offs in Bezug auf Leistungsfähigkeit – ,-Entwicklerproduktivität – ,-Typ Sicherheit –,- und Aufrechterhaltungsfähigkeit –.-

In diesem umfassenden Zwei---Teile-Leitfaden erkunden wir uns mit den populärsten Datenzugriffsmustern in ..NET... Während wir PostgreSQL mit Npgsql in unseren Beispielen verwenden, gelten die Konzepte , und trade.MSC10offs gleichermaßen für SQL Server, ,. MySQ,. SQLite,. und andere relationale Datenbanken... Die Prinzipien bleiben derselbe ?-? nur der SQL-Dialekt und einige spezifische Merkmale unterscheiden sich von ..

Teil dieses Artikels konzentriert sich auf Entity Framework Core, ,-SQL-Generation und gemeinsame Fallstricke. Teil wird Dapper, ,, rohe ADO-.,NET, ,, Objektmappingbibliotheken und hybride Ansätze abdecken

Verwandte Artikel

Wenn Sie sich für praktische EF-Core-Implementierungen interessieren, schauen Sie sich meine anderen Artikel an

Inhaltsverzeichnis

Das Spektrum der Datenzugriffsansätze

Die Datenzugriffslandschaft von -.®NET kann als Spektrum visualisiert werden.

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

Wenn du dich von links nach rechts bewegst, erhältst du Leistung und Kontrolle, aber verlierst Bequemlichkeit und automatische Funktionen.

Vergleich des Datenzugriffs

Hier ist ein visueller Vergleich, wie jeder Ansatz mit einer typischen Abfrage umgeht.

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

Leistung vs. Entwickler Produktivität trade

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

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

Entity-Framework-Kern, der vollwertige ORM

Kern des Rahmens der Einrichtung ist Microsoft-Flaggschiff ORM.,, das eine vollständige Abstraktion über Ihre Datenbank bietet. Es unterstützt PostgreSQL durch die Npgsql-.-EntitätsschemaKörnung.-PostgreSQL Anbieter

Praktische Anleitung zum Einrichten von EF Core in Ihrem Projekt finden Sie unter Hinzufügen von Entity Framework für Blog-Posts.

Hauptmerkmale

  • Nachverfolgung ändern.: . Verfolgt automatisch Entity-Änderungen und generiert entsprechende SQL
  • WanderungsbewegungenCode- und Versionskontrolle des ersten Schemas EF-Migrationen auf dem richtigen Weg)
  • LINQ-AnbieterSichere Abfragen mit Hilfe von Sprachkonstrukten
  • Lazy-/-Eager LadenFlexible Beladungsstrategien für verwandte Unternehmen
  • Erweiterte PostgreSQL-FunktionenWeitere Informationen: http:europa.eu.intsiehe meinen Artikel),, JSON-Säulen,, Arrays,, Bereichstypen
  • Abhörer und VeranstaltungenErweiterbarkeitspunkte für Querschnitte

Beispiel: Basis CRUD mit EF-Kern

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 mit Raw SQL

EF Core unterstützt auch rohe SQL-Abfragen, wenn Sie mehr Kontrolle benötigen

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

Wann EF Core verwendet werden soll

Verwendung des EF-Kerns

  • Aufbau einer neuen Anwendung mit sich entwickelnden Schemaanforderungen
  • Sie benötigen eine starke Eingabe und Compilierung der Abfrage-Validierung
  • Migrationen und Schemaversionierung sind wichtig mein Migrationshandbuch)
  • Ihr Team arbeitet lieber mit Objekten über SQL
  • Sie verwenden komplexe Domänenmodelle mit Beziehungen
  • Entwicklungsgeschwindigkeit ist kritischer als Rohleistung
  • Du benötigst die Übertragbarkeit von cross---Datenbanken, obwohl PostgreSQL---spezifische Features dich in -) sperren.

Vermeiden Sie EF-Kern, wenn

  • Maximale Leistungsfähigkeit ist kritisch (-High--Throughput APIs,-Chargenverarbeitung)-
  • Sie haben komplexe SQL-Abfragen
  • Ihre Abfragen dont-'.t Karte gut, um Graphen zu Objekt
  • Sie benötigen feine Kontrolle über jede SQL-Anweisung
  • Die Speichernutzung ist eine kritische Einschränkung.
  • Sie arbeiten mit Legacy-Schemas, die nicht zu Konventionen führen

EF-Core-SQL-Generierung – :-Verstehen, was ausgeführt wird

Einer der wichtigsten Aspekte des Einsatzes von EF Core effektiv ist, zu verstehen, was SQL es erzeugt .. . EF Core hat deutlich verbessert SQL-Generation im Laufe der Jahre ?, .

SQL-Erzeugung anzeigen

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

Beispiel: Einfache Abfrage

LINQ-:

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

Erzeugte SQL-DatenEF-Kern):

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

Beachten Sie, wie EF-Core erzeugt clean-,. Effiziente SQL mit korrekter Parametrierung. EF-Kern setzt diesen Trend mit noch mehr Verbesserungen fort

Beispiel: Verbindung mit dem EF-Kern

Code des Herstellers:

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

Altes SQL-Kernmaterial (MSK) - kartesische Explosion

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"

Dies schafft eine Kartesisches Erzeugnis Wenn ein Beitrag 10 Kommentare ,, dass Zeile wiederholt wird 10 times

Beispiel: geteilte Abfragen

Code mit geteilter Abfrage

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

Erzeugte SQL-Daten Mehrsprachige Abfragen):

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

Dies beseitigt das kartesische Produkt und ist oft viel schneller für Sammlungen

Beispiel: :-gefiltert Einschließlich (-EF-Kern

Code des Herstellers:

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

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

Beispiel: JSON-Säulenfragen

Code des Herstellers:

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

Generiert SQL-:

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

EF Core kann JSON-Eigenschaftszugriff auf PostgreSQL-JSON-Betreiber übersetzen

Beispiel: Massenaktualisierung von (.EF-Kernen

Alte Art und Weise

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!

Neue Art und Weise

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

Erzeugte SQL-Datei (Single Query)

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

Das ist ein massiv Verbesserung Eine SQL-Anweisung anstelle von NB!

Beispiel: :-Gülle Löschen (-EF-Kern

Alte Art und Weise

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

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

Neue Art und Weise

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

Generiert SQL-:

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

Beispiel: Komplexe Aggregation

Code des Herstellers:

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

Generierter SQL-Kern (MSK)

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 Volltextsuche

Für einen tieferen Tauchgang in die vollständige --Textsuche siehe meinen Artikel auf Umsetzung der Volltextsuche mit EF Core.

Code des Herstellers:

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

Generiert 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

Kritische EF-Kernwarnungen und Pitfalls

Änderungen der Tracking-Speicherlecks

Das Problem

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

Warum es ein Problem ist

  • Verfolgte Entitäten bleiben für die Lebensdauer der DbContext
  • Der Change-Tracker hält Referenzen bereit.
  • Lange, langanhaltende Kontexte von (e.eg.,e singletons)e
  • Im Kontext von ASPA.-NET wird standardmäßig scoped.(...good.!)...
  • Aber wenn du getrackte Entities zwischengespeichert hast

Die Lösung

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

Proxy-Erzeugung und lazy Belastung Gefahren

KRITISCHE WARNUNG

Lazy Belastungs-Proxys Caching GARANTIERTE MEMORY LEAK

Wenn Sie DbContext-Instanzen oder Cache-Entitäten mit aktivierten Proxies zwischenspeichern WIRD Der Proxy-Mechanismus hält Verweise auf den DbContext.,.Verhindert die Müllsammelung...Dies ist einer der häufigsten und gefährlichsten Fehler in EF Core-Anwendungen.

FaustregelImmer Sammlungen explizit mit einbeziehen .Include()Verwenden Sie Proxies nur dann, wenn Sie die Tradeoffs vollständig verstehen und niemals ,... jemals Proxy-Entitäten zwischenspeichern.

Problem-Abfrage Nightmare

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

Was passiert?

  1. Erste Abfrage lädt alle Beiträge
  2. Für jede StelleZugriff Category löst eine Datenbankabfrage aus
  3. Für jede StelleZugriff Comments löst eine weitere Abfrage aus
  4. Wenn du gerade ausgeführt hast Abfragen!

Problem: Proxy-Proxy-Caching-Speicherleak

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

Warum ist dies katastrophal?

  • Proxy-Entitäten pflegen einen Verweis auf ihre DbContext
  • Das DbContext pflegt einen Verweis auf alle verfolgten Einheiten
  • Ihr Cache verhindert nun, dass der gesamte Objektgraph gesammelt wird
  • Jedes Mal, wenn Sie auf eine Navigationseigenschaft zugreifen, kann es Abfragen mit dem alter Kontext mit Zwischenspeichern
  • Speicher wächst ungebunden, wenn Sie mehr Daten laden
  • You-'-ll läuft schließlich aus dem Speicher oder Abgasanschluss Pools

Die Lösung ist explizit

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

Wenn Proxies akzeptabel sein könnten, verstehen Sie die Tradeoffs.

Lazy Lade-Proxys könnten nur dann akzeptabel sein, wenn

  1. Sie haben kurz, kurz und lang Scoped-Kontexten (MSK) für HTTP-Anfragen
  2. Sie niemals Cache-Entities
  3. Sie sind in Ordnung mit der Abfrageleistung von N.+1.
  4. Sie werden das Prototyping durchführen und später optimieren.
  5. Ihr Team versteht die Implikationen

Aber selbst dann Include() ist fast immer die bessere Wahl, weil

  • Es macht Daten laden explizit und offensichtlich
  • Es handelt sich um einfacher zu optimieren Sie können sehen, was geladen wird.
  • Es ist verhindert unbeabsichtigte Abfragen
  • Es funktioniert korrekt mit Caching und langanhaltenden Kontexten
  • Es ist das empfohlener Ansatz vom EF Core-Team

DbContext Lebenslange Probleme

Weitere Details zur Verwaltung von DbContext-Lebensdauer in der Produktion finden Sie in meinem Artikel EF-Migrationen auf dem richtigen Weg.

Das Problem

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

Warum es ' falsch ist

  • DbContext wird nicht gewindestark
  • Gleichzeitige Anfragen verursachen Datenkorruption
  • Tracker auf unbestimmte Zeit wechseln
  • Ausschöpfung des Verbindungspools
  • Stale Daten aus dem Cache

Die Lösung

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

Nicht beabsichtigt Beinhaltet in Navigationseigenschaften

Das Problem

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

Die Lösung

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

Async vs Sync Mischen

Das Problem

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

Die Lösung

// ✅ 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 - neue und durchbrechende Veränderungen

Mit EF-Kern veröffentlicht neben -.-NET -10, , gibt es mehrere wichtige Änderungen zu beachten, wenn Upgrades-. Für die vollständige Liste finden Sie unter Bruch der Veränderungen im EF-Core.

Anforderungen an die Laufzeit

**Der EF-Kern erfordert ,. ,NET , 10 ,**Es wird nicht auf .-NET ausgeführt.Dies ist die wichtigste Änderung, um sicherzustellen, dass Ihre Projektziele net10.0 vor dem Aufrüsten

Parameterisierte Sammlungsübersetzung

EF Core ändert, wie Contains() mit In---Memory-Sammlungen wird in SQL , . , zuvor , verwendet , , EF Core OpenJson() SQL Server, ) oder ähnliches . Parameter Arrays die einen besseren Abfrageplan für Caching zur Verfügung stellen

AuswirkungenSie können verschiedene SQL-generierte für Abfragen sehen, z.B.

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

EF-Kerne

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

EF-Kernmittel (MSK)

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

Wenn Sie Leistungsregressionen erlebenKehren Sie zum alten Verhalten zurück

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

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

AusführenUpdateAsync Signature Change

Das ExecuteUpdateAsync Signatur hat sich geändert, um nicht zu unterstützen --Expression Lambdas-.. Dies ist flexibler aber bricht Code, der Ausdruck Bäume programmatisch gebaut:

Alte Art und Weise

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

Neu im EF-Kern

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

Umfassende Typ-Säulenbezeichnung

EF Core ändert, wie verschachtelte komplexe Typ Spalten benannt werden, um Datenkorruption zu verhindern.

EF-Kern

NestedComplex_Property

EF-Kern

OuterComplex_NestedComplex_Property

MigrationsauswirkungenWenn Sie vorhandene Tabellen mit komplexen Typen haben, müssen Sie ggf. Spalten umbenennen oder explizite Spaltennamen konfigurieren.

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

SQL Server Spezielle Änderungen von Azure SQL

JSON-Datentyp Standard

Für Azure SQL Database oder SQL Server Standardwerte für das neue native JSON Datentyp statt NVARCHAR(MAX).

Abmelden Wenn Sie Rückwärtskompatibilität benötigen

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

Checkliste aktualisieren

Beim Upgrade von EF Core auf EF Core

  1. Aktualisierung des Zielrahmens auf net10.0
  2. Alle aktualisieren Microsoft.EntityFrameworkCore.* Pakete zu -10.-x
  3. Aktualisierung Npgsql.EntityFrameworkCore.PostgreSQL bis zu 10.-x
  4. Überprüfung von Abfragen mit Contains() mit Sammlungen für Performance-Änderungen
  5. Testen Sie jeden Code, der programmatisch baut ExecuteUpdateAsync Ausdrücke
  6. Prüfen Sie komplexe Typ Spaltennamen, wenn verschachtelte Komplextypen verwendet werden
  7. Überprüfe die Verwendung von SQL Server JSON-Spalten, wenn Azure SQL®/®SQL Server verwendet wird.

Was ist '?Neuen Höhepunkte

  • Verbesserte LINQ-ÜbersetzungBessere SQL-Generierung für komplexe Abfragen
  • Nicht-Ausdruck Lambdas in AusführenUpdateAsyncMehr Flexibilität bei Massenaktualisierungen
  • Bessere ParameterbehandlungVerbessertes Abfrageplan-Caching
  • Verstärkte JSON-UnterstützungNative JSON-Typ auf SQL Server
  • LeistungsverbesserungenSchnellere Materialisierung und Änderungsverfolgung

Leistungsmerkmale

  • AbfrageleistungIm Vergleich zu Dapper für einfache Abfragen
  • SpeichernutzungHöher durch Änderungsverfolgung und Proxy-Generierung
  • Erste AbfrageLangsame Zusammenstellung und Caching
  • Nachfolgende FragenSchneller durch kompilierten Abfrage-Cache
  • Einfügungen-/-UpdatesAutomatische Änderungsverfolgung fügt Overhead hinzu
  • MassenbetriebSchlechte Leistung mit Standard-Methoden EFCore-.-BulkExtensionen oder ExecuteAktualisierung/-AusführenLöschen Sie im EF-Kern aus.

Best Practices für EF Core

Allgemeine Leitlinien

  1. AsNoTracking verwenden für nur Abfragen von Lese--, um den Speicherüberkopf zu reduzieren
  2. Abfragen vermeiden Verwendung Include() oder aufgeteilte Abfragen angemessen
  3. Kompilierte Abfragen verwenden für wiederholte Abfragemuster
  4. Betrachten Sie alsSplitQuery-() für Komplex beinhaltet, um kartesische Produkte zu vermeiden
  5. Chargen verwenden für mehrere Inserts-/-Updates
  6. Projekt zu DTOs frühen Datenübertragung und Speicherverbrauch zu reduzieren
  7. Leverage ExecuteAktualisierung/-AusführenLöschen Für Massengut-Betriebe
  8. Immer loggen und überprüfen generiert SQL in der Entwicklung

Bei der Arbeit mit PostgreSQL

  1. Volle Textsuche verwenden MerkmaleMein FührerNicht mehr als LIKE Abfragen
  2. Leverage-JSONB-Spalten für flexible Schema-Daten
  3. Array-Typen verwenden für Sammlungen innerhalb von Einheiten
  4. Konfigurieren von Verbindungspooling richtig für Ihre Arbeitsbelastung
  5. Indexieren Sie Ihre tsvector Spalten mit GIN-Indizes
  6. Verwendungsbereichstypen für Datums-/-Zeitbereiche

In Teil kommend

Im nächsten Artikel erkunden wir uns mit :

  • DapperDie Mikro-- ORM Sweet Spot
  • Rohes ADO-.-NETT/-NpgsqlMaximale Leistung und Kontrolle
  • Objektmapping-BibliothekenKartenster gegen Automapper
  • Hybride AnsätzeKombination von EF-Kern und Dapper-Mustern aus (CQRS
  • LeistungsvergleicheVergleich der realen Werte der Welt
  • EntscheidungsmatrixWählen Sie das richtige Werkzeug für Ihr Szenario

Literaturhinweise und weitere Lektüre

Verwandte Artikel auf diesem Blog


Im Teil tauchen wir in Dapper , , , roh Npgsql ,, , und erforschen Sie, wie man mehrere Ansätze für optimale Leistung und Wartungsfähigkeit kombinieren

logo

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