Entity Framework Core (EF Core)

Entity Framework Core (EF Core)

Entity Framework (EF) Core is a modern Object-Relational Mapper (ORM) for .NET. It allows developers to work with a database using C# objects, eliminating the need to write most of the data-access code that developers usually need to write.


1. The Core: DbContext & DbSet

The DbContext is the primary class that coordinates Entity Framework functionality for a given data model. It represents a session with the database and allows you to query and save instances of your entities.

  • DbSet<TEntity>: Represents a table in the database. You use it to perform CRUD (Create, Read, Update, Delete) operations.
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }
}

2. Configuration: Data Annotations vs. Fluent API

There are two ways to configure how your C# classes map to database tables.

A. Data Annotations (Attributes)

Simple and easy to read. You apply attributes directly to your class properties.

  • [Key]: Defines the Primary Key.
  • [Required]: Makes a column NOT NULL.
  • [StringLength(100)]: Sets the maximum character limit.
  • [ForeignKey("Name")]: Explicitly defines a foreign key relationship.
  • [Table("MyTableName")]: Maps the class to a specific table name.

B. Fluent API (OnModelCreating)

More powerful and keeps your entity classes "clean" of database-specific attributes. It is defined inside your DbContext.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.Name)
        .IsRequired()
        .HasMaxLength(200);

    modelBuilder.Entity<Product>()
        .HasIndex(p => p.Sku)
        .IsUnique();
}

3. Mastering Relationships

One-to-Many (The Standard)

A single Category can have many Products.

public class Category {
    public int Id { get; set; }
    public List<Product> Products { get; set; } = new();
}

public class Product {
    public int Id { get; set; }
    public string Name { get; set; }
    public int CategoryId { get; set; } // Foreign Key
    public Category Category { get; set; } // Navigation Property
}

One-to-One

A User has exactly one Profile.

public class User {
    public int Id { get; set; }
    public UserProfile Profile { get; set; }
}

public class UserProfile {
    public int Id { get; set; }
    public int UserId { get; set; } // Foreign Key
    public User User { get; set; }
}

Many-to-Many

A Student can enroll in many Courses, and a Course can have many Students. In EF Core 5.0+, this is handled automatically with a "shadow" join table.

public class Student {
    public int Id { get; set; }
    public ICollection<Course> Courses { get; set; }
}

public class Course {
    public int Id { get; set; }
    public ICollection<Student> Students { get; set; }
}

4. Database Migrations

Migrations allow you to evolve your database schema as your C# models change, without losing data.

The Lifecycle:

  1. Modify Models: Add a property or a new class.
  2. Add Migration: Generate the C# code that describes the change.
    dotnet ef migrations add InitialCreate
    
  3. Update Database: Apply the changes to the actual SQL database.
    dotnet ef database update
    

5. Eager Loading (Include)

By default, EF Core does not load related data (to save performance). To load a Product along with its Category, you must use the .Include() method.

var products = context.Products
    .Include(p => p.Category)
    .Where(p => p.Price > 100)
    .ToList();

6. Performance Best Practices

  1. AsNoTracking(): Use this for read-only queries. It tells EF Core not to waste memory tracking changes for those objects.
  2. AnyAsync(): Use this to check for existence instead of .Count() > 0 for better performance.
  3. Filtered Includes: In modern EF Core, you can filter the data inside an .Include() call.