CommunityCodierung & Entwicklunggithub.com

efcore-patterns

Entity Framework Core Best Practices inklusive NoTracking als Standard, Query-Splitting für Navigationssammlungen, Migrationsverwaltung, dedizierte Migrationsdienste und häufige Fallstricke.

Was ist efcore-patterns?

Dieses Skill beschreibt etablierte Best Practices für die Arbeit mit Entity Framework Core und zielt darauf ab, die Leistung und Wartbarkeit von Datenzugriffsschichten zu optimieren. Es behandelt essenzielle Muster wie die Konfiguration des NoTracking-Verhaltens als Standard, um den Overhead von Change Tracking bei leselastigen Workloads zu vermeiden, und zeigt explizit, wie Updates bei deaktiviertem Tracking korrekt vorgenommen werden. Zudem betont das Skill die strikte Nutzung von EF Core CLI-Befehlen zur Migrationsverwaltung, warnt vor manuellen Eingriffen und schlägt die Auslagerung der Migrationsausführung in einen dedizierten Dienst vor, um eine saubere Trennung von der Anwendungslogik zu erreichen – insbesondere in Kombination mit einer Aspire-Orchestrierung. Ein weiterer Schwerpunkt ist die Verwendung der `CreateExecutionStrategy()` für robuste Wiederholungsversuche bei transienten Datenbankfehlern und der effiziente Einsatz von `ExecuteUpdateAsync` für Massenoperationen, einschließlich der Vermeidung typischer Fehler wie N+1-Abfragen. Das Skill unterstützt Entwicklerteams, die mit .NET und EF Core arbeiten, und ist sowohl für neue Projekte als auch für die Optimierung bestehender Anwendungen geeignet.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/efcore-patterns

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

Entity Framework Core Patterns

Wann dieses Skill verwenden?

Verwenden Sie dieses Skill, wenn:

  • Sie EF Core in einem neuen Projekt einrichten
  • Sie die Abfrageleistung optimieren möchten
  • Sie Datenbankmigrationen verwalten
  • Sie EF Core in .NET Aspire integrieren
  • Sie Probleme mit Change Tracking debuggen
  • Sie mehrere Navigationssammlungen effizient laden möchten (Query Splitting)

Kernprinzipien

  1. NoTracking als Standard – Die meisten Abfragen sind schreibgeschützt; wählen Sie explizit Tracking aus.
  2. Niemals Migrationen manuell bearbeiten – Verwenden Sie immer CLI-Befehle.
  3. Dedizierter Migrationsdienst – Trennen Sie die Migrationsausführung vom Anwendungsstart.
  4. ExecutionStrategy für Wiederholungen – Behandeln Sie transiente Datenbankfehler.
  5. Explizite Updates – Bei NoTracking markieren Sie Entitäten explizit für das Update.

Pattern 1: NoTracking als Standard

Konfigurieren Sie Ihren DbContext so, dass Change Tracking standardmäßig deaktiviert ist. Dies verbessert die Leistung bei leselastigen Workloads.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
        // Change Tracking standardmäßig deaktivieren für bessere Leistung bei schreibgeschützten Abfragen
        // Verwenden Sie explizit .AsTracking() für Abfragen, die Änderungen verfolgen müssen
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    }

    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();
}

Wenn NoTracking aktiv ist

Schreibgeschützte Abfragen funktionieren normal:

// ✅ Schnelles Lesen – kein Tracking-Overhead
var orders = await dbContext.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

Schreiboperationen erfordern explizite Behandlung:

// ❌ FALSCH – Entität wird nicht verfolgt, SaveChanges tut nichts
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Es passiert nichts!

// ✅ RICHTIG – Entität explizit für Update markieren
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
dbContext.Orders.Update(order); // Markiert gesamte Entität als geändert
await dbContext.SaveChangesAsync();

// ✅ AUCH RICHTIG – AsTracking() für die Abfrage verwenden
var order = await dbContext.Orders
    .AsTracking()
    .FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Funktioniert!

Wann Tracking verwenden

SzenarioTracking verwenden?Grund
Daten in der Benutzeroberfläche anzeigenNeinSchreibgeschützt, keine Updates
API GET-EndpunkteNeinDaten zurückgeben, keine Mutationen
Einzelne Entität aktualisierenJa oder explizites Update()Änderungen speichern müssen
Komplexe Updates mit NavigationenJaTracking behandelt Beziehungen
Batch-OperationenNein + ExecuteUpdateEffizienter

Explizites Add/Update-Muster

public class OrderService
{
    private readonly ApplicationDbContext _db;

    // CREATE – Immer Add verwenden (funktioniert unabhängig vom Tracking)
    public async Task<Order> CreateOrderAsync(Order order)
    {
        _db.Orders.Add(order);
        await _db.SaveChangesAsync();
        return order;
    }

