EF Core best practices

Effiziente nutzung des ORM Frameworks
Mark N.
02/2026

Effiziente Nutzung des ORM‑Frameworks von .NET, um maximale Leistung und stabile Abfragen zu erzielen

Entwickler arbeiten früher oder später mit relationalen Datenbanken — unabhängig von der verwendeten Programmiersprache. Allen gemeinsam ist der Einsatz von objektrelationalen Mapper‑Frameworks (ORM), die den Zugriff auf Datenbanken vereinfachen. Obwohl das grundlegende Konzept in allen ORM‑Frameworks ähnlich ist, unterscheiden sie sich erheblich in Implementierung, Nutzung und Optimierungsmöglichkeiten, um effiziente, schnelle SQL‑Abfragen mit minimalen Round‑Trips zu erreichen.

Entity Framework Core (EF Core) ist ein vielseitiges ORM‑Framework für .NET, das Flexibilität, Leistung und Funktionalität verbindet. Dieser Artikel stellt zehn Best Practices für EF Core vor, die häufig offensichtlich erscheinen, jedoch nicht immer intuitiv sind und viele Performance‑Fallstricke vermeiden.

Beispielcode und das zugehörige Repository stehen auf GitHub zur Verfügung.

Warum lohnt es sich, Best Practices zu befolgen?

Die Arbeit mit einer Datenbank in EF Core lässt sich mit dem Bau eines Holzgegenstands mit Hammer und Nägeln vergleichen: Der richtige Einsatz der Werkzeuge führt zu stabilen Ergebnissen; der falsche Weg ist mühsam, langsam und ineffizient.

Verwendung von Indizes (Indexing)

Ein Index ist ein Datenbankobjekt, das Abfragen erheblich beschleunigt. Ohne Indizes muss jede Abfrage einen vollständigen Tabellenscan durchführen — vergleichbar mit dem Durchlesen jedes Buchkapitels, um ein Wort zu finden. Ein Index fungiert wie ein Buchregister und ermöglicht eine gezielte Suche.

Beispiel für die Konfiguration mit der Fluent API:

1protected override void OnModelCreating(ModelBuilder modelBuilder) 
2{ 
3    base.OnModelCreating(modelBuilder); 
4 
5    modelBuilder.Entity<Blog>() 
6        .HasIndex(b => b.Url);

Ein zusammengesetzter Index (Composite Index) kann ebenfalls konfiguriert werden:

1modelBuilder.Entity<Blog>() 
2    .HasIndex(b => new { b.Url, b.Title });

Es ist zu beachten, dass Indizes von Natur aus nicht eindeutig sind und explizit mit der Methode IsUnique() als eindeutig gekennzeichnet werden müssen.

Projektionen: „Nur das laden, was wirklich gebraucht wird“

Ein häufiger Fehler besteht darin, komplette Entitäten zu laden und anschließend im Speicher in DTOs umzuwandeln. Dadurch werden unnötige Daten abgerufen, was Speicherverbrauch und Antwortzeiten erhöht.

Ohne Projektion:

1app.MapGet("api/blogs/no-projection", async (CoreDbContext db) => { 
2        var blogs = await db.Blogs.ToListAsync(); 
3 
4        // Entities are loaded into memory, and then projected into DTOs 
5        return Results.Ok(blogs.Select(x => new BlogDto(x.Id, x.Url, x.Title, x.Content))); 
6 
7    }) 
8    .WithTags("Blogs") 
9    .WithName("GetBlogsWithoutProjection"); 

Erzeugt folgende SQL-Abfrage:

SELECT b."Id", b."Content", b."Created", b."Modified", b."Title", b."Url"
FROM "Blogs" AS b 

Die Abfrage holt alle verfügbaren Informationen für die gegebene Entität, selbst wenn wir sie nicht benötigen. Da diese Daten im Arbeitsspeicher projiziert werden, tragen ungenutzte Daten lediglich zu unnötigem Speicherverbrauch bei. In kleinem Maßstab mag das irrelevant sein, doch je größer die Entität, desto schlechter wird die Abfrage.

Mit Projektion:

1app.MapGet("api/blogs/projection", async (CoreDbContext db) => { 
2        var blogs = await db.Blogs 
3            .Select(x => new BlogDto(x.Id, x.Url, x.Title, x.Content)) 
4            .ToListAsync(); 
5 
6        return Results.Ok(blogs); 
7    }); 

Erzeugt folgende SQL-Abfrage:

SELECT b."Id", b."Url", b."Title", b."Content" 
FROM "Blogs" AS b 

Es ist gut zu wissen, dass auch anonyme Objekte für Abfrageprojektionen verwendet werden können! Dies ist nützlich, wenn ein Sonderfall auftritt und die Erstellung eines vollständigen Datenobjekts dafür keinen Mehrwert bietet.

Darüber hinaus vereinfacht die Auslagerung der Projektion in eine statische Methode nicht nur den Code, sondern ermöglicht auch die Verwendung von Pattern Matching, da ein Ausdrucksbaum keine Pattern-Matching-Konstrukte wie is not null oder Switch-Ausdrücke enthalten darf.

Explizites Laden (Eager Loading) gezielt einsetzen

Beim Abrufen verknüpfter Daten kann unbedachtes Eager Loading zu einer kartesischen Explosion führen — einer unerwartet hohen Datenmenge aufgrund von Joins.

Beispiel ohne Projektion:

1app.MapGet("api/blogs/no-projection/with-author", async (CoreDbContext db) => { 
2        var blogs = await db.Blogs 
3            .AsNoTracking() 
4            .Include(b => b.Author) 
5            .ToListAsync(); 
6 
7        return Results.Ok(blogs.Select(x => BlogWithAuthorDto.CreateInstance(x.Id, x.Url, x.Title, x.Content, x.Author.Id, x.Author.Pseudonym))); 
8    }) 
9    .WithTags("Blogs").WithName("BlogsWithAuthor"); 

Abfrage ohne Projektion:

SELECT b."Id", b."AuthorId", b."Content", b."Created", b."Hidden", b."Modified", b."Title", b."Url", s."Id", s."BirthDate", s."FirstName", s."LastName", s."Biography", s."Pseudonym" 
FROM "Blogs" AS b 
INNER JOIN ( 
SELECT p."Id", p."BirthDate", p."FirstName", p."LastName", a."Biography", a."Pseudonym" 
FROM "People" AS p 
INNER JOIN "Authors" AS a ON p."Id" = a."Id) 
 AS s ON b."AuthorId" = s."Id" 

Mit Projektion:

1app.MapGet("api/blogs/projection/with-author", async (CoreDbContext db) => { 
2        var blogs = await db.Blogs 
3            .AsNoTracking() 
4            .Select(x => BlogWithAuthorDto.CreateInstance(x.Id, x.Url, x.Title, x.Content, x.Author.Id, x.Author.Pseudonym)) 
5            .ToListAsync(); 
6 
7        return Results.Ok(blogs); 
8    }) 
9    .WithTags("Blogs") 
10    .WithName("GetBlogsWithAuthor-Projection"); 

Abfrage mit Projektion:

SELECT b."Id", b."Url", b."Title", b."Content", s."Id", s."Pseudonym" 
FROM "Blogs" AS b 
INNER JOIN ( 
SELECT p."Id", a."Pseudonym" 
FROM "People" AS p 
INNER JOIN "Authors" AS a ON p."Id" = a."Id") 
 AS s ON b."AuthorId" = s."Id" 

AsNoTracking für schreibgeschützte Abfragen

AsNoTracking() reduziert den Speicher‑ und CPU‑Verbrauch, da EF Core die geladenen Entitäten nicht nachverfolgt — ideal für reine Leseszenarien:

1var blogs = await db.Blogs 
2    .AsNoTracking() 
3    .Select(x => new BlogDto(x.Id, x.Url, x.Title, x.Content)) 
4    .ToListAsync(); 

Ineffiziente Aktualisierungen vermeiden

Angenommen, alle Blogs sollen ausgeblendet werden. Eine übliche EF Core-Operation würde wie folgt aussehen:

app.MapPatch("api/blogs/hide", async (CoreDbContext db) => { 
        foreach (var blog in db.Blogs) 
            blog.Hidden = true; 
 
        await db.SaveChangesAsync(); 
        return Results.NoContent(); 
    }) 
    .WithTags("blogs") 
    .WithName("HideAllBlogs"); 

Obwohl dies gültiger Code ist, werden dadurch alle irrelevanten Informationen in den Speicher geladen, die wir nicht benötigen, und insgesamt zwei Datenbank-Roundtrips erzeugt. Die Änderungsverfolgung von EF Core erstellt beim Laden der Entitäten Snapshots und vergleicht diese dann mit den Instanzen, um herauszufinden, welche Eigenschaften sich geändert haben. Darüber hinaus wächst die Abfrage exponentiell mit der Anzahl der vorhandenen Datensätze.

Stattdessen können wir die Methode ExecuteUpdateAsync verwenden:

1app.MapPatch("api/blogs/show", async (CoreDbContext db) => { 
2        await db.Blogs.ExecuteUpdateAsync(s => s 
3            .SetProperty(blog => blog.Hidden, false) 
4        ); 
5 
6        await db.SaveChangesAsync(); 
7        return Results.NoContent(); 
8    }) 
9    .WithTags("blogs") 
10    .WithName("ShowBlogs"); 

Dadurch wird der gesamte Vorgang in einem einzigen Roundtrip ausgeführt, ohne dass tatsächliche Daten in die Datenbank geladen oder an diese gesendet werden und ohne dass die Änderungsverfolgung von EF Core verwendet wird:

UPDATE "Blogs" AS b 
SET "Hidden" = FALSE 

Eine weitere Option ist das Löschen. Dies wird mit der Methode ExecuteDeleteAsync() erreicht.

Transaktionen bei Batch‑Operationen einführen

Massenupdates oder -löschungen mit ExecuteUpdateAsync oder ExecuteDeleteAsync nutzen standardmäßig keine impliziten Transaktionen. Füge notwendige Transaktionslogik hinzu:

1await using var transaction = await db.Database.BeginTransactionAsync(); 
2try 
3{ 
4    await db.Blogs.ExecuteUpdateAsync(s => s 
5        .SetProperty(blog => blog.Hidden, false) 
6    ); 
7    db.Blogs.Add(new Blog 
8    { 
9        AuthorId = Guid.CreateVersion7()// dummy placeholder 
10    }); 
11    await db.SaveChangesAsync(); 
12    await transaction.CommitAsync(); 
13} 
14catch (Exception) 
15{ 
16    await transaction.RollbackAsync(); 
17} 

Asynchrone Operationen korrekt nutzen

EF Core‑Methoden wie ToListAsync() sind asynchron — sie werden jedoch sequenziell ausgeführt und unterstützen keine parallelen Datenbankzugriffe auf demselben Kontext.

Dieses Verhalten unterstützt ACID‑Prinzipien und vermeidet unerwartete Fehler im hochskalierten Betrieb.

Navigationseigenschaften korrekt initialisieren

Navigationseigenschaften sollten nicht mit Standardwerten instanziiert werden (z. B. = new()), da EF Core sonst neue Objekte annehmen könnte, die eingefügt werden sollen.

Falsch:

public Author Author { get; set; } = new();

Richtig:

public Author Author { get; set; } = null!;

oder Nullable declaration, wenn Beziehung optional ist.

Primärschlüssel korrekt delegieren

Das manuelle Setzen von Primärschlüsseln kann zu Kollisionen bei großen Datenmengen führen. EF Core kann Schlüssel automatisch generieren — z. B. GUID v7 in PostgreSQL.

Pagination statt vollständiges Laden

Nutze Skip() und Take(), um zu große Datenmengen zu vermeiden:

1var blogs = await db.Blogs
2    .AsNoTracking()
3    .Skip(1)
4    .Take(10)
5    .ToListAsync();

Abbrechbarkeit von Abfragen (Cancellation Tokens)

Asynchrone EF Core‑Methoden unterstützen Abbruchtokens — wichtig für API‑Szenarien, bei denen laufende Abfragen vorzeitig beendet werden müssen, um Ressourcen freizugeben.

Split Queries vs. Single Queries

Vorteil Split Queries:

  • Reduzierte Redundanz
  • Weniger Speicherverbrauch
  • Vermeidung extrem großer Join‑Ergebnisse

Nachteile:

  • Mehrer Roundtrips
  • Potenziell inkonsistente Teilergebnisse

Nutze AsSplitQuery() dort, wo viele Includes geladen werden.

Fazit

Effiziente EF Core‑Abfragen basieren nicht auf Tricks, sondern auf fundiertem Verständnis von Datenbankoperationen und deren Kosten. Die konsequente Anwendung der oben genannten Best Practices verbessert Performance, reduziert Speicher‑ und CPU‑Last und führt zu wartbaren, skalierbaren Anwendungen.

Schnelle Abfragen bedeuten weniger Wartezeit für Endnutzer — und beeinflussen damit direkt die Benutzererfahrung (UX).

Beachte: Nicht alle Best Practices sind in jedem Kontext relevant — sie sollten stets kontextbezogen bewertet werden.

No items found.
Foto von Mark
Mark N.

Mehr zum Thema

Pfeil nach rechts (Verlinkung)
Ado .NET vs EF Core
01/2026

ADO.NET vs. EF Core

Blauer Pfeil nach rechts (Verlinkung)
Business-Central-Webservice – Authentifizierung in der Praxis (C#)
12/2025

Business-Central-Webservice – Authentifizierung in der Praxis (C#)

Blauer Pfeil nach rechts (Verlinkung)
12/2025

Architekturentscheidungen mit EF Core Architektur

Blauer Pfeil nach rechts (Verlinkung)
Strategy Pattern in C#: Flexibler & wartbarer Code
10/2025

Strategy Pattern in C#: Flexibler & wartbarer Code

Blauer Pfeil nach rechts (Verlinkung)

Lassen Sie uns gemeinsam wachsen.

Devware GmbH verpflichtet sich, Ihre Privatsphäre zu schützen. Wir benötigen Ihre Kontaktinformationen, um Sie bezüglich unserer Produkte und Dienstleistungen zu kontaktieren. Mit Klick auf Absenden geben Sie sich damit einverstanden. Weitere Informationen finden Sie unter Datenschutz. Ihre Daten behandeln wir vertraulich. Versprochen.
Vielen Dank für Ihr Vertrauen.
Unser Team prüft Ihre Anfrage sorgfältig und meldet sich in der Regel innerhalb von 48 Stunden bei Ihnen zurück.
Falls es besonders eilig ist, erreichen Sie uns auch telefonisch:
+ 49 (0) 202 478 269 0.
Da ist etwas schief gegangen beim Absenden des Formulars.