Claude Code Plugins

Community-maintained marketplace

Feedback

linq-optimization-patterns

@thapaliyabikendra/ai-artifacts
0
0

Master LINQ and EF Core query optimization including N+1 prevention, eager loading, projections, and performance patterns. Use when: (1) fixing N+1 queries, (2) optimizing slow queries, (3) implementing efficient data access, (4) reducing database load.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name linq-optimization-patterns
description Master LINQ and EF Core query optimization including N+1 prevention, eager loading, projections, and performance patterns. Use when: (1) fixing N+1 queries, (2) optimizing slow queries, (3) implementing efficient data access, (4) reducing database load.
layer 2
tech_stack dotnet, csharp, efcore
topics query-optimization, n-plus-one, eager-loading, projections, performance
depends_on
complements efcore-patterns
keywords Include, ThenInclude, Select, AsNoTracking, AsSplitQuery, WhereIf, N+1

LINQ Optimization Patterns

Optimize LINQ queries and EF Core data access for high-performance ABP applications.

When to Use

  • Fixing N+1 query problems
  • Optimizing slow repository queries
  • Implementing efficient pagination
  • Reducing database round trips
  • Improving API response times

N+1 Query Prevention

Problem: N+1 Queries

// BAD: N+1 queries - executes 1 + N queries
public async Task<List<DoctorDto>> GetDoctorsWithAppointmentsAsync()
{
    var doctors = await _doctorRepository.GetListAsync();

    var dtos = new List<DoctorDto>();
    foreach (var doctor in doctors)
    {
        // Each iteration executes a query!
        var appointments = await _appointmentRepository
            .GetListAsync(a => a.DoctorId == doctor.Id);

        dtos.Add(new DoctorDto
        {
            Id = doctor.Id,
            Name = doctor.FullName,
            AppointmentCount = appointments.Count
        });
    }
    return dtos;
}

Solution 1: Eager Loading with Include

// GOOD: Single query with Include
public async Task<List<DoctorDto>> GetDoctorsWithAppointmentsAsync()
{
    var query = await _doctorRepository.GetQueryableAsync();

    var doctors = await query
        .Include(d => d.Appointments)
        .ToListAsync();

    return doctors.Select(d => new DoctorDto
    {
        Id = d.Id,
        Name = d.FullName,
        AppointmentCount = d.Appointments.Count
    }).ToList();
}

Solution 2: Projection (Best for DTOs)

// BETTER: Project to DTO directly - only selects needed columns
public async Task<List<DoctorDto>> GetDoctorsWithAppointmentsAsync()
{
    var query = await _doctorRepository.GetQueryableAsync();

    return await query
        .Select(d => new DoctorDto
        {
            Id = d.Id,
            Name = d.FullName,
            AppointmentCount = d.Appointments.Count
        })
        .ToListAsync();
}

Solution 3: Batch Loading

// GOOD: Two queries instead of N+1
public async Task<List<DoctorDto>> GetDoctorsWithAppointmentsAsync()
{
    var doctors = await _doctorRepository.GetListAsync();
    var doctorIds = doctors.Select(d => d.Id).ToList();

    var appointmentCounts = await (await _appointmentRepository.GetQueryableAsync())
        .Where(a => doctorIds.Contains(a.DoctorId))
        .GroupBy(a => a.DoctorId)
        .Select(g => new { DoctorId = g.Key, Count = g.Count() })
        .ToDictionaryAsync(x => x.DoctorId, x => x.Count);

    return doctors.Select(d => new DoctorDto
    {
        Id = d.Id,
        Name = d.FullName,
        AppointmentCount = appointmentCounts.GetValueOrDefault(d.Id, 0)
    }).ToList();
}

Eager Loading Patterns

Include and ThenInclude

var query = await _appointmentRepository.GetQueryableAsync();

// Single level include
var appointments = await query
    .Include(a => a.Patient)
    .Include(a => a.Doctor)
    .ToListAsync();

// Nested include
var doctors = await (await _doctorRepository.GetQueryableAsync())
    .Include(d => d.Appointments)
        .ThenInclude(a => a.Patient)
    .Include(d => d.Specializations)
        .ThenInclude(s => s.Specialization)
    .ToListAsync();

