這篇文章最初發表在 rollgate.io/blog/feature-flags-aspnet-core.
每一個 .NET團隊最終都會遇到同一個障礙:你有一個功能在測試環境中準備好了,但將其推送到生產環境意味著需要一次性為所有用戶切換開關。如果出現問題——錯誤假設、邊緣情況、性能回退——你唯一的恢復方法就是回滾並重新部署。
特性旗標透過將部署與發布分離來解決這個問題。您將程式碼背後加上旗標,然後從儀表板控制誰能看見它,而無需觸碰您的管線。從1%的使用者開始,觀察您的指標,擴展到100%。如果任何階段的錯誤突增,可在幾秒內禁用旗標.
快速啟動:.NET 8 中的特性旗標
從 NuGet 安裝 Rollgate SDK:
dotnet add package Rollgate.SDK
應用程式啟動時初始化客戶端:
using Rollgate.SDK;
var client = new RollgateClient(new RollgateConfig
{
ApiKey = Environment.GetEnvironmentVariable("ROLLGATE_API_KEY") ?? "",
});
await client.InitializeAsync();
if (client.IsEnabled("new-checkout", false))
{
Console.WriteLine("New checkout enabled");
}
client.Dispose();
在InitializeAsync()之後,每一個IsEnabled呼叫都從記憶體字典讀取 — 單位數微秒開銷.
註冊 Dependency Injection
在 ASP.NET Core 中,將客戶端註冊為單例,並加上一個小的 IFeatureFlags 抽象,以便控制項保持可測試:
// Program.cs
builder.Services.AddSingleton<RollgateClient>(sp =>
{
var client = new RollgateClient(new RollgateConfig
{
ApiKey = builder.Configuration["Rollgate:ApiKey"] ?? "",
RefreshInterval = TimeSpan.FromSeconds(30),
});
// Tutorial simplicity. In production, prefer IHostedService.
client.InitializeAsync().GetAwaiter().GetResult();
return client;
});
builder.Services.AddSingleton<IFeatureFlags, RollgateFeatureFlags>();
public interface IFeatureFlags
{
bool IsEnabled(string flagKey, bool defaultValue = false);
}
public sealed class RollgateFeatureFlags : IFeatureFlags
{
private readonly RollgateClient _client;
public RollgateFeatureFlags(RollgateClient client) => _client = client;
public bool IsEnabled(string key, bool def = false) => _client.IsEnabled(key, def);
}
ASP.NET Core 控制項中的功能旗標
注入 IFeatureFlags,而不是直接注入 SDK 類型:
[ApiController]
[Route("api/[controller]")]
public class CheckoutController : ControllerBase
{
private readonly IFeatureFlags _flags;
public CheckoutController(IFeatureFlags flags) => _flags = flags;
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] OrderRequest request)
{
return _flags.IsEnabled("checkout-v2", false)
? Ok(await ProcessV2Async(request))
: Ok(await ProcessV1Async(request));
}
}
識別用戶 — 每次會話,而不是每次請求
RollgateClient.IdentifyAsync發送一個HTTP請求並觸發旗標刷新。不要在每次請求上調用它 — 這會為每個終端點增加一個網絡往返,並摧毀內存中的評估模型。
正確的位置是一個動作過濾器,每個用戶只執行一次,然後短路:
public class FeatureFlagIdentityFilter : IAsyncActionFilter
{
private readonly RollgateClient _client;
private static readonly HashSet<string> _identified = new();
private static readonly SemaphoreSlim _gate = new(1, 1);
public FeatureFlagIdentityFilter(RollgateClient client) => _client = client;
public async Task OnActionExecutionAsync(ActionExecutingContext ctx, ActionExecutionDelegate next)
{
var userId = ctx.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (!string.IsNullOrEmpty(userId) && !_identified.Contains(userId))
{
await _gate.WaitAsync();
try
{
if (!_identified.Contains(userId))
{
await _client.IdentifyAsync(new UserContext { Id = userId });
_identified.Add(userId);
}
}
finally { _gate.Release(); }
}
await next();
}
}
生產環境的清理:在登入時呼叫IdentifyAsync一次,之後直到登出前都不再呼叫.
僅有 API 中的功能旗標
app.MapPost("/api/search", async (SearchRequest req, IFeatureFlags flags) =>
{
return flags.IsEnabled("semantic-search", false)
? Results.Ok(await RunSemanticSearchAsync(req.Query))
: Results.Ok(await RunKeywordSearchAsync(req.Query));
});
透過終點過濾器封鎖整條路徑,從請求服務中解析IFeatureFlags:
public static class FeatureFlagEndpointExtensions
{
public static TBuilder RequireFeature<TBuilder>(this TBuilder builder, string flagKey)
where TBuilder : IEndpointConventionBuilder
{
return builder.AddEndpointFilter(async (context, next) =>
{
var flags = context.HttpContext.RequestServices.GetRequiredService<IFeatureFlags>();
if (!flags.IsEnabled(flagKey, false)) return Results.NotFound();
return await next(context);
});
}
}
app.MapGet("/api/v2/analytics", GetAnalyticsV2Handler)
.RequireFeature("analytics-v2")
.RequireAuthorization();
Blazor Server 中的功能旗標
@page "/checkout"
@inject IFeatureFlags Flags
@inject AuthenticationStateProvider AuthStateProvider
@if (_showNewCheckout) { <NewCheckoutFlow /> } else { <LegacyCheckoutFlow /> }
@code {
private bool _showNewCheckout;
protected override void OnInitialized()
{
// Runs once per circuit, not per render.
_showNewCheckout = Flags.IsEnabled("checkout-v2", false);
}
}
對於 Blazor WebAssembly,在Program.cs之前從您的伺服器中擷取旗標RunAsync():
var host = builder.Build();
await host.Services.GetRequiredService<FlagService>().LoadAsync();
await host.RunAsync();
在 C 語言中測試功能旗標
由於控制器依賴於 IFeatureFlags,因此不需要模擬框架:
public class FakeFeatureFlags : IFeatureFlags
{
private readonly Dictionary<string, bool> _flags;
public FakeFeatureFlags(Dictionary<string, bool>? f = null) => _flags = f ?? new();
public bool IsEnabled(string key, bool def = false)
=> _flags.TryGetValue(key, out var v) ? v : def;
}
public class CheckoutControllerTests
{
[Fact]
public async Task Returns_V2_When_Flag_Enabled()
{
var flags = new FakeFeatureFlags(new() { ["checkout-v2"] = true });
var controller = new CheckoutController(flags);
var result = await controller.CreateOrder(new OrderRequest { Amount = 99 });
// assert v2 path
}
}
務必測試兩種旗標狀態.
逐步推出和用戶定位
識別時 (每個會話一次) 請傳入使用者屬性:
await _client.IdentifyAsync(new UserContext
{
Id = userId,
Email = userEmail,
Attributes = new Dictionary<string, object?>
{
["plan"] = user.SubscriptionPlan,
["country"] = user.Country,
}
});
在儀表板中設定百分比推出和屬性針對 — SDK 在本地評估所有規則,無需每個評估的 API 呼叫.
常見問題解答
2026年採用哪種 .NET 功能旗標方法?
- 簡單的開關,無需運行時變更 →
Microsoft.FeatureManagement - 中立CNCF SDK →
OpenFeature .NET - 儀表板 + 精準定位 + 漸進式發布 → 受管理服務 (Rollgate, LaunchDarkly 等)
它是否適用於 .NET 背景服務?
是。註冊 RollgateClient 為單例,注入至 BackgroundService,在 IsEnabled 中使用 ExecuteAsync。
如果 InitializeAsync 在啟動時失敗怎麼辦?
若沒有快取,就會拋出錯誤。您可以捕捉它並使用預設值繼續,或者讓它顯現出來(通常快速失敗比較安全)。
閱讀完整版本 — 包括斷路器配置、針對關閉開關的 SSE 流式傳輸,以及 Blazor WebAssembly 模式 — 在 rollgate.io/blog/feature-flags-aspnet-core.
在 https://app.rollgate.io/register 註冊免費的 Rollgate 帳號。 — 不需要信用卡。











