Fortgeschrittenes EF Core: Migrations, Performance und Raw SQL
# Fortgeschrittenes Entity Framework Core: Migrationen, Performance und Raw SQL
Entity Framework Core deckt die Grundlagen gut ab, doch Produktionssysteme verlangen deutlich mehr. Sobald Ihre Anwendung uber eine Handvoll Tabellen und ein paar hundert Anfragen pro Sekunde hinauswachst, brauchen Sie fortgeschrittene Migrationsstrategien, gezielte Performance-Optimierung und die Sicherheit, bei Bedarf auf Raw SQL zuruckzugreifen. In hochvolumigen Datensystemen, mit denen ich gearbeitet habe, war der Unterschied zwischen einem naiven EF-Core-Setup und einem sauber konfigurierten oft der Unterschied zwischen 50ms und 500ms Antwortzeit unter Last.
Fortgeschrittene Migrationsstrategien
Migrationen sind Deployment-Artefakte. Behandeln Sie sie mit der gleichen Sorgfalt wie Ihren Anwendungscode.
Daten-Seeding in Migrationen
`HasData` von EF Core ist praktisch fur Referenzdaten, hat aber Einschrankungen -- es funktioniert nur mit konstanten Werten und generiert das vollstandige Seeding bei jeder Migration neu. Fur dynamisches Seeding verwenden Sie Raw SQL innerhalb der Migration:
public partial class SeedOrderStatuses : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: class="code-string">"OrderStatuses",
columns: new[] { class="code-string">"Id", class="code-string">"Name", class="code-string">"Description" },
values: new object[,]
{
{ class="code-number">1, class="code-string">"Pending", class="code-string">"Bestellung wurde aufgegeben" },
{ class="code-number">2, class="code-string">"Processing", class="code-string">"Bestellung wird vorbereitet" },
{ class="code-number">3, class="code-string">"Shipped", class="code-string">"Bestellung wurde versandt" },
{ class="code-number">4, class="code-string">"Delivered", class="code-string">"Bestellung wurde zugestellt" },
{ class="code-number">5, class="code-string">"Cancelled", class="code-string">"Bestellung wurde storniert" }
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: class="code-string">"OrderStatuses",
keyColumn: class="code-string">"Id",
keyValues: new object[] { class="code-number">1, class="code-number">2, class="code-number">3, class="code-number">4, class="code-number">5 });
}
}Bei grossen Seed-Datensatzen oder komplexen Transformationen sollten Sie das Daten-Seeding in einer separaten Migration von Schemaanderungen halten. Das macht Rollbacks vorhersagbar.
Idempotente Migrationen
Produktions-Deployments sollten immer idempotente Skripte verwenden. Wenn ein Deployment mittendrin fehlschlagt, muss ein erneutes Ausfuhren des Skripts sicher sein:
dotnet ef migrations script --idempotent -o migrate.sqlDas generierte Skript umschliesst jede Migration mit einer Existenzprufung gegen die `__EFMigrationsHistory`-Tabelle. Ich generiere und prufe diese Skripte immer, bevor ich sie auf Staging oder Produktion anwende -- fuhren Sie niemals `dotnet ef database update` direkt gegen eine Produktionsdatenbank aus.
Rollback-Muster fur die Produktion
Jede Migration braucht eine funktionierende `Down`-Methode. Daruber hinaus verwende ich bei risikoreichen Schemaanderungen ein bestimmtes Muster:
public partial class RenameCustomerEmailColumn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
class=class="code-string">"code-comment">// Schritt class="code-number">1: Neue Spalte hinzufugen
migrationBuilder.AddColumn<string>(
name: class="code-string">"EmailAddress",
table: class="code-string">"Customers",
type: class="code-string">"nvarchar(class="code-number">256)",
nullable: true);
class=class="code-string">"code-comment">// Schritt class="code-number">2: Daten kopieren
migrationBuilder.Sql(
class="code-string">"UPDATE Customers SET EmailAddress = Email");
class=class="code-string">"code-comment">// Schritt class="code-number">3: Nach dem Kopieren non-nullable setzen
migrationBuilder.AlterColumn<string>(
name: class="code-string">"EmailAddress",
table: class="code-string">"Customers",
type: class="code-string">"nvarchar(class="code-number">256)",
nullable: false,
defaultValue: class="code-string">"");
class=class="code-string">"code-comment">// Schritt class="code-number">4: Alte Spalte erst nach Validierung entfernen
class=class="code-string">"code-comment">// Dies erfolgt in einer SEPARATEN Migration nach der Deployment-Validierung
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: class="code-string">"EmailAddress",
table: class="code-string">"Customers");
}
}Benennen Sie eine Spalte niemals in einer einzigen Migration um. Das Expand-Contract-Muster -- neue hinzufugen, kopieren, verifizieren, alte entfernen -- verhindert Datenverlust und ermoglicht Zero-Downtime-Deployments.
Performance im Detail
Kompilierte Abfragen (Compiled Queries)
Bei Abfragen, die tausendmal pro Minute ausgefuhrt werden, summiert sich der Overhead der Ausdrucksbaumkompilierung. Kompilierte Abfragen zahlen diesen Preis nur einmal:
public class ProductRepository
{
private static readonly Func<AppDbContext, int, CancellationToken, Task<Product?>>
GetByIdQuery = EF.CompileAsyncQuery(
(AppDbContext ctx, int id, CancellationToken ct) =>
ctx.Products.FirstOrDefault(p => p.Id == id));
private static readonly Func<AppDbContext, string, CancellationToken, Task<List<Product>>>
GetByCategoryQuery = EF.CompileAsyncQuery(
(AppDbContext ctx, string category, CancellationToken ct) =>
ctx.Products
.Where(p => p.Category == category && p.IsActive)
.OrderBy(p => p.Name)
.ToList());
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context) => _context = context;
public Task<Product?> GetByIdAsync(int id, CancellationToken ct)
=> GetByIdQuery(_context, id, ct);
public Task<List<Product>> GetByCategoryAsync(string category, CancellationToken ct)
=> GetByCategoryQuery(_context, category, ct);
}In hochvolumigen Datensystemen, die ich profiliert habe, reduzierten kompilierte Abfragen auf den 10 meistgenutzten Endpoints den durchschnittlichen Abfrage-Overhead um 3-4ms. Das klingt gering pro Aufruf, aber bei 10.000 Anfragen pro Minute summiert sich die CPU-Ersparnis erheblich.
Aufgeteilte Abfragen (Split Queries)
Wenn eine Abfrage mehrere Collection-Navigationen enthalt, generiert EF Core eine einzelne SQL-Abfrage mit JOINs, die zu einer kartesischen Explosion fuhren kann. Ein Elternelement mit 10 Positionen und 5 Tags erzeugt 50 statt 15 Zeilen:
class=class="code-string">"code-comment">// Einzelabfrage: Risiko einer kartesischen Explosion
var orders = await _context.Orders
.Include(o => o.OrderItems)
.Include(o => o.Tags)
.Include(o => o.AuditEntries)
.ToListAsync(ct);
class=class="code-string">"code-comment">// Aufgeteilte Abfrage: mehrere Roundtrips, aber keine Datenduplizierung
var orders = await _context.Orders
.Include(o => o.OrderItems)
.Include(o => o.Tags)
.Include(o => o.AuditEntries)
.AsSplitQuery()
.ToListAsync(ct);Aufgeteilte Abfragen tauschen ein einzelnes grosses Ergebnis gegen mehrere kleinere ein. Die insgesamt ubertragenen Bytes sind oft dramatisch geringer. Wenn ich mehr als eine Collection-Navigation einbinde, verwende ich standardmassig aufgeteilte Abfragen.
Massenoperationen mit ExecuteUpdate und ExecuteDelete
EF Core 7+ fuhrte satzbasierte Operationen ein, die direkt in SQL ubersetzt werden, ohne Entitaten zu laden:
class=class="code-string">"code-comment">// Alle Bestellungen archivieren, die alter als class="code-number">2 Jahre sind -- einzelnes UPDATE
await _context.Orders
.Where(o => o.CreatedAt < DateTime.UtcNow.AddYears(-class="code-number">2))
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.IsArchived, true)
.SetProperty(o => o.ArchivedAt, DateTime.UtcNow), ct);
class=class="code-string">"code-comment">// Archivierte Audit-Logs bereinigen -- einzelnes DELETE
await _context.AuditLogs
.Where(a => a.IsArchived && a.CreatedAt < DateTime.UtcNow.AddYears(-class="code-number">5))
.ExecuteDeleteAsync(ct);
class=class="code-string">"code-comment">// Massenhafte Preiserhohung fur eine Kategorie
await _context.Products
.Where(p => p.Category == class="code-string">"Elektronik" && p.IsActive)
.ExecuteUpdateAsync(s => s
.SetProperty(p => p.Price, p => p.Price * class="code-number">1.1m)
.SetProperty(p => p.LastModified, DateTime.UtcNow), ct);Diese umgehen den Change Tracker vollstandig. Keine Entitaten werden materialisiert, kein Speicher wird verbraucht. Fur Massenoperationen auf Zehntausenden von Zeilen ist das um Grossenordnungen schneller als das Laden und Aufrufen von `SaveChanges`.
Raw SQL und SqlQuery
LINQ ist hervorragend fur 90% aller Abfragen. Fur die restlichen 10% ist Raw SQL kein Kompromiss -- es ist das richtige Werkzeug.
FromSqlRaw fur Entity-Abfragen
var topProducts = await _context.Products
.FromSqlRaw(@class="code-string">"
SELECT p.*
FROM Products p
INNER JOIN OrderItems oi ON p.Id = oi.ProductId
GROUP BY p.Id, p.Name, p.Price, p.Category, p.IsActive,
p.CreatedAt, p.LastModified, p.IsDeleted
HAVING COUNT(oi.Id) > {class="code-number">0}
ORDER BY COUNT(oi.Id) DESC", minimumOrders)
.AsNoTracking()
.ToListAsync(ct);`FromSqlRaw` gibt standardmassig getrackte Entitaten zuruck. Sie konnen LINQ-Operatoren darauf verketten -- EF Core umschliesst Ihr SQL als Unterabfrage.
SqlQuery fur beliebige Ergebnismengen
EF Core 8 fuhrte `SqlQuery
var salesReport = await _context.Database
.SqlQuery<MonthlySalesDto>($@class="code-string">"
SELECT
DATEPART(YEAR, o.CreatedAt) AS Year,
DATEPART(MONTH, o.CreatedAt) AS Month,
COUNT(*) AS OrderCount,
SUM(o.TotalAmount) AS TotalRevenue,
AVG(o.TotalAmount) AS AverageOrderValue
FROM Orders o
WHERE o.CreatedAt >= {startDate}
AND o.IsDeleted = class="code-number">0
GROUP BY DATEPART(YEAR, o.CreatedAt), DATEPART(MONTH, o.CreatedAt)
ORDER BY Year DESC, Month DESC")
.ToListAsync(ct);Das ist unverzichtbar fur Berichtsabfragen, komplexe Aggregationen und jedes Szenario, in dem die Zuordnung zum Entity-Modell erzwungen oder verschwenderisch ware.
ExecuteSqlRaw fur DDL und Non-Query-Operationen
class=class="code-string">"code-comment">// Einen gefilterten Index erstellen, den EF-Core-Migrationen nicht ausdrucken konnen
await _context.Database.ExecuteSqlRawAsync(@class="code-string">"
CREATE INDEX IX_Orders_ActivePending
ON Orders (CustomerId, CreatedAt)
WHERE IsDeleted = class="code-number">0 AND Status = class="code-string">'Pending'", ct);Interceptors und Abfragefilter
Globale Abfragefilter
Abfragefilter werden automatisch auf jede Abfrage einer bestimmten Entitat angewendet. Sie bilden die Grundlage fur Mandantenfahigkeit und Soft Delete:
public class AppDbContext : DbContext
{
private readonly ITenantProvider _tenantProvider;
public AppDbContext(
DbContextOptions<AppDbContext> options,
ITenantProvider tenantProvider)
: base(options)
{
_tenantProvider = tenantProvider;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
class=class="code-string">"code-comment">// Soft-Delete-Filter
modelBuilder.Entity<Order>()
.HasQueryFilter(o => !o.IsDeleted);
class=class="code-string">"code-comment">// Mandantenfilter
modelBuilder.Entity<Order>()
.HasQueryFilter(o => !o.IsDeleted
&& o.TenantId == _tenantProvider.CurrentTenantId);
}
}SaveChanges-Interceptors
Interceptors ermoglichen es Ihnen, sich in die Persistenz-Pipeline einzuklinken, ohne Ihre Domainlogik zu verunreinigen:
public class AuditInterceptor : SaveChangesInterceptor
{
private readonly ICurrentUserService _currentUser;
public AuditInterceptor(ICurrentUserService currentUser)
=> _currentUser = currentUser;
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
var context = eventData.Context!;
foreach (var entry in context.ChangeTracker
.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = DateTime.UtcNow;
entry.Entity.CreatedBy = _currentUser.UserId;
break;
case EntityState.Modified:
entry.Entity.ModifiedAt = DateTime.UtcNow;
entry.Entity.ModifiedBy = _currentUser.UserId;
break;
}
}
return base.SavingChangesAsync(eventData, result, ct);
}
}
class=class="code-string">"code-comment">// Registrierung
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
options.UseSqlServer(connectionString)
.AddInterceptors(sp.GetRequiredService<AuditInterceptor>()));Dieses Muster trennt Audit-Belange vollstandig von der Geschaftslogik. Jede Entitat, die `IAuditable` implementiert, erhalt automatisch Zeitstempel und Benutzer-Tracking.
Value Converter und Owned Types
Value Converter
Value Converter transformieren Daten zwischen Ihrem Domainmodell und der Datenbank. Sie sind unerlasslich zum Speichern von Enums, stark typisierten IDs und komplexen Werttypen:
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
class=class="code-string">"code-comment">// Enum als String speichern fur bessere Lesbarkeit
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(class="code-number">30);
class=class="code-string">"code-comment">// Benutzerdefinierter Converter fur stark typisierte ID
builder.Property(o => o.Id)
.HasConversion(
id => id.Value,
value => new OrderId(value));
class=class="code-string">"code-comment">// JSON-Converter fur Metadaten
builder.Property(o => o.Metadata)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null),
v => JsonSerializer.Deserialize<Dictionary<string, string>>(
v, (JsonSerializerOptions?)null)!)
.HasColumnType(class="code-string">"nvarchar(max)");
}
}Owned Types fur Wertobjekte
Owned Types bilden reichhaltige Domainobjekte auf Spalten in der Elterntabelle ab, ohne separate Tabellen zu erzeugen:
builder.OwnsOne(o => o.ShippingAddress, address =>
{
address.Property(a => a.Street).HasMaxLength(class="code-number">200).HasColumnName(class="code-string">"Ship_Street");
address.Property(a => a.City).HasMaxLength(class="code-number">100).HasColumnName(class="code-string">"Ship_City");
address.Property(a => a.PostalCode).HasMaxLength(class="code-number">20).HasColumnName(class="code-string">"Ship_PostalCode");
address.Property(a => a.Country).HasMaxLength(class="code-number">60).HasColumnName(class="code-string">"Ship_Country");
});
builder.OwnsOne(o => o.BillingAddress, address =>
{
address.Property(a => a.Street).HasMaxLength(class="code-number">200).HasColumnName(class="code-string">"Bill_Street");
address.Property(a => a.City).HasMaxLength(class="code-number">100).HasColumnName(class="code-string">"Bill_City");
address.Property(a => a.PostalCode).HasMaxLength(class="code-number">20).HasColumnName(class="code-string">"Bill_PostalCode");
address.Property(a => a.Country).HasMaxLength(class="code-number">60).HasColumnName(class="code-string">"Bill_Country");
});Dieser Ansatz halt Ihr Domainmodell sauber und vermeidet unnotige JOINs. Das `Address`-Wertobjekt lebt als Spalten innerhalb der `Orders`-Tabelle.
Temporale Tabellen und Soft Delete
SQL Server Temporale Tabellen
EF Core 6+ bietet eingebaute Unterstutzung fur temporale SQL-Server-Tabellen, die automatisch die vollstandige Anderungshistorie jeder Zeile verfolgen:
builder.Entity<Product>(b =>
{
b.ToTable(class="code-string">"Products", tb => tb.IsTemporal(ttb =>
{
ttb.HasPeriodStart(class="code-string">"ValidFrom");
ttb.HasPeriodEnd(class="code-string">"ValidTo");
ttb.UseHistoryTable(class="code-string">"ProductsHistory");
}));
});Historische Daten abfragen:
class=class="code-string">"code-comment">// Produktzustand zu einem bestimmten Zeitpunkt abrufen
var productSnapshot = await _context.Products
.TemporalAsOf(new DateTime(class="code-number">2025, class="code-number">1, class="code-number">15, class="code-number">0, class="code-number">0, class="code-number">0, DateTimeKind.Utc))
.Where(p => p.Id == productId)
.SingleOrDefaultAsync(ct);
class=class="code-string">"code-comment">// Vollstandige Anderungshistorie abrufen
var priceHistory = await _context.Products
.TemporalAll()
.Where(p => p.Id == productId)
.OrderBy(p => EF.Property<DateTime>(p, class="code-string">"ValidFrom"))
.Select(p => new
{
p.Price,
ValidFrom = EF.Property<DateTime>(p, class="code-string">"ValidFrom"),
ValidTo = EF.Property<DateTime>(p, class="code-string">"ValidTo")
})
.ToListAsync(ct);Robustes Soft-Delete-Muster
Abfragefilter und Interceptors zusammen schaffen ein nahtloses Erlebnis:
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
string? DeletedBy { get; set; }
}
public class SoftDeleteInterceptor : SaveChangesInterceptor
{
private readonly ICurrentUserService _currentUser;
public SoftDeleteInterceptor(ICurrentUserService currentUser)
=> _currentUser = currentUser;
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
foreach (var entry in eventData.Context!.ChangeTracker
.Entries<ISoftDeletable>()
.Where(e => e.State == EntityState.Deleted))
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAt = DateTime.UtcNow;
entry.Entity.DeletedBy = _currentUser.UserId;
}
return base.SavingChangesAsync(eventData, result, ct);
}
}Der Interceptor wandelt physische Loschungen transparent in logische Loschungen um. In Kombination mit dem Abfragefilter werden geloschte Datensatze fur normale Abfragen unsichtbar, bleiben aber wiederherstellbar.
Nebenleufigkeitsbehandlung (Concurrency Handling)
Optimistische Nebenleufigkeit verhindert stilles Uberschreiben von Daten in Mehrbenutzer-Szenarien:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public decimal Price { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; } = default!;
}Oder mit der Fluent API:
builder.Property(p => p.RowVersion)
.IsRowVersion()
.IsConcurrencyToken();Konfliktbehandlung:
public async Task UpdatePriceAsync(int productId, decimal newPrice, CancellationToken ct)
{
var product = await _context.Products.FindAsync(new object[] { productId }, ct)
?? throw new NotFoundException(productId);
product.Price = newPrice;
try
{
await _context.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync(ct);
if (databaseValues is null)
throw new ConflictException(class="code-string">"Produkt wurde von einem anderen Benutzer geloscht.");
var dbProduct = (Product)databaseValues.ToObject();
throw new ConflictException(
$class="code-string">"Produkt wurde von einem anderen Benutzer geandert. " +
$class="code-string">"Aktueller Preis in der Datenbank: {dbProduct.Price}. " +
$class="code-string">"Ihr versuchter Preis: {newPrice}.");
}
}In hochvolumigen Datensystemen fuge ich Entitaten, die von mehreren Benutzern oder Hintergrundprozessen gleichzeitig geandert werden konnen, immer Concurrency-Tokens hinzu. Die Alternative -- der letzte Schreiber gewinnt -- fuhrt zu stiller Datenbeschadigung, die im Nachhinein nahezu unmoglich zu debuggen ist.
Haufige fortgeschrittene EF-Core-Fehler
1. Fremdschlussel-Indizes vergessen
EF Core erstellt automatisch Indizes fur Fremdschlussel bei Beziehungen, die es kennt. Wenn Sie jedoch Fremdschlusselspalten manuell hinzufugen oder Shadow Properties verwenden, mussen Sie den Index selbst erstellen:
class=class="code-string">"code-comment">// Fehlender Index bei manuell deklariertem FK
builder.Property(o => o.AssignedAgentId);
builder.HasIndex(o => o.AssignedAgentId); class=class="code-string">"code-comment">// Nicht vergessen2. DbContext als Singleton verwenden
Der `DbContext` ist nicht threadsicher. Ihn als Singleton zu registrieren oder in einem statischen Feld zu halten, verursacht Datenbeschadigungen und intermittierende Absturze, die ausserst schwer zu reproduzieren sind:
class=class="code-string">"code-comment">// FALSCH: DbContext ist nicht threadsicher
services.AddSingleton<AppDbContext>();
class=class="code-string">"code-comment">// RICHTIG: Scoped Lebensdauer, einer pro Request
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));3. Interaktionen von Abfragefiltern ignorieren
Bei mehreren Abfragefiltern werden diese mit AND-Logik kombiniert. Wenn Sie vergessen, dass ein Filter existiert, liefern Abfragen stillschweigend unvollstandige Ergebnisse:
class=class="code-string">"code-comment">// Beide Filter gelten -- beim Debuggen, warum ein Datensatz
class=class="code-string">"code-comment">// "nicht existiert", vergisst man leicht den Mandantenfilter
modelBuilder.Entity<Order>().HasQueryFilter(
o => !o.IsDeleted && o.TenantId == _tenantProvider.CurrentTenantId);
class=class="code-string">"code-comment">// Zum Debuggen: alle Filter vorubergehend umgehen
var allOrders = await _context.Orders
.IgnoreQueryFilters()
.Where(o => o.Id == missingOrderId)
.SingleOrDefaultAsync(ct);4. DbContext in Hintergrunddiensten nicht ordnungsgemaess entsorgen
In Hosted Services oder Hintergrund-Jobs mussen Sie Scopes manuell erstellen und entsorgen:
public class OrderCleanupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public OrderCleanupService(IServiceScopeFactory scopeFactory)
=> _scopeFactory = scopeFactory;
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider
.GetRequiredService<AppDbContext>();
await context.Orders
.Where(o => o.IsArchived && o.ArchivedAt < DateTime.UtcNow.AddYears(-class="code-number">3))
.ExecuteDeleteAsync(ct);
await Task.Delay(TimeSpan.FromHours(class="code-number">1), ct);
}
}
}5. Getrackte und ungetrackte Entitaten mischen
Das Anhangen einer ungetrackten Entitat mit dem gleichen Schlussel wie eine bereits getrackte Entitat wirft eine Ausnahme. Dies passiert haufig beim Deserialisieren einer Entitat aus einem Cache oder einer Nachrichtenwarteschlange und dem Versuch, sie zu aktualisieren:
class=class="code-string">"code-comment">// Wirft eine Ausnahme, wenn die Entitat bereits getrackt wird
_context.Products.Update(deserializedProduct);
class=class="code-string">"code-comment">// Sicherer Ansatz: zuerst prufen, dann Eigenschaften aktualisieren
var tracked = await _context.Products.FindAsync(deserializedProduct.Id);
if (tracked is not null)
{
_context.Entry(tracked).CurrentValues.SetValues(deserializedProduct);
}Fazit
Fortgeschrittene EF-Core-Nutzung bedeutet zu wissen, wann LINQ und der Change Tracker Ihnen gut dienen und wann Sie daruber hinausgehen mussen. Kompilierte Abfragen, aufgeteilte Abfragen und Massenoperationen heben die Performance-Grenze an. Raw SQL und Interceptors behandeln die Randfalle, die kein ORM perfekt abstrahieren kann. Temporale Tabellen und Concurrency-Tokens schutzen die Datenintegritat auf eine Weise, die Anwendungscode allein nicht leisten kann.
Die Muster in diesem Artikel stammen aus Produktionssystemen, die taglich Millionen von Zeilen verarbeiten. Sie sind nicht theoretisch -- jedes einzelne hat ein reales Problem unter realer Last gelost.
Ich kann Ihnen helfen, Ihre EF-Core-Datenschicht auf Performance, Korrektheit und Migrationssicherheit zu auditieren.
Verwandte Artikel
Entity Framework Core: Der moderne Weg für Datenbankoperationen
Verwalten Sie Datenbankoperationen mit Entity Framework Core. Code-First, Migrations und Performance.
Clean Architecture in .NET: Skalierbare Projektstruktur
Wenden Sie Clean Architecture in .NET-Projekten an. Schichten, Abhängigkeiten und testbarer Code.
.NET Performance-Optimierung: Profiling und Best Practices
Optimieren Sie die Performance von .NET-Anwendungen. Profiling, Memory Management und Async-Patterns.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen