Code First Approach in EF Core
What is Code First?
Code First is an approach where you define your domain model using C# classes, and Entity Framework Core creates the database schema based on these classes. This approach gives developers full control over the domain model and database structure through code.
Basic Code First Workflow
- Create entity classes (POCOs)
- Create DbContext
- Configure entities (optional)
- Create migration
- Update database
Creating Entity Classes
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
// Navigation properties
public int CategoryId { get; set; }
public Category Category { get; set; }
public ICollection<OrderItem> OrderItems { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<Product> Products { get; set; }
}
public class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public string CustomerName { get; set; }
public ICollection<OrderItem> OrderItems { get; set; }
}
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public int OrderId { get; set; }
public Order Order { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; }
}Creating DbContext
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply configurations
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
}Configuration Methods
1. Data Annotations
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Product
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Name { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[Column("ProductDescription")]
public string Description { get; set; }
[ForeignKey("Category")]
public int CategoryId { get; set; }
public Category Category { get; set; }
}2. Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
// Primary key
entity.HasKey(e => e.Id);
// Properties
entity.Property(e => e.Name)
.IsRequired()
.HasMaxLength(200);
entity.Property(e => e.Price)
.HasColumnType("decimal(18,2)")
.IsRequired();
entity.Property(e => e.Description)
.HasColumnName("ProductDescription")
.HasMaxLength(1000);
// Indexes
entity.HasIndex(e => e.Name);
entity.HasIndex(e => new { e.CategoryId, e.IsActive });
// Relationships
entity.HasOne(e => e.Category)
.WithMany(c => c.Products)
.HasForeignKey(e => e.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
// Table name
entity.ToTable("Products");
});
}3. Separate Configuration Classes
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name)
.IsRequired()
.HasMaxLength(200);
builder.Property(p => p.Price)
.HasColumnType("decimal(18,2)");
builder.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId);
builder.ToTable("Products");
}
}
// Apply in OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ProductConfiguration());
// Or apply all configurations from assembly
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}Creating Migrations
Using .NET CLI
# Add a new migration
dotnet ef migrations add InitialCreate
# Update database
dotnet ef database update
# Add another migration
dotnet ef migrations add AddProductDescription
# Update to specific migration
dotnet ef database update AddProductDescription
# Remove last migration (if not applied)
dotnet ef migrations remove
# Generate SQL script
dotnet ef migrations scriptUsing Package Manager Console
# Add migration
Add-Migration InitialCreate
# Update database
Update-Database
# Remove migration
Remove-Migration
# Generate script
Script-MigrationSeeding Data
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Seed categories
modelBuilder.Entity<Category>().HasData(
new Category { Id = 1, Name = "Electronics", Description = "Electronic devices" },
new Category { Id = 2, Name = "Books", Description = "Books and magazines" },
new Category { Id = 3, Name = "Clothing", Description = "Apparel and accessories" }
);
// Seed products
modelBuilder.Entity<Product>().HasData(
new Product
{
Id = 1,
Name = "Laptop",
Price = 999.99m,
CategoryId = 1,
Stock = 10,
IsActive = true,
CreatedAt = DateTime.UtcNow
},
new Product
{
Id = 2,
Name = "C# Programming Book",
Price = 49.99m,
CategoryId = 2,
Stock = 50,
IsActive = true,
CreatedAt = DateTime.UtcNow
}
);
}Relationship Configurations
One-to-Many
modelBuilder.Entity<Product>()
.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Cascade);One-to-One
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public UserProfile Profile { get; set; }
}
public class UserProfile
{
public int Id { get; set; }
public string Bio { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
modelBuilder.Entity<User>()
.HasOne(u => u.Profile)
.WithOne(p => p.User)
.HasForeignKey<UserProfile>(p => p.UserId);Many-to-Many
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Course> Courses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public ICollection<Student> Students { get; set; }
}
// EF Core 5.0+ automatically creates join table
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students);
// Or configure explicitly
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses"));Advanced Configurations
Composite Keys
public class OrderItem
{
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
}
modelBuilder.Entity<OrderItem>()
.HasKey(oi => new { oi.OrderId, oi.ProductId });Indexes
modelBuilder.Entity<Product>()
.HasIndex(p => p.Name)
.IsUnique();
modelBuilder.Entity<Product>()
.HasIndex(p => new { p.CategoryId, p.IsActive })
.HasDatabaseName("IX_Product_Category_Active");Default Values
modelBuilder.Entity<Product>()
.Property(p => p.CreatedAt)
.HasDefaultValueSql("GETUTCDATE()");
modelBuilder.Entity<Product>()
.Property(p => p.IsActive)
.HasDefaultValue(true);Computed Columns
modelBuilder.Entity<OrderItem>()
.Property(oi => oi.TotalPrice)
.HasComputedColumnSql("[Quantity] * [UnitPrice]");Connection String Configuration
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=MyAppDb;Trusted_Connection=true;TrustServerCertificate=true;"
}
}Program.cs / Startup.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection"),
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
}));Migration Best Practices
1. Review Generated Migrations
public partial class AddProductDescription : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Description",
table: "Products",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: "Products");
}
}2. Custom Migration Logic
protected override void Up(MigrationBuilder migrationBuilder)
{
// Add column
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Users",
nullable: true);
// Populate with existing data
migrationBuilder.Sql(
@"UPDATE Users
SET FullName = FirstName + ' ' + LastName");
// Make required
migrationBuilder.AlterColumn<string>(
name: "FullName",
table: "Users",
nullable: false);
}Advantages of Code First
- Full control over domain model
- Version control for database schema
- Easy team collaboration
- Automated database updates
- Type safety and IntelliSense
- Refactoring support
- Test-driven development friendly
Interview Tips
- Explain Code First concept: Define model in code, generate database
- Show entity creation: POCO classes with properties
- Demonstrate configuration: Data annotations vs Fluent API
- Explain migrations: Version control for database schema
- Show relationship setup: One-to-many, one-to-one, many-to-many
- Discuss seeding: Initial data population
- Mention best practices: Separate configurations, review migrations
Summary
Code First approach in EF Core allows developers to define the database schema using C# classes and configurations. It provides full control over the domain model, supports version control through migrations, and enables automated database updates. This approach is ideal for new projects and teams that prefer working with code rather than database designers.
Test Your Knowledge
Take a quick quiz to test your understanding of this topic.