In one of my client projects a few years ago, we had to extend an existing HR and payroll system. At first, I thought it would just be another Code-First Entity Framework (EF) task. After all, that’s what I’ve always used in my freelance work.
But when I saw their database, it was a beast. Hundreds of tables. Long column names like tbl_Emp_Mst_Active_Flag_YN. And no one in the company even remembered why some tables existed. So I had no choice: I had to use Database-First to hook into their existing system.
That project taught me something valuable: neither Code-First nor Database-First is inherently “better.” It depends on what you’re working with. And in this post, I’ll try to help you figure out which approach suits your situation.
So… What Are These Two Approaches?
When working with EF Core, you need to map your database schema to C# objects. You can either:
- Define your C# classes first and let EF Core build the database for you (Code-First), or
- Start from an existing database and let EF Core generate the C# classes and DbContext (Database-First).
Code-First In a Nutshell
You model your entities in C#, and EF Core creates the database. Here’s a quick example:
public class Employee
{
public int EmployeeId { get; set; }
public string FullName { get; set; }
public DateTime HireDate { get; set; }
public ICollection<Payroll> Payrolls { get; set; }
}
public class Payroll
{
public int PayrollId { get; set; }
public int EmployeeId { get; set; }
public decimal GrossPay { get; set; }
public Employee Employee { get; set; }
}
public class PayrollDbContext : DbContext
{
public DbSet<Employee> Employees { get; set; }
public DbSet<Payroll> Payrolls { get; set; }
}
After defining this, you can run migrations:
dotnet ef migrations add InitialCreate
dotnet ef database update
EF Core will generate the SQL scripts and create the tables.
Database-First In a Nutshell
You already have a database. So you run EF Core’s scaffold command to generate the code.
Example:
dotnet ef dbcontext scaffold "Server=.;Database=HRDB;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -o Models
EF Core reads the schema and creates all your entity classes and DbContext.
Here’s what the generated code might look like:
public partial class Employee
{
public int EmployeeId { get; set; }
public string FullName { get; set; }
public DateTime? HireDate { get; set; }
}
This is quick and easy when you have no control over the database.
Why I Prefer Code-First (When I Can)
In my personal SaaS projects, I always use Code-First. It gives me control over naming conventions, schema design, and incremental migrations.
For example, in my HR & Payroll SaaS MVP, I started with Code-First like this:
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }
public ICollection<Employee> Employees { get; set; }
}
And in the DbContext’s OnModelCreating I added more rules:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Department>()
.Property(d => d.Name)
.HasMaxLength(100)
.IsRequired();
modelBuilder.Entity<Employee>()
.HasOne(e => e.Department)
.WithMany(d => d.Employees)
.HasForeignKey(e => e.DepartmentId);
}
This schema is very clean and can evolve easily as the business requirements change.
Why I Love Code-First
- Full control of design
- Works well with CI/CD pipelines
- Migrations make deployment easier
- No surprises with cryptic table/column names
When I Had to Use Database-First
When extending a legacy HR system, Code-First was impossible. Their database was messy and already in production.
So I used Database-First. After running scaffold, I got about 200 auto-generated models. But I didn’t stop there, I used partial classes to add validation and helper methods without touching the scaffolded code:
public partial class Employee
{
public string FullNameWithHireDate()
{
return $"{FullName} (Hired: {HireDate?.ToShortDateString() ?? "N/A"})";
}
}
Then I could still keep my code clean while working with their messy schema.
Why I Respected Database-First
- Works when DB already exists
- Respects DBA’s schema decisions
- Lets you focus on business logic
- Fast to get up and running
Side-By-Side Comparison
Here’s a more detailed table for you:
Feature | Code-First | Database-First |
---|---|---|
Starting Point | C# Classes | Existing DB |
Who Controls Schema | Developer | DBA or Existing System |
Migration | Built-In | Needs Extra Care |
Best Use Case | Greenfield | Legacy / Integration |
Development Speed | Slower (at first) | Faster (at first) |
Naming Conventions | Developer-defined | DB-defined |
Maintenance | Easier with Migrations | Harder if DB changes a lot |
Learning Curve | Steeper (migrations) | Easier for new devs |
Mixing Both (Yes, You Can!)
In a fintech payment gateway I worked on, we mixed both approaches.
We scaffolded the tables from the acquirer’s database, then added our own Code-First tables (for internal audit logs, error tracking, etc.).
We just made sure to configure migrations to ignore the scaffolded tables by excluding them:
modelBuilder.Entity<ExternalTransaction>().ToTable("ExternalTransaction", t => t.ExcludeFromMigrations());
This way, the code and database played nicely together.
Common Pitfalls to Avoid
With Code-First:
- Forgetting to run migrations before deploying.
- Making schema-breaking changes without versioning.
- Not seeding critical data.
With Database-First:
- Overwriting customizations every time you re-scaffold. Use partial classes!
- Assuming DB constraints will enforce all your validations, they might not.
- Not syncing changes when DB is updated externally.
Tips From My Experience
- Use naming conventions and Fluent API to keep Code-First schemas clean.
- Use partial classes and metadata attributes to enhance Database-First models.
- Always test migrations in a staging DB before production.
- When working with a DBA team, communicate clearly about schema changes.
Sample Queries and Usage
Once you’ve set up either approach, usage is identical. Here’s a quick example:
Adding a new employee
var employee = new Employee
{
FullName = "Juan Dela Cruz",
HireDate = DateTime.Today
};
_context.Employees.Add(employee);
_context.SaveChanges();
Querying employees
var employees = await _context.Employees
.Where(e => e.HireDate >= new DateTime(2020, 1, 1))
.ToListAsync();
No matter which approach you choose, querying stays the same.
Suggested Reading & Sources
- Microsoft Docs: EF Core Overview
- EF Core Code-First Documentation
- EF Core Scaffolding (Database-First)
- Julie Lerman, Programming Entity Framework Core, O’Reilly Media (great book!)
Final Thoughts
If you’re starting fresh and want clean control, go Code-First.
If you’re integrating with an existing DB or working in a DBA-heavy environment, go Database-First.
And if you’re somewhere in between, don’t be afraid to mix approaches. EF Core is flexible enough to handle it.
Thanks for sticking with me till the end of this long post. Good luck with your EF Core journey!