Spis treści:

Kategoria:C#


Własne sekcje w app.config i web.config

Konfiguracja po swojemu

Obserwując ewolucję systemów informatycznych można odnieść wrażenie, że ich konfiguracja staje się coraz bardziej złożona. Pliki konfiguracyjne, dawniej skromne, teraz zawierają coraz więcej parametrów. Wyróżniłbym dwa główne czynnyki takiego stanu rzeczy. Po pierwsze, same systemy są coraz większe, więc i parametrów więcej. Po drugie, zawsze miło powiedzieć, że nasz system jest konfigurowalny. Konfigurowalny to modne, trochę tajemnicze słowo, a i dobrze się sprzedaje.

Sam niedawno, we wpisie Wzorzec dekoratora w praktyce wspomniałem o możliwości dodatkowej konfiguracji. Jakie są możliwości?

Do sklepu z artykułami motoryzacyjnymi wchodzi młodzieniec i pyta:
 - Czy są łańcuchy do jawy?
 - Nie ma, są tylko do emzetki.
 - To poproszę do emzetki.
 - Ale ten łańcuch nie pasuje do jawy!
 - Nic nie szkodzi, po meczu wszystko mi jedno czym biję!

Jedną z alternatyw jest wykorzystanie zwykłego słownika pra klucz-wartość. W sekcji configuration/appSettings można dodać dowolną ilość elementów, a każdy z nich reprezentowany będzie przez element add. Rolę klucza pełni atrybut key, rolę wartości atrybut value. Takie rozwiązanie jest akceptowalne, ale ma pewne wady. O ile pojedyncze elementy można tam umieszczać bez problemu, a tyle z listami już tak łatwo nie idzie. To tak, jakbyśmy chcieli reprezentować tablicę za pomocą zmiennych (klucz jako nazwa zmiennej). Jeżeli jest nam wszystko jedno, tak jak dżentelmenowi z historyjki, możemy skorzystać z tej metody i zakończyć czytanie.

Jest też rozwiązanie drugie, a mianowicie stworzenie własnej sekcji konfiguracyjnej. Wymaga większego nakładu pracy, ale jest bardziej eleganckie i daje nam większą swobodę. Generalnie rzecz ujmując - nasze możliwości są praktycznie nieograniczone. Przejdźmy zatem do rzeczy!

Planowany wygląd sekcji

Plan własnej sekcji app.config lub web.config zawsze zaczynam od strony XML. Kod pisze się raz, a sekcję konfiguracji, o ile była potrzeba jej powstania, zmienia się więcej niż raz. To konfiguracja ma być przyjemna. Chciałbym, aby wyglądała ona następująco:

<myExtensions>
  <add name="Loghandler="MyNamespace.LogClass, MyAssembly"/>
  <add name="Securityhandler="MyNamespace.SecurityClass, MyAssembly"/>
</myExtensions>

Sekcja będzie otoczona znacznikami myExtensions, a kolejne elementy będą dodawane tak, jak wszystkie inne elementy kolekcji w plikach konfiguracyjnych. Każdy z takich elementów będzie posiadał nazwę rozszerzenia oraz wskazanie na klasę, która będzie realizować funkcję rozszerzenia. To takie nawiązanie do wspomnianego wcześniej wpisu Wzorzec dekoratora w praktyce.

Nic nie stoi na przeszkodzie, aby taką sekcję w plikach konfiguracyjnych umieścić i odczytać ją tak, jak zwykłe pliki XML. Można to jednak zrobić elegancko.

Obiektowa reprezentacja plików konfiguracyjnych

Każda sekcja pliku konfiguracyjnego reprezentowana jest przez klasę ConfigurationSection. Aby wprowadzić do niej własne elementy, wystarczy wykorzystać dziedziczenie i wstawić w klasie pochodnej wszystko to, co nam będzie potrzebne. A będzie to kolekcja.

