Снова привет, Хабр!
В первой части статьи мы разбирали, что такое изоляция микросервисов, как в этом помогают переключатели функциональности, и как создать простое ASP.NET приложение с поддержкой feature toggles, которое будет показывать прогноз погоды. В этой части закончим работу — напишем заглушку, сделаем экспериментальную конечную точку, функциональность которой можно включать или выключать, не останавливая работу приложения, и разберёмся с экстренными ситуациями, которые могут возникнуть при разработке приложения.
Дисклеймер: в статье много тяжёлых иллюстраций, берегите мобильный интернет.
Шаг 1. Пишем заглушку
Для написания заглушки понадобится реализовать интерфейс IWeatherForecastService
. Используем сгенерированный код контроллера WeatherForecastController
из первой части статьи и поместим его в соответствующий класс. Затем дорабатываем до следующего вида:
public class OpenWeatherServiceStub : IWeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public Task<IEnumerable<WeatherForecastResponse>> GetByIdAsync(string cityId)
{
return GenerateAsync();
}
private Task<IEnumerable<WeatherForecastResponse>> GenerateAsync()
{
var rng = new Random();
var result = Enumerable.Range(1, 5).Select(index => new WeatherForecastResponse
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
});
return Task.FromResult(result);
}
}
Шаг 2. Добавляем флаги функциональности
Некоторые разработчики предлагают использовать специальный класс с объявлением наименований флагов функциональностей. В нашем проекте этот класс будет выглядеть очень просто:
public static class FeatureFlags
{
/// <summary>
/// Использовать заглушку для внешней зависимости OpenWeather.
/// </summary>
public const string UseOpenWeatherStub = nameof(UseOpenWeatherStub);
}
Объявление констант сильно упрощает работу:
помогает однообразно обращаться к флагам функциональности
упростит рефакторинг и поиск использования
помогает видеть документирующие комментарии в IDE
сократит вероятность ошибочного написания строковых наименований флагов до нуля.
Выражу на этот счет своё мнение. Отдельный класс для объявления флагов функциональности нужен только если наименования флагов упоминаются в коде более одного раза. Если упоминание будет единичным, при конфигурировании приложения в классе Startup, использование специального класса можно считать избыточным.
Для использования флага требуется завести соответствующий раздел в файле конфигурации приложения appsettings.json
:
{
"FeatureManagement": {
// Использовать заглушку для внешней зависимости OpenWeather.
"UseOpenWeatherStub": false,
},
// ...
}
Раздел FeatureManagement
лучше располагать первым в файле конфигурации, чтобы любой заинтересованный начал знакомиться с файлом именно с этого раздела.
Я также рекомендую копировать комментарии, сопровождающие флаг, чтобы облегчить понимание и назначение каждого конфигурационного значения.
Шаг 3. Подключаем Microsoft.FeatureManagement
На данном этапе следует подключить в проект NuGet-пакет Microsoft.FeatureManagement любым из этих двух способов.
С использованием dotnet CLI:
dotnet add package Microsoft.FeatureManagement
dotnet add package Microsoft.FeatureManagement.AspNetCore
или используя диспетчер пакетов Visual Studio:
Install-Package Microsoft.FeatureManagement
Install-Package Microsoft.FeatureManagement.AspNetCore
В Startup.cs
добавляется соответствующая регистрация:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddFeatureManagement();
// ...
}
Шаг 4. Регистрируем заглушку
Для удобства подключения заглушек будем использовать методы расширения AddTransientFeature
, AddScopedFeature
, AddSingletonFeature
для упрощения их регистрации.
Главный момент в реализации этих функций — регистрация дескриптора, содержащего метод создания экземпляра:
var descriptor = new ServiceDescriptor(
typeof(TService),
serviceProvider =>
{
var settings = serviceProvider.GetService<IImplementationSwitchSettings<TService>>();
var featureManagerSnapshot = serviceProvider.GetRequiredService<TFeatureManager>();
var task = featureManagerSnapshot.IsEnabledAsync(settings.FeatureName);
bool result = task.GetAwaiter().GetResult();
if (result)
return serviceProvider.GetRequiredService<TFeatureImplementationOn>();
else
return serviceProvider.GetRequiredService<TFeatureImplementationOff>();
},
lifetime);
collection.Add(descriptor);
Узким местом такой реализации является получение асинхронного результата. Здесь для качественного функционирования ответственность перекладывается на реализацию провайдера конфигурации. Его нужно реализовывать так, чтобы задержки для получения значений были либо минимальными (например, из кэша), или приводили к возврату значений в синхронном режиме, посредством Task.FromResult.
У этих методов расширений есть ряд преимуществ.
Наглядность. Учитывая семантику метода, легко понять назначение самодокументируемой регистрации:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddScopedFeature<IWeatherForecastService, OpenWeatherServiceStub, OpenWeatherService>(FeatureFlags.UseOpenWeatherStub);
// ...
}
Легкая замена обычной регистрации, которая обычно выглядит так:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddScoped<IWeatherForecastService, OpenWeatherService>();
// ...
}
Методы расширений позволяют сосредоточить в Startup.cs
использование всех флагов функциональности, а не размазывать логику их использования по всему решению, что обычно приводит к массе проблем.
Это удобно: класс Startup
— это, пожалуй, второй файл, после appsettings.json
, который исследует любой, кто знакомится с проектом, а значит мимо его внимания не пройдет факт использования флагов функциональности.
Как я уже показывал, легко будет избавиться от ставших ненужными флагов, удаляя код заглушек и заменяя соответствующие методы расширений на их штатные аналоги: AddTransientFeature
— AddTransient
, AddScopedFeature
— AddScoped
, AddSingletonFeature
— AddSingleton
.
Важно! После регистрации методами группы ...Feature
необходимо заменить регистрацию HTTP-клиента с такого варианта:
services.AddHttpClient<IWeatherForecastService, OpenWeatherService>( //...
на такой:
services.AddHttpClient<OpenWeatherService>( //...
Так происходит, потому что с помощью дескриптора разрешается не интерфейс, а класс, который его реализует.
Метод
AddSingletonFeature
на самом деле не выполняет поставленных задач и не переключает функциональность во время работы приложения после его первого разрашения. Это связано с тем, что создание экземпляра происходит лишь один раз. Тем не менее, я решил добавить его «для комплекта». Возможно он тоже найдет своё применение в виду его единообразного использования наряду сAddTransientFeature
иAddScopedFeature
.
Названные методы расширений AddTransientFeature
, AddScopedFeature
и AddSingletonFeature
я выделил в NuGet-пакет FeatureManagement.ImplementationSwitchExtensions
.
Команда подключения NuGet-пакета в проект с использованием dotnet CLI:
dotnet add package FeatureManagement.ImplementationSwitchExtensions
или используя диспетчер пакетов Visual Studio:
Install-Package FeatureManagement.ImplementationSwitchExtensions
Шаг 5. Используем заглушку
Теперь, реализация готова, можно воспользоваться заглушкой. Для этого нужно установить значение true
для UseOpenWeatherStub
в конфигурационном файле.
{
"FeatureManagement": {
// Использовать заглушку для внешней зависимости OpenWeather.
"UseOpenWeatherStub": true,
},
// ...
}
Управление секретами пользователей
Я настоятельно рекомендую разработчикам не изменять значения конфигурационного файла appsettings.json
.
Все значения в нём должны оставаться в исходном виде, пригодном для промышленной эксплуатации, кроме особых значений, которые не должны попадать в репозиторий (пароли и другие «секреты»).
Разработчикам рекомендую использовать файл секретов, в который следует продублировать нужный раздел конфигурации и только там изменять значения. Таким образом любые эксперименты с конфигурацией никогда не покинут компьютер разработчика и не попадут, даже случайно, в кодовую базу репозитория.
В результате видим, что вместо реального обращения к удалённому сервису используется заглушка:
Шаг 6. Создаём экспериментальную конечную точку
Создадим экспериментальную конечную точку веб API, позволяющую получить прогноз погоды по наименованию города.
С помощью Microsoft.FeatureManagement
можно ограничивать доступ самыми разными способами, но мы рассмотрим самую простую настройку: включено/выключено. К тому же, отличия этих подходов будут закладываться главным образом в конфигурационном файле и никак не повлияют на реализацию.
Добавляем флаг функциональности
Добавляем флаг в класс:
public static class FeatureFlags
{
// ...
/// <summary>
/// Использовать конечную точку 'by-name'.
/// </summary>
public const string UseOpenWeatherByCityName = nameof(UseOpenWeatherByCityName);
}
Указываем флаг в appsettings.json
:
"FeatureManagement": {
// ...
// Использовать конечную точку 'by-name'.
"UseOpenWeatherByCityName": false
},
Добавляем метод сервиса
Добавляем в интерфейс метод, который примет такой окончательный вид:
public interface IWeatherForecastService
{
public Task<IEnumerable<WeatherForecastResponse>> GetByIdAsync(string cityId);
public Task<IEnumerable<WeatherForecastResponse>> GetByNameAsync(string cityName);
}
Реализация вызова удалённого сервиса очень проста:
public class OpenWeatherService : IWeatherForecastService
{
// ...
public Task<IEnumerable<WeatherForecastResponse>> GetByNameAsync(string cityName)
{
string path = $"forecast?q={cityName}&appid={_apiKey}&units=metric&cnt=5&lang=ru";
return GetByPathAsync(path);
}
}
Так же просто дополняем класс заглушки:
public class OpenWeatherServiceStub : IWeatherForecastService
{
public Task<IEnumerable<WeatherForecastResponse>> GetByNameAsync(string cityName)
{
return GenerateAsync();
}
}
Добавляем метод действия в контроллер
При добавлении нового метода действия добавляем к нему атрибут FeatureGateAttribute
с указанием наименования флага FeatureFlags.UseOpenWeatherByCityName
.
Для использования указанного атрибута потребуется зависимость от NuGet-пакета Microsoft.FeatureManagement.AspNetCore
.
[Route("weather-forecast")]
public class WeatherForecastController
{
// ...
[FeatureGate(FeatureFlags.UseOpenWeatherByCityName)]
[HttpGet("by-name")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<WeatherForecastResponse>))]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<IEnumerable<WeatherForecastResponse>> GetByName(
[Required] string cityName,
[FromServices] IWeatherForecastService weatherForecastService)
{
var result = await weatherForecastService.GetByNameAsync(cityName);
return result;
}
}
В результате получили экспериментальную конечную точку, функциональность которой можно включать или выключать, не останавливая работу приложения.
Единственным нюансом будет не очень корректный ответ по умолчанию с кодом 404 Not Found
.
Исправим это. Понадобится например такая реализация интерфейса IDisabledFeaturesHandler
:
public class RedirectDisabledFeatureHandler : IDisabledFeaturesHandler
{
public Task HandleDisabledFeatures(IEnumerable<string> features, ActionExecutingContext context)
{
context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); // Generate a 403.
return Task.CompletedTask;
}
}
Кратко дополним регистрацию в виде вызова UseDisabledFeaturesHandler
:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddFeatureManagement()
.UseDisabledFeaturesHandler(new RedirectDisabledFeatureHandler());
// ...
}
Вот и готово. Можно экспериментировать:
Feature toggles для экстренных мер
В большинстве случаев авторы статей по теме упоминают лишь популярные и «правильные» решения. Но в реальном программировании мы сталкиваемся и с непростыми ситуациями. Давайте рассмотрим одну из них и поймём, что с этим можно сделать.
Проблема с зависимостями при запуске приложения
В реальных проектах могут быть зависимости, которые мешают отладить код «здесь и сейчас». Для воссоздания такой ситуации добавим контроллеру атрибут AuthorizeAttribute
:
[Authorize(Policy = "Something")]
[Route("weather-forecast")]
public class WeatherForecastController
{
// ...
}
Соответственно реализация в Startup.cs
была бы похожей на эту:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
services.AddAuthorization(
options =>
options.AddPolicy(
"Something",
policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything");
}));
}
и далее:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
app.UseAuthentication();
app.UseAuthorization();
// ...
}
В этом примере не так важно, как в приложении реализуется авторизация пользователя и заполняются его права на доступ, способ может быть произвольным. Суть в том, что теперь при попытке вызова будет возвращаться статус 401 - Unauthorized
:
Используем feature toggles на старте
Я продемонстрирую, как можно преодолевать подобные проблемы с зависимостями на старте приложения.
Шаг 1 — добавление флага:
public static class FeatureFlags
{
//...
/// <summary>
/// При запуске сервиса: подавлять авторизацию.
/// </summary>
public const string OnStart_SuppressAuth = nameof(OnStart_SuppressAuth);
}
Теперь в добавим пару строк в appsettings.json
. В дальнейшем мы увидим на примере, как работает подавление авторизации.
{
"FeatureManagement": {
// ...
// При запуске сервиса: подавлять авторизацию.
"OnStart_SuppressAuth": false
},
//...
}
Особо хочу подчеркнуть, что нужно явно показывать в имени флага то, что он работает только на старте приложения, без возможности «горячего» переключения во время работы сервиса.
Использование флага функциональности в методе ConfigureServices
класса Startup
:
Код
public void ConfigureServices(IServiceCollection services)
{
// ...
bool suppressAuth = Configuration.GetValue<bool>($"FeatureManagement:{FeatureFlags.OnStart_SuppressAuth}");
if (suppressAuth)
services.AddRouting(r => r.SuppressCheckForUnhandledSecurityMetadata = true);
else
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
services.AddAuthorization(
options =>
options.AddPolicy(
"Something",
policy =>
{
policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim("Permission", "CanViewPage", "CanViewAnything");
}));
}
// ...
}
Использование флага функциональности в методе Configure
класса Startup
:
Код
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IFeatureManager featureManager)
{
//...
bool suppressAuth = featureManager.IsEnabledAsync(FeatureFlags.OnStart_SuppressAuth).GetAwaiter().GetResult();
if (!suppressAuth)
{
app.UseAuthentication();
app.UseAuthorization();
}
//...
}
Включение флага функциональности, работающего на старте приложения
Теперь приложение получило возможность запускаться с отключенными зависимостями или функционалом — для этого достаточно управлять значением флага FeatureFlags.OnStart_SuppressAuth
.
Флаги могут помочь быстро выключить что-то, что мешает текущей разработке или тестированию.
Еще раз подчеркну: пример из этого раздела предназначен для использования в качестве экстренной меры. Не следует применять этот подход повсеместно, поскольку он противоречит главному посылу — переключать функциональность без перезапуска приложения.
Шаг 7. Готовим и настраиваем релиз
Чтобы убедиться, что все работает, как было задумано, подготовим релиз приложения.
Переходим в папку файлов релиза и открываем файл appsettings.json
.
Настройка исходной конфигурации
Исходное состояние appsettings.json
должно получиться таким:
appsettings.json
{
"FeatureManagement": {
// Использовать заглушку для внешней зависимости OpenWeather.
"UseOpenWeatherStub": false,
// Использовать конечную точку 'by-name'.
"UseOpenWeatherByCityName": false,
// При запуске сервиса: подавлять авторизацию.
"OnStart_SuppressAuth": false
},
"App": {
// Конфигурация взаимодействия с сервисом прогноза погоды OpenWeather.
"OpenWeather": {
// Базовый адрес сервиса.
"BaseAddress": "https://api.openweathermap.org/data/2.5/",
// Идентификатор города, для которого запрашивается прогноз.
"CityId": "551487", // Казань, РТ.
// API-ключ, доступный после регистрации по адресу https://home.openweathermap.org/api_keys.
"ApiKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
},
// ...
}
На этом этапе нужно установить значение ApiKey
, поскольку для релиза пользовательские секреты разработчика учитываться не будут.
Шаг 8. Запускаем приложение FeatureToggleApp.exe и браузер
Проверяем авторизацию по адресу http://localhost:5000/weather-forecast
Ввиду того, что значение флага OnStart_SuppressAuth
настраивает приложение только на старте, а дальнейшие изменения не учитываются, закрываем приложение и включаем подавление авторизации в appsettings.json
.
{
"FeatureManagement": {
// ...
// При запуске сервиса: подавлять авторизацию.
"OnStart_SuppressAuth": true
},
// ...
}
Перезапускаем приложение, обновляем страницу и…
Шаг 9. Видим переключатели функциональности в действии
Прогноз погоды, полученный с OpenWeather, доступен нам по адресу http://localhost:5000/weather-forecast. Обратите внимание на плавное изменение температуры — в самый раз для октября.
Теперь менять значения флагов функциональности можно без перезапуска приложения. Так, например, переключаемся на использование заглушки:
{
"FeatureManagement": {
// Использовать заглушку для внешней зависимости OpenWeather.
"UseOpenWeatherStub": true,
// ...
},
// ...
}
А вот случайный прогноз погоды, генерируемый заглушкой — здесь температура скачет, как и было задумано.
Теперь попытаемся получить прогноз погоды по наименованию города — ничего не получится, потому что мы запретили эту фичу флагом UseOpenWeatherByCityName
.
Чтобы это исправить, разрешаем фичу вызова прогноза погоды по наименованию города.
{
"FeatureManagement": {
// ...
// Использовать конечную точку 'by-name'.
"UseOpenWeatherByCityName": true,
// ...
},
// ...
}
После сохранения измененного файла конфигурации и обновления страницы браузера отображается прогноз погоды по наименованию города, как и ожидалось.
Прогноз по наименованию города генерируется заглушкой, которую можно снова отключить.
Итого. Как добавить feature toggles в веб-приложение на ASP.net
В двух частях статьи мы сделали много действий и посмотрели на большое количество кода, но на самом деле добавить флаги функциональности совсем не сложно — вот буквально все шаги, которые нужно сделать.
Добавление инфраструктуры:
Добавление в проект зависимости на NuGet-пакет
Microsoft.FeatureManagement.AspNetCore
(1 команда);Добавление в проект зависимости на NuGet-пакет
FeatureManagement.ImplementationSwitchExtensions
с методами расширений типаAddScopedFeature
(1 команда);Создание раздела
FeatureManagement
вappsettings.json
(1 строка);Добавление регистрации
services.AddFeatureManagement()
в методConfigureServices
классаStartup
(1 строка);(опционально) добавление пустого класса
FeatureFlags
в проект (1 строка).
Добавление одного флага функциональности:
(опционально) Добавление константы в класс
FeatureFlags
(1 строка);Копирование флага в
appsettings.json
с указанием значения по умолчанию (1 строка);Замена регистрации в методе
ConfigureServices
классаStartup
на соответствующий с постфиксом...Feature
и указанием типа заглушки (1 строка).
Таким образом, добавление инфраструктуры флагов функциональности осуществляется двумя командами и тремя строчками кода по принципу copy/paste, а тремя строчками кода внедряется использование каждого нового флага функциональности.
Исходные коды библиотеки и примера доступны здесь. Спасибо за внимание!