ASP.NET MVC: Читаем настройки сайта из своей секции файла конфигурации web.config
Много раз приходилось сохранять настройки сайта в файле конфигурации. Раздел appSettings предоставляет возможность хранить настройки по принципу "ключ" = "значение" (Dictionary). Я же хочу показать как можно создать свою секцию в файле конфигурации, как читать из нее данные значения.
Постановка задачи
Требуется создать свою секцию настроек в конфигурационном файле web.config, со структурой более "продвинутой" чем appSettings (то есть нечто большее, чем просто Dictionary). А так же надо реализовать механизм считывания и обновления данных в этой секции.
Как сохранить настройки или выбор стратегии
Снова напомню, что есть серия статей "История одного проекта" (далее ИОП), которая посвящена разработке сайта “музей юмора”. И поэтому, давайте снова поговорим про этот проект, как можно расширить возможности по администрированию этого сайта. Предположим, что лента экспонатов, должна хранить записи за какой-то определенный срок. Например, за одну неделю, то есть семь дней. А если я захочу изменить срок, например на 14 дней? А если я захочу вообще временно отключить удаление записей? А если я хочу временно запретить публикацию экспонатов на ленте через IE-ускоритель? А если... Слишком много "а если"... Надо просто эти параметры где-то хранить с возможностью управления ими непосредственно со страниц сайта. Один из вариантов - файл конфигурации web.config, можно также хранить в базе данных, или, например, в текстовом файле, или сделать другой сайт, на котором сделать web-сервис, который сможет "выдать" всем моим сайта настройки для каждого или общие для всех. Или вообще, всё выложить в облако Azure и платить некоторую сумму в Microsoft... Всё ограничено лишь вашей фантазией. Мы будем говорить про первый вариант – сохранение настроек сайта в файле конфигурации.
Итак, хранение настроек в web.config тоже можно реализовать как минимум двумя способами. Читать/писать данные в файле конфигурации можно:
- Из секции appSettings. ASP.NET предоставляет программистам в своё распоряжение самый простой вариант. Информация в этой секции хранится по принципу Dictionary (ключ = значение).
- Из собственной секции файла конфигурации. ASP.NET также предоставляет такую возможность. Структура данных в этом случае может быть сколь угодно сложной (комплексные типы), более того, вы можете гарантировать значения по умолчанию для своих параметров и даже валидацию ввода при изменении свойств, что является неоспоримым плюсом для этого способа.
Возьмем второй вариант. Мне хочется показать пример посложнее, да и структура у меня будет развиваться и совершенствоваться, а потому очень не хочется осознать в какой-то момент, что надо переписывать половину приложения, чтобы реализовать возможность читать/писать данные и настройки в своей секции конфигурации, а не из appSettings.
Начнем с простого. Пусть на данный момент мне требуется такая ветка в файле конфигурации:
<SiteSettingsGroup>
<SiteSettings>
<PagerSize>
<Entity Name="Logs" Size="20" />
<Entity Name="Exhibit" Size="10" />
<Entity Name="Lenta" Size="15" />
</PagerSize>
<Lenta DeleteAfterDays="17" AllowPostFromShare="false" />
</SiteSettings>
</SiteSettingsGroup>
Если описать вкратце, то SiteSettingsGroup – название группы. SiteSettings – это та самая секция, которую мы будем создавать. PagerSize – это коллекция с настройками размера страницы при отображении в пейджере для определенного типа сущности. Lenta – это дополнительный раздел настроек для ленты анекдотов, а значения атрибутов, итак понятны.
Немного специальных классов
Создадим несколько классов. Для начала – главный класс SiteSettings (также называется и файл .cs). Чтобы все получилось, нужно для начала добавить namespace:
using System.Configuration;
Для начала создадим первый “кирпичик” выбранной структуры – класс LentaElement из указанной выше конфигурации:
public class LentaElement : ConfigurationElement {
[ConfigurationProperty("DeleteAfterDays", DefaultValue = 7, IsRequired = true)]
[IntegerValidator(MinValue = 3, MaxValue = 30, ExcludeRange = false)]
public int DeleteAfterDays {
get {
return (int)this["DeleteAfterDays"];
}
set {
this["DeleteAfterDays"] = value;
}
}
[ConfigurationProperty("AllowPostFromShare", DefaultValue = true, IsRequired = true)]
public bool AllowPostFromShare {
get {
return (bool)this["AllowPostFromShare"];
}
set {
this["AllowPostFromShare"] = value;
}
}
}
Обратите внимание на то, что класс унаследован от ConfigurationElement. Это один из основных базовых классов, которые используются в web.config для разделов. Дальше будет понятнее.
Далее надо бы создать главный раздел для моей секции и уже использовать LentaElement как свойство. Класс я назову SiteSettings:
public class SiteSettings : ConfigurationSection {
[ConfigurationProperty("Lenta", IsRequired = true)]
public LentaElement Lenta {
get {
return (LentaElement)this["Lenta"];
}
set {
this["Lenta"] = value;
}
}
}
Между делом или Config-помощник
Для того, чтобы “достучаться” до любого значения любого параметра моей конфигурации, давайте прямо сейчас создадим специальный класс-помощник. Я буду его использовать чтобы протестировать работоспособность конфигурации:
public class Config {
/// <summary>
/// скроем конструктор от любопытных программеров
/// </summary>
internal Config() { }
/// <summary>
/// наименование раздела конфигурации
/// </summary>
private const string CONFIGGROUPNAME = "SiteSettingsGroup/";
/// <summary>
/// наименование секции
/// </summary>
private const string CONFIGSECTIONNAME = "SiteSettings";
/// <summary>
/// Чтение раздела конфигурации целиком
/// </summary>
/// <returns></returns>
internal static SiteSettings Get() {
var config = (SiteSettings)ConfigurationManager
.GetSection(String.Concat(CONFIGGROUPNAME, CONFIGSECTIONNAME));
return config;
}
}
А теперь пришло время попробовать что получилось. Попробуем прочитать информацию из конфигурации. Я предварительно в файле web.config пока убрал (закомментировал) из своей конфигурации секцию PagerSize. Потому что она еще не реализована, а значит приложение даже не будет пытаться запуститься, а просто вывалится с ошибкой.
В головном контролере в методе Index, я для проверки вставил пару строк, чтобы проверить что конфигурация читается, запустил и…

