# Tạo migration
dotnet ef migrations add AddOrderStatus --project src/Infrastructure --startup-project src/Api
# Review SQL trước khi apply
dotnet ef migrations script --idempotent --output migration.sql
# Apply migration
dotnet ef database update
# Rollback
dotnet ef database update PreviousMigrationName
# List migrations
dotnet ef migrations list
Production Migration Strategy
csharp
// KHÔNG dùng Database.Migrate() trong production startup// Thay vào đó, dùng migration bundles hoặc SQL scripts// Migration bundle (EF Core 6+)// dotnet ef migrations bundle --self-contained -r linux-x64// Hoặc generate idempotent SQL script cho DBA review// dotnet ef migrations script --idempotent > deploy/migration.sql
Migration Best Practices
Practice
Mô tả
Idempotent scripts
Dùng --idempotent flag - safe to run multiple times
Review generated SQL
Luôn xem SQL trước khi apply production
Small migrations
Mỗi migration làm một việc - dễ rollback
No data in migrations
Tách data seeding ra khỏi schema migrations
Test rollback
Verify migration có thể Down() cleanly
CI/CD integration
Automate migration trong pipeline
Zero-Downtime Schema Changes
Expand-Contract Pattern
Thay đổi schema không breaking bằng cách chia thành 3 phases:
Ví dụ: Rename Column CustomerName → FullName
csharp
// Phase 1: EXPAND - Add new column (Deploy #1)publicpartialclassAddFullNameColumn : Migration
{
protectedoverridevoidUp(MigrationBuilder migrationBuilder)
{
// Add new column, nullable vì data chưa có
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customers",
type: "nvarchar(200)",
nullable: true);
// Copy existing data
migrationBuilder.Sql(
"UPDATE Customers SET FullName = CustomerName WHERE FullName IS NULL");
}
protectedoverridevoidDown(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "FullName", table: "Customers");
}
}
// Application code: Write to BOTH columns, read from newpublicclassCustomer
{
publicstring FullName { get; set; } // New columnpublicstring? CustomerName { get; set; } // Old column - still maintained
}
// Phase 2: MIGRATE - Backfill + verify (Deploy #2)// - Batch update remaining rows// - Verify data consistency// - Application reads only from FullName// Phase 3: CONTRACT - Remove old column (Deploy #3)publicpartialclassRemoveCustomerNameColumn : Migration
{
protectedoverridevoidUp(MigrationBuilder migrationBuilder)
{
// Make new column NOT NULL after all data migrated
migrationBuilder.AlterColumn<string>(
name: "FullName",
table: "Customers",
type: "nvarchar(200)",
nullable: false,
defaultValue: "");
// Drop old column
migrationBuilder.DropColumn(name: "CustomerName", table: "Customers");
}
}
Dangerous Operations - Cách Handle
Operation
Risk
Safe Alternative
DROP COLUMN
Data loss, app crash
Expand-contract pattern
RENAME COLUMN
Breaks existing queries
Add new + copy + drop old
ADD NOT NULL
Fails if null data exists
Add nullable → backfill → alter to NOT NULL
ADD INDEX (large table)
Locks table
CREATE INDEX CONCURRENTLY hoặc online index
CHANGE DATA TYPE
Data loss, locks
Add new column → convert → drop old
Online Index Creation (SQL Server)
sql
-- Online index - không lock tableCREATE NONCLUSTERED INDEX IX_Orders_CustomerId
ON Orders (CustomerId)
WITH (ONLINE =ON, SORT_IN_TEMPDB =ON, MAXDOP =4);
Data Classification
PII & Sensitive Data Categories
Category
Mô tả
Ví dụ
Protection Level
PII
Personally Identifiable
Name, email, phone, address
Encryption + access control
Sensitive PII
High-risk personal data
SSN, passport, health records
Encryption + audit + masking
Financial
Payment/financial data
Credit card, bank account
PCI DSS compliance
Authentication
Credentials
Passwords, tokens, API keys
Hash/encrypt, never log
Business Confidential
Internal business data
Revenue, strategy docs
Access control
EF Core - Data Protection Integration
csharp
// Attribute-based classificationpublicclassCustomer
{
publicint Id { get; set; }
[PersonalData]
[ProtectedPersonalData] // Auto-encrypt in storepublicrequiredstring Email { get; set; }
[PersonalData]
publicrequiredstring Name { get; set; }
// Not personal data - no special handlingpublicrequiredstring PreferredLanguage { get; set; }
}
// Custom value converter for encryptionpublicclassEncryptedStringConverter : ValueConverter<string, string>
{
publicEncryptedStringConverter(IDataProtector protector)
: base(
v => protector.Protect(v),
v => protector.Unprotect(v))
{ }
}
// Register in DbContextprotectedoverridevoidOnModelCreating(ModelBuilder modelBuilder)
{
var protector = _dataProtectionProvider
.CreateProtector("CustomerData.v1");
modelBuilder.Entity<Customer>(entity =>
{
entity.Property(e => e.Email)
.HasConversion(new EncryptedStringConverter(protector));
});
}
Data Retention Policies
Implementation Strategy
csharp
// Retention policy configurationpublicclassRetentionPolicy
{
publicrequiredstring EntityType { get; init; }
public TimeSpan RetentionPeriod { get; init; }
public RetentionAction Action { get; init; }
}
publicenum RetentionAction
{
Delete, // Hard delete
Anonymize, // Replace PII with anonymous data
Archive // Move to cold storage
}
// Background service cho data retentionpublicclassDataRetentionService : BackgroundService
{
privatereadonly IServiceScopeFactory _scopeFactory;
privatereadonly ILogger<DataRetentionService> _logger;
privatereadonly RetentionPolicy[] _policies = new[]
{
new RetentionPolicy
{
EntityType = "ApplicationLogs",
RetentionPeriod = TimeSpan.FromDays(90),
Action = RetentionAction.Delete
},
new RetentionPolicy
{
EntityType = "UserSessions",
RetentionPeriod = TimeSpan.FromDays(30),
Action = RetentionAction.Delete
},
new RetentionPolicy
{
EntityType = "InactiveAccounts",
RetentionPeriod = TimeSpan.FromDays(730), // 2 years
Action = RetentionAction.Anonymize
},
new RetentionPolicy
{
EntityType = "CompletedOrders",
RetentionPeriod = TimeSpan.FromDays(2555), // 7 years (tax)
Action = RetentionAction.Archive
}
};
protectedoverrideasync Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
foreach (var policy in_policies)
{
await ApplyRetentionPolicyAsync(policy, ct);
}
// Run daily at 2 AMawait Task.Delay(TimeSpan.FromHours(24), ct);
}
}
privateasync Task ApplyRetentionPolicyAsync(RetentionPolicy policy, CancellationToken ct)
{
usingvar scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var cutoffDate = DateTimeOffset.UtcNow - policy.RetentionPeriod;
_logger.LogInformation(
"Applying retention policy: {Entity} older than {Date}, Action={Action}",
policy.EntityType, cutoffDate, policy.Action);
// Process in batches to avoid memory/lock issuesconstint batchSize = 1000;
// ... batch processing logic
}
}
GDPR Right to Deletion
Deletion Pipeline
Database Versioning Strategies
Approaches
Strategy
Tool
Pros
Cons
EF Core Migrations
dotnet ef
Integrated, C# code
Tied to EF Core
DbUp
DbUp library
Simple SQL scripts, idempotent
Manual migration tracking
Flyway
Flyway CLI
Platform-agnostic, checksums
Java-based, separate tool
DACPAC
SQL Server Data Tools
Declarative, diff-based
SQL Server only
DbUp Example (alternative to EF Migrations)
csharp
// Simple, script-based database versioningvar upgrader = DeployChanges.To
.SqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.WithTransaction() // Each script in a transaction
.LogToConsole()
.Build();
var result = upgrader.PerformUpgrade();
if (!result.Successful)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(result.Error);
return-1;
}