MvcConfig: Храним настройки ASP.NET MVC приложения в файле, а получаем как сервис через Dependency Injection.
Полезности | создано: 15.11.2014 | опубликовано: 15.11.2014 | обновлено: 13.01.2024 | просмотров: 9062 | всего комментариев: 8
Мне трудно представить себе сайт, который бы не использовал какие-либо настройки доступные из любого места программы. Например, адрес электронной почты системного администратора, для отправки ему сообщений или количество строк на странице пейджера. Итак, задача на проект: Требуется создать систему настроек в приложении.
Задача на проект
Требуется реализовать хранение настроек программы в отдельном файле. Настройки должны быть иметь возможность расширения новыми свойствами. Они должны иметь возможность вливаться как Dependency Injection. В этой статье покажу одну свою наработку, которая избавляет от траты времени на реализацию такого механизма. Добавлю, что настройки должны, ко всему прочему, еще и храниться в разных форматах (XML, JSON). Для того, чтобы показать в полном объеме возможности сборки MvcConfig, я создам новый проект.
Создаем проект
В шаблонах Visual Studio 2013 я выбрал Empty и поставил галку MVC, чтобы у меня открылся пустой, необременённый лишними классами, проект:
В папке Controllers нет ни одного контролера, поэтому сразу же создаю новый:
и сразу же представление (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:
Отлично! Работает, а значит можно двигаться дальше.
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. При первом старте, если программа “не найдет” эту папку и файл, то она создаст их, поставив значения по умолчанию для всех свойств.
Содержание файла по умолчанию:
<?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); } }
И запустим приложение:
Настройки “пришли” в контролер, и на форме они тоже наблюдаются:
Свои параметры
Давайте предположим, что нам требуется хранить информацию о наборе строк, не важно каких и еще какое-нибудь целое значение. Это только для примера. Как было сказано выше, чтобы создать свои настройки надо унаследоваться от AppSettings:
public class CurrentAppSettings: AppSettings { public string[] Items { get; set; } public int PersonId { get; set; } }
Теперь во всех представлениях и во всех регистрациях поменяю AppSettings на новый класс CurrentAppSettings и запущу проект:
Свойства появились, но они не заполнены. Давайте допишем недостающие настройки в файл конфигурации:
<?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.
Если теперь запустить приложение и посмотреть результат, то мы увидим следующее:
Настройки в 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. Теперь можно запускать:
Система создала новый файл с настройками:
{ "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
В новой версии добавил возможность читать значения файла конфигурации “напрямую”. Предположим у меня есть такой конфигурационный файл:
Так вот чтобы прочитать параметры я могу использовать новый метод:
//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, чтобы всё заработало с новой сборкой.
Заключение
И в качестве заключения, предоставлю пару-тройку ссылок по теме:
- Nuget-пакет MvcConfig
- Обновленная версия Calabonga.Configuration и статья про нее
- DI-контейнер Autofac
- Dependency Injection на WiKi
Комментарии к статье (8)
слушай а какой контролл используешь для постинга статей на сайте, чтобы одноврмеменно текст редактировать и картинки вставлять, может быть уже обсуждалось у тебя на сайте,?
Но вы также можете реализовать интерфейс ICacheService самостоятельно и тогда всё получится!
Вопрос, папка App_Config не создалась, конфиг лег в корень... это особенность последних версий, или я не правильно что то сделал ? =) (судя по исходнику, так и должно быть ?)
конфиг создается при первом обращении