Kỹ thuật Đa Tenant (Multitenancy) cho Giao diện Người dùng trong ASP.NET Core

Ngày đăng: 24 tháng 11, 2025

Cập nhật mã expander view, cảm ơn Thomas Ardal!

Giới thiệu

Tôi đã viết một số bài viết về đa tenant (multitenancy) với ASP.NET Core. Cho đến nay, chúng ta đã có:

Lần này tôi sẽ nói về Giao diện Người dùng (UI) và đưa ra một số gợi ý cho:

  • Tải nội dung trong view một cách có điều kiện theo từng tenant
  • Tải các view khác nhau cho các tenant khác nhau
  • Sử dụng code để đưa ra các lựa chọn phụ thuộc vào tenant

Chúng ta sẽ tham chiếu đến cùng một abstraction ITenantIdProvider đã được trình bày trước đây. Tất cả các view mà tôi sẽ đề cập đều là Razor views, có thể là view trong ứng dụng MVC hoặc Razor Pages.

Tải các View khác nhau cho mỗi Tenant

Views được sử dụng để render nội dung HTML trong một dự án MVC. Dịch vụ chịu trách nhiệm định vị các file view là IViewLocationExpander. Chúng ta sẽ triển khai phiên bản của riêng mình, thêm một đường dẫn khác vào danh sách các đường dẫn được sử dụng để tìm kiếm view (các đường dẫn gốc đến từ RazorViewEngineOptions.ViewLocationFormats):

public sealed class TenantViewLocationExpander : IViewLocationExpander
{
    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {  
        var tenantIdProvider = context.ActionContext.HttpContext.RequestServices.GetService<ITenantIdProvider>();
        var tenantId = tenantIdProvider.GetTenantId();
        foreach (var location in viewLocations)
        {
            yield return location.Replace("{0}", tenantId + "/{0}");
            yield return location;
        }
    }

    public void PopulateValues(ViewLocationExpanderContext context)
    {
    }
}

Như vậy, với mỗi đường dẫn vật lý, chúng ta cũng trả về một đường dẫn khác có kèm thư mục tenant.

Đừng quên đăng ký implementation của chúng ta trong RazorViewEngineOptions.ViewLocationExpanders:

builder.Services.Configure<RazorViewEngineOptions>(options =>
{
    options.ViewLocationExpanders.Insert(0, new TenantViewLocationExpander());
});

Với điều này, chúng ta thêm một đường dẫn mới vào danh sách các vị trí mà ASP.NET Core (hay chính xác hơn là view engine) sẽ tìm kiếm view:

  • /Views/<controller>/<tenant>
  • /Views/<controller>
  • /Views/Shared

Bởi vì chúng ta thêm đường dẫn mới lên đầu tiên, nếu có một thư mục với tên tenant id bên trong thư mục Views và thư mục <controller> (ví dụ: /Views/Home/Foo), thì các view sẽ được tải từ đó trước một cách trong suốt. Nếu thư mục tenant không tồn tại, hoặc bản thân view không tồn tại, nó sẽ fallback về các vị trí mặc định.

Sử dụng Tag Helpers để Hiển thị Nội dung có Điều kiện

Một lựa chọn khác chúng ta có là, bên trong một view, render các phần nội dung có điều kiện, tùy thuộc vào tenant hiện tại. Để làm điều đó, chúng ta sẽ sử dụng tag helper. Tag helpers cho phép khai báo các tag tùy chỉnh (hoặc sửa đổi hành vi của các tag hiện có). Để biết thêm thông tin về cách tạo tag helpers, vui lòng đọc bài viết này.

[HtmlTargetElement("tenant")]
public sealed class TenantTagHelper(ITenantIdProvider tenantIdProvider) : TagHelper
{
    public string? Name { get; set; }
    
    public bool Except { get; set; }
    
    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var tenants = Name?.Split(',') ?? [];
        var tenant = tenantIdProvider.GetTenantId();
        
        if (Except)
        {
            if (tenants.Any(t => t == tenant))
            {
                output.SuppressOutput();
            }
        }
        else
        {
            if (!tenants.Any(t => t == tenant))
            {
                output.SuppressOutput();
            }
        }
        
        return base.ProcessAsync(context, output);
    }
}

Tag helpers cần được đăng ký trước khi sử dụng thông qua addTagHelper (trong đó MyAssembly là tên của assembly dự án MVC):

@addTagHelper *, MyAssembly

Và sau đó có thể được sử dụng như, ví dụ:

<tenant name="Foo">Nội dung cho Foo</tenant>
<tenant name="Bar">Nội dung cho Bar</tenant>
<tenant name="Foo,Other">Foo cũng ở đây</tenant>
<tenant name="Bar" except="true">Không phải Bar ở đây</tenant>

Tag helper này tạo một tag mới, <tenant>, có các tham số sau dưới dạng thuộc tính:

  • name: danh sách các tenant id được phân tách bằng dấu phẩy
  • except: một giá trị boolean cho biết logic có nên được đảo ngược hay không (mặc định là false)

Hai cách sử dụng:

  1. Với danh sách tenant id phân tách bằng dấu phẩy trong name, nội dung sẽ được render nếu bất kỳ tenant nào trong danh sách khớp với tenant hiện tại
  2. Nếu Except được đặt thành true, logic sẽ đảo ngược và nội dung sẽ chỉ được render cho tất cả các tenant ngoại trừ những tenant trong name

Với tenant “Foo“, kết quả sẽ hiển thị:

Nội dung cho Foo
Foo cũng ở đây
Không phải Bar ở đây

Các vấn đề với cách tiếp cận này bao gồm việc chúng ta cần biết tenant id để áp dụng logic và chúng ta phải viết khá dài dòng, mặc dù chúng ta có thể sử dụng Except để phủ định chỉ cho một số tenant.

Sử dụng Code

Tất nhiên, trong một Razor view, bạn cũng có thể inject ITenantIdProvider và thực hiện logic có điều kiện tùy thuộc vào tenant:

@inject ITenantIdProvider TenantIdProvider
@{
    var tenantId = TenantIdProvider.GetTenantId();
}

@if (tenantId == "Foo")
{
    //thực hiện điều gì đó
}
else
{
    //thực hiện điều gì đó khác
}

Tất nhiên, với cách tiếp cận này, bạn phải hardcode tenant id trong code, điều này có thể chấp nhận được hoặc không. Một lựa chọn khác là gọi một service nào đó biết cách xử lý cho các tenant cụ thể:

@inject ITenantStrategyService TenantStrategy

@if (TenantStrategy.ShouldProcess())
{
    //ok cho tenant hiện tại
}
else
{
    //điều gì đó khác
}

ITenantStrategyService, tất nhiên, là một service giả định nào đó biết tenant hiện tại là gì và đóng gói logic để xử lý nó (và các tenant khác).

Kết luận

Có nhiều cách tồn tại để đạt được kết quả tương tự, nhưng đây là những gợi ý của tôi, hy vọng bạn thấy hữu ích. Tôi sẽ tiếp tục viết về đa tenant, vì vậy hãy theo dõi nhé. Như mọi khi, nếu bạn có bất kỳ câu hỏi hoặc phản đối nào, hãy để lại bình luận!


Từ khóa: .net, asp.net core, multitenancy, web

Địa điểm: Coimbra, Bồ Đào Nha

Chỉ mục