Entity Framework Core (EF Core)

Aus dev.kaibel.net
Zur Navigation springen Zur Suche springen


Entity Framework Core (kurz EF Core) ist ein modernes Object-Relational Mapping Framework (ORM) für .NET. Es ermöglicht .NET-Anwendungen den Zugriff auf relationale und teilweise auch nichtrelationale Datenbanken, ohne dass für jede Datenbankoperation manuell SQL geschrieben werden muss.

EF Core bildet Tabellen und Datensätze einer Datenbank auf C#-Klassen und Objekte ab. Datenbankabfragen können dabei überwiegend mit LINQ formuliert werden. Entity Framework Core übersetzt diese LINQ-Abfragen anschließend in die Abfragesprache des jeweiligen Datenbanksystems.

Entity Framework Core ist der moderne Nachfolger des klassischen Entity Framework und wurde speziell für das plattformübergreifende .NET entwickelt.

Mit EF Core können unter anderem Anwendungen für folgende Plattformen entwickelt werden:

  • Windows
  • Linux
  • macOS
  • Webserver mit ASP.NET Core
  • Blazor
  • Desktop-Anwendungen
  • Worker Services
  • Cloud-Anwendungen

Die aktuelle LTS-Generation ist Entity Framework Core 10 und basiert auf .NET 10.

Grundprinzip

Ohne ein ORM muss eine Anwendung SQL-Abfragen normalerweise selbst erstellen und die zurückgegebenen Daten anschließend manuell auf Objekte abbilden.

Eine SQL-Abfrage könnte beispielsweise folgendermaßen aussehen:

SELECT Id, Name, Price
FROM Products
WHERE Price > 100;

Anschließend müssten die zurückgegebenen Datensätze beispielsweise mit ADO.NET gelesen und in C#-Objekte umgewandelt werden.

Mit Entity Framework Core kann dieselbe Abfrage stattdessen mit LINQ formuliert werden:

var products = await context.Products
    .Where(p => p.Price > 100)
    .ToListAsync();

EF Core übersetzt diese Abfrage in ein für den verwendeten Datenbankanbieter geeignetes SQL-Statement.

Das grundlegende Prinzip lautet:

C#-Anwendung
     │
     ▼
Entity Framework Core
     │
     ▼
Datenbankprovider
     │
     ▼
Datenbank

Beim Lesen von Daten funktioniert die Verarbeitung entsprechend in die Gegenrichtung:

Datenbank
     │
     ▼
Datenbankprovider
     │
     ▼
Entity Framework Core
     │
     ▼
C#-Objekte

Object-Relational Mapping

EF Core gehört zur Kategorie der Object-Relational Mapper.

Ein ORM versucht, das objektorientierte Datenmodell einer Anwendung mit dem relationalen Datenmodell einer Datenbank zu verbinden.

Zuordnung zwischen Objektmodell und relationaler Datenbank
Objektorientierte Anwendung Relationale Datenbank
Klasse Tabelle
Objekt Datensatz / Tabellenzeile
Property Spalte
Objekt-ID Primärschlüssel
Referenz auf ein Objekt Fremdschlüssel
Collection Beziehung zu mehreren Datensätzen

Eine einfache C#-Klasse könnte beispielsweise folgendermaßen aussehen:

public class Product
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public decimal Price { get; set; }
```

}

EF Core kann daraus beispielsweise eine Datenbanktabelle mit folgendem Aufbau erzeugen:

Beispiel für die Tabelle Products
Id Name Price
1 Kalkammonsalpeter 289,00
2 Harnstoff 315,00
3 Kali 60 245,00

Wichtige Bestandteile von EF Core

Zu den zentralen Bestandteilen von Entity Framework Core gehören:

  • Entity – C#-Klasse, die Daten eines bestimmten Typs repräsentiert
  • DbContext – zentrale Schnittstelle zwischen Anwendung und Datenbank
  • DbSet – repräsentiert eine Menge von Entities
  • LINQ – Abfragesprache innerhalb von .NET
  • Change Tracker – überwacht Änderungen an geladenen Entities
  • Database Provider – stellt die Verbindung zu einem konkreten Datenbanksystem her
  • Migrations – verwalten Änderungen am Datenbankschema
  • Model Configuration – definiert die Abbildung zwischen C#-Modell und Datenbank
  • Query Translation – übersetzt LINQ-Abfragen in SQL beziehungsweise die Abfragesprache des Providers

Installation

EF Core wird normalerweise über NuGet in ein .NET-Projekt eingebunden.

Basispaket

Das grundlegende EF-Core-Paket kann über die Kommandozeile installiert werden:

dotnet add package Microsoft.EntityFrameworkCore

Zusätzlich wird normalerweise ein Datenbankprovider benötigt.

Microsoft SQL Server

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

SQLite

dotnet add package Microsoft.EntityFrameworkCore.Sqlite

Design-Paket

Für Migrationen und verschiedene Entwicklungswerkzeuge wird zusätzlich das Design-Paket benötigt:

dotnet add package Microsoft.EntityFrameworkCore.Design

dotnet-ef installieren

Für die Verwaltung von Migrationen über die Kommandozeile kann das Werkzeug dotnet-ef installiert werden:

dotnet tool install --global dotnet-ef

Eine vorhandene Installation kann aktualisiert werden:

dotnet tool update --global dotnet-ef

Die installierte Version kann überprüft werden:

dotnet ef --version

Datenbankprovider

EF Core selbst enthält keine direkte Implementierung für jedes Datenbanksystem. Stattdessen werden sogenannte Database Provider verwendet.

Ein Provider übersetzt EF-Core-Operationen in Befehle für das jeweilige Datenbanksystem.

Häufig verwendete Datenbankprovider
Datenbanksystem NuGet-Paket
Microsoft SQL Server / Azure SQL Microsoft.EntityFrameworkCore.SqlServer
SQLite Microsoft.EntityFrameworkCore.Sqlite
Azure Cosmos DB Microsoft.EntityFrameworkCore.Cosmos
EF-Core-In-Memory-Datenbank Microsoft.EntityFrameworkCore.InMemory
PostgreSQL Npgsql.EntityFrameworkCore.PostgreSQL
MySQL / MariaDB beispielsweise Pomelo.EntityFrameworkCore.MySql

Bei Drittanbieter-Providern muss darauf geachtet werden, dass die verwendete Version mit der eingesetzten EF-Core-Hauptversion kompatibel ist.

Entity

Eine Entity ist normalerweise eine einfache C#-Klasse.

Beispiel:

public class Product
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public decimal Price { get; set; }

public decimal Stock { get; set; }
```

}

EF Core erkennt viele Eigenschaften anhand von Konventionen automatisch.

Die Property:

public int Id { get; set; }

wird beispielsweise standardmäßig als Primärschlüssel interpretiert.

Alternativ wird normalerweise auch:

public int ProductId { get; set; }

als Primärschlüssel der Entity Product erkannt.

DbContext

Der DbContext ist die zentrale Komponente von Entity Framework Core.

Ein DbContext stellt eine Arbeitseinheit beziehungsweise Sitzung mit einer Datenbank dar.

Er ist unter anderem verantwortlich für:

  • Datenbankabfragen
  • Erstellen neuer Datensätze
  • Ändern vorhandener Datensätze
  • Löschen von Datensätzen
  • Change Tracking
  • Transaktionen
  • Verwaltung des Datenmodells
  • Ausführen von SaveChanges
  • Verwaltung von Datenbankverbindungen

Ein einfacher DbContext kann folgendermaßen aussehen:

using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}

```
public DbSet<Product> Products => Set<Product>();
```

}

DbSet

Ein DbSet repräsentiert eine Menge von Entities eines bestimmten Typs.

Beispiel:

public DbSet<Product> Products => Set<Product>();

Über dieses DbSet können beispielsweise Produkte abgefragt werden:

var products = await context.Products.ToListAsync();

Ein DbSet kann vereinfacht als objektorientierte Darstellung einer Datenbanktabelle betrachtet werden.

Dabei ist jedoch zu beachten, dass ein DbSet nicht zwangsläufig exakt einer einzelnen physischen Tabelle entsprechen muss.

Verbindung zur Datenbank

Eine Datenbankverbindung wird normalerweise über einen Connection String definiert.

Ein Connection String für SQL Server könnte beispielsweise so aussehen:

Server=localhost;Database=TestDb;Trusted_Connection=True;TrustServerCertificate=True

In einer ASP.NET-Core-Anwendung wird der Connection String normalerweise in der Datei appsettings.json gespeichert.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=TestDb;Trusted_Connection=True;TrustServerCertificate=True"
  }
}

Anschließend kann der DbContext in Program.cs registriert werden:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection")));

Dependency Injection

ASP.NET Core besitzt ein integriertes Dependency-Injection-System.

Der DbContext kann dort beispielsweise folgendermaßen registriert werden:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(
        builder.Configuration.GetConnectionString("DefaultConnection"));
});

Danach kann er in anderen Komponenten verwendet werden:

public class ProductService
{
    private readonly ApplicationDbContext _context;

```
public ProductService(ApplicationDbContext context)
{
    _context = context;
}
```

}

In klassischen ASP.NET-Core-Anwendungen entspricht die Lebensdauer eines solchen DbContext normalerweise einem HTTP-Request.

CRUD-Operationen

CRUD steht für die vier grundlegenden Datenbankoperationen:

CRUD-Operationen
Abkürzung Bedeutung SQL-Entsprechung
Create Datensatz anlegen INSERT
Read Datensatz lesen SELECT
Update Datensatz ändern UPDATE
Delete Datensatz löschen DELETE

Create

Eine neue Entity wird erzeugt:

var product = new Product
{
    Name = "Harnstoff",
    Price = 315.00m,
    Stock = 12000
};

Anschließend wird sie dem DbContext hinzugefügt:

context.Products.Add(product);

Die Änderung wird mit:

await context.SaveChangesAsync();

in die Datenbank geschrieben.

Komplettes Beispiel:

var product = new Product
{
    Name = "Harnstoff",
    Price = 315.00m,
    Stock = 12000
};

context.Products.Add(product);

await context.SaveChangesAsync();

Read

Alle Produkte laden:

var products = await context.Products
    .ToListAsync();

Einen Datensatz anhand des Primärschlüssels suchen:

var product = await context.Products
    .FindAsync(1);

Produkte anhand einer Bedingung filtern:

var products = await context.Products
    .Where(p => p.Price < 300)
    .ToListAsync();

Update

Eine geladene Entity kann direkt verändert werden:

var product = await context.Products
    .FindAsync(1);

if (product != null)
{
product.Price = 299.00m;

```
await context.SaveChangesAsync();
```

}

EF Core erkennt über den Change Tracker, dass sich der Preis verändert hat.

Delete

Ein Datensatz kann folgendermaßen gelöscht werden:

var product = await context.Products
    .FindAsync(1);

if (product != null)
{
context.Products.Remove(product);

```
await context.SaveChangesAsync();
```

}

LINQ

Language Integrated Query – kurz LINQ – ist die wichtigste Abfragemethode für EF Core.

Eine typische LINQ-Abfrage sieht beispielsweise folgendermaßen aus:

var products = await context.Products
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name)
    .ToListAsync();

EF Core übersetzt diesen Ausdruck in eine SQL-Abfrage.

Deferred Execution

Viele LINQ-Abfragen werden zunächst nur definiert und noch nicht ausgeführt.

Beispiel:

var query = context.Products
    .Where(p => p.Price > 100);

Zu diesem Zeitpunkt wurde die Datenbank normalerweise noch nicht abgefragt.

Die Abfrage wird beispielsweise durch:

var products = await query.ToListAsync();

tatsächlich ausgeführt.

Dieses Verhalten wird als Deferred Execution bezeichnet.

Wichtige LINQ-Methoden

Häufig verwendete LINQ-Methoden mit EF Core
Methode Funktion
Where() Datensätze filtern
Select() Daten beziehungsweise Properties auswählen
OrderBy() aufsteigend sortieren
OrderByDescending() absteigend sortieren
ThenBy() zusätzliche Sortierung definieren
FirstAsync() ersten Datensatz zurückgeben
FirstOrDefaultAsync() ersten Datensatz oder Standardwert zurückgeben
SingleAsync() genau einen Datensatz erwarten
SingleOrDefaultAsync() genau einen Datensatz oder Standardwert erwarten
AnyAsync() prüfen, ob mindestens ein Datensatz existiert
CountAsync() Datensätze zählen
SumAsync() Summe berechnen
AverageAsync() Durchschnitt berechnen
MinAsync() Minimalwert bestimmen
MaxAsync() Maximalwert bestimmen
ToListAsync() Ergebnis als Liste laden

Projektionen

Es ist häufig nicht notwendig, vollständige Entities aus der Datenbank zu laden.

Angenommen, eine Produktübersicht benötigt ausschließlich ID, Name und Preis.

Statt:

var products = await context.Products
    .ToListAsync();

kann eine Projektion verwendet werden:

var products = await context.Products
    .Select(p => new
    {
        p.Id,
        p.Name,
        p.Price
    })
    .ToListAsync();

Noch sauberer ist häufig die Verwendung eines DTO:

public class ProductListDto
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public decimal Price { get; set; }
```

}

Abfrage:

var products = await context.Products
    .Select(p => new ProductListDto
    {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price
    })
    .ToListAsync();

Dadurch müssen nicht alle Spalten der Tabelle vom Datenbankserver übertragen werden.

Asynchrone Datenbankzugriffe

Insbesondere bei Web- und Serveranwendungen sollten Datenbankzugriffe nach Möglichkeit asynchron ausgeführt werden.

Statt:

var products = context.Products.ToList();

wird normalerweise:

var products = await context.Products.ToListAsync();

verwendet.

Beispiele synchroner und asynchroner EF-Core-Methoden
Synchron Asynchron
ToList() ToListAsync()
First() FirstAsync()
FirstOrDefault() FirstOrDefaultAsync()
Single() SingleAsync()
Count() CountAsync()
SaveChanges() SaveChangesAsync()

Asynchrone Operationen sind insbesondere bei Serveranwendungen wichtig, da der ausführende Thread während einer Datenbankoperation nicht unnötig blockiert werden muss.

Change Tracking

EF Core überwacht standardmäßig Entities, die über einen DbContext geladen wurden.

Dies wird als Change Tracking bezeichnet.

Beispiel:

var product = await context.Products
    .FindAsync(1);

if (product != null)
{
product.Price = 350;

```
await context.SaveChangesAsync();
```

}

EF Core erkennt, dass sich die Property Price verändert hat.

Beim Aufruf von:

SaveChangesAsync()

wird daraus beispielsweise ein entsprechender UPDATE-Befehl erzeugt.

Entity States

Eine vom Change Tracker verwaltete Entity kann unterschiedliche Zustände besitzen.

Zustände einer Entity im Change Tracker
Zustand Bedeutung
Detached Entity wird vom DbContext nicht verfolgt
Unchanged Entity wird verfolgt, wurde aber nicht verändert
Added Entity soll neu eingefügt werden
Modified Entity wurde verändert
Deleted Entity soll gelöscht werden

Der aktuelle Zustand kann beispielsweise ermittelt werden:

var state = context.Entry(product).State;

AsNoTracking

Wenn Daten ausschließlich gelesen werden und anschließend nicht verändert werden sollen, ist das Change Tracking häufig unnötig.

In diesem Fall kann AsNoTracking() verwendet werden:

var products = await context.Products
    .AsNoTracking()
    .Where(p => p.Price > 100)
    .ToListAsync();

Dies kann:

  • Speicher sparen
  • den Verwaltungsaufwand reduzieren
  • Read-Only-Abfragen beschleunigen

Faustregel:

Wenn geladene Entities nicht verändert und wieder gespeichert werden sollen, sollte geprüft werden, ob AsNoTracking() sinnvoll ist.

Konfiguration des Datenmodells

EF Core bietet verschiedene Möglichkeiten zur Konfiguration des Datenmodells.

Möglichkeiten zur Konfiguration eines EF-Core-Modells
Verfahren Beschreibung
Conventions automatische Erkennung anhand von Namens- und Strukturkonventionen
Data Annotations Konfiguration über Attribute an C#-Klassen und Properties
Fluent API Konfiguration über C#-Methoden im ModelBuilder

Conventions

EF Core versucht zunächst, die Struktur eines Datenmodells automatisch zu erkennen.

Beispiel:

public class Customer
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;
```

}

EF Core erkennt dabei automatisch:

  • Customer als Entity
  • Id als Primärschlüssel
  • Name als Datenbankfeld

Diese automatische Konfiguration wird als Convention-based Configuration bezeichnet.

Data Annotations

Eigenschaften einer Entity können über Attribute konfiguriert werden.

Beispiel:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Product
{
[Key]
public int Id { get; set; }

```
[Required]
[MaxLength(200)]
public string Name { get; set; } = string.Empty;

[Column(TypeName = "decimal(10,2)")]
public decimal Price { get; set; }
```

}
Häufig verwendete Data Annotations
Attribut Bedeutung
[Key] definiert einen Primärschlüssel
[Required] markiert ein Pflichtfeld
[MaxLength] definiert eine maximale Länge
[Column] konfiguriert eine Datenbankspalte
[Table] konfiguriert den Tabellennamen
[NotMapped] Property wird nicht in der Datenbank gespeichert
[ForeignKey] definiert beziehungsweise kennzeichnet einen Fremdschlüssel

Fluent API

Die Fluent API bietet mehr Konfigurationsmöglichkeiten als Data Annotations.

Sie wird normalerweise innerhalb von:

OnModelCreating()

verwendet.

Beispiel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>(entity =>
    {
        entity.HasKey(p => p.Id);

```
    entity.Property(p => p.Name)
        .IsRequired()
        .HasMaxLength(200);

    entity.Property(p => p.Price)
        .HasPrecision(10, 2);
});
```

}

IEntityTypeConfiguration

Bei größeren Anwendungen sollte die Konfiguration einzelner Entities häufig in separate Klassen ausgelagert werden.

Beispiel:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

public class ProductConfiguration
: IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);

```
    builder.Property(p => p.Name)
        .IsRequired()
        .HasMaxLength(200);

    builder.Property(p => p.Price)
        .HasPrecision(10, 2);
}
```

}

Die Konfigurationen können anschließend automatisch geladen werden:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfigurationsFromAssembly(
        typeof(ApplicationDbContext).Assembly);
}

Diese Variante sorgt bei umfangreichen Datenmodellen für eine deutlich bessere Übersicht.

Beziehungen zwischen Entities

Relationale Datenbanken arbeiten intensiv mit Beziehungen zwischen Tabellen.

EF Core unterstützt insbesondere:

  • 1:1-Beziehungen
  • 1:n-Beziehungen
  • n:m-Beziehungen
Beziehungstypen
Beziehung Beispiel
1:1 Benutzer und Benutzerprofil
1:n Kunde und Bestellungen
n:m Produkte und Kategorien

1:n-Beziehung

Ein Kunde kann mehrere Bestellungen besitzen.

public class Customer
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public ICollection<Order> Orders { get; set; }
    = new List<Order>();
```

}

Die Bestellung enthält den Fremdschlüssel:

public class Order
{
    public int Id { get; set; }

```
public DateTime OrderDate { get; set; }

public int CustomerId { get; set; }

public Customer Customer { get; set; } = null!;
```

}

Dabei stellt:

public int CustomerId { get; set; }

den Fremdschlüssel dar.

Die Property:

public Customer Customer { get; set; }

ist eine Navigation Property.

1:1-Beziehung

Ein Benutzer kann beispielsweise genau ein Profil besitzen.

public class User
{
    public int Id { get; set; }

```
public string UserName { get; set; } = string.Empty;

public UserProfile? Profile { get; set; }
```

}
public class UserProfile
{
    public int Id { get; set; }

```
public string Description { get; set; } = string.Empty;

public int UserId { get; set; }

public User User { get; set; } = null!;
```

}

Eine explizite Konfiguration könnte folgendermaßen aussehen:

modelBuilder.Entity<User>()
    .HasOne(u => u.Profile)
    .WithOne(p => p.User)
    .HasForeignKey<UserProfile>(p => p.UserId);

n:m-Beziehung

Ein Produkt kann mehreren Kategorien zugeordnet sein. Gleichzeitig kann eine Kategorie mehrere Produkte enthalten.

public class Product
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public ICollection<Category> Categories { get; set; }
    = new List<Category>();
```

}
public class Category
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public ICollection<Product> Products { get; set; }
    = new List<Product>();
```

}

EF Core kann für eine einfache n:m-Beziehung automatisch eine Zwischentabelle verwalten.

Wenn die Beziehung selbst zusätzliche Eigenschaften besitzt, sollte dagegen eine eigene Entity verwendet werden.

Beispiel:

Product
   │
   │ 1:n
   ▼
ProductCategory
   ▲
   │ n:1
   │
Category

Laden zusammengehöriger Daten

EF Core bietet unterschiedliche Möglichkeiten, Navigation Properties zu laden.

Strategien zum Laden von Beziehungen
Strategie Beschreibung
Eager Loading zusammengehörige Daten werden sofort mitgeladen
Explicit Loading zusammengehörige Daten werden gezielt nachgeladen
Lazy Loading Daten werden erst beim Zugriff auf die Navigation Property geladen

Eager Loading

Beim Eager Loading werden Beziehungen direkt mit der ursprünglichen Abfrage geladen.

Dies erfolgt mit Include():

var customers = await context.Customers
    .Include(c => c.Orders)
    .ToListAsync();

Mehrere Ebenen können mit ThenInclude() geladen werden:

var orders = await context.Orders
    .Include(o => o.Positions)
        .ThenInclude(p => p.Product)
    .ToListAsync();

Explicit Loading

Beim Explicit Loading werden Beziehungen gezielt nachgeladen.

var customer = await context.Customers
    .FindAsync(1);

if (customer != null)
{
await context.Entry(customer)
.Collection(c => c.Orders)
.LoadAsync();
}

Lazy Loading

Beim Lazy Loading werden Navigation Properties automatisch geladen, sobald auf sie zugegriffen wird.

Dies kann bequem sein, birgt jedoch die Gefahr einer großen Zahl unbemerkter Datenbankabfragen.

Insbesondere kann dadurch das sogenannte N+1-Problem entstehen.

Lazy Loading sollte deshalb bewusst und nicht automatisch als Standardlösung eingesetzt werden.

N+1-Problem

Angenommen, zunächst werden 100 Kunden geladen.

Anschließend wird für jeden Kunden einzeln dessen Bestellliste geladen.

Dann können beispielsweise folgende Datenbankzugriffe entstehen:

1 Abfrage für die Kunden
+
100 Abfragen für die Bestellungen
=
101 Datenbankabfragen

Bei großen Datenmengen kann dies erhebliche Performanceprobleme verursachen.

Eine mögliche Lösung ist Eager Loading:

var customers = await context.Customers
    .Include(c => c.Orders)
    .ToListAsync();

Alternativ kann eine gezielte Projektion verwendet werden.

Migrationen

Migrations ermöglichen die kontrollierte Weiterentwicklung eines Datenbankschemas zusammen mit dem C#-Datenmodell.

Angenommen, die Entity Product wird um eine Property ergänzt:

public string Description { get; set; } = string.Empty;

Anschließend kann eine Migration erstellt werden:

dotnet ef migrations add AddProductDescription

EF Core erstellt daraus eine Migrationsklasse.

Vereinfacht kann diese beispielsweise folgendermaßen aussehen:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "Description",
        table: "Products",
        type: "nvarchar(max)",
        nullable: false,
        defaultValue: "");
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: "Products");
}

Die Migration kann anschließend auf die Datenbank angewendet werden:

dotnet ef database update

Wichtige Migrationsbefehle

+ Wichtige Befehle für EF-Core-Migrationen
Befehl Funktion
dotnet ef migrations add Name neue Migration erzeugen
dotnet ef migrations list vorhandene Migrationen anzeigen
dotnet ef migrations remove letzte Migration entfernen
dotnet ef database update Datenbank auf die neueste Migration aktualisieren
dotnet ef database update MigrationName Datenbank auf eine bestimmte Migration setzen
dotnet ef migrations script SQL-Skript für Migrationen erzeugen

Migrationen in Produktivsystemen

Migrationen sollten in Produktivumgebungen kontrolliert eingesetzt werden.

Das automatische Ausführen aller Migrationen beim Programmstart ist zwar technisch möglich, für geschäftskritische Systeme jedoch nicht zwangsläufig die beste Lösung.

Ein kontrollierter Deployment-Prozess ermöglicht unter anderem:

  • Prüfung der Datenbankänderungen vor dem Deployment
  • Erstellung und Archivierung von SQL-Skripten
  • Durchführung eines Backups vor einer Migration
  • Planung länger laufender Änderungen
  • kontrollierte Vergabe von Datenbankberechtigungen
  • einfachere Fehleranalyse

Ein SQL-Skript für vorhandene Migrationen kann beispielsweise erzeugt werden:

dotnet ef migrations script

Code First

Beim Code-First-Ansatz wird zunächst das Datenmodell in C# erstellt.

Aus diesem Modell wird anschließend das Datenbankschema erzeugt.

C#-Entity-Klassen
        │
        ▼
EF-Core-Modell
        │
        ▼
Migration
        │
        ▼
Datenbankschema

Vorteile von Code First:

  • Datenmodell befindet sich im Quellcode
  • Datenmodell kann mit Git versioniert werden
  • Migrationen dokumentieren Schemaänderungen
  • gute Integration in CI/CD-Prozesse
  • Datenbank und Anwendung können gemeinsam weiterentwickelt werden

Database First

Beim Database-First-Ansatz existiert die Datenbank bereits.

EF Core kann daraus C#-Klassen und einen DbContext erzeugen.

Dieser Vorgang wird als Reverse Engineering beziehungsweise Scaffolding bezeichnet.

Beispiel:

dotnet ef dbcontext scaffold "CONNECTION_STRING" Microsoft.EntityFrameworkCore.SqlServer

Database First ist insbesondere bei bestehenden beziehungsweise älteren Datenbanken interessant.

Beispielsweise kann es bei der Modernisierung einer Legacy-Anwendung sinnvoll sein, eine bestehende Datenbank zunächst unverändert weiterzuverwenden.

Code First und Database First im Vergleich

Vergleich von Code First und Database First
Eigenschaft Code First Database First
Ausgangspunkt C#-Modell vorhandene Datenbank
Schemaerstellung über EF Core und Migrationen Datenbank existiert bereits
Geeignet für neue Anwendungen sehr gut eingeschränkt
Geeignet für Legacy-Datenbanken möglich sehr gut
Schemaänderungen häufig über Migrationen häufig datenbankseitig

SaveChanges

Mit:

context.SaveChanges();

beziehungsweise:

await context.SaveChangesAsync();

werden die vom Change Tracker registrierten Änderungen in die Datenbank geschrieben.

Vereinfacht ergibt sich folgende Zuordnung:

Zusammenhang zwischen Entity State und Datenbankoperation
Entity State Typische Datenbankoperation
Added INSERT
Modified UPDATE
Deleted DELETE
Unchanged keine Änderung

Transaktionen

EF Core verwendet für einen einzelnen SaveChanges()-Aufruf normalerweise eine Transaktion, sofern der verwendete Provider Transaktionen unterstützt.

Für komplexere Abläufe können Transaktionen explizit verwaltet werden.

Beispiel:

await using var transaction =
    await context.Database.BeginTransactionAsync();

try
{
var customer = new Customer
{
Name = "Max Mustermann"
};

```
context.Customers.Add(customer);

await context.SaveChangesAsync();

var order = new Order
{
    CustomerId = customer.Id,
    OrderDate = DateTime.UtcNow
};

context.Orders.Add(order);

await context.SaveChangesAsync();

await transaction.CommitAsync();
```

}
catch
{
await transaction.RollbackAsync();

```
throw;
```

}

Dadurch wird sichergestellt, dass zusammengehörige Änderungen als Einheit behandelt werden.

Optimistic Concurrency

Mehrere Benutzer oder Prozesse können gleichzeitig denselben Datensatz bearbeiten.

Beispiel:

Benutzer A liest Datensatz
        │
Benutzer B liest Datensatz
        │
Benutzer A verändert und speichert Datensatz
        │
Benutzer B verändert und speichert Datensatz

Ohne zusätzliche Kontrolle könnte Benutzer B die Änderung von Benutzer A überschreiben.

EF Core unterstützt deshalb Optimistic Concurrency.

Bei SQL Server kann beispielsweise eine RowVersion verwendet werden:

public class Product
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public decimal Price { get; set; }

public byte[] RowVersion { get; set; } = Array.Empty<byte>();
```

}

Konfiguration:

modelBuilder.Entity<Product>()
    .Property(p => p.RowVersion)
    .IsRowVersion();

Wurde der Datensatz zwischen Lesen und Schreiben durch einen anderen Prozess verändert, kann EF Core eine:

DbUpdateConcurrencyException

auslösen.

Die Anwendung muss anschließend entscheiden, wie dieser Konflikt behandelt werden soll.

ExecuteUpdate und ExecuteDelete

Bei Massenänderungen ist es häufig unnötig, alle betroffenen Entities zunächst in den Arbeitsspeicher zu laden.

Eine klassische Vorgehensweise wäre:

var products = await context.Products
    .Where(p => p.Price < 100)
    .ToListAsync();

foreach (var product in products)
{
product.Price *= 1.05m;
}

await context.SaveChangesAsync();

Stattdessen kann ExecuteUpdateAsync() verwendet werden:

await context.Products
    .Where(p => p.Price < 100)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(
            p => p.Price,
            p => p.Price * 1.05m));

Mehrere Datensätze können direkt gelöscht werden:

await context.Products
    .Where(p => p.Stock <= 0)
    .ExecuteDeleteAsync();

Diese Verfahren vermeiden das vorherige Laden sämtlicher betroffener Entities.

Raw SQL

EF Core ermöglicht zusätzlich die Ausführung von SQL.

Beispielsweise:

