Chạy MigrateAsync() khi khởi động? Bạn đang trao cho ứng dụng của mình quyền chủ sở hữu database và hy vọng rằng sẽ không có gì sai sót. Có một cách tốt hơn – EF migration bundles cho phép bạn chạy migrations như một bước CI được kiểm soát, giữ cho ứng dụng production của bạn an toàn. Nhưng vấn đề là: đôi khi cách “sai” lại hoàn toàn ổn. Hãy cùng khám phá khi nào nên sử dụng từng cách tiếp cận.
Tài liệu chính thức: Tổng quan về Migrations | Áp dụng Migrations | Bundles
Mục lục
Cách “Sai” (Mà Tôi Vẫn Dùng)
Blog này sử dụng MigrateAsync() khi khởi động – chính cách tiếp cận mà tôi sắp khuyên bạn không nên dùng. Đây là lý do tại sao điều đó ổn với tôi, và tại sao nó có thể không ổn với bạn.
Trong file Program.cs của tôi có đoạn code sau:
using (var scope = app.Services.CreateScope())
{
var blogContext = scope.ServiceProvider.GetRequiredService<IMostlylucidDBContext>();
await blogContext.Database.MigrateAsync();
}
MigrateAsync() áp dụng các migrations đang chờ và tạo database nếu cần. Đơn giản – nhưng có vấn đề:
- Phụ thuộc khi khởi động – Database bị lỗi? Migration thất bại? Ứng dụng của bạn sẽ không khởi động được.
- Vi phạm bảo mật – Ứng dụng của bạn cần quyền
db_owner. Bạn vừa trao cho ứng dụng runtime của mình chìa khóa để xóa bảng.
Tại sao tôi thoát được: dữ liệu công khai, mạng Docker đơn, dự án cá nhân. Bạn có thể không.
Khi Nào Runtime Migrations Ổn
- Phát triển local – Vòng lặp nhanh quan trọng hơn thủ tục
- Dự án cá nhân – Phạm vi ảnh hưởng thấp, không có dữ liệu nhạy cảm
- Môi trường dev Docker-compose – Sự tiện lợi chiến thắng
- Xây dựng prototype – Schema thay đổi liên tục
Khi Nào Không Nên Dùng
- Nhiều instance ứng dụng – Điều kiện tranh chấp (race conditions) tràn lan
- Dữ liệu nhạy cảm – PII, tài chính, dữ liệu được quản lý = cần phân tách đúng cách
- Production với người dùng thực – Migration thất bại = gián đoạn dịch vụ
Cách Đúng: EF Bundles
EF bundle là một file thực thi độc lập chứa các migrations đã được biên dịch của bạn. Hãy nghĩ về nó như dotnet ef database update được đóng gói thành một file .exe độc lập.
Tại sao bundles lại tốt hơn:
- Không phụ thuộc runtime – Máy đích không cần SDK hoặc EF CLI
- Phân tách đúng đắn – Ứng dụng không bao giờ cần
db_owner; chỉ CI runner cần và chỉ trong lúc triển khai - Hiển thị trong CI – Lỗi hiển thị trong log pipeline, không bị chôn vùi trong quá trình khởi động ứng dụng
- An toàn khi rollback – Migration thất bại? Quá trình triển khai dừng lại trước khi code lỗi được đưa lên
- Tính chất đẳng năng (Idempotent) – Theo dõi những gì đã được áp dụng, chỉ chạy những gì cần thiết
Lưu ý: Để đạt bảo mật cấp production, hãy sử dụng Managed Identity thay vì connection strings. Nhưng bundles vẫn là một bước tiến lớn so với runtime migrations.
Ví dụ với GitHub Actions
- name: Install EF Core tools
run: dotnet tool install --global dotnet-ef
- name: Add EF tools to PATH
run: echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
- name: Generate EF migration bundle
run: |
dotnet ef migrations bundle \
--project ${{ env.WEB_PROJECT }} \
--output efbundle.exe \
--configuration ${{ env.BUILD_CONFIGURATION }} \
--runtime ${{ env.RUNTIME_IDENTIFIER }} \
--context AdminDbContext \
env:
AdminSite__ConnectionString: ${{ secrets.PROD_SQL_CONNECTIONSTRING }}
- name: Run EF migration bundle
run: |
./efbundle.exe
env:
AdminSite__ConnectionString: ${{ secrets.PROD_SQL_CONNECTIONSTRING }}
Bundle đọc connection strings từ biến môi trường và áp dụng các migrations đang chờ. Nếu đã được áp dụng rồi? Nó chỉ thoát thành công.
Local Bundles
Không có CI? Muốn kiểm tra trước khi push? Xây dựng bundles local.
Các trường hợp sử dụng: Kiểm tra trước CI, bàn giao cho DBA (file thực thi độc lập, không cần SDK), triển khai staging, gỡ lỗi với --verbose.
Tạo Bundle
# Cài đặt EF CLI (một lần)
dotnet tool install --global dotnet-ef
# Bundle cơ bản
dotnet ef migrations bundle \
--project Mostlylucid.DbContext \
--startup-project Mostlylucid \
--output efbundle.exe
# Tự chứa (bao gồm runtime - có thể chạy trên máy không có .NET)
dotnet ef migrations bundle \
--project Mostlylucid.DbContext \
--startup-project Mostlylucid \
--output efbundle.exe \
--self-contained
# Đa nền tảng (ví dụ: build trên Windows, triển khai lên Linux)
dotnet ef migrations bundle \
--project Mostlylucid.DbContext \
--startup-project Mostlylucid \
--output efbundle \
--runtime linux-x64
Chạy Bundle
# Sử dụng connection string mặc định từ appsettings.json
./efbundle.exe
# Ghi đè với connection string cụ thể
./efbundle.exe --connection "Host=localhost;Database=mostlylucid;Username=postgres;Password=secret"
# Sử dụng biến môi trường (khớp với key config của bạn)
$env:ConnectionStrings__DefaultConnection="Host=localhost;..." # PowerShell
export ConnectionStrings__DefaultConnection="Host=localhost;..." # Bash
./efbundle.exe
Các Tùy Chọn Hữu Ích
# Xem migrations nào sẽ được áp dụng mà không chạy thực tế
./efbundle.exe --dry-run
# Đầu ra chi tiết để gỡ lỗi
./efbundle.exe --verbose
# Áp dụng migrations đến một migration cụ thể (hữu ích cho kiểm thử)
./efbundle.exe --target-migration "20231115_AddUserTable"
# Kết hợp các tùy chọn
./efbundle.exe --verbose --dry-run
Quy Trình Kiểm Thử Local
# 1. Tạo migration
dotnet ef migrations add AddNewFeature \
--project Mostlylucid.DbContext \
--startup-project Mostlylucid
# 2. Xây dựng bundle
dotnet ef migrations bundle \
--project Mostlylucid.DbContext \
--startup-project Mostlylucid \
--output efbundle.exe
# 3. Chạy thử trước
./efbundle.exe --dry-run --verbose
# 4. Chạy thực tế
./efbundle.exe --verbose
# 5. Bị lỗi? Xóa và thử lại
dotnet ef migrations remove \
--project Mostlylucid.DbContext \
--startup-project Mostlylucid
Phát hiện lỗi cú pháp, vi phạm ràng buộc, vấn đề khóa ngoại – tất cả trước khi CI hoặc production.
Hiệu Suất Build
Tạo bundle khá chậm – 30 giây trở lên trên các dự án lớn. Đừng tạo bundle ở mọi lần build.
- Tạo thủ công khi kiểm thử local
- Tạo trong CI chỉ khi triển khai, không phải mọi PR
- Cache bundles nếu migrations chưa thay đổi
Nếu bạn thực sự muốn tự động tạo, hãy thêm một MSBuild target:
<Target Name="BuildMigrationBundle">
<Exec Command="dotnet ef migrations bundle --output $(OutputPath)efbundle.exe --force" />
</Target>
Sau đó: dotnet build -t:BuildMigrationBundle
Phương Pháp Kết Hợp (Hybrid)
Điều tốt nhất của cả hai thế giới: tiện lợi ở local, bảo mật ở production.
if (builder.Environment.IsDevelopment())
{
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IMostlylucidDBContext>();
await context.Database.MigrateAsync();
}
// Production: CI pipeline chạy bundle
Các Giải Pháp Thay Thế Cho Bundles
SQL Scripts
Tạo SQL thuần thay vì file thực thi. Tuyệt vời cho việc DBA xem xét và quy trình quản lý thay đổi hiện có.
# Tất cả migrations
dotnet ef migrations script --output migrations.sql
# Idempotent (an toàn khi chạy nhiều lần) - HÃY DÙNG CÁI NÀY
dotnet ef migrations script --idempotent --output migrations.sql
# Phạm vi migrations
dotnet ef migrations script FromMigration ToMigration --output migrations.sql
Ưu điểm: Hiển thị đầy đủ, bất kỳ SQL client nào cũng có thể chạy, thân thiện với version control, quy trình phê duyệt DBA.
Nhược điểm: Không tự động theo dõi (sử dụng --idempotent), thực thi thủ công, có thể bị lệch nếu script bị sửa đổi.
Xem tài liệu chính thức về SQL scripts.
SQL Scripts trong CI
- name: Generate and apply migrations
run: |
dotnet ef migrations script --idempotent --output migrations.sql
# SQL Server
sqlcmd -S ${{ secrets.DB_SERVER }} -d ${{ secrets.DB_NAME }} -i migrations.sql
# Hoặc PostgreSQL
PGPASSWORD=${{ secrets.DB_PASSWORD }} psql -h ${{ secrets.DB_HOST }} -f migrations.sql
DACPAC (Chỉ SQL Server)
DACPAC là dựa trên trạng thái chứ không phải dựa trên migration. Bạn định nghĩa schema mong muốn, và SqlPackage sẽ so sánh nó với database đích.
SqlPackage.exe /Action:Publish /SourceFile:MyDatabase.dacpac /TargetConnectionString:"..."
Ưu điểm: Schema dưới dạng code, tự động tạo diff, xử lý mọi thứ (bảng, views, SPs, indexes), công cụ doanh nghiệp.
Nhược điểm: Chỉ SQL Server, schema ở hai nơi (EF models + SQL project), engine diff có thể đưa ra quyết định không tốt, đổi tên cột trông giống như drop+add.
Xem tài liệu SqlPackage.
Bảng So Sánh
| Phương pháp | Phù hợp nhất cho | Cần .NET | Tự động theo dõi | Thân thiện DBA | Đa nền tảng DB |
|---|---|---|---|---|---|
MigrateAsync() |
Dev/dự án nhỏ | Có (runtime) | Có | Không | Có |
| EF Bundles | CI/CD pipelines | Không (tự chứa) | Có | Tương đối | Có |
| SQL Scripts | Môi trường do DBA kiểm soát | Không | Với --idempotent |
Có | Có |
| DACPAC | Doanh nghiệp SQL Server | Không | Có (dựa trên trạng thái) | Có | Không |
Mẹo
Bẫy Với File Designer
Migrations hoạt động local nhưng không trong CI? Hãy kiểm tra bạn đã commit cả hai file:
20231115_AddUserTable.cs– Mã migration20231115_AddUserTable.Designer.cs– Model snapshot
Thiếu file Designer = lỗi âm thầm.
Nhiều DbContexts
dotnet ef migrations bundle --context BlogDbContext --output blog-migrations.exe
dotnet ef migrations bundle --context IdentityDbContext --output identity-migrations.exe
Thứ Tự Ưu Tiên Connection String
- Tham số
--connection - Biến môi trường
appsettings.json
Sử dụng biến môi trường trong CI.
IDesignTimeDbContextFactory
Công cụ EF cần khởi tạo DbContext của bạn. Nếu DbContext của bạn nằm trong một dự án riêng hoặc có startup phức tạp, hãy triển khai IDesignTimeDbContextFactory<T>:
public class AdminDbContextFactory : IDesignTimeDbContextFactory<AdminDbContext>
{
public AdminDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.AddEnvironmentVariables()
.AddUserSecrets<AdminDbContextFactory>()
.Build();
var connectionString = config["AdminSite:ConnectionString"]
?? throw new InvalidOperationException("Thiếu connection string");
var optionsBuilder = new DbContextOptionsBuilder<AdminDbContext>();
optionsBuilder.UseSqlServer(connectionString, sql => sql.CommandTimeout(120));
return new AdminDbContext(optionsBuilder.Options);
}
}
Sử dụng khi: DbContext trong dự án riêng, startup phức tạp, cần User Secrets cho design-time.
Các Câu Hỏi Thường Gặp
Các câu hỏi phổ biến và phản hồi tôi đã nhận được.
“Sao không chỉ chạy dotnet ef database update trong CI?”
Đã đề cập ở trên, nhưng phiên bản ngắn gọn: bundles là các tạo phẩm (artifact) có tính di động. Bước triển khai của bạn không cần EF CLI, mã nguồn, hoặc design-time resolution. Cùng một bundle chạy trên test, staging và prod – không sai lệch.
“Điều này có quá mức cần thiết cho ứng dụng nhỏ không?”
Có thể. Nếu bạn làm việc một mình, dữ liệu công khai và phạm vi ảnh hưởng thấp – MigrateAsync() là ổn. Nhưng ngay khi bạn thêm một lập trình viên thứ hai, dữ liệu nhạy cảm hoặc nhiều môi trường, bundles sẽ tự chứng minh giá trị của chúng.
“Còn về rollbacks thì sao?”
EF không tự động rollback. Các lựa chọn:
- Tạo migration
Down()và chạy nó (nhưng bạn phải đã viết nó) - Khôi phục từ bản sao lưu
- Viết migration thủ công để hoàn tác thay đổi
Đối với các hệ thống quan trọng: hãy kiểm thử migrations trên một bản sao database trước.
“Tôi có thể chạy migrations trong Kubernetes init container không?”
Có. Bundle + init container là một mẫu thiết kế tốt:
initContainers:
- name: migrate
image: myapp:latest
command: ["./efbundle.exe"]
env:
- name: ConnectionStrings__Default
valueFrom:
secretKeyRef:
name: db-secrets
key: connection-string
Container ứng dụng đợi init hoàn thành.
“Còn FluentMigrator / DbUp / các công cụ khác thì sao?”
Chúng hoạt động tốt. EF bundles là giải pháp gốc của EF, nhưng FluentMigrator và DbUp cũng có người hâm mộ. Sự khác biệt chính: đó là các công cụ dành riêng cho migration, trong khi EF bundles đến từ mô hình EF hiện có của bạn.
“DBA của tôi muốn xem xét tất cả SQL trước khi chạy”
Sử dụng script --idempotent:
dotnet ef migrations script --idempotent --output migrations.sql
DBA xem xét và phê duyệt. Sau đó:
- Chạy script thủ công, hoặc
- Sau khi được phê duyệt, chạy bundle (thực hiện việc tương tự)
“Làm thế nào để xử lý migrations với zero downtime?”
Đó là câu hỏi về chiến lược triển khai, không phải câu hỏi về migrations. Nhìn chung:
- Làm cho migrations tương thích ngược (thêm cột nullable, không đổi tên)
- Triển khai code mới xử lý cả schema cũ và mới
- Chạy migration
- Triển khai code chỉ sử dụng schema mới
- Dọn dẹp (xóa cột cũ trong migration sau)
Bundles không giải quyết vấn đề này – chúng chỉ làm cho bước 3 dễ dự đoán hơn.



