Установка
Данная библиотека доступна для скачивания через NuGet. Или вы можете скачать исходники из GitHub.
Использование
Для демонстрации возможностей библиотеки давайте представим, что нашей программе требуются настройки для подключения к нескольким внешним сервисам, базам данных и наша система должна уметь отправлять письма администраторам системы. Мы решили, что данные настройки будут находится в трех секциях.
<ExternalSystemSettings>
<AuthenticationSettings>
<Login>DotNetCraft</Login>
<Token>qwerty</Token>
<Urls>
<Url>https://github.com/DotNetCraft/ConfigurationParser</Url>
<Url>https://github.com/DotNetCraft/ConfigurationParser</Url>
</Urls>
</AuthenticationSettings>
<StaffSettings Token="{D0C148F7-83C0-41B0-8F18-B47CAB09AD99}" Url="https://github.com/DotNetCraft/ConfigurationParser"/>
</ExternalSystemSettings>
<DatabasesSettings>
<MongoSettings ConnectionString="mongo.url" DatabaseName="DotNetCraft"/>
<SqlSettings>
<item key="TenantA">
<value>
<SqlSettings ConnectionString="sql.TenantA.com"/>
</value>
</item>
<item>
<key>TenantB</key>
<value>
<SqlSettings>
<ConnectionString>sql.TenantB.com</ConnectionString>
</SqlSettings>
</value>
</item>
</SqlSettings>
</DatabasesSettings>
<SmtpSettings Host="gmail.com" Sender="no-reply">
<Recipients>clien1@gmail.com;clien2@gmail.com;clien3@gmail.com</Recipients>
</SmtpSettings>
Следующим шагом необходимо создать классы, в которых мы будем хранить настройки системы.
#region ExternalSystemSettings
class ExternalSystemSettings
{
public AuthenticationServiceSettings AuthenticationSettings { get; set; }
public StaffServiceSettings StaffSettings { get; set; }
}
class AuthenticationServiceSettings
{
public string Login { get; set; }
public string Token { get; set; }
public List<string> Urls { get; set; }
}
class StaffServiceSettings
{
public Guid Token { get; set; }
public string Url { get; set; }
}
#endregion
#region DatabasesSettings
class DatabasesSettings
{
public MongoDatabaseSettings MongoSettings { get; set; }
public Dictionary<string, SqlSettings> SqlSettings { get; set; }
}
class MongoDatabaseSettings
{
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
}
class SqlSettings
{
public string ConnectionString { get; set; }
}
#endregion
#region Smtp
class SmtpSettings
{
public string Host { get; set; }
public string Sender { get; set; }
[CustomStrategy(typeof(SplitRecipientsCustomStrategy))]
public List<string> Recipients { get; set; }
}
#endregion
В конфигурационном файле нам необходимо объявить, что мы будем использовать ConfigurationParser.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="ExternalSystemSettings" type="DotNetCraft.ConfigurationParser.SimpleConfigurationSectionHandler, DotNetCraft.ConfigurationParser" />
<section name="DatabasesSettings" type="DotNetCraft.ConfigurationParser.SimpleConfigurationSectionHandler, DotNetCraft.ConfigurationParser" />
<section name="SmtpSettings" type="DotNetCraft.ConfigurationParser.SimpleConfigurationSectionHandler, DotNetCraft.ConfigurationParser" />
</configSections>
…
</configuration>
Теперь мы можем считать нашу конфигурацию легко и непринужденно:
ExternalSystemSettings externalSystemSettings = (dynamic)ConfigurationManager.GetSection("ExternalSystemSettings");
DatabasesSettings databasesSettings = (dynamic)ConfigurationManager.GetSection("DatabasesSettings");
После выполнения кода у нас будет создано 2 объекта, которые и будут содержать наши настройки. Как вы можете справедливо заметить, мы не считываем SmtpSettings. Это сделано для того, чтобы продемонстрировать дополнительные возможности утилиты.
В различных проектах вам может понадобиться записывать разнообразные настройки в конфигурационные файлы и иметь свою логику как с ними работать. Утилита спроектирована таким образом, что вы можете ее легко расширить путем добавления новой стратегии и покрыть все сценарии, которые вам нужны. Для примера, давайте рассмотрим секцию Recipients:
<Recipients>clien1@gmail.com;clien2@gmail.com;clien3@gmail.com</Recipients>
Эта секция содержит список email-ов, разделенных точкой с запятой. Согласно нашему ТЗ и классу мы должны записать каждый адрес как отдельный элемент в массиве. Для выполнения поставленной задачи нам надо создать класс SplitRecipientsCustomStrategy и реализовать интерфейс ICustomMappingStrategy
class SplitRecipientsCustomStrategy : ICustomMappingStrategy
{
#region Implementation of ICustomMappingStrategy
public object Map(string input, Type itemType)
{
string[] items = input.Split(';');
List<string> result = new List<string>();
result.AddRange(items);
return result;
}
public object Map(XmlNode xmlNode, Type itemType)
{
string input = xmlNode.InnerText;
return Map(input, itemType);
}
#endregion
}
Внутри нам необходимо реализовать алгоритм распарсивания значения из секции в соответствии с нашим заданием. Необходимо учитывать, что метод Map(string input, Type itemType) вызывается если значение прочитано из атрибута, а метод Map(XmlNode xmlNode, Type itemType) вызывается если значение прочитано из секции. Согласно конфигурационному файлу, значение будет прочитано из секции.
После этого нам необходимо пометить свойство Recipients атрибутом CustomStrategy в котором нужно указать, что за «стратегия» будет использоваться для данного поля:
[CustomStrategy(typeof(SplitRecipientsCustomStrategy))]
public List<string> Recipients { get; set; }
Вот и все, теперь мы можем считать настройки электронной почты.
SmtpSettings smtpSettings = (dynamic)ConfigurationManager.GetSection("SmtpSettings");
Как вы видите, ConfigurationParser делает работу по чтению настроек из конфигурационного файла очень удобной и сохраняет кучу времени. При этом она легко расширяется благодаря подключению новых стратегий.
Если у вас возникнут какие-либо вопросы или предложения, пожалуйста, пишите их в комментариях, я с радостью на них отвечу.
Комментарии (44)
MonkAlex
26.03.2017 16:26+7Ещё наверно тупее вопрос — а почему просто не использовать json? Пишешь нужные тебе классы, автоматически сериализуешь\десериализуешь без всяких монструозных инструментов. Читабельно, быстро, просто, удобно.
DotNetCraft
27.03.2017 01:13+1Приветствую
Listrigon правильно ответил про стандартную систему конфигурационных файлов .NET. От себу лишь слегка добавлю, что в .NET для хранения конфигурации используется App.Config, в котором и записывается все настройки. Формат хранения данных — XML.
Более подробно можно прочитать здесь:
1. https://msdn.microsoft.com/en-us/library/1xtk877y.aspx
2. https://msdn.microsoft.com/en-us/library/ms254494(v=vs.110).aspx
Listrigon
27.03.2017 01:13+1Скорее всего потому, что все это дело является частью стандартной системы конфигурационных файлов .NET.
Чтобы не делать отдельного файла под свои настройки нужно быть частью структуры этого файла, который пользуется и другими библиотеками. Хотя да, с JSON было бы все проще
Caraul
26.03.2017 20:05+1А в чем отличие от Custom Configuration Sections? Только то, что POCO без атрибутов? Я не вижу в System.Configuration особых сложностей и тяжестей.
DotNetCraft
27.03.2017 01:57Приветствую
На мой взгляд прелесть в том, что кода становится меньше.
Для примера возьмем секцию из статьи по вашей ссылке
<StartupFolders> <Folders> <Folder folderType = "A" path="c:\foo" /> <Folder folderType = "B" path="C:\foo1" /> </Folders> </StartupFolders>
Создадим классы
class StartupFolders { public Folders Folders { get; set; } } class Folders { public List<Folders> Folders { get; set; } } class Folder { public string FolderType { get; set; } public string Path { get; set; } }
Вызовем загрузчик
StartupFolders startupFolders= (dynamic)ConfigurationManager.GetSection("StartupFolders");
Ну и как бы все :) Мы загрузили все настройки.
Обратите внимание, что я нигде не использовал ключи а-ля «Folders», что, с моей точки хрения, уменьшает ошибку при копипасте.
На простых примерах можно использовать различные подходы и они будут примерно равнозначны. Все интересное начинается, когда конфигрурация разрастается и там появляются вложенные настройки, а в них еще вложенные и т.д.lair
27.03.2017 02:16… а теперь, значит, вы хотите переименовать свойство
Folders.Folders
вFolders.NewFolders
, но не хотите трогать конфигурационные файлы (т.е., в них элемент так и должен остатьсяFolders
). И как это сделать?DotNetCraft
27.03.2017 02:48+1Если я правильно понял вопрос, то
[PropertyMapping("Folders ")] public List<Folders> NewFolders { get; set; }
Это означает, что в конфигу мы ищем Folders и грузим данные в NewFolders.
Хотя, если честно, то я почти не сталкивался с ситуациями, когда надо было поменять имя… Вот структуру — это да, частое явление. То мы храним это в Листе, то давайте сделаем словарь…
З.Ы. Забыл упомянуть об этом аттрибуте в статье.
SergeyVoyteshonok
26.03.2017 22:05+2вместо
string[] items = input.Split(';'); List<string> result = new List<string>(); result.AddRange(items); return result;
может лучше
return input.Split(';').ToList();
DotNetCraft
27.03.2017 01:36+1Приветствую
Да, соглашусь, что можно input.Split(';').ToList();
Однако, я хотел акцентировать внимание на то, что в этом методе можно написать любую логику. Например, в текущем примере мы можем добавить проверку правильность email'ов и сгенерить Exception если что-то не так.
msin
27.03.2017 01:36+1Зачем такой многострочный метод Map(string input, Type itemType)?
Гораздо короче и понятнее будет так:
public object Map(string input, Type itemType) { return input.Split(';').ToList(); }
DotNetCraft
27.03.2017 01:37+2Приветствую
Да, соглашусь, что можно input.Split(';').ToList();
Однако, я хотел акцентрировать внимание на то, что в этом методе можно написать любую логику. Например, в текущем примере мы можем добавить проверку правильность email'ов и сгенерить Exception если что-то не так.
Deosis
27.03.2017 08:04+2В .NET настройки делятся на настройки приложения и пользовательские. (вторые доступны для изменения, первые только для чтения [если не править файл вручную])
Библиотека как-либо обрабатывает эти различия?
Например, пользователь переопределил несколько настроек и хочет их сохранить.
Что нужно использовать вместо Configuration.Save()?DotNetCraft
27.03.2017 13:00Основное предназначение этой библиотеки — чтение настроек приложения, поэтому методы сохранения не предусмотрены.
Если честно, то я не стрононник что-либо менять в конфиге приложения в рантайме. Если что-то и надо заменить, то, используя, например, TeamCity+Octopus, создаем релиз и деплоим его с измененными настройками. Это относится к настройкам приложения.
Если же речь идет о пользовательских, то, как правило эти данные хранятся в БД и выходят за рамки данной либы.Bronx
29.03.2017 04:43я не стрононник что-либо менять в конфиге приложения в рантайме.
Рантайм — это не единственный сценарий. Есть ещё конфигурационные утилиты, мастера всякие и прочие средства развёртывания, которые помогают правильно готовить конфиги, и им весьма желательно пользоваться теми же самыми классами.
ЗЫ. Атрибут «configSource» продолжает нормально поддерживаться?Bronx
29.03.2017 05:01И, заодно, я так понимаю, у вас нет поддержки стандартного паттерна коллекций в web/app.config?
<items> <clear /> <add name="Item 1" /> <add name="Item 2" /> </items>
DotNetCraft
29.03.2017 12:08Под «рантаймом» я понимаю, когда программа запущена и она сама меняет ее конфиг. Вот я именно против этого. А вот когда в процессе развертывания кто-то меняет конфиг, так это я «за». Например, мы создаем релизный конфиг и там указываем заменить connectionString на реальзое значение в зависимости от среды развертывания. Что-то мне подсказывает, что в этом вопросе мы по одну сторону баррикады.
Что касается <clear/> и configSource… Я с этимт «параметрами» сталкивался очень давно и опыт был негативным, так что я стараюсь их избегать. Более того, это лично мое мнение, задвать в конфигурации, то что конфигурация в таком-то файле как-то странновато. Смысл данного действия для меня представляется только в том, чтобы разделить Debug от Release, ну, или разные конфиги для разных клиентов. Все это легко делается путем создания конфигурационного файла с разметкой и, в момент создания билда, автоматически подставляем нужные параметры.Bronx
29.03.2017 20:59«clear/add/remove» упрощает создание иерархических конфигов с настройками по-умолчанию: есть некий корневой конфиг с дефолтными элементами коллекции, и в своём под-конфиге можно переиспользовать их, можно добавить свои, можно убрать некоторые (или все) дефолтные. Без этого придётся каждый раз указывать все необходимые элементы. С одной стороны, это вроде как хорошо — все элементы явно присутствуют, с другой — конфиг может быть сильно перегружен вспомогательными элементами, появляется соблазн копипастить и т.п.
configSource — это как "#include" или «using» или «import» — полезен тем, способствует модуляризации: можно разбить огромный неудобоваримый конфиг с кучей настроек «обо всём» на несколько более сфокусированных конфигов. Например, ту часть, что настраивается девелопером и далее не предполагается перенастраивать (всякие там assemblyBindings и проч), хранить в основном модуле, а части, которые нужно настраивать при деплое — в своих модулях. Кроме того, можно создать несколько под-конфигов на разные случаи (скажем, с разными тонкими настройками производительности), и подключать/переключать их одним движением, не переписывая основной конфиг, и не дублируя его.
В чём был ваш негативный опыт? У меня как раз негативный опыт от одного супер-конфига, в основном из-за его вырвиглазности и из-за копипасты, распространяющейся как рак.Bronx
29.03.2017 21:09Забыл добавить: при разбиении конфига на части и иерархии/композиции появляется возможность тонко настраивать безопасность, назначая разные права на разные секции.
DotNetCraft
30.03.2017 13:20Если я правильно понял, то все эти приемы направлены чтобы превратить мегагромадный конфиг во что-то удобоваримое…
Опыт как раз в том, что огромный конфиг пытались разбить на подконфиги и тем самым только усугубив проблему, вместо того, чтобы разобраться в чем причина. А проблема-то была в том, что система взяла на себя слишком много (получился этакий монолит-гигант) и плюс не было нормального деплоймента (конфиги ручками менялись для debug, test, sandbox и пр). Зато потом, когда все-таки разбили этот кусок гиганта на сервисы (мини или микро — это пожеланию), то монстрообразный конфиг пропал. Вместо него появились несколько мелких конфигов и для каждой среды свой конфиг.
Пример:
- app.config — это конфигурация у разработкича
- app.Release.config — это для всех остальных (тест, релиз и пр)
При деплое меням переменные в указанном конфиге. Например, при деплое тестировщикам имеем конфиг вида
<DatabaseSettings ConnectionString=#{CurrentConnectionString}/>
В Octopus'e задаем значение для CurrentConnectionString
- CurrentConnectionString = TestDatabase для тестирования
- CurrentConnectionString = SandBox для предрелиза
- CurrentConnectionString = Release для релиза.
Нажимаем на кнопку и деплоим куда надо. Переменная заменится на нужное значение автоматом.Bronx
30.03.2017 21:45Для меня «мегагаромадный» начинается уже с момента, когда кастомные настройки составляют менее 50% от веса всего конфига. Т.е. если конфиг занимает целый экран, и в нём всего одна настройка — коннект к базе данных, — то это кандидат на декомпозицию.
Возможно это связано с тем, что деплоймент у нас действительно отличается от вашего — мы не деплоим сервисы у себя, мы отдаём их заказчикам. И нам нужно обеспечить, чтобы наши конфиги были простыми и понятными, даже если у заказчика нет полноценного деплоймента и он выкатывает сервисы вручную, настраивая конфиги в Блокноте. К сожалению, почти все статьи заточены на единственный сценарий деплоймента сервисов у себя дома, где можно поставить сколь угодно продвинутый деплоймент.DotNetCraft
03.04.2017 13:58Натройка конфига в блокноте — это шедеврально. Посоветуйте им хотя Nodepad++. А вообще, предложите им свои услуги по настройке.
Мы разворачиваем на разных компах, что-то у нас, что-то у заказчика, но всегда стараемся минимизировать ручное редактирование конфига. Плюс, конфигурационный файл должен содержать только ту информацию, которая нужна данному конкретному микросервису. Т.о. образом конфиг не разбухает, до неимоверных размеров.Bronx
04.04.2017 02:54Натройка конфига в блокноте — это шедеврально.
«Когда б вы знали, из какого сора...»
А вообще, предложите им свои услуги по настройке.
У нас есть solution team, но его тоже нужно содержать, обучать и, что хуже, постоянно переобучать, иначе они сами оказываются тормозом, а не источником дохода. Часто проще сделать простые конфиги и внятную документацию, понятную даже дураку.
всегда стараемся минимизировать ручное редактирование конфига.
Мы тоже, но без деплой-сервера (а мы не можем гарантировать его налиичие у каждого из кучи мелких кастомеров) это означает написание собственного конфигурационного визарда для каждого микросервиса. Это дополнительные затраты на девелопмент и тестирование, без особого финансового выхлопа. Мы предпочитаем, чтобы кастомер сам заботился об автоматизации, и чтобы даже при ручном деплое он не сильно отвлекал нас вопросами, которых можно было бы избежать, сделав всё просто и хорошо это документировав.
конфигурационный файл должен содержать только ту информацию, которая нужна данному конкретному микросервису.
И я о том же. Но даже у самого микро- из микросервисов есть два типа конфигурационных данных: те, что диктуются выбранным фреймворком/библиотеками, настроены девелопером и не предполагаются к изменению кастомером (скажем, многочисленные настройки WCF со всем ворохом сервисов, байндингов, behaviours и т.п., или настройки dependency injector-а, или настройки доступа к разным путям), и те, которые предполагаются быть настроенными кастомером (в простейшем случае, секция «appSettings»). Причём настроек первого типа может быть огромное количество, и зависит оно не от девелопера. Девелопер рад бы иметь малеький конфигурационный файл с только теми настройками, которые он сам создал, но без разбиения конфига на части это практически нереально.
Sinatr
27.03.2017 10:31+1Никогда не понимал возни с Settings (или как там этот класс зовут) и вообще использование словарей для хранения/доступа к настройкам…
Пишется свой класс, расставляются аттрибуты, сериализуется/десериализуется чем вам больше нравится (json.net, XmlSerializer, ...) в куда вам необходимо (в MemoryStream -> byte[] -> db или же в FileStream)… Если конфигурация одна — синглтон, несколько — доступ через менеджер (Config.Active, Config.Load, etc.).
Все вышеперечисленное занимает буквально десяток строк (если не реализовывать что-то слишком навороченное, к примеру версионинг).
Имхо нет смысла в надстройке над чем-то дефолтовым и кривым. Плюс я не очень понял чем же лучше
SmtpSettings smtpSettings = (dynamic)ConfigurationManager.GetSection(«SmtpSettings»)
особенно доставляет dynamic и (на внимательность) «StmpSettings».MonkAlex
27.03.2017 11:45Как оказалось, некоторые любят app.config.
Я бы ещё понял web.config, но app.config мне категорически не нравится. Пользователя туда не пошлешь, там могут быть и программные настройки.DotNetCraft
27.03.2017 13:10+1Это утилита работает также с web.config…
Пользователя и не надо туда слать. Для этого существуют другие места. App.Config предназначен для хранения настроек приложения и очень хорошо с этим справлется. Добавляем к нему build-сервер (например, TeamCity) и Octopus (мне он нравится) и мы получаем развертывание приложения в один клик/чекин для разных сред (Dev, Test, Sandbox, Release). Поэтому я не понимаю почему он вам так не нравится…
DotNetCraft
27.03.2017 13:29Как правило это работает на маленький проекта без автоматических билдов и развертываний или в самом начале разработки, когда приложение опять-таки маленькое. Когда приложение разрастается до несколько десятков сервисов, да еще и с подключением к сотне других сторонних программ… Вот тут и начинается, что надо бы такую настроечку, а вот тут при чтении конфигурации обязательно проверить, что переменные в Octopuse имеют правильные значения и получается, что изначальные 10 строчек кода в разных проектах превращабтся в 20 в одном, 50 в другом, 10 в третьем, причем от исходных может вообще ничего не остаться.
IlyaMS
27.03.2017 12:53В .NET Core из коробки идет хорошее решение, которое и вложенность любую поддерживает, и принцип разделения ответственности, и встроенный в фреймворк DI container, и вообще. Вот тут можно глянуть, начиная с раздела «Using Options and configuration objects».
DotNetCraft
27.03.2017 13:22-1Неплохая утилита, но есть как минимум два «но».
1. Читает json.
2. Нужен .NET Core
Мне больше всего не нравится то, что конфигураци хранится в json файлах… Сейчас попробую объяснить почему.
При создание проекта в .NET автоматически будет создан app.config, в который студия поместит какие-то свои данные. Затем, например, мы добавим WCF сервис и в том же файле увидим его параметры. При этом, давайте предположим, что мы используем самописный ORM и поэтому строку подключения можем хранить где угодно и мы выбрали json… Как-то выглядит, что настройки расползлись по двум файлам вместо одного…IlyaMS
27.03.2017 22:43Не, .NET Core же как раз в json переполз настройками, в том числе и теми, что «автоматически будет создан...»
В последней версии, правда, вроде обратно переползли, я её еще не ковырял.
Это что касаемо пункта 1 вашего.
Что касаемо пункта 2 — ну это уже надо как данность воспринимать. Вероятнее всего, что в ближайшие пару лет все дружной толпой будем в сторону .NET Core двигаться.DotNetCraft
28.03.2017 01:11+1Пока это будущее наступит пройдет время в течении которого надо выпускать продукты, да еще и поддерживать и развивать старые…
Но, все равно, спасибо за комментарий и ссылку на статью.
OREZ
28.03.2017 09:11.NET Core поддерживает из коробки чтение JSON и XML конфигураций. По умолчанию просто выбирается JSON.
eugenebb
28.03.2017 01:24+1У нас используется немного другое решение.
T4 читает config file и строит правильные классы, включая анализ какого типа должно быть свойство.
Преимущество того что все доступно на момент компиляции и то что не надо руками ничего делать.
Автоматически включены кэш и т.п.DotNetCraft
28.03.2017 01:24Интересный подход. По-сути, вы написали свою «библиотеку», подобной ConfigurationParser, но на Т4.
Кстати, а если в Т4 появляется ошибка, то как тестировать? Несколько раз посоздавали разные конфиги и на этом все?
На и в догонку, а как быть если адреса почты написаны через точку с запятой, а в системе они должны быть с листе, да еще и проверены с помощью RegEx?
И на последок, если это не коммерческая тайна, то можно взглянуть на код?eugenebb
28.03.2017 18:11T4 тестируется точно также как и любой другой код, т.е. при исполнении если произошла ошибка выдается сообщение где и что случилось. Есть отладчик в котором можно по шагам пройтись. Не помню чтобы были проблемы с тестированием.
Так как это кастомный T4 шаблон, то можно реализовать все что угодно, плюс с появлением partial в C#, автогенерирование и расширение кода делается очень удобно.
Если адреса почты написаны через точку с запятой, то добавляем проверку на этот случай и при совпадении, генерируем код который в результате будет возвращать список строк и при первом обращении парсить значения из конфига и проверять на правильность адреса.
Реальный код скорее всего нельзя, но сделал простой пример, включая список емайлов.
Добавить в проект как файл с раширением .tt в ту же директорию где и web.config (либо добавить относительный путь до него в строчке с Host.ResolvePath(«Web.config»)
В результате у вас будет автоматически создан cs файл.
Если конфиг изменился, выбрать «Run Custom Tool» и cs файл пересоздасться.
Длинный код<#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ assembly name="System.Xml" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Data.SqlClient" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Xml" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Text.RegularExpressions" #> <#@ template language="C#" debug="true" hostspecific="true" #> <#@ output extension = ".cs" #> using System; using System.Configuration; using System.Linq; using System.Text.RegularExpressions; <# var configs = new List<KeyValuePair<String, String>>(); configs.Add(new KeyValuePair<String, String>("TestConfig", System.IO.File.ReadAllText(Host.ResolvePath("Web.config")))); var emailRegex = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)(,|;)([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)"); #> ////////////////////////////////// // Autogenerated class based on web.config file // // WARNING: Any changes to the file will be overwritten the next time the source code will be autogenerated ////////////////////////////////// namespace MyNamespace.Config { <# foreach(var config in configs) { #> public static class <#= config.Key #> { private static readonly object _lock = new object(); private static Regex _emailRegex = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$"); <# XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(config.Value); foreach(XmlElement node in xmlDoc.SelectNodes("/configuration/appSettings/add")) { string key = node.GetAttribute("key"); string keyType = "string"; string converterBegin = ""; string converterEnd = ""; string safeKey = Regex.Replace(key, "[^A-Za-z0-9_]", ""); string keyFirstLower = safeKey.Substring(0,1).ToLower() + safeKey.Substring(1); string keyFirstUpper = safeKey.Substring(0,1).ToUpper() + safeKey.Substring(1); string value = node.GetAttribute("value"); if(emailRegex.IsMatch(value)) { keyType = "string[]"; converterBegin = "!String.IsNullOrEmpty(s) ? "; converterEnd = ".Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) : new string[0];\r\n" + " if(_" + keyFirstLower + ".Any(a => !_emailRegex.IsMatch(a)))\r\n" + " throw(new Exception(\"Invalid email in config\"))"; } else if(Regex.IsMatch(value, @"^[0-9]+$")) { keyType = "int?"; converterBegin = "!String.IsNullOrEmpty(s) ? Int32.Parse("; converterEnd = ") : (int?)null"; } else if(Regex.IsMatch(value, @"^(true|false)$", RegexOptions.IgnoreCase)) { keyType = "bool"; converterBegin = "!String.IsNullOrEmpty(s) ? Boolean.Parse("; converterEnd = ") : false"; } else if(Regex.IsMatch(value, @"^https?://[^{]+$", RegexOptions.IgnoreCase)) { keyType = "Uri"; converterBegin = "!String.IsNullOrEmpty(s) ? new Uri("; converterEnd = ") : null"; } #> private static bool _isLoaded<#= keyFirstUpper #>; private static <#= keyType #> _<#= keyFirstLower #>; public static <#= keyType #> <#= keyFirstUpper #> { get { if(!_isLoaded<#= keyFirstUpper #>) { lock(_lock) { string s = ConfigurationManager.AppSettings["<#= key #>"]; _<#= keyFirstLower #> = <#= converterBegin #>s<#= converterEnd#>; _isLoaded<#= keyFirstUpper #> = true; } } return _<#= keyFirstLower #>; } } <# if(keyType != "string") { #> private static bool _isLoaded<#= keyFirstUpper #>AsString; private static string _<#= keyFirstLower #>AsString; public static string <#= keyFirstUpper #>AsString { get { if(!_isLoaded<#= keyFirstUpper #>AsString) { lock(_lock) { _<#= keyFirstLower #>AsString = ConfigurationManager.AppSettings["<#= key #>"]; _isLoaded<#= keyFirstUpper #>AsString = true; } } return _<#= keyFirstLower #>AsString; } } <# } } #> } <# } #> }
DotNetCraft
29.03.2017 12:27Спасибо за пример. Попробую поиграться…
А тем временем позвольте немного критики.
Если я правильно понял, то вот этот длинный код создает класс с одной переменной и распарсивает значение из конфига.На мой взгляд как-то не рационально.
Также, если мы захотим добавить еще логики, то мы должны менять tt? Например, повторные адреса игнорировать. Можно, конечно, написать partial class и там реализовать метод со всякими проверками, а в tt его только дергать… По-моему как-то слишком сложно. Учитывая, что конфиг это та часть программы, которая очень редко меняется.
З.Ы. Еще раз спасибо за пример — буду разбираться.eugenebb
29.03.2017 16:46Самое простое — скопировать в проект и попробовать, результат сразу будет виден.
Тот пример создает класс с несколькими properties которые будут соответствовать вашей конфигурации.
пример
<configuration> <appSettings> <add key="AdminEmails" value="admin1@test.com,admin2@test.com" /> <add key="EnableRegistration" value="false" /> <add key="TempDir" value="c:\temp\" /> <add key="MaxNumberUsers" value="123" /> <add key="ApiServer" value="http://www.google.com" /> </appSettings> </configuration>
Результат будет что-то типа (упрощенно)
namespace MyNamespace.Config { public static class TestConfig { public static string[] AdminEmails { get; } public static bool EnableRegistration { get; } public static string TempDir { get; } public static int MaxNumberUsers { get; } public static Uri ApiServer { get; } } }
Если в проекте несколько конфигураций, добавить
configs.Add(new KeyValuePair<String, String>("Test2Config", System.IO.File.ReadAllText(Host.ResolvePath("../Project2/Web.config")))); configs.Add(new KeyValuePair<String, String>("Test3Config", System.IO.File.ReadAllText(Host.ResolvePath("../Project3/Web.config"))));
будет созданно еще два класса.
Если вы хотите добавть логики «Например, повторные адреса игнорировать.» — то это должно быть сделано где-то все равно, в tt или еще где-то, разница не большая.
Моя практика показывает что в большинстве случаев, ничего волшебного в конфигах обычно не хранят.
По поводу того что редко меняется, в этом как раз и преимущество, не надо помнить детали и руками что-то делать. Нажал кнопку и у тебя актуальный код.
Преимущество кодогенерации в том что сделал один раз и потом оно за тебя работает.
Если у вас один проект и вы его годами поддерживаете, то наверное как сделали, так и ок.
А если создается что-то новое, более-менее часто, то как скопировав tt в новый проект, у вас уже все сгенерированно.
Нашли ошибку и исправить — поменяли шаблон и везде все поменяно. Надо сгенерировать mock config для тестов — поменяли пару строчек в шаблоне и он вам тестовые классы сделал.
Ну и самое главное, это не единичный шаблон, а часть общей системы которая позволяет автоматизировать рутинные части.
Например для WebApi, шаблоны через reflection создают TypeScript клиент для доступа к этому API.
Есть шаблоны которые по XML и JSON создают структуры классов, другие по структуре базы создают data access layer, по структуре классов создают формы для работы с ними и т.п.
Некоторые шаблоны используются только для scaffolding, другие регенирируют код при обновлениях.
Всегда есть масса мест где можно заменить ручную работу.DotNetCraft
30.03.2017 13:31А если создается что-то новое, более-менее часто, то как скопировав tt в новый проект, у вас уже все сгенерированно.
Новый проект — новый конфиг. А учитывая движение в сторону микросервисов, то получается, что и конфиги почти пыстые. Максимум 5-7 микросервисов (это мой опыт) и всякие частные настройки для конкретного сервиса. Т.о. повторное использование tt тут не подходит. Да и любое повторное использование кода не подходит. Конфиг почти каждый раз пишется с 0.
Надо сгенерировать mock config для тестов — поменяли пару строчек в шаблоне и он вам тестовые классы сделал.
Посмотрите мой ответ Bronx. Там я описываю, как создавать конфиги для разных целей без особых усилий.
В общем, пока я не увидел преимущество автоматической генерации конфигурационных классов с из заполнением. Скорей всего, тут кто к чему привык, ибо серебрянной пули не существует…
Ну и самое главное, это не единичный шаблон, а часть общей системы которая позволяет автоматизировать рутинные части.
Вот после этого я спорить не буду ибо согласен. Если есть смысл и возможность, то надо генерить.
Sirion
Тупой вопрос от js-кодера: а почему плохо просто построить XML DOM и читать всё оттуда?
DotNetCraft
Приветствую
В мире .NET разработчики привыкли работать с объектами. Поэтому практически в любом приложении можно встретить строчки
Используем myAppSettings.ConnectionString по своему усмотрению.
Это очень простой пример, но даже в нем нам приходится собственноручно создавать объект с настройками и заполнять его. Библиотека все это сделает за нас.
Все усложняется, когда появляются настройки со многими вложенностями. Например, в моем текущем проекте у нас в зависимости от страны — свои настройки, в которых прописываются подключения к различные сторонним сервисам, строки подключения к БД и тд. В результате чего для загрузки конфигурационного файла пришлось бы писать очень много кода, а так — создал классы и загрузил все одной строчкой.
Deosis
Дополню ответ DotNetCraft.
В случае прямой работы придется вводить название секций и атрибутов в виде строк.
А это прямой путь к опечаткам и головной боли при переименовании.
Sirion
А каким образом приведённый здесь подход избавляет от опечаток и боли? Всё равно где-то придётся прописывать имя тега, откуда берётся настройка, телепатию в дотнет вроде ещё не завезли. И если что-то переименуется, всё равно придётся что-то в связи с этим менять. Разве нет?
Deosis
В примере автора имя тега совпадает с именем поля, если изменится, то просто дописываем атрибут, а библиотека за нас смаппит. Даже для загрузки секции можно написать обертку, чтобы не вводить её имя вручную.
У вас же при изменении атрибута или секции придется изменять все места, где она используется.
wlbm_onizuka
лучше ошибиться в одном месте, чем в нескольких. разве нет?
DotNetCraft
Дополню Deosis…
Если в конфигурационном файле будет жлемент, а в классах его не будет, то система сгенерит Exception. Таким образом происходит проверка конфига и классов.