Docs .NET Development Entity Framework Core

Entity Framework Core

Guide to using Entity Framework Core for database access

Entity Framework Core

Entity Framework Core (EF Core) is Microsoft’s modern ORM (Object-Relational Mapper). With EF Core, you can work with databases using C# objects without writing SQL directly.

What is an ORM?

An ORM is a layer that connects object code (C#) with relational databases:

C# Objects  <-->  EF Core  <-->  Database

Benefits:

  • No need to write manual SQL
  • Type-safe queries with LINQ
  • Automatic change tracking
  • Migrations for schema version control

Installation

# Core package
dotnet add package Microsoft.EntityFrameworkCore

# Database provider (pick one)
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

# Migration tools
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef

Basic Setup

1. Create Entities (Models)

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    
    // Navigation property
    public Category Category { get; set; } = null!;
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    
    // Navigation property (one-to-many)
    public ICollection<Product> Products { get; set; } = new List<Product>();
}

2. Create the DbContext

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }
    
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure relationships
        modelBuilder.Entity<Product>()
            .HasOne(p => p.Category)
            .WithMany(c => c.Products)
            .HasForeignKey(p => p.CategoryId);
        
        // Seed data
        modelBuilder.Entity<Category>().HasData(
            new Category { Id = 1, Name = "Electronics" },
            new Category { Id = 2, Name = "Books" }
        );
    }
}

3. Register in Program.cs

var builder = WebApplication.CreateBuilder(args);

// SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Or SQLite (for development)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite("Data Source=app.db"));

Migrations

Migrations are how EF Core manages database schema changes:

# Create the first migration
dotnet ef migrations add InitialCreate

# Preview the SQL that will run
dotnet ef migrations script

# Apply the migration to the database
dotnet ef database update

# Roll back a migration
dotnet ef database update PreviousMigrationName

# Remove the last migration (if not yet applied)
dotnet ef migrations remove

CRUD Operations

Create

public class ProductService
{
    private readonly AppDbContext _context;
    
    public ProductService(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<Product> CreateAsync(string name, decimal price, int categoryId)
    {
        var product = new Product
        {
            Name = name,
            Price = price,
            CategoryId = categoryId
        };
        
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        
        return product;
    }
}

Read

// Get all
public async Task<List<Product>> GetAllAsync()
{
    return await _context.Products.ToListAsync();
}

// Get by ID
public async Task<Product?> GetByIdAsync(int id)
{
    return await _context.Products.FindAsync(id);
}

// Include navigation property
public async Task<Product?> GetWithCategoryAsync(int id)
{
    return await _context.Products
        .Include(p => p.Category)
        .FirstOrDefaultAsync(p => p.Id == id);
}

Update

public async Task UpdateAsync(int id, string name, decimal price)
{
    var product = await _context.Products.FindAsync(id);
    if (product == null) return;
    
    product.Name = name;
    product.Price = price;
    
    await _context.SaveChangesAsync();
}

Delete

public async Task DeleteAsync(int id)
{
    var product = await _context.Products.FindAsync(id);
    if (product == null) return;
    
    _context.Products.Remove(product);
    await _context.SaveChangesAsync();
}

LINQ Queries

Filtering

// Where
var expensiveProducts = await _context.Products
    .Where(p => p.Price > 100)
    .ToListAsync();

// Multiple conditions
var filtered = await _context.Products
    .Where(p => p.Price > 50 && p.CategoryId == 1)
    .ToListAsync();

Sorting

// Order by
var sorted = await _context.Products
    .OrderBy(p => p.Name)
    .ToListAsync();

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

// Multiple sort criteria
var multiSort = await _context.Products
    .OrderBy(p => p.CategoryId)
    .ThenBy(p => p.Name)
    .ToListAsync();

Pagination

public async Task<List<Product>> GetPagedAsync(int page, int pageSize)
{
    return await _context.Products
        .Skip((page - 1) * pageSize)
        .Take(pageSize)
        .ToListAsync();
}

Projection

// Select specific fields
var names = await _context.Products
    .Select(p => p.Name)
    .ToListAsync();

// Select into a DTO
var dtos = await _context.Products
    .Select(p => new ProductDto
    {
        Id = p.Id,
        Name = p.Name,
        CategoryName = p.Category.Name
    })
    .ToListAsync();

Aggregation

int count = await _context.Products.CountAsync();
decimal total = await _context.Products.SumAsync(p => p.Price);
decimal avg = await _context.Products.AverageAsync(p => p.Price);
decimal? max = await _context.Products.MaxAsync(p => (decimal?)p.Price);

Relationships

One-to-Many

// Category has many Products
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Product> Products { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}

Many-to-Many

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Product> Products { get; set; }
}

// In OnModelCreating
modelBuilder.Entity<Product>()
    .HasMany(p => p.Tags)
    .WithMany(t => t.Products);

Performance Tips

1. No-Tracking Queries

// For read-only queries
var products = await _context.Products
    .AsNoTracking()
    .ToListAsync();

2. Explicit Loading

var product = await _context.Products.FindAsync(id);
await _context.Entry(product)
    .Reference(p => p.Category)
    .LoadAsync();

3. Split Queries

// For queries with multiple includes
var categories = await _context.Categories
    .Include(c => c.Products)
    .AsSplitQuery()
    .ToListAsync();

4. Raw SQL

// When you need maximum performance
var products = await _context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", 100)
    .ToListAsync();

Repository Pattern (Optional)

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
}

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly AppDbContext _context;
    protected readonly DbSet<T> _dbSet;
    
    public Repository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }
    
    public async Task<T?> GetByIdAsync(int id) => await _dbSet.FindAsync(id);
    public async Task<List<T>> GetAllAsync() => await _dbSet.ToListAsync();
    public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
    public async Task UpdateAsync(T entity) => _dbSet.Update(entity);
    public async Task DeleteAsync(T entity) => _dbSet.Remove(entity);
}

Next Steps

  1. Advanced EF Core — Complex queries, interceptors
  2. Unit of Work Pattern — Transaction management
  3. Database Performance — Indexing, optimization

EF Core is very powerful but can also become a performance bottleneck if used incorrectly. Always monitor the SQL it generates!