Section 7 of 7

Advanced EF Core

🎯 What You'll Learn

  • Raw SQL queries
  • Transactions
  • AsNoTracking for performance
  • Global query filters
  • Compiled queries
  • Performance best practices

Raw SQL Queries

Sometimes you need to execute raw SQL for complex queries or performance optimization.

FromSqlRaw

Execute Raw SQL C#
// Simple query
var products = await _context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > 100")
    .ToListAsync();

// With parameters (prevents SQL injection)
var minPrice = 100;
var products = await _context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)
    .ToListAsync();

// Can combine with LINQ
var products = await _context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)
    .Where(p => p.Quantity > 0)
    .OrderBy(p => p.Name)
    .ToListAsync();

FromSqlInterpolated (Safer)

Interpolated SQL C#
var minPrice = 100;
var products = await _context.Products
    .FromSqlInterpolated($"SELECT * FROM Products WHERE Price > {minPrice}")
    .ToListAsync();

ExecuteSqlRaw (Non-Query)

Execute Commands C#
// Update/Delete/Insert
var rowsAffected = await _context.Database
    .ExecuteSqlRawAsync("UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = {0}", 1);

// Interpolated version
var categoryId = 1;
var rowsAffected = await _context.Database
    .ExecuteSqlInterpolatedAsync($"UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = {categoryId}");

Transactions

Transactions ensure multiple operations succeed or fail together.

Automatic Transactions

SaveChanges is Transactional C#
// All changes saved in a single transaction
_context.Products.Add(new Product { Name = "Laptop", Price = 999 });
_context.Products.Add(new Product { Name = "Mouse", Price = 29 });

// Both saved or both rolled back
await _context.SaveChangesAsync();

Manual Transactions

Explicit Transaction Control C#
using var transaction = await _context.Database.BeginTransactionAsync();

try
{
    // Operation 1
    var product = await _context.Products.FindAsync(1);
    product.Quantity -= 1;
    await _context.SaveChangesAsync();

    // Operation 2
    var order = new Order { CustomerName = "John", OrderDate = DateTime.UtcNow };
    _context.Orders.Add(order);
    await _context.SaveChangesAsync();

    // Commit if all succeeded
    await transaction.CommitAsync();
}
catch
{
    // Rollback on error
    await transaction.RollbackAsync();
    throw;
}

Transaction with Raw SQL

Mix EF and Raw SQL C#
using var transaction = await _context.Database.BeginTransactionAsync();

