0

Ứng Dụng Nhiều Người Dùng (Multi-Tenant) Với EF Cor

Ngày nay, hầu hết các ứng dụng phần mềm đều được xây dựng dựa trên khái niệm đa người dùng (multi-tenancy). Một ứng dụng có thể phục vụ cho nhiều khách hàng khác nhau, trong khi vẫn đảm bảo dữ liệu của họ được cách ly độc lập.

Bạn có thể triển khai mô hình multi-tenancy theo hai cách:

  • Single database (Cơ sở dữ liệu đơn) với logical isolation (cách ly logic) giữa các tenant.
  • Multiple databases (Nhiều cơ sở dữ liệu) với physical isolation (cách ly vật lý) giữa các tenant.

Lựa chọn nào phù hợp phụ thuộc vào yêu cầu của bạn. Một số lĩnh vực như y tế yêu cầu mức độ cách ly dữ liệu cao, do đó việc sử dụng một cơ sở dữ liệu riêng cho mỗi tenant là điều bắt buộc.


Cách Triển Khai Multi-Tenancy Với EF Core

Chúng ta có thể sử dụng Query Filters để áp dụng bộ lọc tenant cho tất cả các truy vấn cơ sở dữ liệu. Chỉ cần triển khai một lần, và bạn có thể gần như "quên" việc phải thêm điều kiện lọc mỗi khi truy vấn.


Sử Dụng EF Core Query Filters

Nếu bạn muốn tìm hiểu chi tiết hơn về Query Filters, hãy xem bài viết Cách Sử Dụng Global Query Filters Trong EF Core.

  • Cấu hình bộ lọc bằng cách gọi HasQueryFilter trên entity.
  • EF sẽ tự động áp dụng nó cho tất cả các truy vấn của entity đó.
  • Có thể tạm thời tắt bằng IgnoreQueryFilters.
  • Mỗi entity chỉ được phép có một query filter duy nhất.

Ví dụ đơn giản:

modelBuilder
   .Entity<Order>()
   .HasQueryFilter(order => !order.IsDeleted);

Tất cả các truy vấn đến bảng Order sẽ tự động thêm điều kiện IsDeleted = FALSE.


Multi-Tenancy Với Cơ Sở Dữ Liệu Đơn

Để triển khai multi-tenancy trên một cơ sở dữ liệu duy nhất, bạn cần:

  • Biết được tenant hiện tại là ai.
  • Lọc dữ liệu chỉ dành cho tenant đó.

Cách phổ biến là thêm một cột TenantId trong các bảng, sau đó lọc dựa vào cột này khi truy vấn.

Ví dụ trong OnModelCreating:

public class OrdersDbContext : DbContext
{
    private readonly string _tenantId;

    public OrdersDbContext(
        DbContextOptions<OrdersDbContext> options,
        TenantProvider tenantProvider)
        : base(options)
    {
        _tenantId = tenantProvider.TenantId;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<Order>()
            .HasQueryFilter(o => o.TenantId == _tenantId);
    }
}

Ở đây, lớp TenantProvider sẽ cung cấp thông tin tenant hiện tại.

public sealed class TenantProvider
{
    private const string TenantIdHeaderName = "X-TenantId";
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantProvider(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string TenantId => _httpContextAccessor
        .HttpContext
        .Request
        .Headers[TenantIdHeaderName];
}

Trong ví dụ này, TenantId được lấy từ HTTP header.
Các cách khác có thể dùng gồm:

  • Query string api/orders?tenantId=example-tenant-id
  • JWT Claim
  • API Key

Nếu muốn bảo mật hơn, bạn nên dùng JWT Claims hoặc API Key để truyền TenantId.


Multi-Tenancy Với Nhiều Cơ Sở Dữ Liệu

Nếu bạn muốn cách ly hoàn toàn dữ liệu giữa các tenant, hãy sử dụng mỗi tenant một database riêng.

Những việc cần làm:

  • Xác định connection string riêng cho từng tenant.
  • Tự động lấy connection string tương ứng cho mỗi yêu cầu.

Lúc này, bạn không thể sử dụng Query Filters, vì mỗi tenant có cơ sở dữ liệu khác nhau. Bạn cần lưu thông tin tenant và connection string ở đâu đó, ví dụ trong appsettings.json:

"Tenants": {
    { "Id": "tenant-1", "ConnectionString": "Host=tenant1.db;Database=tenant1" },
    { "Id": "tenant-2", "ConnectionString": "Host=tenant2.db;Database=tenant2" }
}

Sau đó, đăng ký IOptions chứa danh sách Tenant.

Sửa lại TenantProvider để lấy đúng connection string:

public sealed class TenantProvider
{
    private const string TenantIdHeaderName = "X-TenantId";
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly TenantSettings _tenantSettings;

    public TenantProvider(
        IHttpContextAccessor httpContextAccessor,
        IOptions<TenantSettings> tenantsOptions)
    {
        _httpContextAccessor = httpContextAccessor;
        _tenantSettings = tenantsOptions.Value;
    }

    public string TenantId => _httpContextAccessor
        .HttpContext
        .Request
        .Headers[TenantIdHeaderName];

    public string GetConnectionString()
    {
        return _tenantSettings.Tenants.Single(t => t.Id == TenantId);
    }
}

Cuối cùng, cấu hình DbContext để tự động chọn connection string:

builder.Services.AddDbContext<OrdersDbContext>((sp, o) =>
{
    var tenantProvider = sp.GetRequiredService<TenantProvider>();
    var connectionString = tenantProvider.GetConnectionString();
    o.UseSqlServer(connectionString);
});

Mỗi request sẽ tạo một OrdersDbContext mới và kết nối đến database tương ứng. Bạn nên lưu các connection string trong nơi an toàn.


Kết Luận

Hy vọng bạn đã hiểu rõ hơn cách xây dựng hệ thống multi-tenant với EF Core.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí