Русский
Русский
English
Статистика
Реклама

Configuration

Из песочницы IOptions и его друзья

20.06.2020 00:17:13 | Автор: admin

Во время разработки часто возникает потребность для вынесения параметров в конфигурационные файлы. Да и вообще хранить разные конфигурационный константы в коде является признаком дурного тона. Один из вариантов хранения настроек использования конфигурационных файлов. .Net Core из коробки умеет работать с такими форматами как: json, ini, xml и другие. Так же есть возможность писать свои провайдеры конфигураций. (Кстати говоря за работу с конфигурациями отвечает сервис IConfiguration и IConfigurationProvider для доступа к конфигурациям определенного формата и для написания своих провайдеров)


image


Но главная сложность, на мой взгляд, состоит в том, что у нас имеются аж 3 разные интерфейса для работы с конфигурациями. И не всегда понятно для чего они нужны и какой когда нужно использовать.


На MSDN есть статья, которая должна раскрывать все вопросы. Но, как всегда, не все так просто.


IOptions


Does not support:
Reading of configuration data after the app has started.
Named options

Is registered as a Singleton and can be injected into any service lifetime.

Вцелом, из описания все сразу становится ясно: загружает данные из конфигурационного файла при старте приложения, не подтягивает никаких изменений.


Но у меня большие претензии к Microsoft в плане нейминга. На мой взгляд, если человек недостаточно знаком с технологией, или вообще видит ее в первый раз, то данный интерфейс это то, что первое придет на ум для использования. И потом, вероятно далеко не сразу, человек выяснит, что то все работает совсем не так, как он задумывал.


Я привык, что довольно большая часть конфигураций в моих проектах может довольно часто меняться. И лично мне хотелось бы, что бы интерфейс, который буквально кричит о том, что его надо использовать для конфигурации вел себя более очевидно. (Хотя конечно я могу быть не прав, и это только мои придирки. А большая часть людей использует файлы конфигураций по-другому)


IOptionsSnapshot


Is useful in scenarios where options should be recomputed on every request

Is registered as Scoped and therefore cannot be injected into a Singleton service.

Supports named options

Должен подходить для большинства ситуаций, о которых я писал выше. Обновляет информацию о конфигурации при каждом запросе. И что немаловажно, не изменяет ее во время запроса.
Подходит для множества случаев, например feature toggle.


MSDN нам говорит, что не может быть заинжекчен в Singletone на самом деле может (это прям тема для отдельного поста), но тогда и сам он начинает себя вести как Singletone.


IOptionsMonitor


Is used to retrieve options and manage options notifications for TOptions instances.

Is registered as a Singleton and can be injected into any service lifetime.

Supports:
Change notifications
Named options
Reloadable configuration
Selective options invalidation (IOptionsMonitorCache)

По сути это доступ к вашим конфигурациям в режиме реального времени. Тут стоит быть осторожным. И если вы в процессе какого-то запроса читаете конфигурацию несколько раз стоит быть готовым, что она может измениться.


IOptionsMonitorCache интерфейс для построения обычного кэша на базе IOptionsMonitor.


Практика


Все тесты проводились на следующем окружении


sw_versProductName:    Mac OS XProductVersion: 10.15.5BuildVersion:   19F101dotnet --version3.1.301

Мы посмотрели, что говорит нам документация. Теперь давайте посмотри как это работает.


Сразу оставлю ссылку на код, если кто-то захочет ознакомиться подробнее.


В качестве примера будет простое Web API


 public class Program {     public static void Main(string[] args)     {         CreateHostBuilder(args).Build().Run();     }     public static IHostBuilder CreateHostBuilder(string[] args) =>         Host.CreateDefaultBuilder(args)             .ConfigureWebHostDefaults(webBuilder =>             {                 webBuilder.UseKestrel();                 webBuilder.UseStartup<Startup>();                 webBuilder.UseUrls("http://*:5010/");             })             .UseDefaultServiceProvider(options => options.ValidateScopes = false); }

