---
title: "Migrations"
description: "Manage database schema evolution with handwritten and auto migrations."
canonical: "https://roomsharp.dev/docs/v0.4.7/migrations"
source: "src/content/v0.4.7/migrations.mdx"
---
# Migrations
RoomSharp provides a flexible migration system that supports both declarative and handwritten migrations.
## Migration Types
### Manual Migrations (Recommended)
Implement `IRoomMigration` and use `MigrationContext` for transaction-safe migrations:
```csharp
public sealed class UserMigration_1_2 : IRoomMigration
{
public int StartVersion => 1;
public int EndVersion => 2;
public string Id => "UserMigration_1_2:1->2";
public string Name => nameof(UserMigration_1_2);
public string Checksum => MigrationChecksum.Sha256(
"ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'"
);
public async Task UpAsync(MigrationContext ctx)
{
await using var cmd = ctx.CreateCommand(
"ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'");
await cmd.ExecuteNonQueryAsync(ctx.CancellationToken);
}
}
```
### Auto Migrations (Declarative)
Use `[AutoMigration]` with `IAutoMigrationSpec` for declarative schema changes:
```csharp
[Database(Version = 3, Entities = new[] { typeof(User) })]
[AutoMigration(From = 1, To = 2, Spec = typeof(UserTableSpec))]
[AutoMigration(From = 2, To = 3)]
public abstract class UserDatabase : RoomDatabase
{
protected UserDatabase(IDatabaseProvider provider, ILogger? logger = null)
: base(provider, logger) { }
}
[RenameTable(FromTableName = "old_users", ToTableName = "users")]
[DeleteColumn(TableName = "users", ColumnName = "legacy_flag")]
[ColumnRename(TableName = "users", FromColumnName = "full_name", ToColumnName = "name")]
public sealed class UserTableSpec : IAutoMigrationSpec
{
public async Task OnPostMigrate(MigrationContext ctx)
{
// Execute data migrations within the same transaction
await ctx.ExecuteAsync("UPDATE users SET status='active' WHERE status IS NULL");
}
}
```
### Legacy Migrations
Extend `Migration` and override `Migrate(DbConnection connection)` (supported via adapter, but deprecated):
```csharp
// Deprecated - use IRoomMigration instead
public class LegacyMigration : Migration
{
public override void Migrate(DbConnection connection)
{
// Direct connection access
}
}
```
### Upgrading from Legacy to IRoomMigration
If you have existing `Migration` classes that override `Migrate(DbConnection)`, here's how to upgrade them to the modern `IRoomMigration` interface:
**Before (Legacy):**
```csharp
public class AddStatusColumn : Migration
{
public AddStatusColumn() : base(1, 2) { }
public override void Migrate(DbConnection connection)
{
using var cmd = connection.CreateCommand();
cmd.CommandText = "ALTER TABLE users ADD COLUMN status TEXT";
cmd.ExecuteNonQuery();
}
}
```
**After (Modern):**
```csharp
public sealed class AddStatusColumn : IRoomMigration
{
public int StartVersion => 1;
public int EndVersion => 2;
public string Id => $"{nameof(AddStatusColumn)}:{StartVersion}->{EndVersion}";
public string Name => nameof(AddStatusColumn);
public string Checksum => MigrationChecksum.Sha256("ALTER TABLE users ADD COLUMN status TEXT");
public async Task UpAsync(MigrationContext ctx)
{
// Option 1: Use extension method (recommended)
await ctx.ExecuteAsync("ALTER TABLE users ADD COLUMN status TEXT");
// Option 2: Use CreateCommand directly
// await using var cmd = ctx.CreateCommand("ALTER TABLE users ADD COLUMN status TEXT");
// await cmd.ExecuteNonQueryAsync(ctx.CancellationToken);
}
}
```
**Key differences:**
| Aspect | Legacy | Modern |
|--------|--------|--------|
| Base | `Migration` class | `IRoomMigration` interface |
| Method | `Migrate(DbConnection)` | `UpAsync(MigrationContext)` |
| Transaction | Manual | Automatic via `ctx.Transaction` |
| Checksum | Auto from IL | Explicit property |
| Async | No | Yes |
| Cancellation | No | `ctx.CancellationToken` |
You can keep existing Legacy migrations running - RoomSharp wraps them in `LegacyMigrationAdapter` automatically. But new migrations should use `IRoomMigration`.
### Fluent Migrations
Use the fluent API to build schema changes programmatically. This approach is type-safe and generates appropriate SQL for each database provider:
```csharp
public sealed class CreateUsersTable : Migration
{
public CreateUsersTable() : base(0, 1) { }
public override Task UpAsync(MigrationContext ctx)
{
// Create a table with columns
ctx.CreateTable("users", table =>
{
table.Column("Id", c => c.PrimaryKey(autoIncrement: true));
table.Column("Email", c => c.NotNull().Unique());
table.Column("Name");
table.Column("CreatedAt", c => c.NotNull());
}, ifNotExists: true);
// Create an index
ctx.CreateIndex("users", "IX_users_email", unique: true, ifNotExists: true, "Email");
return Task.CompletedTask;
}
}
```
#### Fluent Operations
| Operation | Description | Example |
|-----------|-------------|---------|
| `CreateTable` | Create a new table | `ctx.CreateTable("users", t => { ... })` |
| `AlterTable` | Modify an existing table | `ctx.AlterTable("users", t => { ... })` |
| `RenameTable` | Rename a table | `ctx.RenameTable("old_name", "new_name")` |
| `DropTable` | Drop a table | `ctx.DropTable("users")` |
| `CreateIndex` | Create an index | `ctx.CreateIndex("table", "idx_name", unique, ifNotExists, "col1", "col2")` |
| `DropIndex` | Drop an index | `ctx.DropIndex("table", "idx_name")` |
#### Column Definition
```csharp
ctx.CreateTable("users", table =>
{
// Primary key with auto-increment
table.Column("Id", c => c.PrimaryKey(autoIncrement: true));
// Required unique string
table.Column("Email", c => c.NotNull().Unique());
// Nullable string
table.Column("Bio");
// With default value
table.Column("IsActive", c => c.NotNull().Default(true));
// Custom type affinity
table.Column("CreatedAt", c => c.TypeAffinity("DATETIME").NotNull());
});
```
#### Alter Table
```csharp
ctx.AlterTable("users", table =>
{
// Add a new column
table.AddColumn("Phone");
// Add column with constraints
table.AddColumn("IsVerified", c => c.NotNull().Default(false));
// Rename a column
table.RenameColumn("OldName", "NewName");
// Drop a column (requires AllowDestructiveOperations)
// table.DropColumn("LegacyColumn");
});
```
Operations like `DropTable`, `DropColumn`, and `DropIndex` are blocked by default. Enable explicitly:
```csharp
ctx.AllowDestructiveOperations();
ctx.DropTable("legacy_table");
```
Adding a `NOT NULL` column without a default to a non-empty table will throw an error:
`Migration rejected: adding NOT NULL column 'X' to non-empty table 'Y' without default value.`
#### Convenience Methods
TableBuilder provides shortcuts for common column patterns:
```csharp
ctx.CreateTable("users", table =>
{
table.Id(); // auto-increment int PK
table.IdLong(); // auto-increment long PK
table.IdGuid(); // GUID PK
table.String("Email"); // required string (NOT NULL)
table.NullableString("Bio"); // nullable string
table.Int("Age"); // int column
table.Bool("IsActive"); // bool column
table.Timestamps(); // CreatedAt + UpdatedAt
table.SoftDelete(); // DeletedAt for soft deletes
table.IsActive(); // bool with default true
});
```
For AlterTable:
```csharp
ctx.AlterTable("users", table =>
{
table.AddTimestamps(); // Add CreatedAt + UpdatedAt
table.AddSoftDelete(); // Add DeletedAt
table.AddIsActive(); // Add IsActive with default true
table.AddString("Phone"); // Add required string
table.AddNullableString("Bio"); // Add nullable string
});
```
#### Fluent Column Chaining
RoomSharp supports two fluent syntax styles for column configuration:
**Style 1: Direct chaining (new!)**
```csharp
// No callback needed - chain methods directly
table.Column("Email").NotNull().Unique();
table.Column("DeletedAt").Nullable();
table.Column("Age").NotNull().Default(0);
// Chain multiple columns together
table.Column("Email").NotNull().Unique()
.Column("Age").NotNull()
.Timestamps();
```
**Style 2: Callback configuration**
```csharp
// Use a callback for configuration
table.Column("Email", c => c.NotNull().Unique().Default(""));
table.Column("CreatedAt", c => c.NotNull().TypeAffinity("DATETIME"));
```
Use Style 1 for cleaner syntax. Use Style 2 when you need complex configuration or conditional logic.
## Builder Configuration
Register migrations when building your database:
```csharp
var db = RoomDatabase.Builder()
.UseSqlite("app.db")
.SetVersion(2)
.AddMigrations(new UserMigration_1_2())
.AddCallback(new LoggingCallback(logger))
.SetJournalMode(JournalMode.WAL)
.EnableMultiInstanceInvalidation()
.SetAutoCloseTimeout(TimeSpan.FromMinutes(2))
.FallbackToDestructiveMigration() // Only for dev/test!
.Build();
```
## Auto Migration Attributes
| Attribute | Description |
|-----------|-------------|
| `[RenameTable]` | Rename a table |
| `[DeleteTable]` | Drop a table |
| `[AddColumn]` | Add a new column |
| `[DeleteColumn]` | Remove a column |
| `[ColumnRename]` | Rename a column |
### AddColumn Options
You can use `AddColumnAttribute` in two ways:
**Option 1: Raw SQL (flexible but provider-specific)**
```csharp
[AddColumn(TableName = "users", ColumnName = "Age",
ColumnDefinition = "INTEGER NOT NULL DEFAULT 0")]
```
**Option 2: Type-safe (recommended)**
```csharp
// Simple int column
[AddColumn(TableName = "users", ColumnName = "Age",
ColumnType = typeof(int), Nullable = false, DefaultValue = 0)]
// String with max length
[AddColumn(TableName = "users", ColumnName = "Status",
ColumnType = typeof(string), MaxLength = 50, DefaultValue = "active")]
// Nullable DateTime
[AddColumn(TableName = "users", ColumnName = "LastLoginAt",
ColumnType = typeof(DateTime), Nullable = true)]
```
| Property | Type | Description |
|----------|------|-------------|
| `TableName` | string | Target table name |
| `ColumnName` | string | New column name |
| `ColumnDefinition` | string | Raw SQL definition (overrides other properties) |
| `ColumnType` | Type | CLR type (typeof(int), typeof(string), etc.) |
| `Nullable` | bool | Allow NULL values (default: true) |
| `DefaultValue` | object | Default value for the column |
| `MaxLength` | int | VARCHAR length for strings (-1 = TEXT) |
## Migrations in Production
- Migrations run under a real DB transaction via `MigrationContext` and are protected by a provider-specific migration lock.
- RoomSharp fails fast if `__room_state.dirty = 1` (previous migration attempt left the database in an unsafe state).
- Recovery: restore from backup or manually repair schema, then clear `dirty`.
- Rollbacks are not supported unless you implement your own "down" strategy.
- Use destructive fallback only in dev/test environments.
## Migration Tracking
RoomSharp tracks migrations in two tables:
| Table | Purpose |
|-------|---------|
| `__room_state` | Current schema version and dirty flag |
| `__room_migrations` | Applied migrations with ID and checksum |
Use `room migrate:verify` in CI/production to detect schema drift by comparing checksums.
## Understanding Checksums
Checksums are SHA256 hashes that uniquely identify a migration. RoomSharp uses them to detect if a migration was modified after being applied.
### Why Checksums Matter
- **Detect tampering**: If someone modifies an applied migration, the checksum mismatch will fail the next migration run
- **Ensure consistency**: All environments run the exact same migration code
- **Audit trail**: `__room_migrations` stores checksums for verification
### How to Calculate Checksums
There are three approaches:
#### 1. Manual Checksum (Simple)
Hash the SQL statement or a descriptive string:
```csharp
public sealed class AddStatusColumn : IRoomMigration
{
public int StartVersion => 1;
public int EndVersion => 2;
public string Id => "AddStatusColumn:1->2";
public string Name => nameof(AddStatusColumn);
// Hash the SQL you're executing
public string Checksum => MigrationChecksum.Sha256(
"ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'"
);
public async Task UpAsync(MigrationContext ctx)
{
await ctx.ExecuteAsync("ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'active'");
}
}
```
#### 2. Fluent Checksum (Recommended for Fluent Migrations)
Use `MigrationSignature.From()` to automatically compute checksum from fluent operations:
```csharp
public sealed class CreateUsersTable : IRoomMigration
{
public int StartVersion => 0;
public int EndVersion => 1;
public string Id => "CreateUsersTable:0->1";
public string Name => nameof(CreateUsersTable);
// Define schema changes in a static method
private static void Build(IMigrationBuilder m)
{
m.CreateTable("users", table =>
{
table.Column("Id", c => c.PrimaryKey(autoIncrement: true));
table.Column("Email", c => c.NotNull().Unique());
table.Column("CreatedAt", c => c.NotNull());
});
m.CreateIndex("users", "IX_users_email", true, true, "Email");
}
// Checksum is computed from the fluent operations
public string Checksum => MigrationSignature.From(DatabaseType.SQLite, Build);
public Task UpAsync(MigrationContext ctx)
{
Build(ctx);
return Task.CompletedTask;
}
}
```
#### 3. Auto-Computed (For Migration Base Class)
When extending `Migration`, the checksum is automatically computed from the IL bytecode:
```csharp
public sealed class AddEmailColumn : Migration
{
public AddEmailColumn() : base(1, 2) { }
// Checksum is auto-computed from UpAsync method body
public override Task UpAsync(MigrationContext ctx)
{
ctx.AlterTable("users", t => t.AddColumn("Email"));
return Task.CompletedTask;
}
}
```
- **Never modify an applied migration** - The checksum will change and cause failures
- **If you need changes**, create a new migration with a new version
- **Use `MigrationSignature.From()`** for fluent migrations to ensure consistency across databases
## Destructive Fallback
`FallbackToDestructiveMigration()` drops all tables and restarts the metadata table when downgrading. Only use in development/testing!
```csharp
// ONLY for dev/test environments
var db = RoomDatabase.Builder()
.UseSqlite("app.db")
.FallbackToDestructiveMigration()
.Build();
```
## CLI Commands
```bash
# Check migration status
room migrate:status -b
# Apply pending migrations
room migrate:up -b
# Preview migration plan
room migrate:plan --to=3 -b
# Verify checksums in production
room migrate:verify -b
```
## Extension Methods
`MigrationContext` provides extension methods for common operations:
```csharp
public override async Task UpAsync(MigrationContext ctx)
{
// Execute simple SQL
await ctx.ExecuteAsync("UPDATE users SET status='active'");
// Execute with parameters
await ctx.ExecuteAsync("UPDATE users SET status=@status", new { status = "active" });
// Check if table exists
if (await ctx.TableExistsAsync("legacy_users"))
{
ctx.AllowDestructiveOperations();
ctx.DropTable("legacy_users");
}
// Check if column exists
if (!await ctx.ColumnExistsAsync("users", "email"))
{
ctx.AlterTable("users", t => t.AddColumn("email"));
}
```
## Programmatic Seeding
For MAUI/mobile apps where CLI seeding isn't possible, use the programmatic API:
### Basic Usage
```csharp
// Run all discovered seeders
await db.SeedAsync();
// Or with specific options
await db.SeedAsync(new SeederOptions
{
Environment = "Development",
AllowProduction = false
});
// Or with explicit seeders
await db.SeedAsync(new SeederOptions
{
Seeders = new IRoomSeeder[] { new CurrencySeeder(), new CategorySeeder() }
});
```
### Creating Seeders
Use the strongly-typed base class for the best developer experience:
```csharp
[Seeder(Order = 1)]
public class CurrencySeeder : RoomSeeder
{
protected override async ValueTask SeedCoreAsync(SeederContext ctx, MyAppDatabase db)
{
await ctx.SeedIfEmptyAsync("currencies", db.CurrencyDao.InsertAsync,
new Currency { Code = "USD", Name = "US Dollar" },
new Currency { Code = "EUR", Name = "Euro" },
new Currency { Code = "SAR", Name = "Saudi Riyal" });
}
}
[Seeder(Order = 2, DependsOn = new[] { typeof(CurrencySeeder) })]
public class AccountSeeder : RoomSeeder
{
protected override async ValueTask SeedCoreAsync(SeederContext ctx, MyAppDatabase db)
{
if (!ctx.IsDevelopment) return;
await ctx.SeedIfEmptyAsync("accounts", db.AccountDao.InsertAsync,
new Account { Name = "Cash", Type = AccountType.Asset });
}
}
```
### SeederContext Helpers
```csharp
// Environment checks
ctx.IsDevelopment // true for "Development" or "Dev"
ctx.IsProduction // true for "Production" or "Prod"
ctx.IsEnvironment("Staging")
// SQL execution
await ctx.ExecuteAsync("UPDATE users SET is_active = 1");
await ctx.ExecuteAsync("INSERT INTO logs VALUES (@msg)", new { msg = "Seeded" });
// Table helpers
await ctx.IsTableEmptyAsync("currencies");
await ctx.TableExistsAsync("legacy_table");
await ctx.GetTableRowCountAsync("users");
// Conditional seeding
await ctx.SeedInDevelopmentAsync(async () => { /* dev-only */ });
await ctx.SeedInEnvironmentsAsync(new[] { "Dev", "Staging" }, async () => { });
```
Use `RoomSeeder` for strongly-typed access to your database and DAOs without manual casting.
## Best Practices
1. **Always use checksums** - Helps detect if migrations were modified after being applied
2. **Test migrations on a copy** - Never test migrations directly on production data
3. **Keep migrations small** - One logical change per migration
4. **Use descriptive names** - `UserMigration_1_2` is better than `Migration1`
5. **Never modify applied migrations** - Create a new migration instead
6. **Back up before migrating** - Especially important for production databases