MvcConfig: Храним настройки ASP.NET MVC приложения в файле, а получаем как сервис через Dependency Injection.

ru-RU | создано: 15.11.2014 | опубликовано: 15.11.2014 | обновлено: 01.01.2018 | просмотров за всё время: 6014

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

Задача на проект

Требуется реализовать хранение настроек программы в отдельном файле. Настройки должны быть иметь возможность расширения новыми свойствами. Они должны иметь возможность вливаться как Dependency Injection. В этой статье покажу одну свою наработку, которая избавляет от траты времени на реализацию такого механизма. Добавлю, что настройки должны, ко всему прочему, еще и храниться в разных форматах (XML, JSON). Для того, чтобы показать в полном объеме возможности сборки MvcConfig, я создам новый проект.

Создаем проект

В шаблонах Visual Studio 2013 я выбрал Empty и поставил галку MVC, чтобы у меня открылся пустой, необременённый лишними классами, проект:

157-10

В папке Controllers нет ни одного контролера, поэтому сразу же создаю новый:

 

157-20

и сразу же представление (View) для этого метода Index.chhtml:

@{
    ViewBag.Title = "Settings";
}

<div class="row">
    <div class="page-header">
        <h2>
            @ViewBag.Title
            <small>view</small>
        </h2>
    </div>
    <div class="col-md-12">
        
    </div>
</div>

Пока просто запустим и проверим, что всё работает.

DI-контейнер

Первым делом я добавлю DI-контейнер. Что такое DI-контейнер останавливаться не буду. Дополнительной информации в интернете очень много. Я предпочитаю использовать Autofac:

PM> Install-Package autofac.mvc5
Attempting to resolve dependency 'Autofac (≥ 3.4.0 && < 4.0.0)'.
Attempting to resolve dependency 'Microsoft.AspNet.Mvc (≥ 5.1.0 && < 6.0.0)'.
Attempting to resolve dependency 'Microsoft.AspNet.WebPages (≥ 3.2.2 && < 3.3.0)'.
Attempting to resolve dependency 'Microsoft.Web.Infrastructure (≥ 1.0.0.0)'.
Attempting to resolve dependency 'Microsoft.AspNet.Razor (≥ 3.2.2 && < 3.3.0)'.
Installing 'Autofac 3.4.0'.
Successfully installed 'Autofac 3.4.0'.
Installing 'Autofac.Mvc5 3.3.3'.
Successfully installed 'Autofac.Mvc5 3.3.3'.
Adding 'Autofac 3.4.0' to MvcConfigDemo.
Successfully added 'Autofac 3.4.0' to MvcConfigDemo.
Adding 'Autofac.Mvc5 3.3.3' to MvcConfigDemo.
Successfully added 'Autofac.Mvc5 3.3.3' to MvcConfigDemo.

PM> 

А теперь настроим контейнер:

public static class AutofacConfig {

    public static void Initialize() {
        var builder = new ContainerBuilder();
        builder.RegisterControllers(Assembly.GetExecutingAssembly());
        builder.RegisterModelBinders(Assembly.GetExecutingAssembly());
        builder.RegisterAssemblyTypes(typeof(MvcApplication).Assembly).AsImplementedInterfaces();
        builder.RegisterModule(new AutofacWebTypesModule());

        builder.RegisterFilterProvider();

        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}

А теперь подключим контейнер Autofac к системе:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    AutofacConfig.Initialize();
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}

Проверим, что контейнер работает, для этого попробуем что-нибудь влить в конструктор контролера. Я возьму класс HttpContextBase, так как его регистрация обеспечена стандартными механизмами Autofac:

157-30

Отлично! Работает, а значит можно двигаться дальше.

MvcConfig

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

PM> Install-Package mvcconfig
Installing 'MvcConfig 1.0.2'.
Successfully installed 'MvcConfig 1.0.2'.
Adding 'MvcConfig 1.0.2' to MvcConfigDemo.
Successfully added 'MvcConfig 1.0.2' to MvcConfigDemo.

PM> 

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

Класс AppSettings

Это класс настроек для приложения, который используется по умолчанию. В нем уже собраны основные свойства для MVC (для моих проекта).

public class AppSettings : IAppSettings {

    public string AdminEmail { get; set; }

    public int DefaultPagerSize { get; set; }

    public string DomainUrl { get; set; }

    public bool IsLogging { get; set; }

    public string RobotEmail { get; set; }

    public string SmtpClient { get; set; }
}

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

ConfigServiceBase<AppSettings>

