{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/optimizing-ef-core-queries",
  "version": "1.0.0",
  "name": "optimizing-ef-core-queries",
  "description": "Optimize Entity Framework Core queries by fixing N+1 problems, choosing correct tracking modes, using compiled queries, and avoiding common performance traps. Use when EF Core queries are slow, generating excessive SQL, or causing high database load.",
  "system_prompt_fragment": "# Optimizing EF Core Queries\n\n## When to Use\n\n- EF Core queries are slow or generating too many SQL statements\n- Database CPU/IO is high due to ORM inefficiency\n- N+1 query patterns are detected in logs\n- Large result sets cause memory pressure\n\n## When Not to Use\n\n- The user is using Dapper or raw ADO.NET (not EF Core)\n- The performance issue is database-side (missing indexes, bad schema)\n- The user is building a new data access layer from scratch\n\n## Inputs\n\n| Input | Required | Description |\n|-------|----------|-------------|\n| Slow EF Core queries | Yes | The LINQ queries or DbContext usage to optimize |\n| SQL output or logs | No | EF Core generated SQL or query execution logs |\n\n## Workflow\n\n### Step 1: Enable query logging to see the actual SQL\n\n```csharp\n// In Program.cs or DbContext configuration:\noptionsBuilder\n    .UseSqlServer(connectionString)\n    .LogTo(Console.WriteLine, LogLevel.Information)\n    .EnableSensitiveDataLogging()  // shows parameter values (dev only!)\n    .EnableDetailedErrors();\n```\n\nOr use the `Microsoft.EntityFrameworkCore` log category:\n\n```json\n{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Microsoft.EntityFrameworkCore.Database.Command\": \"Information\"\n    }\n  }\n}\n```\n\n### Step 2: Fix N+1 query patterns\n\n**The #1 EF Core performance killer.** Happens when loading related entities in a loop.\n\n**Before (N+1 — 1 query for orders + N queries for items):**\n```csharp\nvar orders = await db.Orders.ToListAsync();\nforeach (var order in orders)\n{\n    // Each access triggers a lazy-load query!\n    var items = order.Items.Count;\n}\n```\n\n**After (eager loading — 1 or 2 queries total):**\n```csharp\n// Option 1: Include (JOIN)\nvar orders = await db.Orders\n    .Include(o => o.Items)\n    .ToListAsync();\n\n// Option 2: Split query (separate SQL, avoids cartesian explosion)\nvar orders = await db.Orders\n    .Include(o => o.Items)\n    .AsSplitQuery()\n    .ToListAsync();\n\n// Option 3: Explicit projection (best - only fetches needed columns)\nvar orderSummaries = await db.Orders\n    .Select(o => new OrderSummary\n    {\n        OrderId = o.Id,\n        Total = o.Items.Sum(i => i.Price),\n        ItemCount = o.Items.Count\n    })\n    .ToListAsync();\n```\n\n**When to use Split vs Single query:**\n\n| Scenario | Use |\n|----------|-----|\n| 1 level of Include | Single query (default) |\n| Multiple Includes (Cartesian risk) | `AsSplitQuery()` |\n| Include with large child collections | `AsSplitQuery()` |\n| Need transaction consistency | Single query |\n\n### Step 3: Use NoTracking for read-only queries\n\n**Change tracking overhead is significant.** Disable it when you don't need to update entities:\n\n```csharp\n// Per-query\nvar products = await db.Products\n    .AsNoTracking()\n    .Where(p => p.IsActive)\n    .ToListAsync();\n\n// Global default for read-heavy apps\nservices.AddDbContext<AppDbContext>(options =>\n    options.UseSqlServer(connectionString)\n           .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));\n```\n\n**Use `AsNoTrackingWithIdentityResolution()` when the query returns duplicate entities to avoid duplicated objects in memory.**\n\n### Step 4: Use compiled queries for hot paths\n\n```csharp\n// Define once as static\nprivate static readonly Func<AppDbContext, int, Task<Order?>> GetOrderById =\n    EF.CompileAsyncQuery((AppDbContext db, int id) =>\n        db.Orders\n            .Include(o => o.Items)\n            .FirstOrDefault(o => o.Id == id));\n\n// Use repeatedly — skips query compilation overhead\nvar order = await GetOrderById(db, orderId);\n```\n\n### Step 5: Avoid common query traps\n\n| Trap | Problem | Fix |\n|------|---------|-----|\n| `ToList()` before `Where()` | Loads entire table into memory | Filter first: `.Where().ToList()` |\n| `Count()` to check existence | Scans all rows | Use `.Any()` instead |\n| `.Select()` after `.Include()` | Include is ignored with projection | Remove Include, use Select only |\n| `string.Contains()` in Where | May not translate, falls to client eval | Use `EF.Functions.Like()` for SQL LIKE |\n| Calling `.ToList()` inside `Select()` | Causes nested queries | Use projection with `Select` all the way |\n\n### Step 6: Use raw SQL or FromSql for complex queries\n\nWhen LINQ can't express it efficiently:\n\n```csharp\nvar results = await db.Orders\n    .FromSqlInterpolated($@\"\n        SELECT o.* FROM Orders o\n        INNER JOIN (\n            SELECT OrderId, SUM(Price) as Total\n            FROM OrderItems\n            GROUP BY OrderId\n            HAVING SUM(Price) > {minTotal}\n        ) t ON o.Id = t.OrderId\")\n    .AsNoTracking()\n    .ToListAsync();\n```\n\n## Validation\n\n- [ ] SQL logging shows expected number of queries (no N+1)\n- [ ] Read-only queries use `AsNoTracking()`\n- [ ] Hot-path queries use compiled queries\n- [ ] No client-side evaluation warnings in logs\n- [ ] Include/split strategy matches data shape\n\n## Common Pitfalls\n\n| Pitfall | Solution |\n|---------|----------|\n| Lazy loading silently creating N+1 | Remove `Microsoft.EntityFrameworkCore.Proxies` or disable lazy loading |\n| Global query filters forgotten in perf analysis | Check `HasQueryFilter` in model config; use `IgnoreQueryFilters()` if needed |\n| `DbContext` kept alive too long | DbContext should be scoped (per-request); don't cache it |\n| Batch updates fetching then saving | EF Core 7+: use `ExecuteUpdateAsync` / `ExecuteDeleteAsync` for bulk operations |\n| String interpolation in `FromSqlRaw` | SQL injection risk — use `FromSqlInterpolated` (parameterized) |",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/optimizing-ef-core-queries"
  ],
  "tags": [
    "dotnet-data",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-data/skills/optimizing-ef-core-queries/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-data/skills/optimizing-ef-core-queries/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}