Migration Generation
One of the most powerful features of CloudflareD1.NET's Code-First approach is automatic migration generation. Instead of manually writing SQL migration scripts, you can define your models with attributes and let the framework generate the migrations for you.
Overview
The migration generation feature:
- Compares your Code-First models with the last migration snapshot (not the database)
- Detects changes automatically (new tables, columns, indexes, foreign keys)
- Generates timestamped migration files with Up/Down methods
- Maintains migration history and a JSON snapshot alongside your migrations
How it works (EF-style): after each migration is generated, we save a snapshot file Migrations/.migrations-snapshot.json. Future migrations are diffed against this snapshot so only the delta is generated—no more full re-creation on every change.
Quick Start
1. Define Your Models
using CloudflareD1.NET.CodeFirst.Attributes;
[Table("users")]
public class User
{
[Key]
[AutoIncrement]
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Username { get; set; }
[Required]
[StringLength(100)]
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
[Table("blog_posts")]
public class BlogPost
{
[Key]
[AutoIncrement]
public int Id { get; set; }
[Required]
public string Title { get; set; }
public string Content { get; set; }
public int AuthorId { get; set; }
// Tip: configure the relationship via Fluent API (recommended)
public DateTime CreatedAt { get; set; }
}
2. Create Your DbContext
public class BlogDbContext : D1Context
{
public BlogDbContext(D1Client client) : base(client) { }
public D1Set<User> Users { get; set; }
public D1Set<BlogPost> BlogPosts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>();
modelBuilder.Entity<BlogPost>();
}
}
3. Generate Migration
Build your project first:
dotnet build --configuration Release
Then generate the migration:
dotnet d1 migrations add CreateBlogTables --code-first \
--context YourNamespace.BlogDbContext \
--assembly bin/Release/net8.0/YourApp.dll
4. Review Generated Migration
The CLI will create a migration file like Migrations/20251028120000_CreateBlogTables.cs:
using CloudflareD1.NET.Migrations;
namespace YourApp.Migrations;
public class Migration20251028120000_CreateBlogTables : Migration
{
public override string Id => "20251028120000";
public override string Name => "CreateBlogTables";
public override void Up(MigrationBuilder builder)
{
builder.CreateTable("users", t =>
{
t.Integer("id").PrimaryKey();
t.Text("username").NotNull();
t.Text("email").NotNull();
t.Text("created_at").NotNull();
});
builder.CreateTable("blog_posts", t =>
{
t.Integer("id").PrimaryKey();
t.Text("title").NotNull();
t.Text("content");
t.Integer("author_id").NotNull();
t.Text("created_at").NotNull();
t.ForeignKey("author_id", "users", "id");
});
}
public override void Down(MigrationBuilder builder)
{
builder.DropTable("users");
builder.DropTable("blog_posts");
}
}
5. Apply Migration
dotnet d1 migrations apply
CLI Command Reference
migrations add with --code-first
Generates a migration from Code-First models (snapshot-based, no DB connection required).
Syntax:
dotnet d1 migrations add <MigrationName> --code-first \
--context <ContextTypeName> \
--assembly <PathToAssembly> \
--connection <DatabasePath>
Parameters:
| Parameter | Required | Description |
|---|---|---|
MigrationName | Yes | Name for the migration (e.g., CreateUsersTable) |
--code-first | Yes | Enable Code-First migration generation |
--context | Yes | Full type name of your D1Context (e.g., MyApp.Data.BlogDbContext) |
--assembly | Yes | Path to the compiled assembly containing your models and context |
--connection | No | Not required for generation (used only when applying migrations) |
Example:
dotnet d1 migrations add InitialCreate --code-first \
--context BlogApp.Data.BlogDbContext \
--assembly bin/Release/net8.0/BlogApp.dll \
--connection blog.db
Workflow
Initial Migration
- Define models with Code-First attributes
- Build your project
- Generate migration:
dotnet d1 migrations add InitialCreate --code-first ... - Review the generated migration file
- Apply migration:
dotnet d1 migrations apply
Subsequent Changes
After modifying your models:
- Update models (add/remove properties, change attributes)
- Rebuild project
- Generate new migration:
dotnet d1 migrations add UpdateUserTable --code-first ... - Review changes
- Apply:
dotnet d1 migrations apply
Change Detection
The migration generator automatically detects:
Table Changes
- ✅ New tables
- ✅ Dropped tables
- ✅ Renamed tables (future)
Column Changes
- ✅ New columns
- ✅ Dropped columns (uses SQLite table recreation pattern under the hood)
- ✅ Type changes (future)
- ✅ Constraint changes (future)
Index Changes
- ✅ New indexes
- ✅ Dropped indexes
Foreign Key Changes
- ✅ New foreign keys
- ✅ Dropped foreign keys
Checking for Pending Changes
You can programmatically check if there are pending model changes:
using CloudflareD1.NET.CodeFirst.MigrationGeneration;
// Point the generator at your migrations directory (where the snapshot lives)
var generator = new CodeFirstMigrationGenerator("Migrations");
// Get a summary of changes (diff: current model vs last snapshot)
var summary = await generator.GetChangesSummaryAsync(context);
if (!string.IsNullOrEmpty(summary))
{
Console.WriteLine("Pending changes:");
Console.WriteLine(summary);
}
Or use the ModelDiffer for more control:
using CloudflareD1.NET.CodeFirst.MigrationGeneration;
// Diff against the last saved snapshot
var differ = new ModelDiffer("Migrations");
var metadata = context.GetModelMetadata();
// Quick check
if (await differ.HasChangesAsync(metadata))
{
Console.WriteLine("Your models differ from the last snapshot!");
// Get detailed comparison (lastSnapshot, modelSchema)
var (lastSnapshot, modelSchema) = await differ.CompareAsync(metadata);
// Analyze differences...
}
Best Practices
1. Always Review Generated Migrations
Before applying, review the migration to ensure:
- SQL is correct
- No unintended changes
- Foreign key constraints are in order
- Indexes are appropriate
2. Use Descriptive Migration Names
# Good
dotnet d1 migrations add AddUserEmailIndex --code-first ...
dotnet d1 migrations add UpdateBlogPostsContentType --code-first ...
# Bad
dotnet d1 migrations add Update1 --code-first ...
dotnet d1 migrations add Fix --code-first ...
3. Keep Migrations Small
Generate migrations frequently with focused changes rather than large, complex migrations.
4. Test Migrations Locally
Always test migrations in a local database before applying to production:
# Test with local database
dotnet d1 migrations apply --connection local.db
# Rollback if needed
dotnet d1 migrations revert
5. Version Control
Commit migration files to version control alongside your model changes:
git add Migrations/20251028120000_CreateBlogTables.cs
git add Models/User.cs
git commit -m "Add User model and initial migration"
Example: Full Workflow
See the complete example in the repository:
The sample demonstrates:
- Defining models with attributes
- Creating a DbContext
- Detecting pending changes
- Generating migrations
- Applying migrations
- Inspecting model metadata
To run the sample:
cd samples/CodeFirst.Sample
dotnet run --configuration Release
Troubleshooting
"Could not load assembly"
Ensure you've built the project before generating migrations:
dotnet build --configuration Release
dotnet d1 migrations add ... --assembly bin/Release/net8.0/YourApp.dll
"Context type not found"
Use the full namespace and type name:
# Wrong
--context BlogDbContext
# Correct
--context MyCompany.BlogApp.Data.BlogDbContext
"Database file not found"
Ensure the database path exists or use :memory: for in-memory databases:
--connection :memory:
Navigation Properties
Navigation properties should be configured via the Fluent API to establish relationships. For example:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<BlogPost>()
.HasOne<User>()
.WithMany()
.HasForeignKey(p => p.AuthorId);
}
Note: Navigation properties themselves are not stored as separate columns; the foreign key column (e.g., author_id) is what gets persisted.
Limitations
Current limitations (we're improving these):
- ❌ Column type changes not detected (will require manual migration)
- ❌ Column renames not detected (appears as drop + add)
- ❌ Complex types (owned entities, value objects)
Next Steps
- Learn about Migration Management
- Explore Code-First attributes (see recipes)
- See DbContext configuration (see overview)