Entity Framework Core (EF Core)
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.
| 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:
| 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.
| 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:
| 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
| 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.
| 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.
| 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.
| 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:
Customerals EntityIdals PrimärschlüsselNameals 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; }
```
}
| 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
| 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.
| 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
| 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:
| 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.
| 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.
| 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:
SaveChangesInterceptorDieser 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.
| 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
| 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.
| 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.
| 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.
| 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
- .NET
- ASP.NET Core
- Blazor
- C#
- LINQ
- SQL
- Object-Relational Mapping
- Dependency Injection
- Repository Pattern
- Unit of Work
- SQL Server
- SQLite
- PostgreSQL
- MariaDB
- Docker
- CI/CD
- NuGet
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