ASP.NET MVC: Храним настройки приложения в JSON-файле и получаем через DI-container
Сайтостроение | создано: 22.11.2017 | опубликовано: 24.11.2017 | обновлено: 13.01.2024 | просмотров: 5195
Это продолжение темы из статьи "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