Отлично! Как раз то, что я и указал в своей конфигурации. Дальше - больше. Я добавил еще несколько классов. Это самое интересное. В раздел SiteSettings я добавил определение класса PagerSizeCollection. Чтобы можно было работать с коллекцией настроек. Далее определил класс PageSizeItemsElement, который и представляет собой одну из строчек в списке PagerSize… Наверное, всё-таки имеет смысл показать весь файл SiteSettings целиком:
public class SiteSettings : ConfigurationSection
{
[ConfigurationProperty("PagerSize")]
public PagerSizeCollection PagerSize
{
get
{
return ((PagerSizeCollection)base["PagerSize"]);
}
}
[ConfigurationProperty("Lenta", IsRequired = true)]
public LentaElement Lenta
{
get
{
return (LentaElement)this["Lenta"];
}
set
{
this["Lenta"] = value;
}
}
}
[ConfigurationCollection(typeof(PageSizeItemsElement), AddItemName = "Entity")]
public class PagerSizeCollection : ConfigurationElementCollection
{
public PageSizeItemsElement this[int index]
{
get { return (PageSizeItemsElement)BaseGet(index); }
}
protected override ConfigurationElement CreateNewElement()
{
return new PageSizeItemsElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((PageSizeItemsElement)element).Name;
}
}
public class PageSizeItemsElement : ConfigurationElement
{
[ConfigurationProperty("Size", DefaultValue = 20, IsRequired = true)]
[IntegerValidator(MinValue = 5, MaxValue = 100, ExcludeRange = false)]
public int Size
{
get
{
return (int)this["Size"];
}
set
{
this["Size"] = value;
}
}
[ConfigurationProperty("Name", DefaultValue = "EntityName", IsRequired = true)]
[StringValidator(MinLength = 3, MaxLength = 50,
InvalidCharacters = " ~!@#$%^&*()[]{}/;'\"|\\")]
public String Name
{
get
{
return (String)this["Name"];
}
set
{
this["Name"] = value;
}
}
}
public class LentaElement : ConfigurationElement
{
[ConfigurationProperty("DeleteAfterDays", DefaultValue = 7, IsRequired = true)]
[IntegerValidator(MinValue = 3, MaxValue = 30, ExcludeRange = false)]
public int DeleteAfterDays
{
get
{
return (int)this["DeleteAfterDays"];
}
set
{
this["DeleteAfterDays"] = value;
}
}
[ConfigurationProperty("AllowPostFromShare", DefaultValue = true, IsRequired = true)]
public bool AllowPostFromShare
{
get
{
return (bool)this["AllowPostFromShare"];
}
set
{
this["AllowPostFromShare"] = value;
}
}
}
Чтобы проверить работоспособность проделываю такую же операцию как и в прошлый раз:

Всё работает. Теперь осталось в подключить настройки к системе, то есть начать использовать их. Читается всё замечательно. Попробуем сохранить?
Заключение
В следующей статье я создам форму редактирования настроек (скорее всего это будет knockout.js, то есть это означает AJAX).