var products = await context.Products
    .FromSqlInterpolated(
        $"SELECT * FROM Products WHERE Price > {minimumPrice}")
    .ToListAsync();

Bei SQL-Abfragen müssen immer parameterisierte Verfahren verwendet werden.

Unsicher wäre beispielsweise:

var sql =
    "SELECT * FROM Products WHERE Name = '" +
    userInput +
    "'";

Dies kann zu SQL-Injection-Schwachstellen führen.

Globale Query Filter

Mit einem Global Query Filter kann eine Bedingung definiert werden, die automatisch auf Abfragen einer Entity angewendet wird.

Ein typisches Beispiel ist Soft Delete.

Entity:

public class Product
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public bool IsDeleted { get; set; }
```

}

Konfiguration:

modelBuilder.Entity<Product>()
    .HasQueryFilter(p => !p.IsDeleted);

Eine normale Abfrage:

var products = await context.Products
    .ToListAsync();

liefert dadurch standardmäßig nur nicht gelöschte Produkte.

Der Filter kann bei Bedarf ignoriert werden:

var products = await context.Products
    .IgnoreQueryFilters()
    .ToListAsync();

Weitere Einsatzmöglichkeiten globaler Filter sind beispielsweise:

  • Multi-Tenant-Anwendungen
  • Mandantenfilter
  • Archivierung
  • zeitabhängige Datensichten

Indizes

Datenbankindizes können über EF Core konfiguriert werden.

Beispiel:

modelBuilder.Entity<Product>()
    .HasIndex(p => p.Name);

Ein eindeutiger Index:

modelBuilder.Entity<Customer>()
    .HasIndex(c => c.Email)
    .IsUnique();

Indizes können Leseoperationen erheblich beschleunigen.

Sie haben jedoch auch Nachteile:

  • zusätzlicher Speicherverbrauch
  • zusätzlicher Aufwand bei INSERT
  • zusätzlicher Aufwand bei UPDATE
  • zusätzlicher Aufwand bei DELETE

Die Auswahl sinnvoller Indizes gehört deshalb weiterhin zum Datenbankdesign.

Value Conversions

Mit Value Conversions können .NET-Werte beim Speichern in einen anderen Datenbankwert umgewandelt werden.

Ein Enum kann beispielsweise als String gespeichert werden:

modelBuilder.Entity<Order>()
    .Property(o => o.Status)
    .HasConversion<string>();

Statt:

0
1
2

könnten dann beispielsweise folgende Werte gespeichert werden:

New
Processing
Completed

Complex Types

EF Core unterstützt komplexe Typen zur Abbildung strukturierter Werte.

Beispiel:

public class Address
{
    public string Street { get; set; } = string.Empty;

```
public string City { get; set; } = string.Empty;

public string PostalCode { get; set; } = string.Empty;
```

}

Eine Entity kann diesen Typ verwenden:

public class Customer
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public Address Address { get; set; } = new();
```

}

Geeignete Einsatzbereiche sind beispielsweise:

  • Adressen
  • Kontaktdaten
  • Koordinaten
  • Geldwerte
  • wiederverwendbare Value Objects

Paging

Große Datenmengen sollten normalerweise nicht vollständig auf einmal geladen werden.

Ein einfaches Paging kann mit Skip() und Take() umgesetzt werden:

var products = await context.Products
    .OrderBy(p => p.Id)
    .Skip(100)
    .Take(50)
    .ToListAsync();

Damit werden 50 Datensätze ab Position 101 geladen.

Für sehr große Datenbestände kann sogenanntes Keyset Paging beziehungsweise Cursor Paging effizienter sein.

Split Queries

Bei komplexen Abfragen mit mehreren Includes kann eine einzelne SQL-Abfrage sehr große Ergebnismengen erzeugen.

EF Core unterstützt deshalb Split Queries.

Beispiel:

var customers = await context.Customers
    .Include(c => c.Orders)
    .AsSplitQuery()
    .ToListAsync();

Dabei erzeugt EF Core mehrere SQL-Abfragen.

Ob Single Query oder Split Query effizienter ist, hängt vom jeweiligen Datenmodell und der Datenmenge ab.

Erzeugtes SQL anzeigen

Bei der Analyse komplexer Abfragen ist es häufig sinnvoll, das von EF Core erzeugte SQL zu betrachten.

Eine LINQ-Abfrage kann beispielsweise zunächst gespeichert werden:

var query = context.Products
    .Where(p => p.Price > 100);

Das zugehörige SQL kann anschließend ausgegeben werden:

var sql = query.ToQueryString();

Dies ist besonders hilfreich für:

  • Performanceanalysen
  • Fehlersuche
  • Prüfung von JOINs
  • Prüfung von Filtern
  • Analyse komplexer LINQ-Abfragen

Logging

EF Core integriert sich in das Logging-System von .NET.

Eine einfache Konsolenausgabe kann beispielsweise aktiviert werden:

options
    .UseSqlServer(connectionString)
    .LogTo(Console.WriteLine);

Damit können unter anderem folgende Informationen sichtbar gemacht werden:

  • ausgeführte SQL-Befehle
  • Dauer von Datenbankoperationen
  • EF-Core-Warnungen
  • Fehler
  • Verbindungsinformationen
  • Transaktionen

Sensible Daten sollten in Produktivsystemen nicht unkontrolliert protokolliert werden.

Performance

Die Abstraktion von EF Core erleichtert die Entwicklung, kann jedoch dazu führen, dass teure Datenbankoperationen im C#-Code zunächst unscheinbar wirken.

Bei Performanceproblemen sollten insbesondere folgende Punkte untersucht werden:

  • Anzahl der Datenbankabfragen
  • erzeugtes SQL
  • verwendete Indizes
  • Datenmenge
  • Change Tracking
  • Include-Aufrufe
  • große Objektgraphen
  • N+1-Probleme
  • unnötige Datenbank-Roundtrips
  • Paging
  • Projektionen
  • Datenbankausführungspläne

Nur benötigte Daten laden

Wenn lediglich die Namen von Produkten benötigt werden, sollte nicht die vollständige Entity geladen werden.

Ungünstig:

var products = await context.Products
    .ToListAsync();

Anschließend:

var names = products
    .Select(p => p.Name)
    .ToList();

Besser:

var names = await context.Products
    .Select(p => p.Name)
    .ToListAsync();

Dadurch können Datenmenge und Speicherbedarf reduziert werden.

DbContext-Lebensdauer

Ein DbContext ist als kurzlebige Unit of Work gedacht.

Ein typischer Ablauf sieht folgendermaßen aus:

DbContext erzeugen
       │
       ▼
Daten laden
       │
       ▼
Änderungen durchführen
       │
       ▼
SaveChanges
       │
       ▼
DbContext freigeben

Ein DbContext sollte normalerweise nicht:

  • global gespeichert werden
  • als Singleton verwendet werden
  • über sehr lange Zeiträume existieren
  • gleichzeitig von mehreren Threads verwendet werden

DbContext ist nicht threadsicher.

DbContextFactory

In einigen Anwendungstypen ist es sinnvoll, DbContext-Instanzen über eine Factory zu erzeugen.

Dies gilt beispielsweise für:

  • Blazor
  • Desktop-Anwendungen
  • Background Services
  • Worker Services
  • parallele Verarbeitung

Registrierung:

builder.Services.AddDbContextFactory<ApplicationDbContext>(
    options =>
        options.UseSqlServer(connectionString));

Verwendung:

await using var context =
    await dbContextFactory.CreateDbContextAsync();

EF Core in Blazor

Bei serverseitigen Blazor-Anwendungen muss besonders auf die Lebensdauer eines DbContext geachtet werden.

Ein klassischer ASP.NET-Core-Request existiert nur relativ kurz.

Ein Blazor-Circuit kann dagegen deutlich länger bestehen.

Daher kann die Verwendung von:

IDbContextFactory<ApplicationDbContext>

sinnvoll sein.

Beispiel:

public class ProductService
{
    private readonly IDbContextFactory<ApplicationDbContext>
        _contextFactory;

```
public ProductService(
    IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

public async Task<List<Product>> GetProductsAsync()
{
    await using var context =
        await _contextFactory.CreateDbContextAsync();

    return await context.Products
        .AsNoTracking()
        .ToListAsync();
}
```

}

Dadurch kann für einen Vorgang eine eigene DbContext-Instanz verwendet werden.

Dies reduziert unter anderem Probleme durch:

  • lange Tracking-Zeiträume
  • parallele Datenbankoperationen
  • veraltete Entity-Zustände
  • unnötigen Speicherverbrauch

Repository Pattern

EF Core wird häufig zusammen mit dem Repository Pattern eingesetzt.

Eine Schnittstelle könnte beispielsweise folgendermaßen aussehen:

public interface IProductRepository
{
    Task<Product?> GetByIdAsync(int id);

```
Task<List<Product>> GetAllAsync();

Task AddAsync(Product product);
```

}

Eine Implementierung:

public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

```
public ProductRepository(
    ApplicationDbContext context)
{
    _context = context;
}

public async Task<Product?> GetByIdAsync(int id)
{
    return await _context.Products.FindAsync(id);
}

public async Task<List<Product>> GetAllAsync()
{
    return await _context.Products
        .AsNoTracking()
        .ToListAsync();
}

public async Task AddAsync(Product product)
{
    _context.Products.Add(product);

    await _context.SaveChangesAsync();
}
```

}

EF Core selbst implementiert bereits viele Eigenschaften des Repository- und Unit-of-Work-Patterns.

Zuordnung von EF-Core-Komponenten zu bekannten Patterns
EF-Core-Komponente Ähnliches Entwurfsmuster
DbSet Repository
DbContext Unit of Work

Eine zusätzliche Repository-Schicht sollte deshalb einen konkreten architektonischen Nutzen besitzen.

Ein Repository, das lediglich:

GetAll()
GetById()
Add()
Update()
Delete()

um ein DbSet herum implementiert, erzeugt möglicherweise nur eine zusätzliche Abstraktionsschicht ohne echten Mehrwert.

DTOs und Entities

Datenbank-Entities sollten insbesondere in Webanwendungen nicht automatisch gleichzeitig als API-Datenmodelle verwendet werden.

Häufig ist eine Trennung sinnvoll zwischen:

  • Entity
  • DTO
  • ViewModel
  • Request Model
  • Response Model

Beispiel:

