深度探索.NET Feature Management功能開關的魔法

董瑞鹏發表於2024-03-27

前言

.NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地新增、移除和管理功能。使用 Feature Management,開發人員可以根據不同使用者、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功能,並根據需要快速調整和部署新功能。 Feature Management 還提供了一些方便的工具和 API,幫助開發人員更輕鬆地實現功能管理和控制。

安裝

  • .Net CLI
dotnet add package Microsoft.FeatureManagement.AspNetCore --version 4.0.0-preview2
  • Package Manager
NuGet\Install-Package Microsoft.FeatureManagement.AspNetCore -Version 4.0.0-preview2

或者 Vs Nuget 包管理 管理工具安裝等

依賴注入

.Net 功能管理器是透過框架的本機配置系統配置的,簡單來說只要是.Net 的配置系統支援的資料來源都可以用做功能管理(FeatureManagement)的配置源

.NET 中的配置是使用一個或多個配置提供程式執行的。 配置提供程式使用各種配置源從鍵值對讀取配置資料:

  • 設定檔案,例如 appsettings.json
  • 環境變數
  • Azure Key Vault
  • Azure 應用配置
  • 命令列引數
  • 已安裝或已建立的自定義提供程式
  • 目錄檔案
  • 記憶體中的 .NET 物件
  • 第三方提供程式

.NET 中的配置提供程式

依賴注入:

service.AddFeatureManagement();

預設情況下,功能管理器從 .NET appsettings.json配置資料的 FeatureManagement Section 來獲取資料

  // Define feature flags in config file
  "FeatureManagement": {
    "sayHello": true, // On feature
    "todo": false // Off feature
  }

當然也可以自定義 Section

service.AddFeatureManagement(builder.Configuration.GetSection("CustomFeatureManagement"));
  // Define feature flags in config file
  "CustomFeatureManagement": {
    "sayHello": true, // On feature
    "todo": false // Off feature
  }

功能開關注冊成 Scoped

AddFeatureManagement 方法將特性管理服務作為單例新增到應用程式中,但有些情況下可能需要將特性管理服務新增為Scoped(作用域服務)。例如,我們可能希望使用 Scoped 以獲取上下文資訊的功能過濾器。在這種情況下,應該使用 AddScopedFeatureManagement 方法, 這將確保功能管理服務(包括功能過濾器)被新增為 Scoped 服務。

//功能管理註冊 Scoped 作用域
service.AddScopedFeatureManagement();

功能管理的基本形式是檢查功能標誌是否已啟用,然後根據結果執行操作。這透過 IFeatureManagerIsEnabledAsync 方法來實現。

對我們上面的 FeatureManager 的配置來做一個驗證

  • sayhello 功能開關標誌測試
app.MapGet("/sayHello", async Task<IResult> ([FromServices] IFeatureManager manager, string name) =>
{
    if (await manager.IsEnabledAsync("sayHello"))
    {
        return TypedResults.Ok($"hello {name}");
    }
    return TypedResults.NotFound();

}).WithSummary("sayHello");

呼叫介面檢視一下結果,在配置中我們的sayHello設定為true

image

狀態碼為 200,返回資訊"hello Ruipeng",符合預期,功能開啟正常。

  • todo 功能開關標誌測試
app.MapGet("/todo", async Task<IResult> ([FromServices] IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("todo"))
    {
        return TypedResults.Ok($"todo is enabled !");
    }
    return TypedResults.NotFound();

}).WithSummary("todo");

呼叫介面檢視一下結果,狀態碼 404,返回資訊 Not Found,符合預期,功能未開啟。

image

上面的示例簡單講解了一下功能開關的使用,接下來深入瞭解功能開關的配置

功能開關的定義

功能開關的標誌由兩部分組成:名稱和用於啟用功能的過濾器列表。

功能過濾器(Feature filters)定義了功能應何時啟用的場景。在評估特性是開啟還是關閉時,會遍歷其功能過濾器列表,直到其中一個過濾器決定啟用該特性。如果一個過濾器都沒有標識改功能應該開啟,那此功能標誌是關閉的狀態。

內建過濾器

  • AlwaysOn: 總是開啟
  • PercentageFilter:根據百分比隨機啟用/禁用功能。這個過濾器允許您基於一個百分比值來決定功能被啟用的機率,提供了一種簡單而靈活的機制來控制特性的曝光範圍。
  • TimeWindowFilter:在預定義的時間視窗內啟用特性。這個過濾器允許您指定特性的開始和結束時間,確保特性只在特定的時間段內可用。這對於限時活動或測試場景非常有用。
  • TargetingFilter:(這個主要是在Azure 用為目標受眾啟用功能的分階段推出針對特定使用者或使用者組啟用特性。這個過濾器允許您根據使用者屬性或標識來啟用特性,例如基於使用者 ID、角色、地區等。此外,對於此過濾器,您還可以設定一個百分比值,以進一步控制特性在目標使用者中的啟用機率。

詳細資訊可以參考註冊功能篩選器 Docs

過濾器的配置指南

需要注意的是在功能標誌名稱中禁止使用冒號:,這是為了遵循一定的命名規範,避免與現有的或未來的功能管理系統產生衝突或造成解析錯誤。在定義功能標誌名稱時,請確保使用合法和合適的字元組合,以確保系統的穩定性和可維護性。
功能使用 EnabledFor 屬性來定義它們的功能過濾器


AlwaysOn 過濾器

  // Define feature flags in config file
  "FeatureManagement": {
    //始終啟用該功能
    "featureAlwaysOn": {
      "EnabledFor": [
        {
          "Name": "AlwaysOn"
        }
      ]
    }
  }
app.MapGet("/featureAlwaysOn", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureAlwaysOn"))
    {
        return TypedResults.Ok($"featureAlwaysOn is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("featureAlwaysOn");

呼叫介面檢視測試結果,返回 200,符合預期

image


TimeWindow 過濾器

  "FeatureManagement": {
    "featureTimeWindow": {
      "EnabledFor": [
        {
          "Name": "TimeWindow",
          "Parameters": {
            "Start": "2024-03-26 13:30:00",
            "End": "2024-03-27 13:30:00"
          }
        }
      ]
    }
  }

指定了一個名為 TimeWindow 的功能過濾器。這是一個可配置的功能過濾,具有 Parameters 屬性,配置了功能活動的開始和結束時間 。

app.MapGet("/featureTimeWindow", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureTimeWindow"))
    {
        return TypedResults.Ok($"featureTimeWindow is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("TimeWindow 過濾器測試");

呼叫介面測試:返回 200 符合預期

image


Percentage 過濾器
百分比過濾器(Percentage Filter)它根據指定的百分比值隨機啟用或禁用某個特性。這種過濾器允許您控制特性的曝光率,以便在不同的使用者群體中測試特性的效果,或者在逐步推廣新特性時控制其影響範圍。

  "FeatureManagement": {
    "featurePercentage": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  },

app.MapGet("/featurePercentage", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featurePercentage"))
    {
        return TypedResults.Ok($"featurePercentage is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("Percentage 過濾器測試");

連續測兩次

第一次測試結果: 返回 200
image

第二次測試結果:返回 404
image

透過測試結果可以看出有百分之五十的機率成功,符合預期。

RequirementType

功能標誌的 RequirementType 屬性用於確定在評估功能狀態時,過濾器應該使用任何(Any)還是全部(All)邏輯。如果未指定 RequirementType,則預設值為 Any

  • Any 表示只需一個過濾器評估為 true,特性就會被啟用。
  • All 表示每個過濾器都必須評估為 true,特性才會被啟用。
    RequirementTypeAll 會改變遍歷方式。首先,如果沒有過濾器,則功能將被禁用。然後,遍歷特性過濾器,直到其中一個過濾器決定應將功能禁用。如果沒有過濾器指示應禁用功能,則該功能將被視為已啟用。
  "FeatureManagement": {
    "featureRequirementTypeAll": {
      "RequirementType": "All",
      "EnabledFor": [
        {
          "Name": "TimeWindow",
          "Parameters": {
            "Start": "2024-03-27 13:00:00",
            "End": "2024-05-01 13:00:00"
          }
        },
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  },
app.MapGet("/featureRequirementTypeAll", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureRequirementTypeAll"))
    {
        return TypedResults.Ok($"featureRequirementTypeAll is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("RequirementTypeAll 多過濾器測試");

上面的例項設定為 all 之後此功能標誌的過濾器列表必須全部符合要求才能呼叫成功。

比如上面我設定的開始日期是2024-03-27 13:00:00當前時間小於這個日期
image

無論呼叫幾次還是還是 404,結果符合我們的預期。

自定義過濾器

要實現一個功能過濾器,必須要實現的是一個IFeatureFilter介面,介面包含了一個EvaluateAsync的方法。當功能標誌指定啟用該過濾器時,將呼叫 EvaluateAsync方法,如果方法返回的是true,則表示應該啟用功能。

定義一箇中介軟體介面只對某個使用者組做開放,這個場景在 C 端的產品上比較常見,比如說部分功能的內測。

[FilterAlias("AuthenticatedGroup")]
public class AuthenticatedGroupFilter : IFeatureFilter, IFeatureFilterMetadata, IFilterParametersBinder
{
    public object BindParameters(IConfiguration parameters)
    {
        return parameters.Get<GroupSetting>() ?? new GroupSetting();
    }

    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext)
    {
        GroupSetting filterSettings = ((GroupSetting)featureFilterContext.Settings) ?? ((GroupSetting)BindParameters(featureFilterContext.Parameters));
        // 假設您有一個方法來檢查使用者是否已透過身份驗證
        // 例如,這可能是一個從身份驗證服務或中介軟體中獲得的屬性或方法
        bool isAuthenticated = IsGroupAuthenticated(filterSettings);
        return Task.FromResult(isAuthenticated);
    }


    private bool IsGroupAuthenticated(GroupSetting groupSetting)
    {
        // 在這裡編寫您的身份驗證檢查邏輯
        // 這可能涉及到檢查HTTP請求的上下文、會話狀態、令牌等
        // 具體的實現將取決於您使用的身份驗證機制

        // 示例:返回一個硬編碼的值,表示使用者是否已透過身份驗證
        // 在實際應用中,您應該實現實際的檢查邏輯
        return true; // 或者 false,取決於使用者是否已透過身份驗證
    }
}

FilterAlias是定義過濾器的別名,我們在配置檔案中指定時需要用別名,IFeatureFilter介面返回的資訊決定功能是否啟用,IFeatureFilterMetadata是一個空的標記介面,用於評估功能狀態的特徵過濾器的標記介面,IFilterParametersBinder 介面用於引數繫結。

  • json 配置
  "FeatureManagement": {
    "featureAuthencatedGroup": {
      "EnabledFor": [
        {
          "Name": "AuthenticatedGroup",
          "Parameters": {
            "Groups": [ "AdminGroup", "GroupOne" ]
          }
        }
      ]
    }
  }
  • 依賴注入
services.AddFeatureManagement()
    .AddFeatureFilter<AuthenticatedGroupFilter>();

呼叫 AddFeatureFilter 方法可把自定義的過濾器註冊到功能管理器中。

app.MapGet("/featureAuthencatedGroup", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureAuthencatedGroup"))
    {
        return TypedResults.Ok($"featureAuthencatedGroup is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("AuthencatedGroup 自定義過濾器測試");

測試一下,返回 200 ,符合預期
image

一個小 tips;如果多個過濾器有同一個別名是,可以用名稱空間加別名的方式來定義唯一一個過濾器,例如,Microsoft.Percentage 是一個完全限定的別名,它明確指出了 Percentage過濾器位於 Microsoft 名稱空間下

自定義開啟中介軟體

  "FeatureManagement": {
    "featureMiddleWare": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  }

自定義中介軟體

public class FeatureMiddleWare(RequestDelegate next)
{
    public async Task Invoke(HttpContext context)
    {
        Console.WriteLine("FeatureMiddleWare管道執行之前~");
        await next(context);
        Console.WriteLine("FeatureMiddleWare管道執行之後~");
    }
}

新增擴充套件方法

//測試中介軟體的功能開啟
app.UseMiddlewareForFeature<FeatureMiddleWare>("featureMiddleWare");

隨便呼叫一個介面測試一下,可以看到管道根據百分比觸發成功
image

透過上述呼叫,應用程式新增了一箇中介軟體元件,只有在特性“featureMiddleWare”被啟用時才會出現在請求管道中。如果在執行時啟用/禁用特性,中介軟體管道可以動態更改。

這是建立在基於特性對整個應用程式進行分支的更通用能力之上。

app.UseForFeature(featureName, appBuilder =>
{
appBuilder.UseMiddleware<T>();
});

MinimalApis 整合

在我們的 MVC 或者 Razor Pages 中有如下方案來啟用功能的開關,不過多介紹大家可以官方瀏覽學習。

FeatureManagement-Dotnet

services.AddMvc(o =>
{
    o.Filters.AddForFeature<SomeMvcFilter>("FeatureX");
});
[FeatureGate("FeatureX")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

MinimalAps 中可以利用 endpoint filter來簡化公功能的開關,

  • 第一步建立最小 Api 的基類,所有的 MinimalApis 過濾器都要繼承它
public abstract class FeatureFlagEndpointFilter(IFeatureManager featureManager) : IEndpointFilter
{
    protected abstract string FeatureFlag { get; }

    private readonly IFeatureManager _featureManager = featureManager;

    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var isEnabled = await _featureManager.IsEnabledAsync(FeatureFlag);
        if (!isEnabled)
        {
            return TypedResults.NotFound();
        }
        return await next(context);
    }
}
  • 定義目標 Json 配置
  "FeatureManagement": {
    "featureUserApi": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  • 定義最小 Api 過濾器
public class UserApiFeatureFilter(IFeatureManager featureManager) : FeatureFlagEndpointFilter(featureManager)
{
    protected override string FeatureFlag => "featureUserApi";
}

  • 定義 Api 介面測試
//最小Api分組功能新增
{
    var userGroup = app.MapGroup("User").WithTags("User").AddEndpointFilter<UserApiFeatureFilter>(); ;

    userGroup.MapGet("/featureUserApi", IResult (IFeatureManager manager) =>
    {
        return TypedResults.Ok($"featureUserApi is enabled !");

    }).WithSummary("featureUserApi 最小Api過濾器測試");
}

呼叫測試,可以看出我們配置的百分比過濾器成功。

image

透過對 IEndpointFilter 的封裝藉助最小 ApiMapGroup 可以對一組相關的 Api 進行功能管理,簡化了我們一個個 Api 註冊。

最後

在本文中,我們深入探討了.NET Feature Management 庫的安裝、配置和使用方法,以及如何利用功能開關來動態管理應用程式的功能。以下是關鍵點的總結和提煉:

  • 安裝與依賴注入:透過.NET CLINuGet Package Manager 安裝等方式 Microsoft.FeatureManagement.AspNetCore 庫,並在應用程式中新增功能管理服務的依賴注入。

  • 功能定義與配置:透過.NET 的配置系統,在 appsettings.json 中定義功能標誌,指定功能的啟用和禁用狀態,以及可選的功能過濾器配置。

  • 自定義功能過濾器:實現 IFeatureFilter 介面來定義自定義功能過濾器,根據特定條件決定功能是否啟用,例如基於使用者組、時間視窗或百分比等條件。

  • 功能開關的使用:利用 IFeatureManagerIsEnabledAsync 方法檢查功能是否啟用,根據不同的功能狀態執行相應的邏輯,實現功能的動態控制。

  • RequirementType 設定:可以透過 RequirementType 屬性指定功能過濾器的邏輯要求,是 Any 還是 All,決定多個過濾器的組合邏輯。

  • 自定義中介軟體的動態切換:透過自定義功能過濾器和中介軟體,可以根據功能狀態動態調整請求管道,實現功能開關對中介軟體的控制。

  • 最小 API 整合:在 Minimal APIs 中,利用 IEndpointFilter 介面來簡化功能開關的應用,將功能管理應用到最小 API 的端點上,實現對一組相關 API 的功能管理。

透過以上總結和提煉,您可以更好地瞭解和應用.NET Feature Management 庫,實現靈活的功能管理和動態控制應用程式的功能。

有條件的富哥可以體驗一下在 Azure 應用程式配置中管理功能標誌

更多詳細的內容請瀏覽FeatureManagement-Dotnet

本文測試完整原始碼

相關文章