7.7 — 5. Program.cs và WebApplicationBuilder
Program.cs có hai giai đoạn tách bạch mà builder.Build() là ranh giới: trước nó bạn đăng ký, sau nó bạn cấu hình pipeline. Hiểu ranh giới này giải thích hầu hết lỗi khởi động: đăng ký sau Build() không có tác dụng, và gọi BuildServiceProvider() giữa chừng tạo ra một container thứ hai với bộ singleton riêng — một nguồn bug mà bạn sẽ mất nhiều giờ để tìm ra. Trong giai đoạn hai, thứ tự middleware là ngữ nghĩa: đặt UseAuthorization trước UseAuthentication khiến mọi request đều vô danh, và không có lỗi nào được báo.
Mục tiêu bài học
Sau bài này bạn có thể:
- Mô tả hai giai đoạn của
Program.csvà những gì hợp lệ ở mỗi giai đoạn. - Tổ chức đăng ký theo layer bằng extension method.
- Sắp đúng thứ tự middleware và giải thích hậu quả khi sai.
- Chạy migration hoặc seed lúc khởi động một cách đúng đắn.
- Biết vì sao
BuildServiceProvider()thủ công là lỗi.
Nội dung bài học
7.7.1 — Hai giai đoạn
var builder = WebApplication.CreateBuilder(args);
// ===== GIAI ĐOẠN 1: ĐĂNG KÝ =====
builder.Services.AddControllers();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddApplication();
var app = builder.Build(); // <-- RANH GIỚI. Container được đóng băng.
// ===== GIAI ĐOẠN 2: PIPELINE =====
if (app.Environment.IsDevelopment())
app.UseSwagger().UseSwaggerUI();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Sau Build(), builder.Services vẫn gọi được nhưng không còn tác dụng gì — không exception, không cảnh báo, chỉ là service đó không bao giờ tồn tại. Đây là một trong những lỗi khó tìm nhất với người mới.
WebApplication.CreateBuilder(args) đã làm sẵn khá nhiều: nạp appsettings.json, appsettings.{Environment}.json, biến môi trường, tham số dòng lệnh; cấu hình logging; đăng ký IConfiguration, IWebHostEnvironment, ILogger<T>.
7.7.2 — Đừng gọi BuildServiceProvider() thủ công
// SAI — tạo một container THỨ HAI
builder.Services.AddSingleton<ICache, MemoryCache>();
var sp = builder.Services.BuildServiceProvider(); // container #2
var cache = sp.GetRequiredService<ICache>(); // singleton cua container #2
builder.Services.AddSingleton(cache);
Bạn vừa tạo hai container, mỗi cái có bộ Singleton riêng. Về sau, một phần ứng dụng dùng instance của container #1, phần khác dùng instance của container #2 — cùng một kiểu Singleton nhưng hai đối tượng khác nhau. Cache ghi ở một nơi, đọc ở nơi khác thì rỗng.
Tệ hơn: container #2 không bao giờ được Dispose, nên mọi IDisposable trong đó rò rỉ.
Analyzer ASP0000 cảnh báo chuyện này. Cần cấu hình phụ thuộc vào một service khác thì dùng options pattern với IConfigureOptions<T> (bài 7.8) — nó được resolve sau khi container dựng xong.
7.7.3 — Tách đăng ký theo layer
Khi Program.cs vượt 100 dòng đăng ký, nó trở thành nơi không ai muốn đụng vào. Tách bằng extension method, mỗi layer một file, nằm trong chính project của layer đó:
// Infrastructure/DependencyInjection.cs
public static class InfrastructureServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services, IConfiguration configuration)
{
services.AddDbContext<CrmDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("CrmDb")));
services.AddScoped<ICustomerRepository, EfCustomerRepository>();
services.AddScoped<IUnitOfWork, EfUnitOfWork>();
services.AddOptions<EmailSettings>()
.Bind(configuration.GetSection(EmailSettings.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IEmailService, SmtpEmailService>();
return services; // trả về để nối chuỗi
}
}
// Application/DependencyInjection.cs
public static class ApplicationServiceCollectionExtensions
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<ICustomerService, CustomerService>();
services.AddTransient<ILeadScorer, RuleBasedLeadScorer>();
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(
typeof(ApplicationServiceCollectionExtensions).Assembly));
return services;
}
}
Ba lợi ích thật:
- Ranh giới layer hiện ra.
AddApplication()không nhậnIConfiguration— nếu một ngày nó cần, đó là tín hiệu layer application đang biết về hạ tầng. Program.csđọc được trong 30 giây.- Integration test dùng lại được: gọi đúng các extension đó rồi
Replacevài thứ.
Chỗ này thường bị làm quá tay thành AddCustomerModule(), AddLeadModule(), AddOrderModule()… Khi số extension method vượt số layer, bạn chỉ đang di chuyển sự lộn xộn.
7.7.4 — Thứ tự middleware là ngữ nghĩa
Giai đoạn 2 không phải danh sách bật/tắt — nó là thứ tự thực thi. Mỗi app.UseXxx() thêm một lớp vào pipeline, và request đi qua theo đúng thứ tự bạn viết.
app.UseExceptionHandler("/error"); // ngoài cùng — bắt lỗi của MỌI lớp bên trong
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication(); // BẮT BUỘC trước Authorization
app.UseAuthorization();
app.MapControllers();
Ba hậu quả cụ thể khi sai thứ tự:
| Sai | Hậu quả |
|---|---|
UseAuthorization trước UseAuthentication | User luôn vô danh, mọi [Authorize] từ chối — không có lỗi nào được báo |
UseExceptionHandler đặt sau | Exception ở middleware phía trên nó không được bắt, client nhận trang lỗi mặc định |
UseCors sau UseAuthorization | Preflight request bị chặn trước khi tới CORS, trình duyệt báo lỗi CORS khó hiểu |
UseStaticFiles sau UseAuthentication | File tĩnh đi qua toàn bộ tầng xác thực, chậm không cần thiết |
Quy tắc dễ nhớ: ngoài vào trong — xử lý lỗi, bảo mật truyền tải, định tuyến, xác thực, phân quyền, rồi mới tới endpoint.
7.7.5 — Migration và seed lúc khởi động
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<CrmDbContext>();
await db.Database.MigrateAsync();
}
app.Run();
CreateScope() là bắt buộc: app.Services là root provider, và lấy thẳng một service Scoped từ đó sẽ ném exception khi ValidateScopes bật — hoặc tệ hơn, giam nó nếu không bật (bài 7.5).
Với production nhiều instance, cách này có vấn đề: khi bạn scale lên 3 pod, cả ba cùng chạy migration một lúc. EF Core có khoá nên thường ổn, nhưng cách an toàn là tách migration thành bước riêng trong pipeline triển khai.