public class ProductDto
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public decimal Price { get; set; }
```

}

Dadurch wird verhindert, dass interne Datenbankstrukturen automatisch Bestandteil einer öffentlichen Schnittstelle werden.

Sicherheit

EF Core parametrisiert normale LINQ-Abfragen automatisch.

Beispiel:

var customer = await context.Customers
    .Where(c => c.Name == userInput)
    .FirstOrDefaultAsync();

EF Core verwendet für userInput normalerweise einen Datenbankparameter.

Dies reduziert das Risiko von SQL Injection erheblich.

Trotzdem müssen weitere Sicherheitsmaßnahmen beachtet werden:

  • Connection Strings nicht im Quellcode speichern
  • Zugangsdaten über Secret Stores oder sichere Konfiguration verwalten
  • Least-Privilege-Prinzip für Datenbankkonten verwenden
  • Raw SQL ausschließlich parameterisiert ausführen
  • Benutzereingaben validieren
  • Fehlerdetails nicht ungefiltert an Clients ausgeben
  • sensible Daten nicht unkontrolliert protokollieren
  • produktive Zugangsdaten nicht in Git speichern

Testing

EF-Core-Code kann auf verschiedene Arten getestet werden.

Möglichkeiten zum Testen von EF-Core-Anwendungen
Verfahren Eigenschaften
echte Testdatenbank sehr realitätsnah
temporäre Datenbank gut für automatisierte Integrationstests
SQLite In-Memory relationale Datenbank im Arbeitsspeicher
EF-Core-InMemory-Provider schnell, aber kein vollständiger Ersatz für eine relationale Datenbank
Testcontainer echtes Datenbanksystem innerhalb eines Containers

InMemory Provider

EF Core besitzt einen eigenen InMemory-Provider.

Dieser kann für einfache Tests hilfreich sein.

Er verhält sich allerdings nicht vollständig wie eine relationale Datenbank.

Unterschiede bestehen unter anderem bei:

  • SQL-Übersetzung
  • relationalen Constraints
  • Transaktionen
  • Datenbankfunktionen
  • providerabhängigem Verhalten

Ein Test, der mit dem InMemory-Provider erfolgreich ist, muss deshalb nicht zwangsläufig mit SQL Server, PostgreSQL oder MariaDB funktionieren.

SQLite In-Memory

Für relationale Tests kann SQLite im Arbeitsspeicher eine bessere Annäherung sein.

Auch SQLite verhält sich jedoch nicht vollständig identisch zu anderen Datenbanksystemen.

Wenn providerabhängiges Verhalten getestet werden soll, ist eine Testinstanz des tatsächlich eingesetzten Datenbanksystems zuverlässiger.

Integrationstests mit Containern

Für komplexe Anwendungen können Datenbanken während eines Tests temporär als Container gestartet werden.

Beispielsweise:

Integrationstest
      │
      ▼
Docker / Testcontainer
      │
      ▼
temporärer SQL Server
oder PostgreSQL
      │
      ▼
EF Core

Dadurch können Tests gegen ein echtes Datenbanksystem ausgeführt werden.

Interceptors

EF Core unterstützt Interceptors, mit denen Datenbankoperationen abgefangen oder erweitert werden können.

Einsatzmöglichkeiten sind beispielsweise:

  • Logging
  • Auditing
  • Performance-Messungen
  • Analyse von SQL-Befehlen
  • Überwachung von SaveChanges
  • automatische Ergänzung bestimmter Werte

Ein Beispiel ist:

SaveChangesInterceptor

Dieser kann vor oder nach einem SaveChanges-Vorgang eigenen Code ausführen.

Auditing

Viele Geschäftsanwendungen müssen speichern, wann ein Datensatz erzeugt oder verändert wurde.

Eine gemeinsame Basisklasse könnte beispielsweise folgendermaßen aussehen:

public abstract class EntityBase
{
    public DateTime CreatedAt { get; set; }

```
public DateTime? ModifiedAt { get; set; }
```

}

Weitere mögliche Auditfelder sind:

  • CreatedBy
  • CreatedAt
  • ModifiedBy
  • ModifiedAt
  • DeletedBy
  • DeletedAt

Diese Werte können beispielsweise innerhalb von SaveChanges oder über einen Interceptor automatisch gepflegt werden.

Soft Delete

Beim Soft Delete wird ein Datensatz nicht physisch aus der Datenbank gelöscht.

Stattdessen kann beispielsweise eine Property verwendet werden:

public bool IsDeleted { get; set; }

Beim Löschen wird dann:

product.IsDeleted = true;

gesetzt.

Vor- und Nachteile von Soft Delete
Vorteile Nachteile
Daten können wiederhergestellt werden Datenbank wächst dauerhaft weiter
Historie bleibt erhalten Abfragen müssen gelöschte Datensätze berücksichtigen
Beziehungen bleiben nachvollziehbar Unique Constraints können komplizierter werden
Auditing wird erleichtert Datenschutz kann trotzdem physische Löschung erfordern

Soft Delete wird häufig mit Global Query Filters kombiniert.

Typische Performancefehler

Komplette Tabellen laden

Ungünstig:

var products = await context.Products
    .ToListAsync();

und anschließend im Arbeitsspeicher filtern.

Besser:

var products = await context.Products
    .Where(p => p.Price > 100)
    .ToListAsync();

ToList zu früh aufrufen

Ungünstig:

var products =
    (await context.Products.ToListAsync())
    .Where(p => p.Price > 100);

Hier werden zunächst sämtliche Produkte übertragen.

Besser:

var products = await context.Products
    .Where(p => p.Price > 100)
    .ToListAsync();

Tracking bei Read-Only-Abfragen

Wenn Daten ausschließlich angezeigt werden:

.AsNoTracking()

verwenden.

Zu viele Includes

Eine große Zahl von Include()-Aufrufen kann zu sehr großen SQL-Abfragen und Ergebnismengen führen.

Eine gezielte Projektion kann effizienter sein.

N+1-Abfragen

Viele kleine Datenbankabfragen können erheblich langsamer sein als eine oder wenige gezielt formulierte Abfragen.

Fehlende Indizes

Auch eine gut formulierte EF-Core-Abfrage kann langsam sein, wenn wichtige Datenbankindizes fehlen.

Häufige Fehler und Probleme

Häufige EF-Core-Probleme
Problem Typische Ursache
DbContext wurde bereits disposed DbContext wird außerhalb seiner vorgesehenen Lebensdauer verwendet
A second operation was started on this context mehrere parallele Operationen verwenden denselben DbContext
Entity wird bereits getrackt mehrere Instanzen mit gleichem Primärschlüssel werden gleichzeitig verfolgt
LINQ-Ausdruck kann nicht übersetzt werden verwendeter C#-Ausdruck besitzt keine SQL-Übersetzung
Migration passt nicht zur Datenbank Schema und Migrationshistorie sind nicht synchron

EF Core gegenüber ADO.NET

ADO.NET arbeitet wesentlich näher an der Datenbank.

Bei ADO.NET müssen Entwickler häufig selbst:

  • SQL schreiben
  • Connection verwalten
  • Commands erstellen
  • Parameter definieren
  • DataReader verarbeiten
  • Datensätze auf Objekte abbilden

EF Core übernimmt einen großen Teil dieser Aufgaben.

Vergleich zwischen Entity Framework Core und ADO.NET
Eigenschaft EF Core ADO.NET
Abstraktionsgrad hoch niedrig
Abfragesprache hauptsächlich LINQ hauptsächlich SQL
Objekt-Mapping automatisch normalerweise manuell
Change Tracking integriert normalerweise manuell
Migrationen integriert nicht Bestandteil von ADO.NET
Kontrolle über SQL geringer sehr hoch
Entwicklungsaufwand häufig geringer häufig höher

EF Core gegenüber Dapper

Dapper ist ein sogenannter Micro-ORM.

Dapper konzentriert sich stärker auf das Mapping von SQL-Ergebnissen auf .NET-Objekte.

Vergleich zwischen Entity Framework Core und Dappe
Eigenschaft EF Core Dapper
LINQ-Abfragen Ja normalerweise Nein
automatische SQL-Erzeugung Ja überwiegend Nein
Change Tracking Ja Nein
Migrationen Ja Nein
Beziehungen umfangreiche Unterstützung weitgehend manuell
Abstraktionsgrad hoch niedrig
Kontrolle über SQL mittel bis hoch sehr hoch
CRUD-Entwicklung sehr komfortabel stärker SQL-orientiert

Beide Technologien können innerhalb derselben Anwendung eingesetzt werden.

Ein mögliches Szenario wäre:

  • EF Core für normale CRUD-Funktionen
  • Dapper für einzelne hochoptimierte Spezialabfragen

EF Core ersetzt kein Datenbankwissen

Auch bei der Verwendung eines ORM bleiben Kenntnisse relationaler Datenbanken wichtig.

Entwickler sollten insbesondere folgende Konzepte verstehen:

  • Primärschlüssel
  • Fremdschlüssel
  • Joins
  • Normalisierung
  • Constraints
  • Indizes
  • Transaktionen
  • Isolation Levels
  • Deadlocks
  • Query-Pläne
  • SQL-Datentypen
  • Datenbankperformance

EF Core kann ein schlechtes Datenmodell nicht automatisch in ein gutes Datenmodell umwandeln.

EF Core ersetzt keine SQL-Kenntnisse

Für einfache Anwendungen ist es möglich, einen großen Teil der Datenbankzugriffe ausschließlich mit LINQ umzusetzen.

Bei komplexeren Anwendungen sind SQL-Kenntnisse dennoch wichtig.

Sie helfen beispielsweise bei:

  • Analyse des erzeugten SQL
  • Performanceoptimierung
  • Planung von Indizes
  • Analyse von JOINs
  • Verständnis von Ausführungsplänen
  • Entwicklung komplexer Reports
  • Fehleranalyse
  • Datenbankmigrationen

Die Kombination aus guten EF-Core- und SQL-Kenntnissen ist deshalb besonders wertvoll.

Vorteile von Entity Framework Core

Zu den wichtigsten Vorteilen gehören:

  • hoher Abstraktionsgrad
  • LINQ-Unterstützung
  • starke Typisierung
  • automatische Abbildung zwischen Objekten und Datenbank
  • integriertes Change Tracking
  • Migrationen
  • Unterstützung verschiedener Datenbanksysteme
  • Integration in ASP.NET Core
  • asynchrone Datenbankoperationen
  • umfangreiche Konfigurationsmöglichkeiten
  • integrierte Behandlung von Beziehungen
  • gute Unterstützung moderner .NET-Anwendungen
  • automatische Parameterisierung vieler Datenbankabfragen
  • hohe Entwicklungsgeschwindigkeit

Nachteile von Entity Framework Core

EF Core besitzt auch einige Nachteile beziehungsweise Risiken:

  • zusätzliche Abstraktionsschicht
  • erzeugtes SQL ist nicht unmittelbar sichtbar
  • ungeschickte LINQ-Abfragen können ineffizientes SQL erzeugen
  • komplexe Abfragen können schwer nachvollziehbar sein
  • Change Tracking benötigt zusätzliche Ressourcen
  • providerabhängiges Verhalten
  • Migrationen müssen sorgfältig verwaltet werden
  • komplexe Spezialabfragen können Raw SQL erfordern
  • Entwickler können Performanceprobleme verursachen, ohne dies im C#-Code sofort zu erkennen

Best Practices

DbContext kurzlebig halten

Ein DbContext sollte eine klar begrenzte Arbeitseinheit repräsentieren.

Asynchrone APIs verwenden

In Web- und Serveranwendungen sollten normalerweise Methoden wie:

ToListAsync()

und:

SaveChangesAsync()

verwendet werden.

AsNoTracking bei reinen Leseabfragen

Wenn Entities nur gelesen werden:

.AsNoTracking()

verwenden.

Nur benötigte Daten auswählen

DTO-Projektionen reduzieren häufig Datenmenge und Speicherbedarf.

Migrationen versionieren

Migrationsdateien sollten normalerweise zusammen mit dem Quellcode in Git gespeichert werden.

Migrationen vor dem Deployment prüfen

Vor einer produktiven Migration sollte geprüft werden, welche Änderungen tatsächlich auf der Datenbank ausgeführt werden.

Connection Strings schützen

Produktive Zugangsdaten gehören nicht in das Git-Repository.

CancellationToken verwenden

In Webanwendungen können Abfragen mit einem CancellationToken ausgeführt werden:

var products = await context.Products
    .AsNoTracking()
    .ToListAsync(cancellationToken);

Generiertes SQL prüfen

Komplexe LINQ-Abfragen sollten bei Bedarf mit ToQueryString() analysiert werden.

Datenbankindizes planen

EF Core ersetzt keine Datenbankoptimierung.

Häufig verwendete Filter-, Sortier- und Join-Spalten sollten hinsichtlich sinnvoller Indizes überprüft werden.

DbContext nicht parallel verwenden

Eine einzelne DbContext-Instanz darf nicht für mehrere parallele Datenbankoperationen verwendet werden.

Geschäftslogik vom DbContext trennen

Der DbContext sollte sich primär um Datenzugriff, Mapping und Persistenz kümmern.

Komplexe Geschäftsregeln gehören normalerweise in geeignete Domain- oder Service-Komponenten.

Beispiel einer einfachen EF-Core-Anwendung

Entity

public class Product
{
    public int Id { get; set; }

```
public string Name { get; set; } = string.Empty;

public decimal Price { get; set; }

public decimal Stock { get; set; }
```

}

DbContext

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

```
public DbSet<Product> Products => Set<Product>();

protected override void OnModelCreating(
    ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>(entity =>
    {
        entity.HasKey(p => p.Id);

        entity.Property(p => p.Name)
            .IsRequired()
            .HasMaxLength(200);

        entity.Property(p => p.Price)
            .HasPrecision(10, 2);

        entity.Property(p => p.Stock)
            .HasPrecision(12, 3);
    });
}
```

}

Registrierung

builder.Services.AddDbContext<ApplicationDbContext>(
    options =>
        options.UseSqlServer(
            builder.Configuration
                .GetConnectionString("DefaultConnection")));

Produkt anlegen

var product = new Product
{
    Name = "Kalkammonsalpeter",
    Price = 289.50m,
    Stock = 12500
};

context.Products.Add(product);

await context.SaveChangesAsync();

Produkte lesen

var products = await context.Products
    .AsNoTracking()
    .OrderBy(p => p.Name)
    .ToListAsync();

Produkt ändern

var product = await context.Products
    .FindAsync(productId);

if (product != null)
{
product.Price = 295.00m;

```
await context.SaveChangesAsync();
```

}

Produkt löschen

var product = await context.Products
    .FindAsync(productId);

if (product != null)
{
context.Products.Remove(product);

```
await context.SaveChangesAsync();
```

}

Mögliche Projektstruktur

Für eine größere Anwendung könnte beispielsweise folgende Struktur verwendet werden:

MyApplication
│
├── Domain
│   ├── Entities
│   ├── ValueObjects
│   └── Enums
│
├── Application
│   ├── DTOs
│   ├── Interfaces
│   └── Services
│
├── Infrastructure
│   └── Persistence
│       ├── ApplicationDbContext.cs
│       │
│       ├── Configurations
│       │   ├── ProductConfiguration.cs
│       │   ├── CustomerConfiguration.cs
│       │   └── OrderConfiguration.cs
│       │
│       └── Migrations
│
└── Web
    ├── Components
    ├── Controllers
    └── Program.cs

Bei kleineren Projekten ist eine derart umfangreiche Trennung nicht zwingend erforderlich.

Die Architektur sollte sich an der tatsächlichen Komplexität der Anwendung orientieren.

Aktuelle EF-Core-Versionen

Stand September 2026 ist EF Core 10 die aktuelle stabile LTS-Version.

Aktuell relevante EF-Core-Versionen
EF-Core-Version Zielframework Support
EF Core 10 .NET 10 bis 10. November 2028
EF Core 9 .NET 8 bis 10. November 2026
EF Core 8 .NET 8 bis 10. November 2026

EF Core 10 benötigt das .NET-10-SDK zum Kompilieren und die .NET-10-Runtime zur Ausführung.

Bei einem Upgrade zwischen Hauptversionen sollten grundsätzlich die dokumentierten Breaking Changes überprüft werden.

Die verwendeten EF-Core-Pakete sollten außerdem möglichst dieselbe Hauptversion verwenden.

Beispiel:

Microsoft.EntityFrameworkCore             10.x
Microsoft.EntityFrameworkCore.SqlServer   10.x
Microsoft.EntityFrameworkCore.Design      10.x

Bei Drittanbieter-Providern muss zusätzlich geprüft werden, ob die jeweilige Version die verwendete EF-Core-Hauptversion unterstützt.

Wann ist EF Core sinnvoll?

EF Core eignet sich besonders für:

  • ASP.NET-Core-Webanwendungen
  • REST-APIs
  • Blazor-Anwendungen
  • Desktop-Anwendungen mit relationaler Datenbank
  • Worker Services
  • Geschäftsanwendungen
  • CRUD-lastige Systeme
  • Anwendungen mit vielen Beziehungen zwischen Entities
  • Anwendungen mit Code-First-Datenmodellen
  • Systeme mit versionierten Datenbankmigrationen

Wann ist EF Core möglicherweise nicht die beste Lösung?

Andere Datenzugriffstechnologien können sinnvoller sein, wenn:

  • nur sehr wenige und einfache SQL-Abfragen benötigt werden
  • vollständige Kontrolle über jedes SQL-Statement erforderlich ist
  • extrem performancekritische Spezialabfragen dominieren
  • eine ungewöhnliche Legacy-Datenbank verwendet wird
  • Stored Procedures die gesamte Datenzugriffsschicht bestimmen
  • keine objektorientierte Abbildung der Daten erforderlich ist

Es ist ebenfalls möglich, EF Core mit anderen Technologien wie ADO.NET oder Dapper zu kombinieren.

Zusammenfassung

Entity Framework Core ist das zentrale ORM-Framework für moderne .NET-Anwendungen.

Das grundlegende Modell besteht aus:

Entity
   │
   ▼
DbSet
   │
   ▼
DbContext
   │
   ▼
Entity Framework Core
   │
   ▼
Database Provider
   │
   ▼
Datenbank

Die wichtigsten Konzepte sind:

  • Entities repräsentieren Datensätze.
  • DbSet repräsentiert eine Menge von Entities.
  • DbContext verwaltet Datenbankzugriffe und Änderungen.
  • LINQ dient zur Formulierung von Abfragen.
  • Change Tracking erkennt Veränderungen an Entities.
  • SaveChanges überträgt Änderungen in die Datenbank.
  • Migrationen verwalten Änderungen am Datenbankschema.
  • Navigation Properties bilden Beziehungen zwischen Entities ab.
  • Database Provider ermöglichen die Unterstützung verschiedener Datenbanksysteme.
  • AsNoTracking kann Read-Only-Abfragen effizienter machen.
  • Projektionen reduzieren unnötige Datenübertragungen.
  • DbContext sollte kurzlebig und nicht parallel verwendet werden.
  • Trotz ORM bleiben Kenntnisse über SQL und relationale Datenbanken wichtig.

EF Core bietet für viele Geschäftsanwendungen einen guten Kompromiss zwischen Entwicklungsgeschwindigkeit, Typsicherheit, Wartbarkeit, Abstraktion und Kontrolle über den Datenbankzugriff.

Siehe auch

Quellen und weiterführende Dokumentation

  • Microsoft Learn – Entity Framework Core
  • Microsoft Learn – EF Core Releases and Planning
  • Microsoft Learn – EF Core Database Providers
  • Microsoft Learn – EF Core Change Tracking
  • Microsoft Learn – EF Core Relationships
  • Microsoft Learn – EF Core Migrations
  • Microsoft Learn – EF Core Performance
  • Microsoft Learn – What's New in EF Core 10