    // UPDATE – Explizit als geändert markieren
    public async Task UpdateOrderStatusAsync(Guid orderId, OrderStatus newStatus)
    {
        var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId)
            ?? throw new NotFoundException($"Bestellung {orderId} nicht gefunden");

        order.Status = newStatus;
        order.UpdatedAt = DateTimeOffset.UtcNow;

        // Explizit als geändert markieren, da DbContext standardmäßig NoTracking verwendet
        _db.Orders.Update(order);
        await _db.SaveChangesAsync();
    }

    // DELETE – Anhängen und entfernen
    public async Task DeleteOrderAsync(Guid orderId)
    {
        var order = new Order { Id = orderId };
        _db.Orders.Remove(order);
        await _db.SaveChangesAsync();
    }
}

Pattern 2: Niemals Migrationen manuell bearbeiten

KRITISCH: Verwenden Sie immer EF Core CLI-Befehle, um Migrationen zu verwalten. Niemals:

  • Migrationsdateien manuell bearbeiten (außer für benutzerdefiniertes SQL in Up()/Down())
  • Migrationsdateien direkt löschen
  • Migrationsdateien umbenennen
  • Migrationen zwischen Projekten kopieren

Migrationen erstellen

# Neue Migration erstellen
dotnet ef migrations add AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Mit einem bestimmten DbContext (falls mehrere vorhanden)
dotnet ef migrations add AddCustomerTable \
    --context ApplicationDbContext \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

Migrationen entfernen

# Letzte Migration entfernen (falls noch nicht angewendet)
dotnet ef migrations remove \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# NIEMALS so machen:
# rm Migrations/20240101_AddCustomerTable.cs  # ❌ SCHLECHT!

Migrationen anwenden

# Alle ausstehenden Migrationen anwenden
dotnet ef database update \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Bis zu einer bestimmten Migration anwenden
dotnet ef database update AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Auf eine vorherige Migration zurücksetzen
dotnet ef database update PreviousMigrationName \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

SQL-Skripte generieren

# SQL-Skript für alle Migrationen generieren
dotnet ef migrations script \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api \
    --output migrations.sql

# Idempotentes Skript generieren (sicher mehrfach auszuführen)
dotnet ef migrations script \
    --idempotent \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

Pattern 3: Dedizierter Migrationsdienst mit Aspire

Trennen Sie die Migrationsausführung von Ihrer Hauptanwendung mit einem dedizierten Migrationsdienst. Dies stellt sicher:

  • Migrationen werden abgeschlossen, bevor die App startet
  • Saubere Trennung der Verantwortlichkeiten
  • Kontrolliertes Seeding in Testumgebungen

Projektstruktur

src/
├── MyApp.AppHost/           # Aspire-Orchestrierung
├── MyApp.Api/               # Hauptanwendung
├── MyApp.Infrastructure/    # DbContext und Migrationen
└── MyApp.MigrationService/  # Dedizierter Migrationsausführer

Program.cs des Migrationsdienstes

using MyApp.Infrastructure.Data;
using MyApp.MigrationService;
using Microsoft.EntityFrameworkCore;

var builder = Host.CreateApplicationBuilder(args);

// Aspire-Service-Standards hinzufügen
builder.AddServiceDefaults();

// PostgreSQL DbContext hinzufügen
var connectionString = builder.Configuration.GetConnectionString("appdb")
    ?? throw new InvalidOperationException("Verbindungszeichenfolge 'appdb' nicht gefunden.");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, npgsqlOptions =>
        npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure")));

// Migrationsworker hinzufügen
builder.Services.AddHostedService<MigrationWorker>();

var host = builder.Build();
host.Run();

MigrationWorker.cs

public class MigrationWorker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
    private readonly ILogger<MigrationWorker> _logger;

    public MigrationWorker(
        IServiceProvider serviceProvider,
        IHostApplicationLifetime hostApplicationLifetime,
        ILogger<MigrationWorker> logger)
    {
        _serviceProvider = serviceProvider;
        _hostApplicationLifetime = hostApplicationLifetime;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Migrationsdienst wird gestartet...");

        try
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

            await RunMigrationsAsync(dbContext, stoppingToken);

            _logger.LogInformation("Migrationsdienst erfolgreich abgeschlossen.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Migrationsdienst fehlgeschlagen: {Error}", ex.Message);
            throw;
        }
        finally
        {
            // Anwendung nach Abschluss der Migrationen beenden
            _hostApplicationLifetime.StopApplication();
        }
    }

    private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
    {
        // Ausführungsstrategie für die Behandlung transienter Fehler verwenden
        var strategy = dbContext.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async () =>
        {
            var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);

            if (pendingMigrations.Any())
            {
                _logger.LogInformation("Wende {Count} ausstehende Migrationen an...",
                    pendingMigrations.Count());

                await dbContext.Database.MigrateAsync(ct);

                _logger.LogInformation("Migrationen erfolgreich angewendet.");
            }
            else
            {
                _logger.LogInformation("Keine ausstehenden Migrationen. Die Datenbank ist auf dem neuesten Stand.");
            }
        });
    }
}