Клиент, который будет к нему обращаться


 static async Task Main(string[] args) {     using var client = new HttpClient();     var prevResponse = String.Empty;     while (true)     {         var response = await client.GetStringAsync("http://localhost:5010/settings");         if (response != prevResponse) // пишем в консоль только, если настройки изменились         {             Console.WriteLine(response);             prevResponse = response;         }     } }

В Web API создаем 3 сервиса, который принимает все 3 варианта конфигураций в конструктор и возвращают текущее значение.


private readonly IOptions<TestGroupSettings> _testOptions;private readonly IOptionsSnapshot<TestGroupSettings> _testOptionsSnapshot;private readonly IOptionsMonitor<TestGroupSettings> _testOptionsMonitor;public ScopedService(IOptions<TestGroupSettings> testOptions, IOptionsSnapshot<TestGroupSettings> testOptionsSnapshot,    IOptionsMonitor<TestGroupSettings> testOptionsMonitor){    _testOptions = testOptions;    _testOptionsSnapshot = testOptionsSnapshot;    _testOptionsMonitor = testOptionsMonitor;}

Сервисы будут 3х скоупов: Singletone, Scoped и Transient.


public void ConfigureServices(IServiceCollection services){    services.Configure<TestGroupSettings>(Configuration.GetSection("TestGroup"));    services.AddSingleton<ISingletonService, SingletonService>();    services.AddScoped<IScopedService, ScopedService>();    services.AddTransient<ITransientService, TransientService>();    services.AddControllers();}

В процессе работы нашего Web Api изменяем значение TestGroup.Test файла appsettings.json


Имеем следующую картину:
Сразу после запуска


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0SingletonService IOptionsMonitor value: 0ScopedService IOptions value: 0ScopedService IOptionsSnapshot value: 0ScopedService IOptionsMonitor value: 0TransientService IOptions value: 0TransientService IOptionsSnapshot value: 0TransientService IOptionsMonitor value: 0

Изменяем нашу настройку и получаем интересную картину
Сразу после изменения


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: 0 // не измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: // стала пустойScopedService IOptionsMonitor value: 0 // не измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: // стала пустойTransientService IOptionsMonitor value: 0 // не изменилась

Следующий вывод в консоль (конфиг больше не менялся)


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: 0 // не измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: changed setting // измениласьScopedService IOptionsMonitor value: 0 // не измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: changed setting // измениласьTransientService IOptionsMonitor value: 0 // не изменилась

Последний вывод (конфиг также не менялся)


SingletonService IOptions value: 0SingletonService IOptionsSnapshot value: 0 // не измениласьSingletonService IOptionsMonitor value: changed setting // измениласьScopedService IOptions value: 0ScopedService IOptionsSnapshot value: changed setting // измениласьScopedService IOptionsMonitor value: changed setting // измениласьTransientService IOptions value: 0TransientService IOptionsSnapshot value: changed setting // измениласьTransientService IOptionsMonitor value: changed setting // изменилась

Что имеем в итоге? А имеем то, что IOptionsMonitor не такой шустрый, как нам говорит документация. Как можно заметить IOptionsSnapshot может вернуть пустое значение. Но, он работает быстрее, чем IOptionsMonitor.


Пока не особо понятно откуда берется это пустое значение. И самое интересное, что подобное поведение проявляется не всегда. Как-то через раз в моем примере IOptionsMonitor и IOptionsSnapshot отрабатывают одновременно.


Выводы


Если вам нужно передавать конфигурацию, которые никогда в процессе жизни вашего приложения не будут меняться используете IOptions.


Если ваши конфигурации будут меняться, то тут все как всегда зависит. Если Вам важно, что бы в скоупе вашего запроса настройки были релевантны на момент запроса, IOptionsSnapshot ваш выбор (но не для Singletone, в нем значение никогда не изменится). Но стоит учитывать его странности, хотя и столкнуться с ними вряд ли вам придется.


Если же вам нужны наиболее актуальные значения (или почти) используйте IOptionsMonitor.


Буду рад, если вы запустите пример у себя, и расскажете, повторяется подобное поведение или нет. Возможно мы имеем баг на MacOS, а может это by design.


Продолжу разбираться с этой темой, а пока завел issue, может там прояснят такое поведение.

Подробнее..

Варианты использования конфигурации в ASP.NET Core

08.09.2020 02:17:37 | Автор: admin
Для получения конфигурации приложения обычно используют метод доступа по ключевому слову (ключ-значение). Но это бывает не всегда удобно т.к. иногда требуется использовать готовые объекты в коде с уже установленными значениями, причем с возможностью обновления значений без перезагрузки приложения. В данном примере предлагается шаблон использования конфигурации в качестве промежуточного слоя для ASP.NET Core приложений.

Предварительно рекомендуется ознакомиться с материалом: Metanit Конфигурация, Как работает конфигурация в .NET Core.

Постановка задачи


Необходимо реализовать ASP NET Core приложение с возможностью обновления конфигурации в формате JSON во время работы. Во время обновления конфигурации текущие работающие сессии должны продолжать работать с предыдущим вариантов конфигурации. После обновления конфигурации, используемые объекты должны быть обновлены/заменены новыми. Конфигурация должна быть десериализована, не должно быть прямого доступа к объектам IConfiguration из контроллеров. Считываемые значения должны проходить проверку на корректность, при отсутствии как таковых заменяться значениями по умолчанию. Реализация должна работать в Docker контейнере.

Классическая работа с конфигурацией


GitHub: ConfigurationTemplate_1
Проект основан на шаблоне ASP NET Core MVC. Для работы с файлами конфигурации JSON используется провайдер конфигурации JsonConfigurationProvider. Для добавления возможности перезагрузки конфигурации приложения, во время работы, добавим параметр: reloadOnChange: true.
В файле Startup.cs заменим:
public Startup(IConfiguration configuration) {   Configuration = configuration; }

На
public Startup(IConfiguration configuration) {            var builder = new ConfigurationBuilder()    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);   configuration = builder.Build();   Configuration = configuration;  }

.AddJsonFile добавляет JSON файл, reloadOnChange:true указывает на то, что при изменение параметров файла конфигурации, они будут перезагружены без необходимости перезагружать приложение.
Содержимое файла appsettings.json:
{  "AppSettings": {    "Parameter1": "Parameter1 ABC",    "Parameter2": "Parameter2 ABC"    },  "Logging": {    "LogLevel": {      "Default": "Information",      "Microsoft": "Warning",      "Microsoft.Hosting.Lifetime": "Information"    }  },  "AllowedHosts": "*"}

Контроллеры приложения вместо прямого обращения к конфигурации будут использовать сервис: ServiceABC. ServiceABC класс который первоначальные значения берет из файла конфигурации. В данном примере класс ServiceABC содержит только одно свойство Title.
Содержимое файла ServiceABC.cs:
public class ServiceABC{  public string Title;  public ServiceABC(string title)  {     Title = title;  }  public ServiceABC()  { }}

Для использования ServiceABC необходимо его добавить в качестве сервиса middleware в приложение. Добавим сервис как AddTransient, который создается каждый раз при обращении к нему, с помощью выражения:
services.AddTransient<IYourService>(o => new YourService(param));
Отлично подходит для легких сервисов, не потребляющих память и ресурсы. Чтение параметров конфигурации в Startup.cs осуществляется с помощью IConfiguration, где используется строка запроса с указанием полного пути расположения значения, пример: AppSettings:Parameter1.
В файле Startup.cs добавим:
public void ConfigureServices(IServiceCollection services){  //Считывание параметра "Parameter1" для инициализации сервиса ServiceABC  var settingsParameter1 = Configuration["AppSettings:Parameter1"];  //Добавление сервиса "Parameter1"              services.AddScoped(s=> new ServiceABC(settingsParameter1));  //next  services.AddControllersWithViews();}

Пример использования сервиса ServiceABC в контроллере, значение Parameter1 будет отображаться на html странице.
Для использования сервиса в контроллерах добавим его в конструктор, файл HomeController.cs
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly ServiceABC _serviceABC;  public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)    {      _logger = logger;      _serviceABC = serviceABC;    }  public IActionResult Index()    {      return View(_serviceABC);    }

Добавим видимость сервиса ServiceABC в файл _ViewImports.cshtml
@using ConfigurationTemplate_1.Services

Изменим Index.cshtml для отображение параметра Parameter1 на странице.
@model ServiceABC@{    ViewData["Title"] = "Home Page";}    <div class="text-center">        <h1>Десериализация конфигурации в ASP.NET Core</h1>        <h4>Классическая работа с конфигурацией</h4>    </div><div>            <p>Сервис ServiceABC, отображение Заголовка        из параметра Parameter1 = @Model.Title</p></div>

Запустим приложение:


Итог


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

Использование IConfiguration как Singleton


GitHub: ConfigurationTemplate_2
Второй вариант заключается в помещение IConfiguration(как Singleton) в сервисы. В результате IConfiguration может вызываться из контроллеров и других сервисов. При использовании AddSingleton сервис создается один раз и при использовании приложения обращение идет к одному и тому же экземпляру. Использовать этот способ нужно особенно осторожно, так как возможны утечки памяти и проблемы с многопоточностью.
Заменим код из предыдущего примера в Startup.cs на новый, где
services.AddSingleton<IConfiguration>(Configuration);
добавляет IConfiguration как Singleton в сервисы.
public void ConfigureServices(IServiceCollection services){  //Доступ к IConfiguration из других контроллеров и сервисов  services.AddSingleton<IConfiguration>(Configuration);  //Добавление сервиса "ServiceABC"                            services.AddScoped<ServiceABC>();  //next  services.AddControllersWithViews();}

Изменим конструктор сервиса ServiceABC для принятия IConfiguration
public class ServiceABC{          private readonly IConfiguration _configuration;  public string Title => _configuration["AppSettings:Parameter1"];          public ServiceABC(IConfiguration Configuration)    {      _configuration = Configuration;    }  public ServiceABC()    { }}

Как и в предыдущем варианте добавим сервис в конструктор и добавим ссылку на пространство имен
Для использования сервиса в контроллерах добавим его в конструктор, файл HomeController.cs
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly ServiceABC _serviceABC;  public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)    {      _logger = logger;      _serviceABC = serviceABC;    }  public IActionResult Index()    {      return View(_serviceABC);    }

Добавим видимость сервиса ServiceABC в файл _ViewImports.cshtml:
@using ConfigurationTemplate_2.Services;

Изменим Index.cshtml для отображения параметра Parameter1 на странице.
@model ServiceABC@{    ViewData["Title"] = "Home Page";}<div class="text-center">    <h1>Десериализация конфигурации в ASP.NET Core</h1>    <h4>Использование IConfiguration как Singleton</h4></div><div>    <p>        Сервис ServiceABC, отображение Заголовка        из параметра Parameter1 = @Model.Title    </p></div>


Запустим приложение:


Сервис ServiceABC добавленный в контейнер с помощью AddScoped означает, что экземпляр класса будет создаваться при каждом запросе страницы. В результате экземпляр класса ServiceABC будет создаваться при каждом http запросе вместе с перезагрузкой конфигурации IConfiguration, и новые изменения в appsettings.json будут применяться.
Таким образом, если во время работы приложения изменить параметр Parameter1 на NEW!!! Parameter1 ABC, то при следующем обращение к начальной странице отобразится новое значение параметра.

Обновим страницу после изменения файла appsettings.json:


Итог


Неудобством такого подхода является ручное чтение каждого параметра. И если добавить валидацию параметров, то проверка будет выполняться не после изменения в файле appsettings.json а каждый раз при использование ServiceABC, что является лишним действием. В лучшем варианте валидация параметров должна выполняться только один раз после каждого изменения файла.

Десериализация конфигурации с валидацией (вариант IOptions)


GitHub: ConfigurationTemplate_3
Почитать про Options по ссылке.
В этом варианте необходимость использования ServiceABC отпадает. Вместо него используется класс AppSettings, который содержит параметры из конфигурационного файла и объект ClientConfig. Объект ClientConfig после изменения конфигурации требуется инициализировать, т.к. в контроллерах используется готовый объект. ClientConfig это некий класс, взаимодействующий с внешними системами, код которого нельзя изменять. Если выполнить только десериализацию данных класса AppSettings, то ClientConfig будет в состояние null. Поэтому необходимо подписаться на событие чтения конфигурации, и в обработчике инициализировать объект ClientConfig.
Для передачи конфигурации не в виде пар ключ-значение, а как объекты определенных классов, будем использовать интерфейс IOptions. Дополнительно IOptions в отличие от ConfigurationManager позволяет десерилизовать отдельные секции. Для создания объекта ClientConfig потребуется использовать IPostConfigureOptions, который выполняется после обработки всех конфигурации. IPostConfigureOptions будет выполняться каждый раз после чтения конфигурации, самым последним.
Создадим ClientConfig.cs:
public class ClientConfig{  private string _parameter1;  private string _parameter2;  public string Value => _parameter1 + " " + _parameter2;  public ClientConfig(ClientConfigOptions configOptions)    {      _parameter1 = configOptions.Parameter1;      _parameter2 = configOptions.Parameter2;    }}

В качестве конструктора будет принимать параметры в виде объекта ClientConfigOptions:
public class ClientConfigOptions{  public string Parameter1;  public string Parameter2;} 

Создадим класс настроек AppSettings, и определим в нем метод ClientConfigBuild(), который создаст объект ClientConfig.
Файл AppSettings.cs:
public class AppSettings{          public string Parameter1 { get; set; }  public string Parameter2 { get; set; }          public ClientConfig clientConfig;  public void ClientConfigBuild()    {      clientConfig = new ClientConfig(new ClientConfigOptions()        {          Parameter1 = this.Parameter1,          Parameter2 = this.Parameter2        }        );      }}

Создадим обработчик конфигурации, который будет отрабатываться последним. Для этого он должен быть унаследован от IPostConfigureOptions. Вызываемый последним метод PostConfigure выполнит ClientConfigBuild(), который как раз и создаст ClientConfig.
Файл ConfigureAppSettingsOptions.cs:
public class ConfigureAppSettingsOptions: IPostConfigureOptions<AppSettings>{  public ConfigureAppSettingsOptions()    { }  public void PostConfigure(string name, AppSettings options)    {                  options.ClientConfigBuild();    }}

Теперь осталось внести изменения только в Startup.cs, изменения коснутся только функции ConfigureServices(IServiceCollection services).
Сначала прочитаем секцию AppSettings в appsettings.json
// configure strongly typed settings objectsvar appSettingsSection = Configuration.GetSection("AppSettings");services.Configure<AppSettings>(appSettingsSection);

Далее, для каждого запроса будет создаваться копия AppSettings для возможности вызова постобработки:
services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);

Добавим в качестве сервиса, постобработку класса AppSettings:
services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();

Добавленный код в Startup.cs
public void ConfigureServices(IServiceCollection services){  // configure strongly typed settings objects  var appSettingsSection = Configuration.GetSection("AppSettings");  services.Configure<AppSettings>(appSettingsSection);  services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);                                      services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();              //next  services.AddControllersWithViews();}

Для получения доступа к конфигурации, из контроллера достаточно будет просто внедрить AppSettings.
Файл HomeController.cs:
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly AppSettings _appSettings;  public HomeController(ILogger<HomeController> logger, AppSettings appSettings)    {      _logger = logger;      _appSettings = appSettings;    }

Изменим Index.cshtml для вывода параметра Value объекта СlientConfig
@model AppSettings@{    ViewData["Title"] = "Home Page";}<div class="text-center">    <h1>Десериализация конфигурации в ASP.NET Core</h1>    <h4>Десериализация конфигурации с валидацией (вариант IOptions)</h4></div><div>    <p>        Класс ClientConfig, отображение Заголовка         = @Model.clientConfig.Value    </p></div>

Запустим приложение:


Если во время работы приложения изменить параметр Parameter1 на NEW!!! Parameter1 ABC и Parameter2 на NEW!!! Parameter2 ABC, то при следующем обращении к начальной странице отобразится новое свойства Value:



Итог


Данный подход позволяет десериализовать все значения конфигурации без ручного перебирания параметров. Каждый запрос http работает со своим экземпляром AppSettings и СlientConfig, что исключает ситуацию возникновения коллизий. IPostConfigureOptions гарантирует выполнение в последнюю очередь, когда все параметры будут перечитаны. Недостатком решения является постоянное создание экземпляра СlientConfig для каждого запроса, что является непрактичным т.к. по сути СlientConfig должен пересоздаваться только после изменения конфигурации.

Десериализация конфигурации с валидацией (без использования IOptions)


GitHub: ConfigurationTemplate_4
Использование подхода с использованием IPostConfigureOptions приводит к созданию объекта ClientConfig каждый раз при получении запроса от клиента. Это недостаточно рационально т.к. каждый запрос работает с начальным состоянием ClientConfig, которое меняется только при изменение конфигурационного файла appsettings.json. Для этого откажемся от IPostConfigureOptions и создадим обработчик конфигурации который будет вызваться только при изменении appsettings.json, в результате ClientConfig будет создаваться только один раз, и далее на каждый запрос будет отдаваться уже созданный экземпляр ClientConfig.
Создадим класс SingletonAppSettings конфигурации(Singleton) с которого будет создаваться экземпляр настроек для каждого запроса.
Файл SingletonAppSettings.cs:
public class SingletonAppSettings{  public AppSettings appSettings;    private static readonly Lazy<SingletonAppSettings> lazy = new Lazy<SingletonAppSettings>(() => new SingletonAppSettings());  private SingletonAppSettings()    { }  public static SingletonAppSettings Instance => lazy.Value;}

Вернемся в класс Startup и добавим ссылку на интерфейс IServiceCollection. Он будет использоваться в методе обработки конфигурации
public IServiceCollection Services { get; set; }

Изменим ConfigureServices(IServiceCollection services) и передадим ссылку на IServiceCollection:
Файл Startup.cs:
public void ConfigureServices(IServiceCollection services){  Services = services;  //Считаем секцию AppSettings из конфигурации  var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();  appSettings.ClientConfigBuild();

Создадим Singleton конфигурации, и добавим его в коллекцию сервисов:
SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;singletonAppSettings.appSettings = appSettings;services.AddSingleton(singletonAppSettings);     

Добавим объект AppSettings как Scoped, при каждом запросе будет создаваться копия от Singleton:
services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);

Полностью ConfigureServices(IServiceCollection services):
public void ConfigureServices(IServiceCollection services){  Services = services;  //Считаем секцию AppSettings из конфигурации  var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();  appSettings.ClientConfigBuild();  SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;  singletonAppSettings.appSettings = appSettings;  services.AddSingleton(singletonAppSettings);               services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);  //next  services.AddControllersWithViews();}

Теперь добавить обработчик для конфигурации в Configure(IApplicationBuilder app, IWebHostEnvironment env). Для отслеживания изменения в файле appsettings.json используется токен. OnChange вызываемая функция при изменении файла. Обработчик конфигурации onChange():
ChangeToken.OnChange(() => Configuration.GetReloadToken(), onChange);

Вначале читаем файл appsettings.json и десериализуем класс AppSettings. Затем из коллекции сервисов получаем ссылку на Singleton, который хранит объект AppSettings, и заменяем его новым.
private void onChange(){                          var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();  newAppSettings.ClientConfigBuild();  var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();  serviceAppSettings.appSettings = newAppSettings;  Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");}

В контроллер HomeController внедрим ссылку на AppSettings, как в предыдущем варианте (ConfigurationTemplate_3)
Файл HomeController.cs:
public class HomeController : Controller{  private readonly ILogger<HomeController> _logger;  private readonly AppSettings _appSettings;  public HomeController(ILogger<HomeController> logger, AppSettings appSettings)    {      _logger = logger;      _appSettings = appSettings;    }

Изменим Index.cshtml для вывода параметра Value объекта СlientConfig:
@model AppSettings@{    ViewData["Title"] = "Home Page";}<div class="text-center">    <h1>Десериализация конфигурации в ASP.NET Core</h1>    <h4>Десериализация конфигурации с валидацией (без использования IOptions)</h4></div><div>    <p>        Класс ClientConfig, отображение Заголовка        = @Model.clientConfig.Value    </p></div>


Запустим приложение:


Выбрав режим запуска как консольное приложение, в окне приложения можно увидеть сообщение о срабатывания события изменения файла конфигурации:


И новые значения:


Итог


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

Добавление значений по умолчанию и валидация конфигурации


GitHub: ConfigurationTemplate_5
В предыдущих примерах при отсутствии файла appsettings.json приложение выбросит исключение, поэтому сделаем файл конфигурации опциональным и добавим настройки по умолчанию. При публикации приложения проекта, созданного из шаблона в Visula Studio, файл appsettings.json будет располагаться в одной и той же папке вместе со всеми бинарными файлами, что неудобно при развертывание в Docker. Файл appsettings.json перенесем в папку config/:
.AddJsonFile("config/appsettings.json")

Для возможности запуска приложения без appsettings.json изменим параметр optional на true, который в данном случае означает, что наличие appsettings.json является необязательным.
Файл Startup.cs:
public Startup(IConfiguration configuration){  var builder = new ConfigurationBuilder()     .AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: true);  configuration = builder.Build();  Configuration = configuration;}

Добавим в public void ConfigureServices(IServiceCollection services) к строке десериализации конфигурации случай обработки отсутствия файла appsettings.json:
 var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();

Добавим валидацию конфигурации, на основе интерфейса IValidatableObject. При отсутствующих параметрах конфигурации, будет применяться значение по умолчанию. Наследуем класс AppSettings от IValidatableObject и реализуем метод:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)

Файл AppSettings.cs:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext){  List<ValidationResult> errors = new List<ValidationResult>();  if (string.IsNullOrWhiteSpace(this.Parameter1))    {      errors.Add(new ValidationResult("Не указан параметр Parameter1. Задано " +        "значение по умолчанию DefaultParameter1 ABC"));      this.Parameter1 = "DefaultParameter1 ABC";    }    if (string.IsNullOrWhiteSpace(this.Parameter2))    {      errors.Add(new ValidationResult("Не указан параметр Parameter2. Задано " +        "значение по умолчанию DefaultParameter2 ABC"));      this.Parameter2 = "DefaultParameter2 ABC";    }    return errors;}

Добавим метод вызова проверки конфигурации для вызова из класса Startup
Файл Startup.cs:
private void ValidateAppSettings(AppSettings appSettings){  var resultsValidation = new List<ValidationResult>();  var context = new ValidationContext(appSettings);  if (!Validator.TryValidateObject(appSettings, context, resultsValidation, true))    {      resultsValidation.ForEach(        error => Console.WriteLine($"Проверка конфигурации: {error.ErrorMessage}"));      }    }

Добавим вызов метода валидации конфигурации в ConfigureServices(IServiceCollection services). Если файла appsettings.json отсутствует, то требуется инициализировать объект AppSettings со значениями по умолчанию.
Файл Startup.cs:
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();

Проверка параметров. В случае использования значения по умолчанию в консоль будет выведено сообщение с указанием параметра.
 //Validate            this.ValidateAppSettings(appSettings);            appSettings.ClientConfigBuild();

Изменим проверку конфигурации в onChange()
private void onChange(){                          var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();  //Validate              this.ValidateAppSettings(newAppSettings);              newAppSettings.ClientConfigBuild();  var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();  serviceAppSettings.appSettings = newAppSettings;  Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");}

Если из файла appsettings.json удалить ключ Parameter1, то после сохранения файла в окне консольного приложения появится сообщение об отсутствие параметра:


Итог


Изменение пути для расположения конфигураций в папке config является хорошим решением т.к. позволяет не смешивать все файлы в одну кучу. Папка config определяется только для хранения конфигурационных файлов. Упростили задачу развертывания и конфигурирования приложения для администраторов, благодаря валидации конфигурации. Если добавить вывод ошибок конфигурации в лог, то администратор в случае указания неправильных параметров, получит точную информацию о проблеме, а не как в последнее время программисты на любое исключение стали писать: Что -то пошло не так.
Идеального варианта работы с конфигурацией не существует, все зависит от поставленной задачи, у каждого варианта есть свои плюсы и минусы.

Все шаблоны конфигураций доступны по ссылке.

Литература:
  1. Корректный ASP.NET Core
  2. METANIT Конфигурация. Основы конфигурации
  3. Singleton Design Pattern C# .net core
  4. Reloading configuration in .NET core
  5. Reloading strongly typed Options on file changes in ASP.NET Core RC2
  6. Конфигурация ASP.NET Core приложения через IOptions
  7. METANIT Передача конфигурации через IOptions
  8. Конфигурация ASP.NET Core приложения через IOptions
  9. METANIT Самовалидация модели
Подробнее..
Категории: C , Net , Net core , Configuration , Asp , Asp.net

Конфигурация i3 под ноутбук как свести производительность на 100?

19.08.2020 12:13:25 | Автор: admin

Недавно я понял, что мой ноутбук недостаточно производителен. Ему не хватает мощности вывезти всё вместе: Vim (+ 20 плагинов), VSCode (+ столько же расширений), Google Chrome (+ 20 вкладок) и так далее. Казалось бы, обычная проблема на ноутбуках с 4 ГБ ОЗУ, однако я не стал сдаваться. Я люблю ноутбуки за их компактность, а также за то, что они могут работать на батарее где угодно. Мне нужно было просто подумать, как освободить лишнюю ОЗУ, а также увеличить энергоэффективность.





Если вам нужны сразу конфиги, то пролистывайте вниз к разделу "Разбираем установочку"

Операционная система


Так как мне нужна ОС, которая будет жрать наименьшее количество ОЗУ и батареи, я выбрал Arch Linux. Классика, ничего нового. Его репозитории позволят мне автоматизировать кучу ненужной работы, а AUR позволит сократить ещё больше времени.


Менеджер окон


Я решил взять именно менеджер окон, а не полноценное окружение. Хоть мне и нравятся кеды (KDE), но они все равно кушают достаточно много, из-за того, что они подтягивают за собой достаточно много библиотек и зависимостей. Ну и сама по себе DE'шка жрёт достаточно много из-за всяких ненужных виджетов.





Разбираем установочку


Для начала нам нужно установить все основные пакеты (нужно же нам что-то конфигурировать)


sudo pacman -Sy --noconfirm i3 i3-gaps base-devel rofi okular feh vim code picom kitty ranger git xdotool xautolock i3lock-color scrot imagemagick rxvt-unicode urxvt-perls

Вот примерная схема, как всё будет работать



Какие пакеты для чего нужны?


Пакет Для чего нужен
xwinwrap Нужен для установки файлов с расширением .gif в виде анимированных обоев
polybar Нужен для того, чтобы в менеджере окон отображался топбар
i3 Сам оконный менеджер
i3-gaps Расширение для оконного менеджера
base-devel Компоненты необходимые для установки polybar
rofi Программа для запуска приложений
okular Программа для просмотра документов
zathura Программа для просмотра документов (поддерживает не так много расширений, однако более минималистичная)
feh Программа для просмотра картинок, а также для установки фоновых изображений
vim Основной редактор
code Дополнительный редактор
picom Композитор (программа которая создает тени, прозрачность, блюрит фон)
kitty Основной терминал
urxvt Дополнительный терминал
ranger Файловый менеджер
git Система контроля версий
xdotool Утилита, которая поможет при разработке скриптов и взаимодействии с окнами
xautolock Утилита, которая блокирует компьютер при бездействии и запускает i3-lock
i3lock-color Улучшенная версия i3lock. Программа нужна для блокировки компьютера и ввода пароля
scrot Минималистиченое приложение для создания скриншотов
imagemagick Программа, которая поможет при взаимодействии с картинками (блюрит их заранее, конвертирует, меняет разрешение)

Конфигурируем i3


i3 Оконный менеджер, который не потребляет много ресурсов, тем самым, он будет полезен нам для того, чтобы "эмулировать" другие обычные оконные менеджеры. (В подарок конечно же идёт тилинг способность оконного менеджера раскрывать приложения на всю свободную часть экрана)


Я буду приводить конфиг i3 по частям, для того, чтобы даже новички всё поняли. Начнём с самого главного кнопка $Mod. Она служит для того, чтобы взаимодействовать с i3. Через неё будут проходить все основные хоткеи.


### Tweaks #### Set main key (Win)set $mod Mod4

Далее научим наш оконный менеджер двигать окна мышью при нажатии $mod


# Press MOD key and click on mouse to move your windowfloating_modifier $mod# Focus doesn't follow the mousefocus_follows_mouse no

Установим шрифты для наших приложений, а также для приложений, которые зависят от i3


# Fontsfont pango: JetBrains Mono 10

Моей задумкой было сделать все окна изначально летающими (что, как оказалось, очень удобно). Для пояснения: в i3 есть много видов стыковки окон (Tilling, Fullscreen, Tabbed, Float, Stacking), все они удобны в различных ситуациях, однако я не вижу смысла делать все окна заполняющими весь экран. Пусть лучше они заполняют его при нажатии $mod + f, а висят в воздухе по умолчанию, это я и сделал в следующем куске кода:


# Maximum width for floating windowsfloating_minimum_size 400 x 350floating_maximum_size 1800 x 900# (1920 - 10)/2 and (1080 - 10)/2for_window [class=".*"] floating enablefor_window [class=".*"] resize set 955 535for_window [class=".*"] focus

Для того, чтобы вы не путались выражение (1920 - 10)/2 and (1080 - 10)/2 означает что каждое окно будет занимать четверть экрана, а также будет отступ (друг от друга) ровно в 5 пикселей (5 со всех сторон).

Далее давайте забиндим все основные приложения. Все хоткеи стараются соответствовать данной схеме



## Keyboard Settings ### Appsbindsym $mod+Return exec kittybindsym $mod+Mod1+r exec "kitty sh -c 'ranger'"bindsym $mod+Mod1+g exec google-chrome-stablebindsym $mod+Mod1+c exec codebindsym $mod+Mod1+v exec dolphinbindsym Print exec spectacle

Также, забиндим все основные функции, которые мы выполняем не думая, и которые обязательно должны быть


# System / Volumebindsym XF86AudioMute "exec amixer -D pulse sset Master toggle && notify-send \"Volume\" \"Sound is (un)muted\" --urgency low"bindsym XF86AudioRaiseVolume "exec amixer -D pulse sset Master 5%+ && notify-send \"Volume\" \"Volume added +5%\n    Volume level is now $(amixer -D pulse get Master | awk -F 'Left:|[][]' 'BEGIN {RS=\"\"}{ print $3 }')\" --urgency low"bindsym XF86AudioLowerVolume "exec amixer -D pulse sset Master 5%- && notify-send \"Volume\" \"Volume added -5%\n    Volume level is now $(amixer -D pulse get Master | awk -F 'Left:|[][]' 'BEGIN {RS=\"\"}{ print $3 }')\" --urgency low"# System / Brightnessbindsym XF86MonBrightnessDown exec xbacklight -dec 5bindsym XF86MonBrightnessUp exec xbacklight -inc 5# Moving from one window to anotherbindsym $mod+h focus leftbindsym $mod+j focus downbindsym $mod+k focus upbindsym $mod+l focus right# Choose one of your workspacesbindsym $mod+1 workspace $workspace1bindsym $mod+2 workspace $workspace2bindsym $mod+3 workspace $workspace3bindsym $mod+4 workspace $workspace4# Move window to the workspacebindsym $mod+Shift+1 move container to workspace $workspace1bindsym $mod+Shift+2 move container to workspace $workspace2bindsym $mod+Shift+3 move container to workspace $workspace3bindsym $mod+Shift+4 move container to workspace $workspace4## Floating manipulation ### Make window floatingbindsym $mod+f floating toggle# Change focusbindsym $mod+Shift+f focus mode_toggle# Move windowsbindsym $mod+Shift+h move left 20pxbindsym $mod+Shift+j move down 20pxbindsym $mod+Shift+k move up 20pxbindsym $mod+Shift+l move right 20px# Resizing Windowsbindsym $mod+Ctrl+l resize shrink width 10 px or 10 pptbindsym $mod+Ctrl+k resize grow height 10 px or 10 pptbindsym $mod+Ctrl+j resize shrink height 10 px or 10 pptbindsym $mod+Ctrl+h resize grow width 10 px or 10 ppt# Make window fullscreenbindcode 95 fullscreen toggle# Reload Configurationbindsym $mod+p reload# Kill a windowbindsym $mod+x exec xdotool getwindowfocus windowkill

Сделаем секцию автостарта


### Autostart #### Lockscreen after 10min delayexec --no-startup-id "$HOME/.config/i3/lockscreen"# Convert background gif to jpgexec --no-startup-id convert -verbose $HOME/.config/i3/{gif.gif,gif.jpg}# Generate Colorschemeexec_always --no-startup-id wal -i $HOME/.config/i3/gif-0.jpg# Compositorexec_always --no-startup-id "killall -q picom; picom --config $HOME/.config/picom.conf"# Languageexec --no-startup-id setxkbmap -model pc105 -layout us,ru -option grp:win_space_toggle# Dunstexec --no-startup-id dunst# Kittyexec kitty# Dropboxexec --no-startup-id dropbox &# Polybarexec_always --no-startup-id $HOME/.config/polybar/launch.sh# Cursorexec_always --no-startup-id xsetroot -cursor_name left_ptr

i3-gaps сборка i3, которая добавляет много новых функций. Одной из них является добавление отступов (gaps), которые визуально выглядят очень хорошо.


### i3-gaps #### Borders for windowsfor_window [class=".*"] border pixel 5# Gaps for i3barfor_window [class="i3bar"] gaps outer current set 10# Gapsgaps inner 10gaps outer 4### Topbar and color theme #### Color theme of bordersclient.focused              #bf616a #2f343f #d8dee8 #bf616a #d8dee8client.focused_inactive     #2f343f #kf343f #d8dee8 #2f343f #2f343fclient.unfocused            #2f343f #2f343f #d8dee8 #2f343f #2f343fclient.urgent               #2f343f #2f343f #d8dee8 #2f343f #2f343fclient.placeholder          #2f343f #2f343f #d8dee8 #2f343f #2f343fclient.background           #2f343f

Что получилось?


А получилась довольно минималистичная сборка на i3, которая очень шустро работает на ноутбуках и даёт неплохие показатели



Так как конфигураций я написал ещё очень много (что отлично видно на скриншоте) их можно найти в репозитории Great i3.

Ещё пару скриншотов




Подробнее..

Конфигурация Java систем как убрать боль

29.01.2021 16:08:57 | Автор: admin
Читать конфиги через Java-интерфейсЧитать конфиги через Java-интерфейс

Проблема

Если вы хотя бы однажды разрабатывали большую систему на Java и сопровождали её, то, наверняка, сталкивались с неудобствами настройки тех или иных процессов в системе.

А именно: размеры различных буферов; параметры почтового ящика; хост, порт, логин, пароль вызова внешних сервисов; всякие таймауты и многое другое.

Каждый раз менять всё это в коде, пересобирать и перенакатывать на реал не комильфо.

Естественно все эти параметры нужно выносить в файлы конфигов и считывать их оттуда все так делают.

В Java из коробки для этого есть некий Properties. Но пользоваться им крайне неудобно. Во-первых, UTF-8 там не работают, во-вторых если вы поменяли какой-нибудь параметр в конфиге, то чтобы новое значение попало в систему требуется перезапуск приложения. А если вы не хотите его перезапускать, или это невозможно в 11 утра час пиковой нагрузки. И отложить на потом не вариант нужно срочно. Что делать? Нужно чтобы конфиги перечитывались на горячую, т.е. без перезапуска системы.

А ещё очень важно: нужно как-то так придумать чтобы имена параметров конфигов в коде программы соответствовали тем, которые в файле. Т.е. чтобы трудно было ошибиться. Обычно используют для этого константы помогает, но хотелось бы что-то удобнее, проще и гибче.

И вот ещё что: представьте у вас крупная система в которой уже накопилось около тридцати конфигурационных файлов, и в каждом по десятку параметров. И вам нужно накатить новую инстанцию. Как вы будете настраивать эти конфиги? Создавать каждый вручную? В каждом прописывать имена параметров и их значения, вспоминая что каждый из них значит? А если забыли? А есть документация? А эта документация актуальная? А если вы ошибётесь в одной буковке параметра позволит ли вам система при старте сразу объяснить что не так? Или она свалится в час ночи, когда вы крепко спите? Вам придётся просыпаться, включать как-то мозг и разбираться во всей этой истории

Хотелось бы избежать всей этой нервотрёпки.

Надо как-то сделать так, чтобы система создавала все эти конфиги сама, и описывала их всех что каждый конфиг делает в общем, и каждый параметр в частности. Ну и разработчик не должен сильно напрягаться чтобы поддерживать всю эту автоматизацию в актуальном состоянии.

Всё это должно быстро и легко активироваться подключил какую-то маленькую библиотечку, прописал какой-то магический код размером не больше пяти-десяти строк (автосгенерированные вашей IDE строки не считаем) и вуаля всё работает.

Можно конечно использовать конфигурационный сервер, и даже кто-то это делает (вроде), но это как-то напряжно хочется что-нибудь проще, но эффективно.

Мы в greetgo! поискали в разных интернетах, но так ничего подходящего не нашли пришлось пилить самим.

Решение

Основной идеей было использовать Java-интерфейс для чтения данных конфига, и, одновременно, формирования структуры конфига. Вы один раз в интерфейсе написали имя метода и это имя автоматически становится именем параметра в конфиге. Вам не нужно его писать второй раз в файле конфига, опасаясь допустить ошибку в одной букве файл пусть создаётся автоматически со всеми параметрами вам нужно будет только подкорректировать значения параметров.

Например вы хотите вынести в конфиг updateTimeout и batchSize потому что трудно их подобрать изначально. Для этого создаёте Java-интерфейс:

public interface MyMigrationConfig {long updateTimeoutMs();  int batchSize();}

Хотя вот так будет лучше:

@Description("Миграция состояний в ядро системы");public interface MyMigrationConfig {@Description("Максимальное время миграции одной порции в миллисекундах."               + " Если оно будет превышено, то миграция будет немедленно"               + " прервана с ошибкой")@DefaultLongValue(30000)long updateTimeoutMs();  @Description("Размер порции мигрируемых данных в записях")@DefaultIntValue(150)int batchSize();}

Теперь этот интерфейс надо зарегистрировать в какой-то классной штуке, которая умеет автоматически создавать магические реализации подобных интерфейсов, и после этого пользоваться, например, таким способом:

@Autowireprivate MyMigrationConfig config; public void migrate() {  // ...  System.out.println(" updateTimeoutMs = " + config.updateTimeoutMs());  System.out.println(" batchSize       = " + config.bachSize());  // ...}

Такой простой подход позволяет легко настраивать и сопровождать любые настройки системы, как во время разработки так и во время сопровождения.

Нами была разработана библиотека, которая позволяет реализовать такой подход. Она получилась маленькой и у неё нет никаких других зависимостей. И она уже долго и интенсивно нами используется, другими словами она production ready. Лицензия MIT.

Называется она greetgo conf она есть и в maven и в github: https://github.com/greetgo/greetgo.conf

Подключив её к проекту, необходимо создать фабрику конфигов примерно так:

public class MyConfigFactory extends FileConfigFactory { @Overridepublic Path getBaseDir() {return Paths.get("/path/to/directory/where/config/files/are/located");}  @Overrideprotected String getConfigFileExt() {return ".conf";}}

Дальше создаём инстанцию этой фабрики и пользуемся магическим методом createConfig:

MyConfigFactory confFactory = new MyConfigFactory();MyMigrationConfig config = confFactory.createConfig(MyMigrationConfig.class);

Теперь у нас есть инстанция интерфейса конфига и мы можем считывать данные конфига. Они будут считываться из файла с именем MyMigrationConfig.conf. Этот файл создастся автоматически, если его ещё нет, с примерно таким содержимым:

# Created at 2021-01-29 11:03:21# Миграция состояний в ядро системы# Размер порции мигрируемых данныхbatchSize=150# Максимальное время миграции одной порции в миллисекундах. Если оно будет# превышено, то миграция будет немедленно прервана с ошибкойupdateTimeoutMs=30000

Теперь вы можете менять эти параметры и система автоматически их будет перечитывать. Естественно про оптимизацию мы подумали всё это внимательно кэшируется, и, если вы будете очень часто считывать параметры (тысячи раз в секунду), то это не вызовет большую нагрузку на файловую систему.

Возможности

Со временем у нас появились распределённые проекты с большим количеством нод и с использованием kubernetes. Хранить настройки в файлах в таких проектах оказалось не удобно, и мы сделали расширение библиотеки, которое позволяет хранить настройки в Zookeeper, для этого нужно просто заменить FileConfigFactory на AbstractZookeeperConfigFactory, ну и прописать параметры доступа к Zookeeper.

Также есть реализация JdbcConfigFactory которая позволяет хранить настройки в реляционной БД.

Архитектура библиотеки позволяет достаточно просто разрабатывать свои фабрики конфигов, например чтобы хранить конфиги в яндекс-диске.

Также есть возможность использовать аннотацию @FirstReadEnv которая считывает помеченный ей параметр не из конфига, а из переменной окружения в kubernetes некоторые параметры удобнее задавать через переменные окружения.

На этом все. Спасибо за внимание.

Подробнее..
Категории: Open source , Java , Configuration , Interface

Flutter Flavoring

10.05.2021 16:07:24 | Автор: admin

Хотелось ли вам иметь несколько версий одного приложения?

Чтобы одной командой вы могли собрать приложение под определенное окружение?

Сталкивались ли вы с тем, что одновременно нельзя было установить несколько версий одного приложения на одном устройстве?

Всем привет!

Меня зовут Андрей!

И в этой статье я расскажу, как настроить сборку приложения для разных окружений.

Сразу отмечу, что слова версия, окружение и флейвор (flavor) будут взаимозаменяемыми.

Не смотря на то, что материал называется Flutter Flavoring, бОльшая часть работы будет в нативном пространстве (в папках android/ и ios/). Приведённые мной инструкции используются так же и для нативных приложений, а не только для Flutter приложений.

  • Overview

  • Create the App

  • Переменные окружения в .env

  • Android Flavoring

  • iOS Flavoring

  • App Icons

  • Firebase Projects

  • Заключение

GitHub: https://github.com/AndrewPiterov/flutter_starter_app/

Видео версия на YouTube:

Overview

Мы настроим сборку приложения для двух окружений: DEVELOPMENT и PRODUCTION.

У каждой версии будут свои

  • иконки

  • наименования

  • application ID

  • переменные окружения, т.к. адрес к API серверу

  • Firebase проекты

Начнём...

Create the App

Для начала создадим наш новый флаттер проект и мигрируем его сразу на null safety

$ flutter create flutter_starter_app$ cd flutter_starter_app && dart migrate --apply-changes

Откроем проект в любимом IDE.

Переменные окружения в .env

Первым делом настроим переменные окружения для нашего проекта.

Эти переменные я предпочитаю хранить в файле assets/.env. И в зависимости какую версию приложения мы собираем, мы указываем в этом файле соответствующие переменные. Изменять этот файл будем в CI/CD (Continuous integration & continuous delivery) в следующих статьях, а пока укажем значения в этом файле один раз и продолжим.

# assets/.envENVIRONMENT=devAPI_URI=https://api.mydev.com

Добавим в pubspec.yaml пакет flutter_dotenv, который облегчит нам считывание этого .env файла:

dependencies:# ...    flutter_dotenv: ^4.0.0-nullsafety.0

И укажем, что вместе с проектом идут следующие файлы (assets):

assets:    - assets/

Добавляем класс, который будет считывать наши переменные с этого .env файла и предоставлять доступ к этим переменным через свойства:

import 'package:flutter/foundation.dart';import 'package:flutter_dotenv/flutter_dotenv.dart';import 'package:flutter_dotenv/flutter_dotenv.dart' as DotEnv;class AppConfig {  factory AppConfig() {    return _singleton;  }  AppConfig._();  static final AppConfig _singleton = AppConfig._();  static bool get IS_PRODUCTION =>      kReleaseMode || ENVIRONMENT.toLowerCase().startsWith('prod');  static String get ENVIRONMENT => env['ENVIRONMENT'] ?? 'dev';  static String get API_URI => env['API_URI']!;  Future<void> load() async {    await DotEnv.load(fileName: 'assets/.env');    debugPrint('ENVIRONMENT: $ENVIRONMENT');    debugPrint('API ENDPOINT: $API_URI');  }}

Подгрузим наши переменные окружения в самом начале запуска приложения в main.dart:

Future main() async {  WidgetsFlutterBinding.ensureInitialized();  await AppConfig().load();  runApp(MyApp());}

И где-то на скрине в приложении отобразим наши переменные:

Column(  mainAxisAlignment: MainAxisAlignment.center,  children: [    Text(      AppConfig.ENVIRONMENT,    style: TextStyle(fontSize: 50),    ),    Text(      AppConfig.API_URI,        style: TextStyle(fontSize: 30),    ),  ],)

Запускаем приложение:

$ flutter run

Результат:

Изменим значения в .env, перезапустим приложение, и увидим новые значения на экране.

Не забудьте поместить .env в .gitignore

На этом настройка в Flutter пространстве (в папке lib/) закончена, следующие настройки будут в нативном пространстве, т.е. в папках android/ и ios/.

Android Flavoring

Для Android настройка очень простая. Достаточно указать следующие параметры в android/app/gradle

android {    compileSdkVersion 30// ...    flavorDimensions "starter_app"    productFlavors {        dev {            dimension "starter_app"            applicationIdSuffix ".dev"            resValue "string", "app_name", "Starter(Dev)"            versionNameSuffix ".dev"        }        prod {            dimension "starter_app"            resValue "string", "app_name", "Starter"        }    }

Где указали какие флейворы нам нужны, и у каждого флейвора свой applicationId и наименование.

В AndroidManifest.xml укажем ссылку на переменную app_name с наименованием из флейвора:

<application        ...        android:label="@string/app_name"

Запускаем приложение на Android под каждую версию:

$ flutter run --flavor=dev$ flutter run --flavor=prod

Результат: установилось два приложения с разными наименованиями.

iOS Flavoring

В iOS нет такого понятия как Flavor, которое есть в Android.И в iOS используется Схемы (Schema) и их Конфигурации (Configuration).

На картинке ниже изображено, что у каждой Схемы есть свои Конфигурации. И у каждой Конфигурации есть свои параметры, которые мы можем кастомизировать. Например, applicationId, название приложения и иконки приложения под разные версии.

Первым делом нам нужно добавить наши Схемы, и добавить к каждой схеме её конфигурации. Для этого мы откроем XCode, и сверху нажимаем на Runner -> New scheme и добавляем нашу новую dev Схему.

Далее добавим devконфигурации. Для этого выбираем Project -> Runner, где видим раздел наших Конфигураций. Чтобы добавить новые конфигурации, нам нужно продублировать имеющиеся конфигурации и назвать их соответсnвующим образом с суффиксом -dev, например:

Дальше переименуем нашу Runner схему вprod

Далее нужно привязать dev Конфигурации к dev схеме. На текущий момент у dev схемы указаны Debug, Release, Profile конфигурации (те, что без суффикса -dev), т.к. мы создали новую dev схему когда еще не было -dev конфигураций.

Переименуем Debug, Release, Profile, добавив к ним суффикс -prod:

Сейчас у нас две схемы с их отдельными конфигурациями. И мы можем кастомизировать параметры для каждой отдельной схемы. И первым делом, выставим каждой конфигурации свой applicationId:

Кастомизируем наименование приложения для каждой отдельной конфигурации:

И добавим в ios/Runner/Info.plist новое свойство для нашей переменной:

<dict>...<key>CFBundleDisplayName</key><string>$(APP_DISPLAY_NAME)</string>...</dict>

Запускаем приложение на iOS под каждую версию:

$ flutter run --flavor=dev$ flutter run --flavor=prod

Результат: установилось два приложения с разными наименованиями.


App Icons

Мы воспользуемся плагином flutter_launcher_icons, который сгенерирует для нас иконки для каждой платформы и для каждой версии по отдельности.

dev_dependencies:    # ...    flutter_launcher_icons: ^0.8.1

Добавим в корне проекта файлы конфигурации для этого плагина под каждую версию, в которых укажем какие картинки брать для генерации иконок.

# flutter_launcher_icons-dev.yamlflutter_icons:  android: true  ios: true  # image_path: "assets/app_icon/dev.jpg"  image_path_android: "assets/app_icon/android_dev.png"  image_path_ios: "assets/app_icon/ios_dev.png"
# flutter_launcher_icons-prod.yamlflutter_icons:  android: true  ios: true  # image_path: "assets/app_icon/prod.jpg"  image_path_android: "assets/app_icon/android_prod.png"  image_path_ios: "assets/app_icon/ios_prod.png"

Запускаем следующую команду генерации иконок:

flutter pub run flutter_launcher_icons:main -f flutter_launcher_icons*

И посмотрим, где добавились сгенерированные иконки:

Для Android все готово, но для iOS нужно снова вернуться в XCode и так же, как и в случае с наименованием и application ID, указать у каждой конфигурации свою иконку:

Запускаем приложение под каждую версию на iOS и Android, и увидим результат - иконки наших уже установленных приложений обновились:


Firebase Projects

Прежде всего создадим два Firebase проекта под каждую версию через firebase console .

В каждом проекте добавим Android и iOS приложения и скачаем файлы конфигурации Firebase проектов:

  • google-services.json Android приложения - 2 штуки

  • GoogleService-Info.plist iOS приложения - 2 штуки

Для теста, можем для каждого Firebase проекта активировать Firestore, в котором одна коллекция secrets с одним элементом, у которого есть поле value. У prod версии значение в value равно PRODUCTION, у dev версии - DEVELOPMENT.

В pubscpec.yaml добавляем Firebase зависимости

dependencies:# ...    # Firebase    firebase_core: ^1.1.0    cloud_firestore: ^2.0.0

В main.dart проинициализируем Firebase приложение

Future main() async {// ...await Firebase.initializeApp();    runApp(MyApp());}

И для теста, где-то на скрине приложения отобразим наше значение value

StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(  stream:      FirebaseFirestore.instance        .collection('secrets').snapshots(),  builder: (_, snapshot) {    if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {      return CircularProgressIndicator();    }    final first = snapshot.data!.docs.first.data();    return Text(      'Firebase: ' + first['value'],      style: TextStyle(        fontSize: 25,        fontWeight: FontWeight.bold,        color: Colors.blue,      ),    );  },),

Настроим iOS и Android для Firebase. Более подробно о настройке можно почитать на официальном сайте.

Настройка Firebase на iOS

В файле ios/Podfile укажем минимальную версию iOS 10

platform :ios, '10'

И в этом же фале в методе target 'Runner' добавим следующую строчку, из-за которой наше приложение будет собираться быстрее:

# ...target 'Runner' do  pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '7.11.0'# ...end

Далее кладем файлы конфигурации для Firebase в проекте в папках config/prod и config/dev

И добавим новый Build Phase Script, указанный ниже, который будет во время сборки определенной версии приложения брать соответсвующий файл Firebase конфигурации и помещать его в папку Runner:

environment="default"# Regex to extract the scheme name from the Build Configuration# We have named our Build Configurations as Debug-dev, Debug-prod etc.# Here, dev and prod are the scheme names. This kind of naming is required by Flutter for flavors to work.# We are using the $CONFIGURATION variable available in the XCode build environment to extract # the environment (or flavor)# For eg.# If CONFIGURATION="Debug-prod", then environment will get set to "prod".if [[ $CONFIGURATION =~ -([^-]*)$ ]]; thenenvironment=${BASH_REMATCH[1]}fiecho $environment# Name and path of the resource we're copyingGOOGLESERVICE_INFO_PLIST=GoogleService-Info.plistGOOGLESERVICE_INFO_FILE=${PROJECT_DIR}/config/${environment}/${GOOGLESERVICE_INFO_PLIST}# Make sure GoogleService-Info.plist existsecho "Looking for ${GOOGLESERVICE_INFO_PLIST} in ${GOOGLESERVICE_INFO_FILE}"if [ ! -f $GOOGLESERVICE_INFO_FILE ]thenecho "No GoogleService-Info.plist found. Please ensure it's in the proper directory."exit 1fi# Get a reference to the destination location for the GoogleService-Info.plist# This is the default location where Firebase init code expects to find GoogleServices-Info.plist filePLIST_DESTINATION=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.appecho "Will copy ${GOOGLESERVICE_INFO_PLIST} to final destination: ${PLIST_DESTINATION}"# Copy over the prod GoogleService-Info.plist for Release buildscp "${GOOGLESERVICE_INFO_FILE}" "${PLIST_DESTINATION}"

Называем эту Build Phase понятным именем и перемещаем ее немного выше:

Не забудьте поместить GoogleService-Info.plist в .gitignore

Запускаем приложение и видим результат.

Настройка Firebase на Android

Первое добавим зависимость для плагина google services в android/build.gradle

# android/build.gradlebuildscript {  dependencies {    // ... other dependencies    classpath 'com.google.gms:google-services:4.3.3'  }}

Используем плагин в android/app/build.gradle

apply plugin: 'com.google.gms.google-services'

Выставим минимальную версию SDK как 21

android {    defaultConfig {        // ...        minSdkVersion 21            // <------ THIS        targetSdkVersion 28        multiDexEnabled true    }}

Добавим файлы конфигурации Firebase в соответствующие папки каждого флейвора:

Не забудьте поместить google-services.json в .gitignore

Запускаем каждую версию на Андроиде и проверяем результат:


Заключение

Таким образом, мы настроили флейворы или сборку разных версий нашего приложения, что у каждой версии свои:

  • application id

  • иконки

  • наименования

  • переменные окружения

  • Firebase бэкенд

Надеюсь материал был полезен для вас.

Всем happy coding!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru