Изолируем микросервисы с помощью Feature toggles в ASP.NET Core. Практика

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Снова привет, Хабр! 

В первой части статьи мы разбирали, что такое изоляция микросервисов, как в этом помогают переключатели функциональности, и как создать простое 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, который исследует любой, кто знакомится с проектом, а значит мимо его внимания не пройдет факт использования флагов функциональности. 

Как я уже показывал, легко будет избавиться от ставших ненужными флагов, удаляя код заглушек и заменяя соответствующие методы расширений на их штатные аналоги: AddTransientFeatureAddTransient, AddScopedFeatureAddScoped, AddSingletonFeatureAddSingleton.

Важно! После регистрации методами группы ...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

В двух частях статьи мы сделали много действий и посмотрели на большое количество кода, но на самом деле добавить флаги функциональности совсем не сложно — вот буквально все шаги, которые нужно сделать.

Добавление инфраструктуры:

  1. Добавление в проект зависимости на NuGet-пакет Microsoft.FeatureManagement.AspNetCore (1 команда); 

  2. Добавление в проект зависимости на NuGet-пакет FeatureManagement.ImplementationSwitchExtensions с методами расширений типа AddScopedFeature (1 команда); 

  3. Создание раздела FeatureManagement в appsettings.json (1 строка); 

  4. Добавление регистрации services.AddFeatureManagement() в метод ConfigureServices класса Startup (1 строка); 

  5. (опционально) добавление пустого класса FeatureFlags в проект (1 строка). 

Добавление одного флага функциональности:

  1. (опционально) Добавление константы в класс FeatureFlags (1 строка); 

  2. Копирование флага в appsettings.json с указанием значения по умолчанию (1 строка); 

  3. Замена регистрации в методе ConfigureServices класса Startup на соответствующий с постфиксом ...Feature и указанием типа заглушки (1 строка). 

Таким образом, добавление инфраструктуры флагов функциональности осуществляется двумя командами и тремя строчками кода по принципу copy/paste, а тремя строчками кода внедряется использование каждого нового флага функциональности.

Исходные коды библиотеки и примера доступны здесь. Спасибо за внимание!

Источник: https://habr.com/ru/company/akbarsdigital/blog/597543/


Интересные статьи

Интересные статьи

Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционн...
Всем привет, сегодня мы будем разбираться с алгоритмом сжатия JPEG. Многие не знают, что JPEG — это не столько формат, сколько алгоритм. Большинство JPEG-изображений, которые вы вид...
За прошлый год я собрал и настроил автоматизацию для небольшой квартиры около оживленной дороги. В этой статье я расскажу об использованных решениях в климатической системе, освещении, мультимеди...
Проблема автоматического поиска текста на изображениях существует достаточно давно, как минимум с начала девяностых годов прошлого века. Они могли запомниться старожилам повсеместным распростране...
Вместо введения В статье описывается исследование, проведенное с целью проверки утверждения центральной предельной теоремы о том, что сумма N независимых и одинаково распределенных случайных вел...