Базовый класс для работы с файлом конфигурации. Этот класс является абстрактным, значит нам потребуется создать наследника от этого класса, чтобы его можно было использовать во вливаниях через DI-контейнер. Так же этот класс реализует интерфейса IConfigService<T>, его мы будем использовать в настройке DI-контейнера.

IConfigSerializer

Этот интерфейс, вернее реализация этого интерфейса в классе позволит переопределить сериализатор (не знаю, можно так говорить или нет) настроек. В MvcConfig существует класс DefaultConfigSerializer реализующий этот интерфейс, который использует XML-сериализацию, об этом чуть позже.

Наш первый наследник

Давайте создадим класс AppSettingsManager наследник от ConfigServiceBase<AppSettings>. Я положу его в папку Models, чтобы она не пустовала. Код прост и выглядит так:

public class AppSettingsManager : ConfigServiceBase<AppSettings> {
    public AppSettingsManager(IConfigSerializer serializer)
        : base(serializer) {
    }

    public AppSettingsManager(string configFileName, IConfigSerializer serializer)
        : base(configFileName, serializer) {
    }
}

Теперь давайте зарегистрируем в DI-контейнере нашего наследника и классы, которые предоставлены в сборке MvcConfig.

public static class AutofacConfig {

    public static void Initialize() {
        var builder = new ContainerBuilder();
        builder.RegisterControllers(Assembly.GetExecutingAssembly());
        builder.RegisterModelBinders(Assembly.GetExecutingAssembly());
        builder.RegisterAssemblyTypes(typeof(MvcApplication).Assembly).AsImplementedInterfaces();
        builder.RegisterModule(new AutofacWebTypesModule());

        builder.RegisterFilterProvider();

        builder.RegisterType<DefaultConfigSerializer>().As<IConfigSerializer>();
        builder.RegisterType<AppSettingsManager>().As<IConfigService<AppSettings>>();

        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}

Я добавил в AutofacConfig.cs пару строк. В строке 12 регистрируется сериализатор по умолчанию, а в строке 13 регистрируется менеджер настроек, который только что мы создали, унаследовавшись от базового класса.

Как работает MvcConfig

Настройки программы, которые описаны как AppSettings  сохраняются в папку App_Config в файл AppConfig.cfg. При первом старте, если программа “не найдет” эту папку и файл, то она создаст их, поставив значения по умолчанию для всех свойств.

157-40

Содержание файла по умолчанию:

<?xml version="1.0"?>
<AppSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <IsLogging>true</IsLogging>
  <DefaultPagerSize>10</DefaultPagerSize>
  <RobotEmail>robot@domain.com</RobotEmail>
  <AdminEmail>admin@domain.com</AdminEmail>
  <IsHtmlForEmailMessagesEnabled>true</IsHtmlForEmailMessagesEnabled>
  <SmtpClient>localhost</SmtpClient>
  <DomainUrl>http://www.domain.com</DomainUrl>
</AppSettings>

Вы можете изменить значения по умолчанию на свои собственные предпочтения. Но это не самое интересное. Я добавил в представление (view) немного разметки, чтобы было нагляднее:

@model Calabonga.Portal.Config.AppSettings
@{
    ViewBag.Title = "Settings";
}

<div class="row">
    <div class="page-header">
        <h2>
            @ViewBag.Title
            <small>view</small>
        </h2>
    </div>
    <div class="col-md-12">
        @using (Html.BeginForm()) {

            @Html.ValidationSummary()
            @Html.AntiForgeryToken()

            <div class="form-group">
                @Html.LabelFor(x => x.IsLogging)<br />
                @Html.CheckBoxFor(x => x.IsLogging)
            </div>

            <div class="form-group">
                @Html.LabelFor(x => x.IsHtmlForEmailMessagesEnabled)<br />
                @Html.CheckBoxFor(x => x.IsHtmlForEmailMessagesEnabled)
            </div>

            <div class="form-group">
                @Html.LabelFor(x => x.AdminEmail)
                @Html.TextBoxFor(x => x.AdminEmail, new { @class = "form-control" })
                @Html.ValidationMessageFor(x => x.AdminEmail)
            </div><div class="form-group">
                @Html.LabelFor(x => x.RobotEmail)
                @Html.TextBoxFor(x => x.RobotEmail, new { @class = "form-control" })
                @Html.ValidationMessageFor(x => x.RobotEmail)
            </div>
            <div class="form-group">
                @Html.LabelFor(x => x.SmtpClient)
                @Html.TextBoxFor(x => x.SmtpClient, new { @class = "form-control" })
                @Html.ValidationMessageFor(x => x.SmtpClient)
            </div>   <div class="form-group">
                @Html.LabelFor(x => x.DomainUrl)
                @Html.TextBoxFor(x => x.DomainUrl, new { @class = "form-control" })
                @Html.ValidationMessageFor(x => x.DomainUrl)
            </div>
            <div class="form-group">
                @Html.LabelFor(x => x.DefaultPagerSize)
                @Html.TextBoxFor(x => x.DefaultPagerSize, new { @class = "form-control" })
                @Html.ValidationMessageFor(x => x.DefaultPagerSize)
            </div>

            <p>
                <button class="btn btn-primary">Сохраннить</button>
            </p>
        }
    </div>
</div>

Обратите внимание на тип модели, которая подключена в строке 1. А с полями формы, которые соответствуют свойствам класса AppSettings, думаю, всё понятно.

Добавим возможность получения модели в представлении, проще говоря, допишем код в контролер:

public class HomeController : Controller
{
    private readonly IConfigService<AppSettings> _configService;

    public HomeController(IConfigService<AppSettings> configService )
    {
        _configService = configService;
    }

    public ActionResult Index()
    {

        var settings = _configService.Config;
        return View(settings);
    }
}

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

157-50

Настройки “пришли” в контролер, и на форме они тоже наблюдаются:

 

157-60

Свои параметры

Давайте предположим, что нам требуется хранить информацию о наборе строк, не важно каких и еще какое-нибудь целое значение. Это только для примера. Как было сказано выше, чтобы создать свои настройки надо унаследоваться от AppSettings:

public class CurrentAppSettings: AppSettings
{
    public string[] Items { get; set; }

    public int PersonId { get; set; }
}

Теперь во всех представлениях и во всех регистрациях поменяю AppSettings на новый класс CurrentAppSettings и запущу проект:

157-70

Свойства появились, но они не заполнены. Давайте допишем недостающие настройки в файл конфигурации:

<?xml version="1.0"?>
<CurrentAppSettings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <IsLogging>true</IsLogging>
    <DefaultPagerSize>10</DefaultPagerSize>
    <RobotEmail>robot@domain.com</RobotEmail>
    <AdminEmail>admin@domain.com</AdminEmail>
    <IsHtmlForEmailMessagesEnabled>true</IsHtmlForEmailMessagesEnabled>
    <SmtpClient>localhost</SmtpClient>
    <DomainUrl>http://www.domain.com</DomainUrl>
    <PersonId>2324</PersonId>
    <Items>
        <string>строка 1</string>
        <string>строка 2</string>
        <string>строка 3</string>
        <string>строка 4</string>
    </Items>
</CurrentAppSettings>

Обратите внимание не только на строки с 10 по 17 но и на 2 и 18. Вместо AppSettings, что был в предыдущем листинге, я поменял на имя CurrentAppSettings, то есть на имя класса наследника от AppSettings.

Если теперь запустить приложение и посмотреть результат, то мы увидим следующее:

157-80

Настройки в JSON

Чтобы настройки программы хранились в формате JSON, надо создать новый класс, в котором реализовать интерфейс IConfigService. Перед тем как создавать новый класс, я установлю еще один nuget-пакет:

PM> Install-Package Newtonsoft.Json
Installing 'Newtonsoft.Json 6.0.6'.
Successfully installed 'Newtonsoft.Json 6.0.6'.
Adding 'Newtonsoft.Json 6.0.6' to MvcConfigDemo.
Successfully added 'Newtonsoft.Json 6.0.6' to MvcConfigDemo.

PM> 

А теперь можно создавать новый файл JsonConfigSerializer:

public class JsonConfigSerializer : IConfigSerializer {
    public T DeserializeObject<T>(string value) {
        return JsonConvert.DeserializeObject<T>(value);
    }

    public string SerializeObject<T>(T config) where T : class {
        return JsonConvert.SerializeObject(config);
    }
}

Реализация очень простая. Далее требуется в конфигурации Autofac настроить работу на новый сериализатор вместо DefaultConfigSerializer:

public static class AutofacConfig {

    public static void Initialize() {
        var builder = new ContainerBuilder();
        builder.RegisterControllers(Assembly.GetExecutingAssembly());
        builder.RegisterModelBinders(Assembly.GetExecutingAssembly());
        builder.RegisterAssemblyTypes(typeof(MvcApplication).Assembly).AsImplementedInterfaces();
        builder.RegisterModule(new AutofacWebTypesModule());

        builder.RegisterFilterProvider();

        builder.RegisterType<JsonConfigSerializer>().As<IConfigSerializer>();
        builder.RegisterType<AppSettingsManager>().As<IConfigService<CurrentAppSettings>>();

        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}

Изменим строка 12 на новый класс. Если сейчас запустить приложение, то оно выдаст ошибку, потому что файл AppConfig.cfg содержит данных созданные другим сериализатором. Чтоб не удалять этот файл, применим маленькую хитрость. Переопределим название файла настроек.

public static class AutofacConfig {

    public static void Initialize() {
        var builder = new ContainerBuilder();
        builder.RegisterControllers(Assembly.GetExecutingAssembly());
        builder.RegisterModelBinders(Assembly.GetExecutingAssembly());
        builder.RegisterAssemblyTypes(typeof(MvcApplication).Assembly).AsImplementedInterfaces();
        builder.RegisterModule(new AutofacWebTypesModule());

        builder.RegisterFilterProvider();

        builder.RegisterType<JsonConfigSerializer>().As<IConfigSerializer>();
        builder.RegisterType<AppSettingsManager>().As<IConfigService<CurrentAppSettings>>()
            .WithParameter("configFileName","AppConfigJson.json");

        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}

К строке 13 я добавил опцию WithParameter. Теперь можно запускать:

157-90

Система создала новый файл с настройками:

{
    "Items": null,
    "PersonId": 45,
    "IsLogging": true,
    "DefaultPagerSize": 10,
    "RobotEmail": "robot@domain.com",
    "AdminEmail": "admin@domain.com",
    "IsHtmlForEmailMessagesEnabled": true,
    "SmtpClient": "localhost",
    "DomainUrl": "http://www.domain.com"
}

Версия 1.0.3

В новой версии добавил возможность читать значения файла конфигурации “напрямую”. Предположим у меня есть такой конфигурационный файл:

157-100

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

//Get value of the property by string parameter
var appSettings0 = _configService.ReadValue<string>("Administrators");

// or you can use Expressions
var appSettings1 = _configService.ReadValue(x => x.Devices);

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

Версия 1.1.1

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

public static class AutofacConfig {

    public static void Initialize() {
        var builder = new ContainerBuilder();
        builder.RegisterControllers(Assembly.GetExecutingAssembly());
        builder.RegisterModelBinders(Assembly.GetExecutingAssembly());
        builder.RegisterAssemblyTypes(typeof(MvcApplication).Assembly).AsImplementedInterfaces();
        builder.RegisterModule(new AutofacWebTypesModule());

        builder.RegisterFilterProvider();

        builder.RegisterType<CacheService>().As<ICacheService>();
        builder.RegisterType<JsonConfigSerializer>().As<IConfigSerializer>();
        builder.RegisterType<AppSettingsManager>().As<IConfigService<CurrentAppSettings>>()
            .WithParameter("configFileName","AppConfigJson.json");

        var container = builder.Build();
        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    }
}

Естественно, что и в классе AppSettingsManager, тоже надо добавить в оба конструктора параметер ICacheService, чтобы всё заработало с новой сборкой.

Заключение

И в качестве заключения, предоставлю пару-тройку ссылок по теме:

Комментарии к статье (8)

11.12.2014 17:54:55 kamaz
Калабонга, привет,
слушай а какой контролл используешь для постинга статей на сайте, чтобы одноврмеменно текст редактировать и картинки вставлять, может быть уже обсуждалось у тебя на сайте,?
14.12.2014 23:46:24 Сalabonga
Я использую CKEditor (ckeditor.net) и в блоге есть упоминания о нем.
21.05.2015 21:23:48 Антон
Привет, у тебя в заголовке опечатка "проложения".
21.05.2015 23:28:21 Calabonga
Спасибо, Антон, поправил опечатку.
05.03.2016 14:14:37 Олег
День добрый! В общем пытаюсь реализовать все так, как Вы написали - не работает. При реализации класса AppSettingsManager конструктор базового типа требует третий параметр типа ICacheService. без него появляются ошибки на этапе компиляции. Передаю null в качестве третьего параметра конструктора - ошибки пропадают. Но не создается файл appconfig.cfg(даже после компиляции). Поправьте меня если я что то не так делаю. Спасибо.
05.03.2016 16:28:03 Calabonga
Олег, спасибо что читаете мой блог. Но статья написана очень давно и на данный момент не актуальна. В новой версии ASP.NET уже реализована возможность хранить настройки в формате JSON. Используйте новую версию.

Но вы также можете реализовать интерфейс ICacheService самостоятельно и тогда всё получится!
05.08.2016 15:52:31 Роман
Спасибо за модуль! Как раз собирался что то такое для себя сделать, а тут такая годная вещь!
Вопрос, папка App_Config не создалась, конфиг лег в корень... это особенность последних версий, или я не правильно что то сделал ? =) (судя по исходнику, так и должно быть ?)
23.08.2016 17:32:48 calabonga

конфиг создается при первом обращении