ASP.NET MVC: Храним настройки приложения в JSON-файле и получаем через DI-container
Сайтостроение | создано: 22.11.2017 | опубликовано: 24.11.2017 | обновлено: 13.01.2024 | просмотров: 4671
Это продолжение темы из статьи "MvcConfig: Храним настройки ASP.NET MVC приложения", которая была опубликована на сайте много ранее. На этот раз версия сборки обновилась на столько сильно, что я принял решение написать новую статью с описанием и примерами использования новой сборки.
Предисловие
Как я уже писал в предыдущей статье, которая была опубликована в ноябре 2014 года:
Мне трудно представить себе сайт, который бы не использовал какие-либо настройки доступные из любого места программы. Например, адрес электронной почты системного администратора, для отправки ему сообщений или количество строк на странице пейджера. Итак, задача на проект: Требуется создать систему настроек в приложении.
С тех пор мало что изменилось, настройки всё также хранят в специальных файлах. Изначально, начиная с первой версии ASP.NET WebForm, Microsoft "предоложила" хранить настройки пользователя и приложения в файле web.config, вернее сказать в иерархие файлов web.config. После этого с приходом платформы ASP.NET Core все настройки "перекочевали" в файл appSettings.json.
Неудобство хранения в файле web.config в том, что при исполнении приложения (runtime) при внесении изменений в этот самый файл, система обязательно перезапускала процесс с хостом. Конечно же существуют разные обходные пути не перезапускать сайт, но это плохая идея, потому что пока AppDomain не перезапустится, изменения конфигурации не вступят в силу. Поэтому возникла идея хранить свои настройки вне файла web.config.
Задачи
Конфигурация приложения, далее будем ее называть ApplicationSettings должна:
- иметь возможность вливаться через dependency injection;
- иметь возможность обновиться в процессе работы системы без перезагрузки таковой;
- иметь варианты модификация RELEASE, DEBUG и другие, настроенные дополниельно;
- иметь формат возможность хранения в файле в формате и JSON, и XML, а также иметь возможность другого формата определенного разработчиком;
- иметь события, уведомляющее об загрузки (Deserialize completed) данных из файла;
- иметь кэширование загруженных данных;
- иметь возможность сбросить (перечитать) данных и файла конфигурации;
- иметь простой способ развертывания и поддержания.
Calabonga.Configuration
В отличии от предыдущей версии, все перечисленные задачи решает новая версия системы хранения конфигурации, Чтобы долго не расписывать как это всё работает, я создам демонстрационный проект, в котором покажу что нужно сделать, чтобы настройки хранились в JSON-файле.
Создаем новый проект в Visual Studio. Как обычно обновляем все существующие nuget-пакеты, выполнив команду:
PM> Update-Package
После этого установливаем Calabonga.Configuration:
PM> Install-Package Calabonga.Configuration
Далее создаем файл ApplicationSettings.cs в котором перечислим все необходимые нам настройки приложения. Я придумал вот что:
/// <summary> /// System Configuration /// </summary> public class ApplicationSettings { public string AdministrtorEmail { get; set; } public int DeafultPageSize { get; set; } public string ApplicationName { get; set; } public string ApplicationDomain { get; set; } public string SecretKey { get; set; } public EmailServer EmailServer { get; set; } } /// <summary> /// System Configuration: Email Server /// </summary> public class EmailServer { public string Host { get; set; } public string UserName { get; set; } public string Password { get; set; } public int Port { get; set; } }
В общем, ничего сложного, поехали дальше.
По умолчанию Calabonga.Configuration ищет в корне сайта файл Config.json, создадим его:
{ "AdministrtorEmail": "admin@yahoo.eu", "DeafultPageSize": 10, "ApplicationName": "Demonstration", "ApplicationDomain": "www.domian.com", "SecretKey": "SuperSecretKey", "EmailServer": { "Host": "smtp.yandex.ru", "UserName": "", "Password": "SuperSecretPassword", "Port": 956 } }
Далее создадим менеджер, который будет нашей основной для вливания. Для этого подключим пространство имен using Calabonga.Configurations и унаследуемся базового класса, указав наш класс ApplicationSettings в качестве обобщенного параметра:
/// <summary> /// Configuration manager /// </summary> public class SettingsManager : Configuration<ApplicationSettings> { public SettingsManager(IConfigSerializer serializer, ICacheService cacheService) : base(serializer, cacheService) { } }
Обратите внимание, что для корректной работы класса нам потребуется еще два класса, вернее сказать, два интерфейса и их реализация: IConfigSerializer и ICacheService. В сборке существуют реализация обоих этих абстракций. Для IConfigSerializer есть JsonConfigSerializer и XmlConfigSerializer. Думаю, из названий ясно, как и что они сериализуют. А для ICacheService существует реализация в класее CacheService.
Если вам не нравятся названия или еще что-то, то вы можете в любой момент написать собственную реализацию этих интерфейсов, зарегистрировать их в контейнере... Стоп, контейнер! Про контейнер-то забыли!
Dependency Injection Container
Вначале статьи был упомянут DI-контейнер, давайте его установим. Я предпочитаю использовать Autofac:
PM> Install-Package Autofac.Mvc5
Теперь создадим конфигурацию для DI-контейнера и настроем DependencyResolver для ASP.NET MVC:
/// <summary> /// Autofac container configuration /// </summary> public static class DependencyContainer { public static void Initialize() { var builder = new ContainerBuilder(); builder.RegisterControllers(Assembly.GetExecutingAssembly()); // Calabonga.Configuration injections builder.RegisterType<CacheService>().As<ICacheService>(); builder.RegisterType<JsonConfigSerializer>().As<IConfigSerializer>(); builder.RegisterType<SettingsManager>().AsSelf(); var container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); } }
В строке 12 и строке 13 мы регистрируем два упомянутых выше интерфейса из библиотеки Calabonga.Configuration. Далее в файле Global.asax.cs, который является CompositionRoot для ASP.NET MVC приложения, подключаем инициализацию DI-контейнера:
public class MvcApplication : HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); DependencyContainer.Initialize(); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } }
Собственно говоря, нам больше ничего не мешает сделать вливание ApplicationSettings, например, в контролере HomeController.
Лирическое отступление. В процессе написания статьи, меня просто вывела из себя навязчивость Microsoft.ApplicationInsight.*, поэтому я удалил все nuget-пакеты и сборки вместе с ними. И только на старте приложения высвободилось порядка 40 Мб оперативной памяти.
Для того чтобы влить зависимость в контролер надо указать в конструкторе SettingsManager и обратиться к свойству Config:
public class HomeController : Controller { private readonly ApplicationSettings _settings; public HomeController(SettingsManager settings) { _settings = settings.Config; } public ActionResult Index() { return View(); } /* cutted for briefly */ }
Раз уже всё готово, запустим сайт...
Другой "ракурс"...
Место для хранения
Посмотрите на предыдущую картинку. Свойства DirectoryName и FileName вы можете при неоходимости переопределить. Для этого в файле SettingsManager надо проделать следующее.
То есть перенести файл настроек в папку и "сказать" об этом менеджеру настроек. А еще можно использовать разные условия для выбора конфигурации (файлов).
Ну, и на последок, более сложная вариация на тему управления конфигурациями в "большом" приложение. Создаем интерфейс ISettingsManager:
/// <summary> /// Settings manager /// </summary> public interface ISettingsManager { ApplicationSettings Config { get; } }
Далее нужно применить первой конфигурации предварительно переименовав ее либо создать новую:
/// <summary> /// Configuration manager /// </summary> public class ProductionSettingsManager : Configuration<ApplicationSettings>, ISettingsManager { public ProductionSettingsManager(IConfigSerializer serializer, ICacheService cacheService) : base(serializer, cacheService) { } public override string DirectoryName { get { return HttpContext.Current.Server.MapPath("~/Configurations"); } } public override string FileName { #if !DEBUG get { return "appsettings.production.json"; } #else get { return "appsettings.json"; } #endif } }
В 4-ой строке применен интерфейс. А далее создаем еще одну конфигурацию с таким же интерфейсом:
/// <summary> /// Configuration manager /// </summary> public class LocalSettingsManager : Configuration<ApplicationSettings>, ISettingsManager { public LocalSettingsManager(IConfigSerializer serializer, ICacheService cacheService) : base(serializer, cacheService) { } public override string DirectoryName { get { return HttpContext.Current.Server.MapPath("~/Configurations"); } } public override string FileName { get { return "appsettings.local.json"; } } }
Теперь у нас две конфигурации, причем они могу находиться вообще разных сборках и попадать в контейнер при регистрации модулей (plugins). Для нашего случая я зарегистрирую их в одном контейнере с разными именами (Autofac легко позволяет сделать это):
/// <summary> /// Autofac container configuration /// </summary> public static class DependencyContainer { public static void Initialize() { var builder = new ContainerBuilder(); builder.RegisterControllers(Assembly.GetExecutingAssembly()); // Calabonga.Configuration injections builder.RegisterType<CacheService>().As<ICacheService>(); builder.RegisterType<JsonConfigSerializer>().As<IConfigSerializer>(); builder.RegisterType<LocalSettingsManager>().Keyed<ISettingsManager>("LOCAL"); builder.RegisterType<ProductionSettingsManager>().Keyed<ISettingsManager>("PRODUCTION"); var container = builder.Build(); DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); } }
После регистрации таким образом конфигураций можно вливать их, например в контролер, сразу все и использовать ту, которая больше нравится на выбор:
public class HomeController : Controller { private readonly ApplicationSettings _settingsProd; private readonly ApplicationSettings _settingsLocal; public HomeController(IIndex<string, ISettingsManager> configurations) { _settingsProd = configurations["PRODUCTION"].Config; _settingsLocal = configurations["LOCAL"].Config; } public ActionResult Index() { var emailServer = _settingsProd.EmailServer; var secretKey = _settingsLocal.SecretKey; return View(); } /* cutted for briefly */ }
Ничто не мешает использовать параметры из разных конфигураций, о чем свидетельствуют 14 и 15 строки.
Заключение
В качестве заключения могу добавить следующее, сборка Calabonga.Configuration имеет в своем арсенале следующие методы и события:
Reload() - позволяте "на лету" перечитать данные из конфигурационного файла.
ReadValue<TValue>(Expression<Func<T, TValue>> e) - прочитать значение из параметра (лямбда)
ReadValue<TValue>(string propertyName) - прочитать значение параметра (название)
SaveChanges() - сохранить (сериализовать) конфигурацию в файл.
public event ConfigurationLoadedEventHandler<T> ConfigurationLoaded - событие, которые срабатывает, когда из файла только прочитаны данные
Такого набора хватает, чтобы реализовать модель поведения любой сложности. А если ко всему перечисленному добавить возможности DI-контейнера (lifecycle managment, IModule, XML configuration without explicite referrence и другие фишки), то это позволяет не просто решать любые вопросы, но и выбирать из нескольких вариантов решений.
Calabonga.Configuration nuget-пакет
Demo проект на github