Phần 5 - Đảm bảo cờ tính năng nhất quán trên các yêu cầu

Đây là bài thứ 5 trong loạt bài: Thêm cờ tính năng vào ứng dụng ASP.NET Core.

Trong các bài viết đầu tiên và thứ hai của loạt bài này, tôi đã giới thiệu các thư viện Microsoft.FeatureManagement và Microsoft.FeatureManagement.AspNetCore, đồng thời hướng dẫn cách sử dụng chúng để thêm các cờ tính năng vào ứng dụng ASP.NET Core.

Trong bài viết thứ ba và thứ tư, tôi đã chỉ ra cách sử dụng bộ lọc tính năng, cho phép bạn bật cờ tính năng dựa trên dữ liệu tùy ý. Ví dụ: bạn có thể bật một tính năng dựa trên tiêu đề trong một yêu cầu đến, dựa trên thời gian hiện tại hoặc cho một tỷ lệ phần trăm người dùng nhất định.

Tôi đã đề cập đến một vấn đề với một bộ lọc như vậy PercentageFilter, trong bài viết thứ ba. Trong bài viết, bộ lọc này trả về true khi gọi phương thức IsEnabled() cho 10% yêu cầu.

Thật không may, nếu bạn gọi bộ lọc nhiều lần trong một yêu cầu duy nhất, bạn có thể nhận được các kết quả khác nhau. Chưa kể rằng mỗi yêu cầu có thể đưa ra một kết quả khác nhau cho cùng một người dùng.

Trong bài viết này, tôi mô tả hai cách để cải thiện tính nhất quán của cờ tính năng của bạn đối với người dùng:

  • Cách tiếp cận đầu tiên, sử dụng IFeatureManagerSnapshot đảm bảo tính nhất quán trong một yêu cầu nhất định.
  • Cách tiếp cận thứ hai, sử dụng ISessionManager tùy chỉnh, cho phép bạn mở rộng sự nhất quán này giữa các yêu cầu.

Vấn đề: kết quả khác nhau với mọi yêu cầu.

Để giải thích vấn đề, tôi sẽ tạo cờ tính năng bằng cách sử dụng cờ PercentageFilter như được mô tả trong bài đăng trước của tôi. Tôi sẽ sử dụng bộ lọc này để đặt cờ tính năng có tên "NewExperience", cờ này được cấu hình để bật cho 50% yêu cầu:

{
  "FeatureManagement": 
  {
    "NewExperience": 
    {
      "EnabledFor": 
      [
        {
          "Name": "Microsoft.Percentage",
          "Parameters": 
          {
            "Value": 50
          }
        }
      ]
    }
  }
}

Trên trang chủ của ứng dụng, tôi sẽ đưa một IFeatureManager vào Index.cshtml và yêu cầu giá trị của tính năng này 10 lần:

@page
@model IndexModel
@inject Microsoft.FeatureManagement.IFeatureManager FeatureManager
@{
    ViewData["Title"] = "Home page";
}

<ul>
@for (var i = 0; i < 10; i++)
{
    <li>Flag is: @FeatureManager.IsEnabled(FeatureFlags.NewExperience)</li>
}
</ul>

Nếu bạn chạy ứng dụng, bạn có thể thấy rằng mỗi khi bạn gọi IsEnabled, có 50% khả năng cờ tính năng sẽ được bật:

Vấn đề: kết quả khác nhau với mọi yêu cầu.

Điều này rất không mong muốn trong ứng dụng của bạn. Bạn gần như chắc chắn không muốn một lá cờ chuyển đổi giữa bật và tắt trong ngữ cảnh của cùng một yêu cầu! Tùy thuộc vào mức độ nhất quán mà bạn cần, có hai cách tiếp cận chính để giải quyết vấn đề này. Đầu tiên là sử dụng IFeatureManagerSnapshot.

Duy trì tính nhất quán trong một yêu cầu với IFeatureManagerSnapshot

IFeatureManagerSnapshot được đăng ký với DI Container theo mặc định khi bạn sử dụng cờ tính năng và hoạt động như một bộ đệm theo yêu cầu cho cờ tính năng.

Nó bắt nguồn từ IFeatureManager, vì vậy bạn sử dụng nó theo cùng một cách. Bạn chỉ cần thay thế các instance của IFeatureManager bằng IFeatureManagerSnapshot:

@page
@model IndexModel
@inject Microsoft.FeatureManagement.IFeatureManagerSnapshot SnapshotManager
@{
    ViewData["Title"] = "Home page";
}

<ul>
@for (var i = 0; i < 10; i++)
{
    <li>Flag is: @SnapshotManager.IsEnabled(FeatureFlags.NewExperience)</li>
}
</ul>

Nếu bạn làm mới trang một vài lần, bạn sẽ thấy rằng trong một yêu cầu, tất cả các lệnh gọi IsEnabled() trả về cùng một giá trị. Tuy nhiên, giữa các yêu cầu, có 50% khả năng bạn sẽ được chuyển qua lại giữa kích hoạt và vô hiệu hóa:

Duy trì tính nhất quán trong một yêu cầu với IFeatureManagerSnapshot

Tùy thuộc vào những gì bạn đang sử dụng cờ tính năng của mình, điều này thể đủ cho nhu cầu của bạn. Cách tiếp cận này cũng có một lợi thế nếu bạn có bộ lọc tính năng đắt tiền để tính toán - sử dụng IFeatureManagerSnapshot để cache giá trị cho toàn bộ yêu cầu, thay vì đánh giá lại nhiều lần. Nói chung, tôi cảm thấy IFeatureManagerSnapshot sẽ là thứ mà bạn cần trong hầu hết các trường hợp.

Tuy nhiên, nó sẽ không giải quyết tất cả các vấn đề của bạn. Tôi tin rằng đối với hầu hết các ứng dụng, bạn sẽ muốn cờ tính năng tồn tại cho tất cả các yêu cầu của một người dùng nhất định, vì vậy người dùng không thấy các tính năng bị bật và tắt. Nếu đúng như vậy, bạn sẽ muốn xem qua ISessionManager.

Duy trì sự nhất quán giữa các yêu cầu với ISessionManager

ISessionManager giống như IFeatureManagerSnapshot nhưng bạn có thể lưu trữ dữ liệu ở bất cứ đâu bạn muốn. Một lựa chọn rõ ràng có thể là sử dụng tính năng ASP.NET Core Session để lưu trữ các kết quả cờ tính năng.

Điều này sẽ đảm bảo rằng khi người dùng kiểm tra cờ tính năng, kết quả (được bật hoặc tắt) vẫn tồn tại cho người dùng đó. Điều đó ngăn chặn vấn đề các tính năng bị "bật rồi tắt" được mô tả ở trên.

ISessionManager là một interface nhỏ để triển khai, chỉ bao gồm hai phương thức:

public interface ISessionManager
{
    void Set(string featureName, bool enabled);
    bool TryGet(string featureName, out bool enabled);
}

Phương thức Set() được gọi sau khi giá trị của cờ đặc trưng được xác định lần đầu tiên và được sử dụng để lưu trữ kết quả.

Phương thức TryGet() được gọi mỗi khi IFeatureManager.IsEnabled() được gọi, để kiểm tra xem một giá trị cho cờ tính năng đã được đặt trước đó hay chưa. Nếu có, TryGet() trả về true và enabled chứa kết quả cờ tính năng.

Ví dụ bên dưới cho thấy một triển khai của ISessionManager sử dụng ASP.NET Core Session để lưu trữ kết quả cờ tính năng:

public class SessionSessionManager : ISessionManager
{
    private readonly IHttpContextAccessor _contextAccessor;
    public SessionSessionManager(IHttpContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public void Set(string featureName, bool enabled)
    {
        var session = _contextAccessor.HttpContext.Session;
        var sessionKey = $"feature_{featureName}";
        session.Set(sessionKey, new[] {enabled ? (byte) 1 : (byte) 0});
    }

    public bool TryGet(string featureName, out bool enabled)
    {
        var session = _contextAccessor.HttpContext.Session;
        var sessionKey = $"feature_{featureName}";
        if (session.TryGetValue(sessionKey, out var enabledBytes))
        {
            enabled = enabledBytes[0] == 1;
            return true;
        }

        enabled = false;
        return false;
    }
}

Vì điều này đang sử dụng Session (sử dụng cookie để đặt Id Session), bạn cần truy cập vào HttpContext, vì vậy bạn cần sử dụng HttpContextAccessor.

Việc triển khai ở trên là một trình bao bọc rất mỏng xung quanh Session: bạn lưu trữ kết quả cờ tính năng dưới dạng 0 hoặc 1, sử dụng một khóa khác nhau cho mỗi bộ lọc và truy xuất giá trị bằng cách sử dụng cùng một khóa.

Để kích hoạt ISessionManager, bạn cần thêm nó vào DI Container. Bạn cũng cần thêm các service ASP.NET Core Session và middleware cho việc triển khai này.

Lưu ý rằng ISessionManager và bản thân thư viện FeatureManagement không phụ thuộc vào ASP.NET Core Session - nó chỉ bắt buộc vì tôi đã chọn sử dụng nó trong quá trình triển khai này.

Đăng ký ISessionManager và các dịch vụ phụ thuộc trong phương thức Startup.ConfigureServices():

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSession();
    services.AddHttpContextAccessor();
    services.AddTransient<ISessionManager, SessionSessionManager>();

    services.AddFeatureManagement()
        .AddFeatureFilter<PercentageFilter>();
}

Ngoài ra, hãy thêm middleware Session vào đường ống middleware trong Startup.Configure(), ngay trước middleware MVC:

public void Configure(IApplicationBuilder app)
{
    // ...
    app.UseSession();
    app.UseMvc();
}
Hãy nhớ rằng, ASP.NET Core Session sẽ trống cho đến sau khi middleware Session chạy, do đó, ISessionManager cũng sẽ trống cho đến khi đó. Nếu bạn sắp sử dụng quản lý tính năng trong đường ống middleware của mình, thì bạn cần thêm middleware Session vào trước.

Lặp lại cùng một thử nghiệm với ISessionManager, bạn sẽ nhận được cờ tính năng nhất quán trên tất cả các yêu cầu cho người dùng, cho đến khi phiên của họ kết thúc hoặc họ chuyển đổi trình duyệt.

Nếu bạn cần mức độ nhất quán thậm chí cao hơn (gắn với ID của người dùng thay vì phiên của họ, ví dụ: lưu trữ cờ tính năng trên trình duyệt), bạn có thể triển khai một ISessionManager khác.

Duy trì sự nhất quán giữa các yêu cầu với ISessionManager

Nhìn chung, ISessionManager rõ ràng là được thiết kế để giải quyết vấn đề về tính nhất quán, nhưng tôi nghĩ bạn sẽ cần phải cẩn thận cách sử dụng nó, vì cách triển khai cơ bản được hiển thị trong bài viết này có thể sẽ không triệt để.

Hạn chế của triển khai ISessionManger tùy chỉnh

Vấn đề chính với việc triển khai ISessionManager tùy chỉnh là nó lưu trữ các giá trị của tất cả các cờ tính năng trong Session. Điều đó thể ổn, nhưng nếu bạn có các bộ lọc tính năng khác thì gần như chắc chắn là không.

Lấy ví dụ TimeWindowFilter từ bài viết trước của tôi. Bộ lọc này sẽ trả về false trừ khi thời gian hiện tại nằm giữa các giá trị đã cấu hình. Chúng ta không muốn ISessionManager lưu giá trị vào bộ nhớ cache cho toàn bộ phiên, nếu không, bộ lọc tính năng có thể không bao giờ bật, ngay cả khi thời gian trôi qua!

Có một giải pháp cho điều này - hạn chế ISessionManager cache một tính năng duy nhất. Ví dụ:

public class NewExperienceSessionManager : ISessionManager
{
    public void Set(string featureName, bool enabled)
    {
        if(featureName != FeatureFlags.NewExperience) { return; }
        // ... implementation as before
    }

    public bool TryGet(string featureName, out bool enabled)
    {
        if(featureName != FeatureFlags.NewExperience) 
        { 
            enabled = false;
            return false;
        }
        // ... implementation as before
    }
}

Cách tiếp cận này về mặt kỹ thuật sẽ giải quyết vấn đề. Nhưng tại thời điểm này, bạn đang xem xét việc tạo một ISessionManager cho mỗi tính năng.

Điều đó có thể ổn; Tôi chưa quyết định. Cá nhân tôi nghĩ sẽ rất tuyệt nếu có thể cấu hình rõ ràng ISessionManager cho từng tính năng trong cấu hình, thay vì có thêm code chuyển hướng mà thiết kế hiện tại yêu cầu.

Lưu ý rằng bạn có thể đăng ký nhiều instance ISessionManager với DI Container và IFeatureManager sẽ gọi Set() TryGet() trên tất cả chúng, do đó, phương pháp tiếp cận một ISessionManager cho mỗi tính năng sẽ hoạt động tốt.

Một cách tiếp cận khả thi khác là tránh sử dụng ISessionManager hoàn toàn và thay vào đó tạo các bộ lọc tính năng trả về giá trị nhất quán dựa trên người dùng hiện tại.

Ví dụ: bạn có thể tạo một bộ lọc PercentageFilter tùy chỉnh sử dụng ID của người dùng hiện tại làm nguồn gốc cho trình tạo số ngẫu nhiên, do đó, cùng một ID người dùng luôn trả về cùng một giá trị. Điều này có những hạn chế riêng của nó, nhưng nó loại bỏ sự cần thiết của ISessionManager hoàn toàn.

Một hạn chế quan trọng khác áp dụng cho toàn bộ thư viện Microsoft.FeatureManagement là các API đều đồng bộ. Không có async/await nào được phép.

Đây không phải là vấn đề trong các thử nghiệm ban đầu của tôi với thư viện, nhưng tôi có thể dễ dàng hình dung ra một bộ lọc tính năng yêu cầu tra cứu cơ sở dữ liệu hoặc lệnh gọi HTTP - tất cả đều trở nên nguy hiểm khi bạn đang thực hiện đồng bộ hóa qua không đồng bộ.

Thư viện về cơ bản không được thiết kế để xử lý các tác vụ adhoc async như thế này, điều này hạn chế các phương pháp tiếp cận bạn có thể thực hiện.

Mọi tác vụ chậm hoặc không đồng bộ phải chạy ở chế độ nền và cập nhật cấu hình bên dưới mà bộ lọc tính năng sau đó có thể sử dụng.

Tóm lược

Trong bài viết này, tôi đã mô tả một số thách thức trong việc duy trì các giá trị nhất quán cho các cờ tính năng của bạn, đặc biệt là khi sử dụng bộ lọc PecentageFilter, điều này sẽ quyết định ngẫu nhiên xem cờ tính năng có được bật mỗi khi IsEnabled() được gọi hay không.

Tôi đã chỉ ra cách sử dụng IFeatureManagerSnapshot để duy trì tính nhất quán trong kết quả cờ tính năng trong một yêu cầu nhất định.

Tôi cũng đã giới thiệu ISessionManager có thể được sử dụng để lưu kết quả cờ tính năng vào bộ nhớ cache giữa các yêu cầu.

Triển khai ví dụ mà tôi đã cung cấp sử dụng ASP.NET Core Session để lưu kết quả vào bộ nhớ cache cho người dùng. Cách tiếp cận này có một số hạn chế cần lưu ý khi thực hiện trong các dự án của riêng bạn.

ASP.NET CoreASP.NET Core MVC.NET CoreLập Trình C#
Bài Viết Liên Quan:
Phần 6 - Các lựa chọn thay thế cho Microsoft.FeatureManagement
Trung Nguyen 13/03/2022
Phần 6 - Các lựa chọn thay thế cho Microsoft.FeatureManagement

Trong bài viết này, tôi giới thiệu sơ lược về một số lựa chọn thay thế cho thư viện Microsoft.FeatureManagement và mô tả sự khác biệt của chúng

Phần 4 - Tạo bộ lọc tính năng tùy chỉnh
Trung Nguyen 13/03/2022
Phần 4 - Tạo bộ lọc tính năng tùy chỉnh

Trong bài viết này, tôi sẽ chỉ cho bạn cách tạo bộ lọc tính năng tùy chỉnh của riêng mình bằng cách sử dụng IFeatureFilter trong ASP.NET Core.

Phần 3 - Tạo cờ tính năng động với bộ lọc tính năng
Trung Nguyen 12/03/2022
Phần 3 - Tạo cờ tính năng động với bộ lọc tính năng

Trong bài này, tôi sẽ hướng dẫn cách sử dụng hai bộ lọc PercentageFilter và TimeWindowFilter để tạo cờ tính năng động trong ứng dụng ASP.NET Core.

Phần 2 - Lọc các action method với cờ tính năng
Trung Nguyen 12/03/2022
Phần 2 - Lọc các action method với cờ tính năng

Trong bài này, tôi giới thiệu thư viện Microsoft.FeatureManagement.AspNetCore bổ sung các tính năng dành riêng cho ASP.NET Core để làm việc với cờ tính năng