WithDetails (ABP Extension)

// ABP provides WithDetails for common includes
public class DoctorRepository : EfCoreRepository<ClinicDbContext, Doctor, Guid>, IDoctorRepository
{
    public async Task<Doctor> GetWithAppointmentsAsync(Guid id)
    {
        var query = await GetQueryableAsync();

        return await query
            .IncludeDetails() // Uses default includes from repository
            .FirstOrDefaultAsync(d => d.Id == id);
    }

    public override async Task<IQueryable<Doctor>> WithDetailsAsync()
    {
        return (await GetQueryableAsync())
            .Include(d => d.Appointments)
            .Include(d => d.Specializations);
    }
}

Split Queries (Multiple Collections)

// Avoid Cartesian explosion with multiple collections
var doctors = await (await _doctorRepository.GetQueryableAsync())
    .Include(d => d.Appointments)
    .Include(d => d.Schedules)
    .Include(d => d.Specializations)
    .AsSplitQuery() // Executes 4 queries instead of 1 with Cartesian product
    .ToListAsync();

AsNoTracking for Read-Only

// GOOD: Read-only queries should use AsNoTracking
public async Task<List<PatientDto>> GetPatientsAsync()
{
    var query = await _patientRepository.GetQueryableAsync();

    return await query
        .AsNoTracking() // Skip change tracking - faster
        .Select(p => new PatientDto
        {
            Id = p.Id,
            FullName = $"{p.FirstName} {p.LastName}",
            Email = p.Email
        })
        .ToListAsync();
}

// Note: Projections with Select() are automatically NoTracking
// AsNoTracking is implicit when projecting to non-entity types

Projection Patterns

Select Only Needed Columns

// BAD: Loads entire entity
var patients = await query.ToListAsync();
var emails = patients.Select(p => p.Email);

// GOOD: Only loads Email column from database
var emails = await query
    .Select(p => p.Email)
    .ToListAsync();

Project to DTO

// GOOD: Project to DTO in query
public async Task<PagedResultDto<PatientListDto>> GetListAsync(GetPatientListInput input)
{
    var query = await _patientRepository.GetQueryableAsync();

    var totalCount = await query.CountAsync();

    var patients = await query
        .WhereIf(!input.Filter.IsNullOrWhiteSpace(),
            p => p.FirstName.Contains(input.Filter) ||
                 p.LastName.Contains(input.Filter) ||
                 p.Email.Contains(input.Filter))
        .OrderBy(input.Sorting ?? nameof(Patient.LastName))
        .PageBy(input)
        .Select(p => new PatientListDto
        {
            Id = p.Id,
            FullName = $"{p.FirstName} {p.LastName}",
            Email = p.Email,
            DateOfBirth = p.DateOfBirth,
            AppointmentCount = p.Appointments.Count
        })
        .ToListAsync();

    return new PagedResultDto<PatientListDto>(totalCount, patients);
}

Conditional Projection

// Include related data only when needed
public async Task<PatientDto> GetAsync(Guid id, bool includeAppointments = false)
{
    var query = await _patientRepository.GetQueryableAsync();

    if (includeAppointments)
    {
        return await query
            .Where(p => p.Id == id)
            .Select(p => new PatientDto
            {
                Id = p.Id,
                FullName = $"{p.FirstName} {p.LastName}",
                Appointments = p.Appointments.Select(a => new AppointmentDto
                {
                    Id = a.Id,
                    Date = a.AppointmentDate,
                    Status = a.Status
                }).ToList()
            })
            .FirstOrDefaultAsync();
    }

    return await query
        .Where(p => p.Id == id)
        .Select(p => new PatientDto
        {
            Id = p.Id,
            FullName = $"{p.FirstName} {p.LastName}"
        })
        .FirstOrDefaultAsync();
}

Efficient Pagination

Cursor-Based Pagination (Best for Large Data)

// Better than Skip/Take for large datasets
public async Task<List<PatientDto>> GetPatientsAfterAsync(
    DateTime? lastCreatedAt,
    Guid? lastId,
    int take = 20)
{
    var query = await _patientRepository.GetQueryableAsync();

    if (lastCreatedAt.HasValue && lastId.HasValue)
    {
        query = query.Where(p =>
            p.CreationTime < lastCreatedAt.Value ||
            (p.CreationTime == lastCreatedAt.Value && p.Id.CompareTo(lastId.Value) < 0));
    }

    return await query
        .OrderByDescending(p => p.CreationTime)
        .ThenByDescending(p => p.Id)
        .Take(take)
        .Select(p => new PatientDto { /* ... */ })
        .ToListAsync();
}

Offset Pagination with Count Optimization

// Only count when needed (first page or explicitly requested)
public async Task<PagedResultDto<PatientDto>> GetListAsync(
    GetPatientListInput input,
    bool includeCount = true)
{
    var query = await _patientRepository.GetQueryableAsync();
    query = ApplyFilters(query, input);

    long totalCount = 0;
    if (includeCount)
    {
        totalCount = await query.LongCountAsync();
    }

    var patients = await query
        .OrderBy(input.Sorting ?? "LastName")
        .PageBy(input)
        .Select(p => new PatientDto { /* ... */ })
        .ToListAsync();

    return new PagedResultDto<PatientDto>(totalCount, patients);
}

Filtering Patterns

ABP WhereIf Extension

public async Task<List<AppointmentDto>> GetListAsync(GetAppointmentListInput input)
{
    var query = await _appointmentRepository.GetQueryableAsync();

    return await query
        .WhereIf(input.DoctorId.HasValue, a => a.DoctorId == input.DoctorId)
        .WhereIf(input.PatientId.HasValue, a => a.PatientId == input.PatientId)
        .WhereIf(input.Status.HasValue, a => a.Status == input.Status)
        .WhereIf(input.FromDate.HasValue, a => a.AppointmentDate >= input.FromDate)
        .WhereIf(input.ToDate.HasValue, a => a.AppointmentDate <= input.ToDate)
        .WhereIf(!input.Filter.IsNullOrWhiteSpace(),
            a => a.Patient.FirstName.Contains(input.Filter) ||
                 a.Patient.LastName.Contains(input.Filter))
        .OrderByDescending(a => a.AppointmentDate)
        .Select(a => new AppointmentDto { /* ... */ })
        .ToListAsync();
}

Search with Full-Text (PostgreSQL)

// Use EF.Functions for database-specific operations
var patients = await query
    .Where(p => EF.Functions.ILike(p.FirstName, $"%{searchTerm}%") ||
                EF.Functions.ILike(p.LastName, $"%{searchTerm}%"))
    .ToListAsync();

Aggregation Patterns

Efficient Counting

// BAD: Loads all entities to count
var count = (await _patientRepository.GetListAsync()).Count;

// GOOD: Count at database level
var query = await _patientRepository.GetQueryableAsync();
var count = await query.CountAsync();

// GOOD: Count with filter
var activeCount = await query
    .Where(p => p.Status == PatientStatus.Active)
    .CountAsync();

Grouped Aggregations

// Get appointment counts by status
var query = await _appointmentRepository.GetQueryableAsync();

var statusCounts = await query
    .GroupBy(a => a.Status)
    .Select(g => new
    {
        Status = g.Key,
        Count = g.Count()
    })
    .ToDictionaryAsync(x => x.Status, x => x.Count);

Existence Checks

// BAD: Loads entity to check existence
var patient = await _patientRepository.FirstOrDefaultAsync(p => p.Email == email);
var exists = patient != null;

// GOOD: Use Any() - stops at first match
var query = await _patientRepository.GetQueryableAsync();
var exists = await query.AnyAsync(p => p.Email == email);

Raw SQL (When Necessary)

