EF Core Code First: Adding and Rolling Back Migrations
Author: Oğuzhan KARAGÜZEL ·

On This Page
With the Code First approach you write the C# classes first and let EF Core's migration mechanism generate the database from them. In this article we build a market model from scratch, create the first migration and apply it, then change the model and roll that change back. I assume you have finished the environment setup (SQL Server, NuGet packages) from the first article of the series, EF Core and Database First.
What is Code First?
Code First is a method where the code is written first and the database structure is created from it. It gives you more flexibility and control: you can review every database change as a migration file before applying it, apply it if it looks right, and easily roll back your latest change. Because migration files live in Git with the code, the history of the database is tracked together with the history of the code.
Starting from a clean slate
You can delete the database and the generated classes from the Database First article and start fresh. To drop the database from the Package Manager Console the following command is enough; EF Core asks for confirmation before deleting.
Drop-DatabaseThe .NET CLI equivalent is dotnet ef database drop. Then delete the Context and Entities folders in the project and continue with a clean application.
Step 1: Entity classes and the DbContext
First we write the entities that correspond to our tables. The model has four classes: Musteri (customer), Siparis (order), Urun (product) and Fatura (invoice). The relationships: a customer has many orders (one-to-many), an order has a single invoice (one-to-one), and an order contains many products while a product appears in many orders (many-to-many). All of them derive from a common BaseEntity.
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
}
public class Musteri : BaseEntity // Customer
{
public string MusteriAdi { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public ICollection<Siparis> Siparisler { get; set; } = new List<Siparis>();
}
public class Urun : BaseEntity // Product
{
public string UrunAdi { get; set; } = string.Empty;
public decimal Fiyat { get; set; }
public ICollection<Siparis> Siparisler { get; set; } = new List<Siparis>();
}
public class Siparis : BaseEntity // Order
{
public DateTime SiparisTarihi { get; set; }
public int MusteriId { get; set; }
public Musteri Musteri { get; set; } = null!;
public Fatura? Fatura { get; set; }
public ICollection<Urun> Urunler { get; set; } = new List<Urun>();
}
public class Fatura : BaseEntity // Invoice
{
public DateTime FaturaTarihi { get; set; }
public int SiparisId { get; set; }
public Siparis Siparis { get; set; } = null!;
}Note: in a one-to-one relationship the foreign key must live on only one side. Here Fatura.SiparisId does that job; adding an extra FaturaId to Siparis would produce a redundant and misleading column.
Next comes the DbContext. We state the relationships explicitly with the Fluent API in OnModelCreating. If your code follows EF Core's naming conventions, most of this configuration is detected automatically, but spelling it out is safer for one-to-one relationships and for fields such as money values (HasPrecision).
public class MarketDbContext : DbContext
{
public MarketDbContext() { }
public MarketDbContext(DbContextOptions<MarketDbContext> options)
: base(options) { }
public DbSet<Urun> Urunler => Set<Urun>();
public DbSet<Siparis> Siparisler => Set<Siparis>();
public DbSet<Fatura> Faturalar => Set<Fatura>();
public DbSet<Musteri> Musteriler => Set<Musteri>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Do not hard-code the connection string: read it from an environment variable.
if (!optionsBuilder.IsConfigured)
{
var connectionString = Environment.GetEnvironmentVariable("MARKETDB_CONNECTION")
?? throw new InvalidOperationException("MARKETDB_CONNECTION is not set.");
optionsBuilder.UseSqlServer(connectionString);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Many-to-many: Siparis <-> Urun (EF Core creates the join table)
modelBuilder.Entity<Siparis>()
.HasMany(s => s.Urunler)
.WithMany(u => u.Siparisler);
// One-to-many: Musteri -> Siparis
modelBuilder.Entity<Siparis>()
.HasOne(s => s.Musteri)
.WithMany(m => m.Siparisler)
.HasForeignKey(s => s.MusteriId);
// One-to-one: Siparis -> Fatura (the FK lives on Fatura)
modelBuilder.Entity<Fatura>()
.HasOne(f => f.Siparis)
.WithOne(s => s.Fatura)
.HasForeignKey<Fatura>(f => f.SiparisId);
modelBuilder.Entity<Urun>()
.Property(u => u.Fiyat)
.HasPrecision(18, 2);
// Seed data. Use a fixed date: DateTime.Now/UtcNow would produce a
// different value on every build and trigger needless migrations.
var seedDate = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
modelBuilder.Entity<Musteri>().HasData(
new Musteri { Id = 1, MusteriAdi = "Ahmet Yilmaz", Email = "ahmet@example.com", CreatedDate = seedDate },
new Musteri { Id = 2, MusteriAdi = "Ayse Demir", Email = "ayse@example.com", CreatedDate = seedDate });
modelBuilder.Entity<Urun>().HasData(
new Urun { Id = 1, UrunAdi = "Laptop", Fiyat = 5000, CreatedDate = seedDate },
new Urun { Id = 2, UrunAdi = "Phone", Fiyat = 3000, CreatedDate = seedDate });
}
}Instead of embedding the connection string in code we read it from an environment variable; you may need to restart Visual Studio after defining it. In an ASP.NET Core project the connection string lives in appsettings.json or User Secrets and is injected with AddDbContext; never put the password into source code or Git.
Step 2: Creating the first migration
We use migration commands to generate the database structure from the code. Enter the command in the Package Manager Console with a migration name. The name becomes a C# class name, so it must not contain spaces, dots or hyphens. I prefer descriptive names; for the very first migration the classic name is InitialCreate.
Add-Migration InitialCreate.NET CLI: dotnet ef migrations add InitialCreate. The command creates a Migrations folder with three files: the migration class, its .Designer.cs file and MarketDbContextModelSnapshot.cs.
The snapshot is a copy of the complete current state of your model. When EF Core generates the next migration it compares the new model with this snapshot to compute the difference, so it is more than a simple routing file and should not be edited by hand.
The migration class has two methods. Up contains the operations applied to the database when the migration runs; Down contains the operations performed when it is reverted. If you spot a problem in Up before applying it, you can delete the migration and start over. Reading Down before a rollback is a good habit too.
If you read Up you will find a table you never wrote:
migrationBuilder.CreateTable(
name: "SiparisUrun",
columns: table => new
{
SiparislerId = table.Column<int>(type: "int", nullable: false),
UrunlerId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SiparisUrun", x => new { x.SiparislerId, x.UrunlerId });
table.ForeignKey(
name: "FK_SiparisUrun_Siparisler_SiparislerId",
column: x => x.SiparislerId,
principalTable: "Siparisler",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SiparisUrun_Urunler_UrunlerId",
column: x => x.UrunlerId,
principalTable: "Urunler",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});We defined a many-to-many relationship between orders and products; such a relationship needs a join table, and EF Core creates and manages it (SiparisUrun) for you. If you need extra columns on the join table (for example quantity or discount), you have to define it as an entity yourself and configure the relationships accordingly. For a pure link table you do not have to write anything.
Step 3: Creating the database
To create the database run Update-Database. If the database exists it applies the pending migrations; if not, it creates the database from scratch.
Update-Database.NET CLI: dotnet ef database update. When it finishes you can see the tables, relationships and seed data in DBeaver. A table named __EFMigrationsHistory is also created; it records which migrations have been applied.
If you get an error, check two things first: the connection string, and whether the project builds. If the project does not compile, none of the migration commands will run.
Changing the model: a second migration
Now let's add a new column to the shared base class and a new table. First add a field to BaseEntity:
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
public bool IsDeleted { get; set; } = false; // new
}Then create an unrelated, empty entity and add it to the DbContext:
public class Calisan : BaseEntity // Employee
{
public string Adi { get; set; } = string.Empty;
}
// Inside MarketDbContext:
public DbSet<Calisan> Calisanlar => Set<Calisan>();Create and apply the second migration:
Add-Migration AddIsDeletedAndCalisan
Update-DatabaseAn IsDeleted column is added to every table whose entity derives from BaseEntity, and the Calisanlar table is created. Had you done this in the database by hand, you would have to add each column separately and then update the code. With Code First, one model change and two commands are enough.
Rolling the change back
There are two cases when rolling back migrations.
The migration has not been applied to the database yet (you did not run Update-Database): remove the latest migration with Remove-Migration.
The migration has been applied: first revert the database to the previous migration. Give Update-Database the name of the target migration; EF Core runs the Down methods of the migrations being reverted.
Update-Database InitialCreateThe console reports that AddIsDeletedAndCalisan was reverted. The migration class is still in your project, though, and leaving it there would cause trouble in future updates. To remove it:
Remove-MigrationWhy use Remove-Migration instead of deleting the file directly? Because the command refuses to remove a migration that has already been applied to the database (silently deleting an applied migration leaves the database and the code inconsistent), and it also brings the snapshot file up to date. A stale snapshot often does not fail immediately, but the next migration may compute the wrong difference. To revert every migration use Update-Database 0.
Note: before moving on to the examples in the next article, remember to remove the IsDeleted field and the Calisan class from your code as well.
Applying migrations in production
Update-Database is a convenient tool on your development machine, but for a live database it is better to generate the migrations as a SQL script or a bundle and run them in your deployment pipeline instead of sending commands from Visual Studio:
dotnet ef migrations script --idempotent -o migrate.sql
dotnet ef migrations bundle -o efbundleReviewing the script before running it helps you spot operations that risk data loss (dropping columns, changing types). Always take a backup of a live database first.
Pros and cons of Code First
Advantages
- You can control and manage the database structure from code.
- Migration and update operations are automated.
- More flexibility and full control over the code.
- With Git you can follow migrations step by step and roll back mistakes.
- You can review changes before applying them and revert them easily.
- When you work in a team, everyone keeps the database schema up to date together with the code.
Disadvantages
- It is harder to work with an existing database (Database First or Mixed fits better then).
- Advanced database-specific features (views, stored procedures, custom indexes) may require additional hand-written migration code.
In the next article we use this model to work with relationships and cover the Mixed approach and choosing the right approach.