try
{
    // EF Core operation
    _context.Products.Add(new Product { Name = "Laptop", Price = 999 });
    await _context.SaveChangesAsync();

    // Raw SQL operation
    await _context.Database
        .ExecuteSqlRawAsync("UPDATE Inventory SET LastUpdated = GETDATE()");

    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

AsNoTracking for Performance

AsNoTracking() disables change tracking for read-only queries, improving performance.

Read-Only Queries C#
// Tracked (default) - slower, can update
var products = await _context.Products.ToListAsync();

// No tracking - faster, read-only
var products = await _context.Products
    .AsNoTracking()
    .ToListAsync();

// Use for API GET endpoints
[HttpGet]
public async Task<ActionResult> GetProducts()
{
    var products = await _context.Products
        .AsNoTracking()
        .ToListAsync();
    
    return Ok(products);
}

Global No Tracking

Configure Globally C#
builder.Services.AddDbContext<InvenTrackDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

Global Query Filters

Global query filters automatically apply filters to all queries.

Soft Delete Example C#
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public bool IsDeleted { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // Automatically filter out deleted products
    modelBuilder.Entity<Product>()
        .HasQueryFilter(p => !p.IsDeleted);
}

// All queries automatically exclude deleted products
var products = await _context.Products.ToListAsync(); // Only non-deleted

// Ignore filter when needed
var allProducts = await _context.Products
    .IgnoreQueryFilters()
    .ToListAsync(); // Includes deleted

Compiled Queries

Compiled queries improve performance by caching query translation.

Define Compiled Query C#
private static readonly Func<InvenTrackDbContext, int, Task<Product?>> _getProductById =
    EF.CompileAsyncQuery((InvenTrackDbContext context, int id) =>
        context.Products.FirstOrDefault(p => p.Id == id));

// Use compiled query
var product = await _getProductById(_context, 1);

Batch Operations

Bulk Insert C#
// Add multiple entities
var products = new List<Product>
{
    new Product { Name = "Laptop", Price = 999 },
    new Product { Name = "Mouse", Price = 29 },
    new Product { Name = "Keyboard", Price = 79 }
};

_context.Products.AddRange(products);
await _context.SaveChangesAsync();

// Remove multiple
_context.Products.RemoveRange(products);
await _context.SaveChangesAsync();

Performance Best Practices

1. Use AsNoTracking for Read-Only

Read-Only Queries C#
var products = await _context.Products.AsNoTracking().ToListAsync();

2. Project Only Needed Data

Use Select C#
var products = await _context.Products
    .Select(p => new { p.Id, p.Name, p.Price })
    .ToListAsync();

3. Avoid N+1 with Include

Eager Load C#
var products = await _context.Products
    .Include(p => p.Category)
    .ToListAsync();

4. Use Pagination

Skip and Take C#
var products = await _context.Products
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToListAsync();

5. Use Indexes

Create Indexes C#
modelBuilder.Entity<Product>()
    .HasIndex(p => p.Name);

// Composite index
modelBuilder.Entity<Product>()
    .HasIndex(p => new { p.CategoryId, p.Price });

6. Enable Query Logging

Log SQL Queries C#
builder.Services.AddDbContext<InvenTrackDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    options.LogTo(Console.WriteLine, LogLevel.Information);
    options.EnableSensitiveDataLogging(); // Dev only!
});

Complete InvenTrack Example

Optimized Service C#
public class ProductService
{
    private readonly InvenTrackDbContext _context;

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

    // Optimized read
    public async Task<List<ProductDto>> GetProductsAsync(int page, int pageSize)
    {
        return await _context.Products
            .AsNoTracking()
            .Include(p => p.Category)
            .OrderBy(p => p.Name)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(p => new ProductDto
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price,
                CategoryName = p.Category.Name
            })
            .ToListAsync();
    }

    // Transactional operation
    public async Task ProcessOrderAsync(int productId, int quantity)
    {
        using var transaction = await _context.Database.BeginTransactionAsync();

        try
        {
            // Reduce inventory
            var product = await _context.Products.FindAsync(productId);
            if (product == null || product.Quantity < quantity)
                throw new InvalidOperationException("Insufficient quantity");

            product.Quantity -= quantity;
            await _context.SaveChangesAsync();

            // Create order
            var order = new Order
            {
                CustomerName = "Customer",
                OrderDate = DateTime.UtcNow
            };
            _context.Orders.Add(order);
            await _context.SaveChangesAsync();

            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

Key Takeaways

  • Raw SQL: FromSqlRaw/FromSqlInterpolated for complex queries
  • Transactions: BeginTransaction for multi-step operations
  • AsNoTracking: Use for read-only queries (better performance)
  • Global filters: HasQueryFilter for automatic filtering
  • Compiled queries: Cache query translation
  • Batch operations: AddRange/RemoveRange for bulk
  • Indexes: HasIndex for frequently queried columns
  • Logging: Enable to see generated SQL
🎉 Part IX Complete!

Congratulations! You've completed Part IX: Entity Framework Core. You now understand:

  • ✅ What ORMs are and why use EF Core
  • ✅ DbContext and entity configuration
  • ✅ Code-First Migrations for schema management
  • ✅ Querying with LINQ (Where, Select, OrderBy, aggregation)
  • ✅ Relationships (one-to-many, one-to-one, many-to-many)
  • ✅ Loading strategies (eager, lazy, explicit, select)
  • ✅ Advanced features (raw SQL, transactions, performance)

You now have the skills to build data-driven applications with Entity Framework Core! Keep up the excellent work! 🚀