[Ww.Text] CustomConfig2 - czyli jak zrobic aby sie nie narobic
Wczoraj przeczytaLem post Darka omawiający hierarchiczne pliki konfiguracyjne. Podczas czytania naszły mnie 3 mysli.
- Kropkowana notacja przypomina mi Javowe properties (czy to jescze tak sie nazywa?). Mialem z tym doczynnienia daawno temu jak jeszcze cos robilem przy Java. Srednio mi sie to posdobalo i do dzis uwazam ze reprezentacja hierarchii wartosci za pomoca notacji kropkowej a nie skorzystanie z gotowego, znanego, popularnego, sprawdzonego rozwiazania nie jest najszczesliwszym rozwiazaniem (do typowych zastosowan, jako ze na pewno znajda sie zastosowanie gdzie take java-ini-like rozwiazanie sprawdzi sie duzo lepiej niz XML).
- Po moich ostatnich "zawodowych" walkach z przetwarzaniem plikow tekstowych i problemami stron kodowych, unicode, kompatybilnosci wstecz i wprzod (na szczescie bez kompatybilnosci gora-dol czy prawo-lewo ;-) ) ustawiem regionalnych, jezyka systemu i non-win systemow - cisnienie mi skoczylo na mysl o sledzeniu problemow i ich rozwiazywaniu. Po pierwszym tekscie nie widac na razie jak tego typu problemy moga byc rozwiazane. Prawdopodobnie dlatego ze Darek nie wymaga podobnej funkcjonalnosci od tego rozwiazania i jemu to wystarcza to jest calkowicie w porzadku.
- Pozazdroscilem checi pisania, testowania, dokumentowania, wdrazania tego rozwiazania w zespole, jakoze jestem leniwa osoba. Z tego tez powodu powstal ten blog, poniewaz postanowilem zrobic podobna funkcjonalnosc korzystajac z wbudowanej funkcjonalnosci w .NET Frameworka.
Configuration w .NET Frameworku wspiera chyba od poczatku hierarchiczne pliki konfiguracyjne. Przeciez po instalacji mamy machine.config i "app.config". Ten drugi moze zmienic "odziedziczone" wlasnosci z machine.config lub dodac nowe a nawet usunac - oczywiscie wszystko "lokalnie" w ramach aplikacji. Wiec wykorzystajmy ta infrastrukture do "naszych" celow - czyli rozwiazania ktore posiada glowny plik konfiguracyjny oraz opcjonalne per user nadpisane ustawienia.
Nie stadardowy plik konfiguracyjny
Najpierw pokaze jak zaladowac konfiguracje z prawie dowolnego pliku. korzystajac z metody ConfigurationManager.OpenExeConfiguration(string exePath) mozemy zaladowac plik konfiguracyjny dla wskazanego pliku "exe" (a tak naprawde dowolnego pliku) gdzie plik konfiguracyjny to exePath z dodanym sufiksem ".config".
const string cfg = @"<?xml version='1.0' encoding='utf-8' ?>
<configuration>
<configSections>
<section name='mySection' type='ZineNet.CustomCfg2.MySection, ZineNet.CustomCfg2'/>
</configSections>
<mySection>
<myProperties>
<add name='p1' value='v1' />
<add name='p2' value='v2' />
<add name='p4' value='v4' />
</myProperties>
</mySection>
</configuration>";
File.WriteAllText(@"CfgDir\Demo1.tmp", "");
File.WriteAllText(@"CfgDir\Demo1.tmp.config", cfg, Encoding.UTF8);
var exeCfg = ConfigurationManager.OpenExeConfiguration(Path.GetFullPath(@"CfgDir\Demo1.tmp"));
var myCfg = (MySection)exeCfg.GetSection("mySection");
foreach (MyPropertyElement p in myCfg.MyProperties) {
Console.WriteLine("{0}:{1}", p.Name, p.Value);
}
W tym przykladzie tworze plik na dysku, gdze zapisuje przykladowa konfiguracje. Nastepnie otwieram plik konfiguracyjny wskazujac na tymczasowy/sztuczny plik CfgDir\Demo1.tmp. Metoda OpenExeConfiguration sprawdza istnienie pliku przekazanego w argumencie exePath, pomimo iz z niech nie korzysta. Dlatego wytwarzam rowniez pusty plik o takiej nazwie.
Dowolne hierarchiczne pliki konfiguracyjne
Teraz przyklad blizszy temu co zaprezentowal Darek w swoim tekscie. Utworze 2 pliki konfiguracyjne, zaladuje oba uzyskujac ten sam typ sekcji konfiguracyjnej jak w przykladzie pierwszym.
const string cfg = @"<?xml version='1.0' encoding='utf-8' ?>
<configuration>
<configSections>
<section name='mySection' type='ZineNet.CustomCfg2.MySection, ZineNet.CustomCfg2' allowExeDefinition='MachineToRoamingUser'/>
</configSections>
<mySection>
<myProperties>
<add name='p1' value='v1' />
<add name='p2' value='v2' />
<add name='p4' value='v4' />
</myProperties>
</mySection>
</configuration>";
const string cfgLocal = @"<?xml version='1.0' encoding='utf-8' ?>
<configuration>
<mySection>
<myProperties>
<remove name='p2' />
<add name='p5' value='v5' />
</myProperties>
</mySection>
</configuration>";
File.WriteAllText(@"CfgDir\Demo2.config", cfg, Encoding.UTF8);
File.WriteAllText(@"CfgDir\Demo2.Local.config", cfgLocal, Encoding.UTF8);
var exeCfg = ConfigurationManager.OpenMappedExeConfiguration(
new ExeConfigurationFileMap {
ExeConfigFilename = Path.GetFullPath(@"CfgDir\Demo2.config"),
RoamingUserConfigFilename = Path.GetFullPath(@"CfgDir\Demo2.Local.config")
},
ConfigurationUserLevel.PerUserRoaming
);
var myCfg = (MySection)exeCfg.GetSection("mySection");
foreach (MyPropertyElement p in myCfg.MyProperties) {
Console.WriteLine("{0}:{1}", p.Name, p.Value);
}
Tym razem uzywam innej metody OpenMappedExeConfiguration gdzie przekazuje obkekt ExeConfigurationFileMap. Obiekt file map zawiera sciezki do konkretnych plikow konfiguracyjnych a nie zaleznych plikow "exe". Za pomoca drugiego argumentu ConfigurationUserLevel specyfikujemy ktore pliki musza byc zaladowane, a ktore zignorowane, jako ze w typie ExeConfigurationFileMap mozeny wyspecyfikowac 4 pliki konfiguracyjne roznego poziomu. Musimy zezwolic na ta operacje przy definicji sekcji w "pierwszym" pliku konfiguracyjnym za pomoca atrybutu "allowExeDefinition".
Jeszcze wiecej funkcjonalnosci we wlasnych plikach
Powyzszy przyklad dostarcza z grubsza funkcjonalnosci zaprezentowanej przez Darka. Lecz mozemy skorzystac bez zmiany kodu przetwarzajacego, z pozostalej funkcjonalnosci. Dla przykladu pokaze mozliwosc blokowania redefiniowania fragmentow konfiguracji.
const string cfg = @"<?xml version='1.0' encoding='utf-8' ?>
<configuration>
<configSections>
<section name='mySection' type='ZineNet.CustomCfg2.MySection, ZineNet.CustomCfg2' allowExeDefinition='MachineToRoamingUser'/>
</configSections>
<mySection>
<myProperties>
<add name='p1' value='v1' />
<add name='p2' value='v2' lockItem='true' />
<add name='p4' value='v4' />
</myProperties>
</mySection>
</configuration>";
const string cfgLocal = @"<?xml version='1.0' encoding='utf-8' ?>
<configuration>
<mySection>
<myProperties>
<remove name='p2' />
</myProperties>
</mySection>
</configuration>";
File.WriteAllText(@"CfgDir\Demo3.config", cfg, Encoding.UTF8);
File.WriteAllText(@"CfgDir\Demo3.Local.config", cfgLocal, Encoding.UTF8);
var exeCfg = ConfigurationManager.OpenMappedExeConfiguration(
new ExeConfigurationFileMap {
ExeConfigFilename = Path.GetFullPath(@"CfgDir\Demo3.config"),
RoamingUserConfigFilename = Path.GetFullPath(@"CfgDir\Demo3.Local.config")
},
ConfigurationUserLevel.PerUserRoaming
);
var myCfg = (MySection)exeCfg.GetSection("mySection");
foreach (MyPropertyElement p in myCfg.MyProperties) {
Console.WriteLine("{0}:{1}", p.Name, p.Value);
}
Ustawiajac atrybut lockItem='true' na jednej z wlasnych wlasciwosci, nie pozwalamy na redefiniowanie go w "potomnym" pliku konfiguracyjnym. Podczas uruchamiania przykladu trzeciego dostaniemy wyjatek o podobnej tresci: "The attribute 'p2' has been locked in a higher level configuration. (K:\ZineNet\ZineNet.CustomCfg2\bin\Debug\CfgDir\Demo3.Local.config line 5)".
Inne uwagi
- Zamiast hierarchii zagniezdzania elementow, mozna ja "rozplaszczyc" i zastosowac notacje kropkowa podobnie jak to uczynil Darek. Poprostu trzeba nadpisac w definicji sekcji sposob deserializacji/serializacji
- Podejscie z zagniezdzaniem elementow XML zamiast notacji kropkowej wymusza na nas stworzenie odpowiednich klas konfiguracji. Zyskujemy na tym walidacje pliku konfiguracyjnegom lecz tracimy na tym ze mamy dodatkowa prace rzemieslnicza zwiazana z ich napisaniem.
Code Sculptor.