Làm thế nào để quản lý EF Core DbContext Lifetime

Làm thế nào để quản lý EF Core DbContext Lifetime

January 23, 2025 0 By Nam Vu

Việc quản lý DbContext Lifetime đóng vai trò quan trọng trong việc đảm bảo hiệu năng và sự ổn định của một ứng dụng. Mặc dù việc Register DbContext với Scoped Lifetime khá đơn giản và mặc định trong Entity Framework nhưng trong một số trường hợp bạn cần kiểm soát chặt chẽ hơn việc khởi tạo và hủy bỏ đối tượng DbContext. 

EF Core phát triển nhiều tính năng mở rộng giúp tầng ORM của bạn được linh hoạt và hiệu quả hơn đặc biệt với các ứng dụng yêu cầu hiệu năng cao hoặc xử lý luồng workflow phức tạp.

  1. Sử dụng DbContext

DbContext là trái tim của EF Core, nó chịu trách nhiệm kết nối với cơ sở dữ liệu và thực hiện các thao tác CRUD. 

DbContext có nhiệm vụ chính là:

– Managing database connections: Quản lý việc đóng mở cơ sở dữ liệu và kết nối khi cần. 

– Change tracking: Theo dõi các trạng thái thay đổi của entity nhằm mục đích ghi nhận vào cơ sở dữ liệu. 

– Query execution: Biên dịch Linq thành SQL và thực thi chúng trên cơ sở dữ liệu.

DbContext được khai báo mặc định DI Lifetime dạng Scope trong DI Container nên nó chỉ có thời gian sống trong scope hiện tại mà thôi vì vậy khi sử dụng DbContext cần lưu ý sau:

– Not thread-safe: Khi làm việc với DbContext bạn không nên chia sẻ giữa nhiều thread cùng lúc. 

– Lightweight: DbContext được thiết kế ra để khởi tạo và hủy bỏ thường xuyên nên khi khởi tạo cũng như dispose cần lưu ý vấn đề này.

– Stateful: Khi làm việc với Change tracking cần theo dõi trạng thái của entity và quản lý sự thay đổi của nó dựa trên thuộc thính identity như khóa chính hay row version. 

Ví dụ đăng ký DbContext trong DI container:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

Đây là một cách dùng khác của DbContext nếu bạn không để nó tự khai báo mặc định DI Scope

using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    await dbContext.Database.MigrateAsync();
}

Tuy nhiên, có nhiều trường hợp bạn cần kiểm soát việc khởi tạo và hủy bỏ DbContext chẳng hạn như sử dụng nhiều Database, nhiều loại cơ sở dữ liệu (No-SQL, SQL, …) trong cùng một service hay ứng dụng của bạn, bạn có thể sử dụng DbContextFactory. 

  1. Sử dụng DbContextFactory

DbContextFactory được sử dụng khi bạn cần tạo và hủy DbContext theo yêu cầu nó hay sử dụng nhất trong các service hay ứng dụng đa luồng (multi-threaded) hoặc background service (Job).

Generic Interface IDbContextFactory<TContext> nằm trong Entity Framework Core cho phép tạo ra DbContext Instance. Nó sẽ giúp bạn chắc chắn mỗi instance được khởi tạo ra có thể sử dụng an toàn trong DI container service lifetime, không bị chồng chéo hay conflict mỗi khi chạy bất đồng bộ hay nhiều DbContext.

IDbContextFactory<TContext> được đăng ký tương tự DbContext tuy nhiên IDbContextFactory lại được register dạng Singleton trong DI container. Bạn có thể sử dụng hàm CreateDbContext hay CreateDbContextAsync từ IDbContextFactory Interface để tạo ra một DbContext cho luồng của mình mà không sợ ảnh hưởng đến luồng khác.

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});

public class HostedService : IHostedService
{
    private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

    public HostedService(IDbContextFactory<ApplicationDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
        var books = await context.Books.ToListAsync(cancellationToken: cancellationToken);
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

Khi sử dụng IDbContextFactory<TContext> thì DbContext sẽ được đóng ngay khi thoát khỏi block using. Tuy nhiên có một nhược điểm của IDbContextFactory mà khi bạn sử dụng nên đảm bảo bạn không tạo ra quá nhiều connection database. Vì khi đóng mở quá nhiều connection thì cơ sở dữ liệu sẽ bị quá tải và deadlock, ví dụ postgresQL mặc định cho phép max 500 connection thôi.

Ngoài ra khi bạn dùng chung cả DbContextFactory và DbContext cũng một lúc thì bạn phải khai báo option tường minh để đảm bảo 2 luồng DI container không bị xung đột (chuyển hết DI về đăng ký Singleton)

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
}, optionsLifetime: ServiceLifetime.Singleton);

builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});

Để giải quyết vấn đề không đóng mở quá nhiều gây giảm hiệu suất và xung đột DbContext thì EF Core sinh ra DbContext Pooling.

  1. Sử dụng DbContext Pooling

DbContext Pooling là tính năng trong EF Core giúp tái sử dụng các instance của DbContext giảm số lần khởi tạo và hủy bỏ lặp đi lặp lại.  DbContext Pooling là như một bể chứa để quản lý các DbContext Instance. Khi có một request mới được gọi xuống DbContext, EF Core sẽ kiểm tra trong bể chứa trước xem nó có tồn tại trước đó hay không để có thể tái sử dụng DbContext mà không cần khởi tạo lại nữa.

Tính năng này giúp tăng performance khi không cần phải đóng mở quá nhiều lần

builder.Services.AddDbContextPool<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});

Để kết hợp DbContextFactory and DbContext thì chúng ta có DbContextFactory Pooling cũng dựa trên nguyên lý trên

  1. Sử dụng DbContextFactory Pooling

Bạn có thể kết hợp DbContextFactory và DbContext Pooling để tạo các instance DbContext được tái sử dụng hiệu quả. 

builder.Services.AddPooledDbContextFactory<ApplicationDbContext>(options =>
{
    options.EnableSensitiveDataLogging().UseNpgsql(connectionString);
});
await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
var books = await context.Books.ToListAsync(cancellationToken: cancellationToken);

Túm cái váy lại:

Việc quản lý vòng đời của DbContext là một trong những yếu tố quan trọng để xây dựng các ứng dụng EF Core một cách hiệu quả.  Và bạn cần lưu ý một số điểm sau:

– DbContextFactory: Sử dụng khi cần tạo DbContext theo request, đặc biệt trong các ứng dụng đa luồng hoặc background tasks. 

– DbContext Pooling: Giảm số lần khởi tạo và hủy bỏ DbContext liên tục. 

– Pooled DbContextFactory: Kết hợp cả hai tính năng để tạo các instance DbContext được tái sử dụng hiệu quả. 

– Dispose DbContext Instances: Sử dụng using hoặc đảm bảo rằng các instance được giải phóng hoặc trả lại pool. 

– Tránh vấn đề thread safety: Không chia sẻ DbContext giữa các thread. 

Hy vọng bài viết giúp bạn quản lý tốt hơn DbContext lifecycle.

Chúc bạn lập trình vui vẻ!

#ntechdevelopers