Z założenia każdy poziom zagłębienia pliku konfiguracyjnego powinien mieć nazwę. Są to przecież odpowiedniki znaczników XML. Jest jednak pewna furtka, z której skorzystałem. Jeden element w danym zakresie może być domyślny. Popatrzmy na klasę reprezentującą sekcję i wszystko się wyjaśni:

public class ExtensionSectionConfigurationSection
{
    [ConfigurationProperty(null, IsDefaultCollection = true, IsRequired = true)]
    public ExtensionCollection Extensions
    {
        getreturn (ExtensionCollection)this[(string)null]; }
        setthis[(string)null] = value; }
    }
}

Klasa ma jedną właściwość, która z kolei ma jeden atrybut. Ten atrybut definiuje związek pomiędzy światem obiektowym C#, a światem konfiguracji XML. Pierwszy argument to nazwa kolekcji, która, poza jednym wyjątkiem (furtka!), powinna być ustawiona. Aby element był domyślny (warunek furtki!), należy ustawić atrybut IsDefaultCollection na true. Jest jeszcze jeden atrybut - IsRequired. Określa on, czy wartość jest obowiązkowa.

Każda z klas reprezentujących jakiś fragment pliku konfiguracyjnego posiada indekser (this[]), który wskazuje na bieżący element. Instrukcja this[NAZWA] będzie zatem wskazywała na element klasy o nazwie NAZWA. Nasz element nie ma nazwy (w zasadzie ma nazwę null, bo jest domyślny), więc odwołujemy się do niego z kluczem null. Cała reszta to rzutowania na nasz własny typ kolekcji.

Obsługa kolekcji

Kolekcje konfiguracyjne dziedziczą z typu ConfigurationElementCollection. Pociąga to za sobą konieczność implementacji dwóch metod wirtualnych: CreateNewElement i GetElementKey. Pierwsza, niejawnie, służy do zdefiniowania typu. Tworzy ona nowy obiekt, który będzie uzupełniany danymi z pliku konfiguracyjnego. Druga metoda pozwala zdefiniować klucz. Elementy kolekcji muszą być w jakiś sposób identyfikowane i to my decydujemy jak. W pokazanym przykładzie będzie to właściwość Name. Popatrzmy na przykład:

public class ExtensionCollectionConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement()
    {
        return new ExtensionElement();
    }
    protected override object GetElementKey(ConfigurationElement element)
    {
        return ((ExtensionElement)element).Name;
    }
}

GetElementKey, typ parametru metody, jest klasą bazową wszystkich elementów pliku konfiguracyjnego (także ConfigurationElementCollection i ConfigurationSection). W przykładzie wykorzystana zostanie własna klasa, pochodna ExtensionElement, o nazwie ExtensionElement. Stąd konieczność rzutowania.

Elementy kolekcji

Przyjrzyjmy się klasie reprezentującej element listy, której kod zaprezentowany jest poniżej:

public class ExtensionElementConfigurationElement
{
    [ConfigurationProperty("name", IsKey = true, IsRequired = true)]
    public string Name
    {
        getreturn (string)this["name"]; }
        setthis["name"] = value; }
    }

    [ConfigurationProperty("handler", IsRequired = true)]
    public string Handler
    {
        getreturn (string)this["handler"]; }
        setthis["handler"] = value; }
    }
}

Atrybut ConfigurationProperty jest już znany i nie będę go drugi raz opisywał. Zwrócę tylko uwagę na jeden parametr, którego wcześniej nie było: IsKey. To on definiuje klucz. Ten właśnie klucz należy utożsamiać z kluczem, którym posługuje się kolekcja. Odpowiednik w pliku XML definiuje atrybut ConfigurationProperty i jego pierwszy parametr.

Konfiguracja konfiguracji w pliku app.config lub web.config

To już prawie koniec. Reszta konfiguracji znajduje się w pliku konfiguracyjnym, zależnym od środowiska. To tam definiuje się nazwę sekcji oraz jej budowę, wskazując klasę typu ConfigurationSection. Popatrzmy na przykładową zawartość:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <configSections>
    <section name="myExtensions"
             type="CustomConfig.ExtensionSection, CustomConfig/>
  </configSections>
  <myExtensions>
    <add name="Loghandler="MyNamespace.LogClass, MyAssembly"/>
    <add name="Securityhandler="MyNamespace.SecurityClass, MyAssembly"/>
  </myExtensions>
</configuration>

Sekcje konfiguracyjne konfiguruje się w sekcji configSections podając nazwę i typ, w którym znajduje się jej implementacja. Nazwa musi się pokrywać ze znacznikiem naszej wymarzonej sekcji.

Wczytanie wartości z własnej sekcji

Prędzej czy później dane z sekcji będą do aplikacji wczytane. Jak? Bardzo podobnie do innych ustawień konfiguracji. Przyjrzyjmy się poniższemy listingowi:

static void Main(string[] args)
{
    var serviceConfigSection = ConfigurationManager.GetSection("myExtensions"as ExtensionSection;
            
    foreach (ExtensionElement item in serviceConfigSection.Extensions)
        Console.WriteLine("{0} = {1}", item.Name, item.Handler);

    //Log = MyNamespace.LogClass, MyAssembly
    //Security = MyNamespace.SecurityClass, MyAssembly
}

Sekcję wczytujemy korzystając z klasy ConfigurationManager i jej statycznej metody GetSection, a potem, po rzutowaniu na właściwy typ, korzystamy jak ze zwykłej klasy.

Własne nazwy elementów kolekcji

Pokazany wcześniej plik konfiguracyjny korzysta z elementów o nazwie add. Można pójść dalej i nadać tym elementom własne nazwy. Stosuje się różne podejścia, co zresztą można odczuć i zaobserwować. Domyślne ustawienia aplikacji to pary klucz-wartość reprezentowane alementem add, ale już konfiguracja sekcji konfiguracyjnej posiada elementy o nazwie section. Jak zatem uzyskać tę drugą wersję? Popatrzmy na nieco zmodyfikowaną klasę ExtensionCollection:

[ConfigurationCollection(typeof(ExtensionElement), AddItemName="myExtension")]
public class ExtensionCollectionConfigurationElementCollection
{
    protected override ConfigurationElement CreateNewElement()
    {
        return new ExtensionElement();
    }
    protected override object GetElementKey(ConfigurationElement element)
    {
        return ((ExtensionElement)element).Name;
    }
}

Drobna zmiana, dodanie atrybutu, załatwia sprawę. Tym razem typ podany jest już jawnie, bo musi być. Pierwszy parametr atrybutu jest obowiązkowy. Drugi definiuję nazwę elementów. Przyjrzyjmy się jeszcze plikowi konfiguracyjnemu, który teraz przyjmie następującą postać:

<?xml version="1.0encoding="utf-8?>
<configuration>
  <configSections>
    <section name="myExtensions"
             type="CustomConfig.ExtensionSection, CustomConfig/>
  </configSections>
  <myExtensions>
    <myExtension name="Loghandler="MyNamespace.LogClass, MyAssembly"/>
    <myExtension name="Securityhandler="MyNamespace.SecurityClass, MyAssembly"/>
  </myExtensions>
</configuration>

Obsługa sekcji w metodzie Main i wynik będą identyczne.

Podsumowanie

Konfiguracja własnych sekcji konfiguracyjnych to dość obszerny temat, ale w większości przypadków nie ma potrzeby wnikania w szczegóły. Każda z klas bazowych ma różne metody, gotowe do nadpisania i wykorzystania, każdy z pokazanych atrybutów ma dodatkowe parametry. Zainteresowanych odsyłam do dokumentacji, zachęcam do dzielenia się spostrzeżeniami. W razie niejasności lub pytań można skorzystać z komentarzy pod tekstem.

Kategoria:C#

, 2013-12-20

Brak komentarzy - bądź pierwszy

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?