AppHost-Konfiguration

var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("appdb");

// Migrationen werden zuerst ausgeführt, dann beendet
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
    .WaitFor(db)
    .WithReference(db);

// API wartet auf Abschluss der Migrationen
var api = builder.AddProject<Projects.MyApp_Api>("api")
    .WaitForCompletion(migrations)  // Entscheidend: wartet auf Abschluss der Migrationen
    .WithReference(db);

Pattern 4: ExecutionStrategy für transiente Fehler

Verwenden Sie immer CreateExecutionStrategy() für Operationen, die vorübergehend fehlschlagen könnten:

public async Task UpdateWithRetryAsync(Guid id, Action<Order> update)
{
    var strategy = _dbContext.Database.CreateExecutionStrategy();

    await strategy.ExecuteAsync(async () =>
    {
        var order = await _dbContext.Orders
            .AsTracking()
            .FirstOrDefaultAsync(o => o.Id == id);

        if (order is null) return;

        update(order);
        await _dbContext.SaveChangesAsync();
    });
}

Wichtig: Sie können CreateExecutionStrategy() nicht mit benutzerinitiierten Transaktionen verwenden. Wenn Sie Transaktionen mit Wiederholungen benötigen:

var strategy = _dbContext.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    // Transaktion muss INNERHALB des Strategy-Rückrufs sein
    await using var transaction = await _dbContext.Database.BeginTransactionAsync();

    try
    {
        // ... Ihre Operationen ...
        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
});

Pattern 5: Massenoperationen mit ExecuteUpdate/ExecuteDelete

Verwenden Sie für Massenoperationen ExecuteUpdateAsync und ExecuteDeleteAsync ab EF Core 7+, anstatt Entitäten zu laden:

// ❌ LANGSAM – Lädt alle Entitäten in den Speicher
var expiredOrders = await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ToListAsync();

foreach (var order in expiredOrders)
{
    order.Status = OrderStatus.Expired;
}
await _db.SaveChangesAsync();

// ✅ SCHNELL – Einzelne SQL UPDATE-Anweisung
await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(o => o.Status, OrderStatus.Expired)
        .SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));

// ✅ SCHNELL – Einzelne SQL DELETE-Anweisung
await _db.Orders
    .Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate)
    .ExecuteDeleteAsync();

Häufige Fallstricke

1. Vergessen zu aktualisieren, wenn NoTracking aktiv ist

// ❌ Stiller Fehler – Entität wird nicht verfolgt
var customer = await _db.Customers.FindAsync(id);
customer.Name = "Neuer Name";
await _db.SaveChangesAsync(); // Tut nichts!

// ✅ Explizites Update
var customer = await _db.Customers.FindAsync(id);
customer.Name = "Neuer Name";
_db.Customers.Update(customer);
await _db.SaveChangesAsync();

2. N+1-Abfrageproblem

// ❌ N+1 Abfragen – eine Abfrage pro Bestellung
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
    var orders = customer.Orders; // Lazy Loading löst Abfrage aus
}

// ✅ Eager Loading – einzelne Abfrage
var customers = await _db.Customers
    .Include(c => c.Orders)
    .ToListAsync();

3. Tracking-Konflikte mit mehreren DbContext-Instanzen

// ❌ Tracking-Konflikt – Entität wird von einem anderen Kontext verfolgt
var order1 = await _db1.Orders.AsTracking().FindAsync(id);
var order2 = await _db2.Orders.AsTracking().FindAsync(id);
order2.Status = OrderStatus.Shipped;
await _db2.SaveChangesAsync(); // Kann werfen oder sich unerwartet verhalten

// ✅ Einzelnen Kontext verwenden oder zuerst trennen
_db1.Entry(order1).State = EntityState.Detached;

4. Async nicht konsistent verwenden

// ❌ Blockierender Aufruf im asynchronen Kontext
var orders = _db.Orders.ToList(); // Blockiert Thread

// ✅ Durchgängig asynchron
var orders = await _db.Orders.ToListAsync();

5. Abfragen innerhalb von Schleifen

// ❌ Abfrage pro Iteration
foreach (var orderId in orderIds)
{
    var order = await _db.Orders.FindAsync(orderId);
    // Bestellung verarbeiten
}

// ✅ Einzelne Abfrage
var orders = await _db.Orders
    .Where(o => orderIds.Contains(o.Id))
    .ToListAsync();

DbContext-Lebensdauer in DI

ASP.NET Core (Standardmäßig Scoped)

// Scoped = eine Instanz pro HTTP-Anfrage
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

...

[gekürzt für die Übersetzung]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

Verwandte Skills