Feature Flags – co to i po co?
Wstęp
Czasem bywa tak, że musimy wyłączyć pewne funkcje w aplikacji. Zazwyczaj dlatego, że nie są jeszcze gotowe/przetestowane w 100% i trudno by było opublikować aplikację bez nich.
W dzisiejszym artykule opowiem, jak podejść do tematu.
Czym są Feature flags?
To specjalny rodzaj flagi mówiącej o tym, czy dana funkcja może być używana. Najprostszą taką flagą będzie dyrektywa kompilatora, np:
#if IMPORT_DB
DbImporter importer = new DbImporter();
importer.Import();
#endif
Jeśli zdefiniowaliśmy flagę IMPORT_DB
, wtedy ten kod się wykona. Tak samo jak wszystkie inne, które będą opatrzone dyrektywami #if..#endif
. O ile dość łatwo to ogarnąć np. w C++, to w C# jest już ciężej z tego powodu, że każdy projekt ma swój własny zestaw definów. I wtedy trzeba pamiętać, żeby do każdego projektu dołączyć plik z tymi feature’ami.
Innym problemem może być (ale nie musi) to, że bez ponownej kompilacji nie odblokujemy funkcji programu. Czasem jest to pożądanie, czasem nie.
Dodatkowo, jakby nie patrzeć, takie dyrektywy zaciemniają w pewien sposób kod.
.NetCore ma jednak sprytny mechanizm do zarządzania flagami funkcji. Nazywa się to FeatureManager i za chwilę Ci go przedstawię.
Instalacja FeatureManager
Najpierw musisz pobrać sobie NuGet: Microsoft.FeatureManagement.AspNetCore
Teraz wystarczy już tylko zarejestrować serwisy z tej biblioteki. Robimy to oczywiście podczas rejestrowania wszystkich innych serwisów.
builder.Services.AddFeatureManagement();
Jeśli się przyjrzysz, to zobaczysz, że AddFeatureManagement
ma dwie wersje. W drugiej możesz przekazać całą sekcję w konfiguracji, w której wyłączasz lub włączasz poszczególne funkcje (domyślnie są odczytywane z sekcji FeatureManagement).
Domyślne działanie jest takie, że FeatureManager odczytuje sobie poszczególne funkcje z appSettings.json
z sekcji „FeatureManagement„. Oczywiście odczytuje to dokładnie tak samo jak wszystkie inne opcje programu. Czyli najpierw appSettings.json, appSettings.{Environment}.json, zmienne środowiskowe itd. Jeśli nie znasz tematu dokładnie, koniecznie przeczytaj ten artykuł (konfiguracja i opcje programu).
Tworzenie flag
Zrobimy sobie przykładowy projekt, który pokazuje działanie flag – symulator telewizora. Tym razem będzie to projekt MVC, żeby móc pokazać więcej rzeczy. Przykład możesz pobrać sobie z GitHuba.
Najpierw zatroszczmy się o flagi. Do appSettings
dodaj taką sekcję:
"FeatureManagement": {
"PowerControl": "true",
"ChannelControl": "false",
"VolumeControl": "false"
}
Zwróć uwagę, że sekcja nazywa się FeatureManagement. Tak jak już mówiłem, to z niej domyślnie są odczytywane wartości flag.
Zdefiniowaliśmy tutaj trzy flagi:
- PowerControl – użytkownik może włączyć i wyłączyć telewizor
- ChannelControl – użytkownik może przełączać kanały. Jak widzisz, w tym momencie flaga jest wyłączona, czyli pozbawiamy użytkownika tej opcji
- VolumeControl – użytkownik może zmieniać głośność. Teraz też go pozbawiamy tej opcji.
Oczywiście będziemy musieli się posługiwać nazwami tych flag później w kodzie. Dlatego też powinniśmy je wyekstrahować albo do jakiś stałych, albo do jakiegoś enuma. Ja wybrałem stałe. Utwórz osobny plik do tego:
public static class FeatureFlags
{
public const string PowerControl = "PowerControl";
public const string ChannelControl = "ChannelControl";
public const string VolumeControl = "VolumeControl";
}
Kontrola funkcji
Oczywiście nie można napisać mechanizmu, który automagicznie wyłączy lub włączy poszczególne funkcje. To musimy zrobić samemu. Możemy to zrobić na dwa sposoby. Spójrz na ten kod w widoku:
@using Microsoft.FeatureManagement
@inject IFeatureManager FeatureManager
<h1>SUPER TV!</h1>
<hr />
<h2>Pilot</h2>
<hr />
@if (await FeatureManager.IsEnabledAsync(FeatureFlags.PowerControl))
{
<div class="row mb-2">
<button class="btn btn-primary col-3 me-2">TV ON</button>
<button class="btn btn-secondary col-3">TV OFF</button>
</div>
}
Na początku wstrzykujemy IFeatureManager
. Następnie sprawdzamy, czy konkretna flaga została włączona, używając metody IsEnabledAsync
. W jej argumencie przekazujemy nazwę flagi.
Jeśli flaga jest włączona, pokazujemy dla niej funkcjonalność. Analogicznie teraz możemy zrobić dla pozostałych flag:
@using Microsoft.FeatureManagement
@inject IFeatureManager FeatureManager
<h1>SUPER TV!</h1>
<hr />
<h2>Pilot</h2>
<hr />
@if (await FeatureManager.IsEnabledAsync(FeatureFlags.PowerControl))
{
<div class="row mb-2">
<button class="btn btn-primary col-3 me-2">TV ON</button>
<button class="btn btn-secondary col-3">TV OFF</button>
</div>
}
@if(await FeatureManager.IsEnabledAsync(FeatureFlags.ChannelControl))
{
<div class="row mb-2">
<button class="btn btn-info col-2 me-1"><strong>+</strong></button>
<span class="col-2 my-auto text-center me-1">CHANNEL</span>
<button class="btn btn-info col-2"><strong>-</strong></button>
</div>
}
@if(await FeatureManager.IsEnabledAsync(FeatureFlags.VolumeControl))
{
<div class="row mb-2">
<button class="btn btn-outline-info col-2 me-1"><strong>+</strong></button>
<span class="col-2 my-auto text-center me-1">VOLUME</span>
<button class="btn btn-outline-info col-2"><strong>-</strong></button>
</div>
}
Interfejs IFeatureManager możesz wstrzyknąć do dowolnej klasy i używać go też na backendzie.
Teraz dodajmy jakieś działanie do tych przycisków. Żeby to zrobić, umieścimy je wszystkie w formularzu, a każdy guzik będzie odnosił na inną końcówkę. Całość będzie wyglądała mniej więcej tak (fragmenty usunąłem dla lepszej czytelności):
<form method="post">
@if (await FeatureManager.IsEnabledAsync(FeatureFlag.PowerControl.ToString()))
{
<div class="row mb-2">
<button class="btn btn-primary col-3 me-2" asp-action="TvOn">TV ON</button>
<button class="btn btn-secondary col-3" asp-action="TvOff">TV OFF</button>
</div>
}
//i dalej sprawdzenie innych flag
</form>
Zabezpieczanie back-endu
Jeśli na froncie nie ma konkretnej funkcji, nie znaczy że nie można jej wywołać na backendzie. Spójrz na ten kod:
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
public IActionResult TvOn()
{
return View("Index");
}
[HttpPost]
public IActionResult VolumeUp()
{
return View("Index");
}
Jeśli teraz w jakiś sposób wywołamy końcówkę VolumeUp
, no to stanie się coś złego. Więc z tej strony też powinniśmy się przed tym zabezpieczyć. FeatureManager daje nam bardzo fajny atrybut do tego FeatureGate
:
[HttpPost]
[FeatureGate(FeatureFlags.VolumeControl)]
public IActionResult VolumeUp()
{
return View("Index");
}
Jeśli teraz spróbujemy wywołać tę końcówkę, dostaniemy błąd 404 – strony nie znaleziono.
Za takie działanie jest odpowiedzialna domyślna implementacja interfejsu IDisabledFeaturesHandler
. Oczywiście możesz sobie ją zmienić tak jak chcesz.
UWAGA! FeatureGate nie działa a RazorPages.
TagHelper
Jeśli nie podoba Ci się ta ifologia w widoku i widzisz tutaj szansę na użycie TagHelpers, dobra wiadomość jest taka, że Microsoft zrobił już to za Ciebie.
Spójrz jeszcze raz na kod widoku:
@if (await FeatureManager.IsEnabledAsync(FeatureFlags.PowerControl))
{
<div class="row mb-2">
<button class="btn btn-primary col-3 me-2" asp-action="TvOn">TV ON</button>
<button class="btn btn-secondary col-3" asp-action="TvOff">TV OFF</button>
</div>
}
@if(await FeatureManager.IsEnabledAsync(FeatureFlags.ChannelControl))
{
<div class="row mb-2">
<button class="btn btn-info col-2 me-1" asp-action="ChannelUp"><strong>+</strong></button>
<span class="col-2 my-auto text-center me-1">CHANNEL</span>
<button class="btn btn-info col-2" asp-action="ChannelDown"><strong>-</strong></button>
</div>
}
@if(await FeatureManager.IsEnabledAsync(FeatureFlags.VolumeControl))
{
<div class="row mb-2">
<button class="btn btn-outline-info col-2 me-1" asp-action="VolumeUp"><strong>+</strong></button>
<span class="col-2 my-auto text-center me-1">VOLUME</span>
<button class="btn btn-outline-info col-2" asp-action="VolumeDown"><strong>-</strong></button>
</div>
}
Tego brzydala można zamienić na TagHelpery
. I to jest drugi sposób ogarnięcia featerów na froncie.
Najpierw do _ViewImports.cshtml
dodaj:
@addTagHelper *, Microsoft.FeatureManagement.AspNetCore
Teraz już możesz używać tag helpera feature
:
<form method="post">
<feature name="@FeatureFlags.PowerControl">
<div class="row mb-2">
<button class="btn btn-primary col-3 me-2" asp-action="TvOn">TV ON</button>
<button class="btn btn-secondary col-3" asp-action="TvOff">TV OFF</button>
</div>
</feature>
<feature name="@FeatureFlags.ChannelControl">
<div class="row mb-2">
<button class="btn btn-info col-2 me-1" asp-action="ChannelUp"><strong>+</strong></button>
<span class="col-2 my-auto text-center me-1">CHANNEL</span>
<button class="btn btn-info col-2" asp-action="ChannelDown"><strong>-</strong></button>
</div>
</feature>
<feature name="@FeatureFlags.VolumeControl">
<div class="row mb-2">
<button class="btn btn-outline-info col-2 me-1" asp-action="VolumeUp"><strong>+</strong></button>
<span class="col-2 my-auto text-center me-1">VOLUME</span>
<button class="btn btn-outline-info col-2" asp-action="VolumeDown"><strong>-</strong></button>
</div>
</feature>
</form>
Przyznasz, że to wygląda zdecydowanie lepiej.
Co więcej, tag helpery dają Ci więcej możliwości niż tylko takie proste działanie. Możesz pokazać fragment, który będzie się pojawiał jeśli flaga będzie wyłączona, np:
<feature name="@FeatureFlags.ChannelControl">
<div class="row mb-2">
<button class="btn btn-info col-2 me-1" asp-action="ChannelUp"><strong>+</strong></button>
<span class="col-2 my-auto text-center me-1">CHANNEL</span>
<button class="btn btn-info col-2" asp-action="ChannelDown"><strong>-</strong></button>
</div>
</feature>
<feature name="@FeatureFlags.ChannelControl" negate="true">
<p>Zmiana kanałów będzie możliwa w przyszłości</p>
</feature>
Możesz też chcieć, żeby fragment kodu był widoczny tylko jeśli wszystkie lub kilka flag są włączone. Bardzo proszę:
<feature name="ChannelControl, VolumeControl" requirement="Any">
<p>ChannelControl lub ValumeControl jest aktywne</p>
</feature>
<feature name="ChannelControl, VolumeControl" requirement="All">
<p>ChannelControl i ValumeControl są aktywne</p>
</feature>
Wystarczy dodać nazwy tych flag do atrybutu name
i posłużyć się atrybutem requirement
. On może mieć dwie wartości – Any
– jedna z flag musi być włączona; All
– wszystkie flagi muszą być włączone.
Filtry i middleware
Jeśli używasz jakiegoś filtru (IAsyncActionFilter
), który ma działać tylko gdy funkcja jest dostępna, możesz to zrobić w konfiguracji.
Dodaj ten filtr w nieco inny sposób niż standardowy:
builder.Services.AddControllersWithViews(o =>
{
o.Filters.AddForFeature<VolumeFilter>(FeatureFlags.VolumeControl);
});
Zwróć uwagę, że nie rejestruję tutaj filtru z użyciem metody Add
, tylko AddForFeature
. W parametrze generycznym podaję typ filtru, a w środku nazwę flagi, z którą ten filtr ma być powiązany. W takim wypadku filtr zostanie odpalony tylko wtedy, jeśli flaga VolumeControl
jest włączona.
Analogicznie można postąpić z middleware. Jeśli masz middleware, który ma być zależny od flagi, wystarczy że dodasz go w taki sposób zamiast standardowego:
app.UseMiddlewareForFeature<ChannelMiddleware>(FeatureFlags.ChannelControl);
To tyle jeśli chodzi o podstawy mechanizmu FeatureManager. To świetnie można połączyć z ustawieniami aplikacji na Azure – wtedy domyślnie stan flag odświeża się co 30 sekund. Ale to jest temat na inny artykuł, który powstanie.
Teraz dziękuję Ci za przeczytanie tego tekstu. Jeśli czegoś nie zrozumiałeś lub znalazłeś jakiś błąd, koniecznie daj znać w komentarzu.
Obrazek wyróżniający: Technologia zdjęcie utworzone przez pvproductions – pl.freepik.com