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:
- Modify Models: Add a property or a new class.
- Add Migration: Generate the C# code that describes the change.
dotnet ef migrations add InitialCreate - 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
AsNoTracking(): Use this for read-only queries. It tells EF Core not to waste memory tracking changes for those objects.AnyAsync(): Use this to check for existence instead of.Count() > 0for better performance.- Filtered Includes: In modern EF Core, you can filter the data inside an
.Include()call.