ASP.NET MVC: Храним настройки приложения в JSON-файле и получаем через DI-container

Сайтостроение | создано: 22.11.2017 | опубликовано: 24.11.2017 | обновлено: 13.01.2024 | просмотров: 4281

Это продолжение темы из статьи "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 должна:

  1. иметь возможность вливаться через dependency injection;
  2. иметь возможность обновиться в процессе работы системы без перезагрузки таковой;
  3. иметь варианты модификация RELEASE, DEBUG и другие, настроенные дополниельно;
  4. иметь формат возможность хранения в файле в формате и JSON, и XML, а также иметь возможность другого формата определенного разработчиком;
  5. иметь события, уведомляющее об загрузки (Deserialize completed) данных из файла;
  6. иметь кэширование загруженных данных;
  7. иметь возможность сбросить (перечитать) данных и файла конфигурации;
  8. иметь простой способ развертывания и поддержания.

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

English version of this article