// For complex queries that can't be expressed in LINQ
public async Task<List<DoctorAvailabilityDto>> GetDoctorAvailabilityAsync(DateTime date)
{
    var context = await _dbContextProvider.GetDbContextAsync();

    return await context.Database
        .SqlQuery<DoctorAvailabilityDto>($@"
            SELECT d.Id, d.FullName, COUNT(a.Id) as AppointmentCount
            FROM Doctors d
            LEFT JOIN Appointments a ON d.Id = a.DoctorId
                AND a.AppointmentDate::date = {date:yyyy-MM-dd}
                AND a.IsDeleted = false
            WHERE d.IsDeleted = false
            GROUP BY d.Id, d.FullName
            ORDER BY AppointmentCount ASC")
        .ToListAsync();
}

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
N+1 queries Multiple round trips Use Include or projection
ToList() then Where() Loads all data to memory Filter in query
Select * equivalent Loads unnecessary columns Project to DTO
Tracking read-only data Overhead for change tracking Use AsNoTracking
Large OFFSET pagination Slow for large offsets Use cursor-based pagination
Count() after ToList() Loads all data Use CountAsync()
Multiple SaveChangesAsync() Multiple transactions Batch changes
Count() after pagination Double query execution Count BEFORE ToListAsync()
GetListAsync() for validation Loads entire table Use filtered AnyAsync() or GetQueryableAsync()
In-memory joins for bulk ops Memory explosion Use IQueryable joins, filter server-side

Critical: Count After Pagination (Double Query)

// ❌ BAD: Executes query TWICE (one for data, one for count)
var dtos = await AsyncExecuter.ToListAsync(
    queryable
    .OrderBy(input.Sorting)
    .Skip(input.SkipCount)
    .Take(input.MaxResultCount)
);
var totalCount = queryable.Count(); // Second execution on same queryable!

// ✅ GOOD: Count FIRST, then paginate
var totalCount = await AsyncExecuter.CountAsync(queryable);
var dtos = await AsyncExecuter.ToListAsync(
    queryable
    .OrderBy(input.Sorting)
    .Skip(input.SkipCount)
    .Take(input.MaxResultCount)
);

Critical: Loading Full Tables for Validation

// ❌ BAD: Loads ALL records to memory for validation
var _projects = await _projectRepository.GetListAsync();
var _customers = await _customerRepository.GetListAsync();
var _licensePlates = await _licensePlateRepository.GetListAsync();

// Then validates with in-memory LINQ
foreach (var item in input)
{
    var project = _projects.FirstOrDefault(p => p.Code == item.ProjectCode);
    // ...
}

// ✅ GOOD: Only load what you need based on input
var projectCodes = input.Select(x => x.ProjectCode).Distinct().ToList();
var customerNames = input.Select(x => x.CustomerName).Distinct().ToList();

var projects = await (await _projectRepository.GetQueryableAsync())
    .Where(p => projectCodes.Contains(p.ProjectCode))
    .ToDictionaryAsync(p => p.ProjectCode, p => p);

var customers = await (await _customerRepository.GetQueryableAsync())
    .Where(c => customerNames.Contains(c.CustomerName))
    .ToDictionaryAsync(c => c.CustomerName, c => c);

// Validate using dictionaries (O(1) lookup)
foreach (var item in input)
{
    if (!projects.TryGetValue(item.ProjectCode, out var project))
    {
        validations.Add($"Invalid project code: {item.ProjectCode}");
    }
}

Performance Checklist

  • No N+1 queries (check with logging)
  • Projections used for DTOs
  • AsNoTracking for read-only queries
  • Appropriate indexes exist for filters
  • Pagination uses cursor or optimized offset
  • Counts done at database level
  • Include only loads needed relations
  • Split queries for multiple collections

Debugging Queries

// Enable EF Core logging in appsettings.Development.json
{
  "Logging": {
    "LogLevel": {
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

// Or use ToQueryString() to see generated SQL
var query = await _patientRepository.GetQueryableAsync();
var sql = query
    .Where(p => p.Status == PatientStatus.Active)
    .ToQueryString();

_logger.LogInformation("Generated SQL: {Sql}", sql);

Integration Points

This skill is used by:

  • abp-developer: Efficient data access implementation
  • abp-code-reviewer: Query performance validation
  • debugger: Performance issue diagnosis