Section 4 of 7

Querying Data with LINQ

🎯 What You'll Learn

  • Basic LINQ queries
  • Filtering with Where
  • Projecting with Select
  • Ordering and paging
  • Aggregation functions
  • Async queries

What is LINQ?

LINQ (Language Integrated Query) allows you to query data using C# syntax instead of SQL. EF Core translates LINQ to SQL.

Basic Queries

Get All Records

ToList() and ToListAsync() C#
// Synchronous
var products = _context.Products.ToList();

// Asynchronous (recommended)
var products = await _context.Products.ToListAsync();

Find by Primary Key

Find() and FindAsync() C#
// Find by ID
var product = await _context.Products.FindAsync(1);

// Returns null if not found
if (product == null)
{
    return NotFound();
}

First, Single, and FirstOrDefault

Different Query Methods C#
// First (throws if empty)
var product = await _context.Products.FirstAsync();

// FirstOrDefault (returns null if empty)
var product = await _context.Products.FirstOrDefaultAsync();

// Single (throws if 0 or >1 results)
var product = await _context.Products.SingleAsync(p => p.Id == 1);

// SingleOrDefault (returns null if 0, throws if >1)
var product = await _context.Products.SingleOrDefaultAsync(p => p.Id == 1);

Filtering with Where

Where Clause C#
// Simple filter
var expensiveProducts = await _context.Products
    .Where(p => p.Price > 100)
    .ToListAsync();

// Multiple conditions (AND)
var products = await _context.Products
    .Where(p => p.Price > 100 && p.Quantity > 0)
    .ToListAsync();

// OR condition
var products = await _context.Products
    .Where(p => p.Price < 50 || p.Price > 500)
    .ToListAsync();

// String operations
var products = await _context.Products
    .Where(p => p.Name.Contains("Laptop"))
    .ToListAsync();

// StartsWith, EndsWith
var products = await _context.Products
    .Where(p => p.Name.StartsWith("Dell"))
    .ToListAsync();

Projecting with Select

Select Specific Columns C#
// Select specific properties
var productNames = await _context.Products
    .Select(p => p.Name)
    .ToListAsync();

// Anonymous type
var products = await _context.Products
    .Select(p => new 
    {
        p.Id,
        p.Name,
        p.Price
    })
    .ToListAsync();

// DTO projection
var products = await _context.Products
    .Select(p => new ProductDto
    {
        Id = p.Id,
        Name = p.Name,
        Price = p.Price
    })
    .ToListAsync();

Ordering

OrderBy and OrderByDescending C#
// Ascending order
var products = await _context.Products
    .OrderBy(p => p.Price)
    .ToListAsync();

// Descending order
var products = await _context.Products
    .OrderByDescending(p => p.Price)
    .ToListAsync();

// Multiple ordering (ThenBy)
var products = await _context.Products
    .OrderBy(p => p.Category)
    .ThenByDescending(p => p.Price)
    .ToListAsync();

Paging

Skip and Take C#
// Page 1: first 20 products
var products = await _context.Products
    .OrderBy(p => p.Id)
    .Take(20)
    .ToListAsync();

// Page 2: skip 20, take 20
var products = await _context.Products
    .OrderBy(p => p.Id)
    .Skip(20)
    .Take(20)
    .ToListAsync();

// Generic paging
int page = 2;
int pageSize = 20;

var products = await _context.Products
    .OrderBy(p => p.Id)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();

Aggregation

Count, Sum, Average, Min, Max C#
// Count
var totalProducts = await _context.Products.CountAsync();

// Count with condition
var expensiveCount = await _context.Products
    .CountAsync(p => p.Price > 100);

// Sum
var totalValue = await _context.Products
    .SumAsync(p => p.Price * p.Quantity);

// Average
var avgPrice = await _context.Products
    .AverageAsync(p => p.Price);

// Min and Max
var minPrice = await _context.Products.MinAsync(p => p.Price);
var maxPrice = await _context.Products.MaxAsync(p => p.Price);

// Any (check if exists)
var hasExpensive = await _context.Products
    .AnyAsync(p => p.Price > 1000);

Grouping

GroupBy C#
// Group by category
var grouped = await _context.Products
    .GroupBy(p => p.CategoryId)
    .Select(g => new
    {
        CategoryId = g.Key,
        Count = g.Count(),
        TotalValue = g.Sum(p => p.Price * p.Quantity),
        AvgPrice = g.Average(p => p.Price)
    })
    .ToListAsync();

Complete InvenTrack Example

Controllers/ProductsController.cs C#
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly InvenTrackDbContext _context;

    public ProductsController(InvenTrackDbContext context)
    {
        _context = context;
    }

    // GET /api/products?page=1&pageSize=20&search=laptop&minPrice=100
    [HttpGet]
    public async Task<ActionResult> GetProducts(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string? search = null,
        [FromQuery] decimal? minPrice = null,
        [FromQuery] decimal? maxPrice = null)
    {
        var query = _context.Products.AsQueryable();

        // Apply filters
        if (!string.IsNullOrEmpty(search))
        {
            query = query.Where(p => p.Name.Contains(search));
        }

        if (minPrice.HasValue)
        {
            query = query.Where(p => p.Price >= minPrice.Value);
        }

        if (maxPrice.HasValue)
        {
            query = query.Where(p => p.Price <= maxPrice.Value);
        }

        // Get total count
        var totalCount = await query.CountAsync();

        // Apply paging
        var products = await query
            .OrderBy(p => p.Name)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(p => new
            {
                p.Id,
                p.Name,
                p.Price,
                p.Quantity
            })
            .ToListAsync();

        return Ok(new
        {
            data = products,
            pagination = new
            {
                page,
                pageSize,
                totalCount,
                totalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
            }
        });
    }

    // GET /api/products/statistics
    [HttpGet("statistics")]
    public async Task<ActionResult> GetStatistics()
    {
        var stats = new
        {
            totalProducts = await _context.Products.CountAsync(),
            totalValue = await _context.Products
                .SumAsync(p => p.Price * p.Quantity),
            averagePrice = await _context.Products.AverageAsync(p => p.Price),
            minPrice = await _context.Products.MinAsync(p => p.Price),
            maxPrice = await _context.Products.MaxAsync(p => p.Price)
        };

        return Ok(stats);
    }
}

Best Practices

  • Always use async: Use ToListAsync(), FirstOrDefaultAsync(), etc.
  • Filter early: Apply Where() before Select() for efficiency
  • Project early: Use Select() to reduce data transfer
  • AsQueryable(): Build queries dynamically
  • Avoid N+1: Use Include() for related data (next section)
  • Use Any(): Instead of Count() > 0
  • Pagination: Always paginate large result sets

Key Takeaways

  • LINQ: Query databases using C# syntax
  • Where(): Filter records
  • Select(): Project specific columns
  • OrderBy(): Sort results
  • Skip/Take: Implement paging
  • Count/Sum/Average: Aggregation functions
  • GroupBy(): Group and aggregate
  • Async methods: Always use for I/O operations
🎯 Next Steps

You now understand querying with LINQ! In the next section, we'll explore Relationships—how to define and work with one-to-one, one-to-many, and many-to-many relationships.