wtorek, 2 listopada 2010

ImmDoc.NET - lekkie narzędzie do generowania dokumentacji HTML

Wstęp

Czy pamiętacie jeszcze Visual Studio 2003 i jego bardzo przydatną funkcjonalność, dzięki której można było generować dokumentację projektu w formacie HTML. Podejrzewam, że wielu z Was pamięta i podobnie jak mi, brakuje Wam tego w nowszych wersjach Visual Studio.

Ostatnio potrzebowałem takiej funkcjonalności, aby udokumentować bibliotekę klas, nad którą pracowałem i niestety nie udało mi się znaleźć odpowiedniego narzędzia. Jestem świadom istnienia projektów NDoc oraz Sandcastle, jednakże pierwszy z nich już od dawna nie jest rozwijany i nie wspiera .NET 2.0 a drugi jest niepotrzebnie skomplikowany w użyciu i niewiarygodnie wolny. Postanowiłem więc poświęcić trochę czasu na opracowanie własnego narzędzia, licząc na to, że przy okazji nauczę się czegoś nowego oraz że projekt będzie użyteczny nie tylko dla mnie, ale również dla innych programistów.

Poniżej zamieszczam listę obszarów, w których musiałem poszerzyć swoją wiedzę, żeby ukończyć ten projekt. Dzięki temu możesz ocenić, czy analiza kodu źródłowego może nauczyć również Ciebie czegoś nowego:

  • mechanizmy refleksji,
  • niuanse języków C# i MSIL (Microsoft Intermediate Language),
  • wyrażenia regularne,
  • składnia plików z komentarzami XML generowanych przez kompilator,
  • czytanie plików XML oraz ich walidacja za pomocą schematów XSD,
  • osadzane zasoby.

Przykład

Zanim zagłębicie się w szczegóły techniczne projektu ImmDoc.NET, być może zechcecie wpierw odwiedzić tę stronę, aby zobaczyć przykładową dokumentację wygenerowaną przez to narzędzie. Jest to dokumentacja wygenerowana na podstawie kilku, przypadkowo wybranych zestawów pochodzących z frameworka .NET: System.Runtime.Remoting, System.Security i System.Transactions.

Użycie

Obecnie jest tylko jeden sposób uruchamiania ImmDoca — z wiersza poleceń. Jest to po części spowodowane tym, że stworzenie GUI zajęłoby trochę czasu. Jednak dzięki takiemu podejściu łatwo jest zautomatyzować generowanie dokumentacji poprzez skrypty wsadowe albo narzędzia wspierające proces budowania projektów, jak np. NAnt.

Korzystanie z ImmDoca jest całkiem proste i intuicyjne. Możesz wszystkie swoje zestawy .NET i pliki z komentarzami XML umieścić w jednym folderze razem z plikiem wykonywalnym ImmDocNet.exe. Gdy uruchomisz program, rozpocznie się przetwarzanie plików wejściowych i po chwili powinieneś zobaczyć folder doc zawierający wygenerowaną dokumentację. Jeżeli chciałbyś, żeby ImmDoc pominął jakieś pliki, możesz to zrobić, korzystając z opcji wiersza poleceń. Wszystkie tego typu opcje wymieniam poniżej:

  • -pn, -ProjectName:STRING - użyj tej opcji, jeśli chcesz nadać swojej dokumentacji jakąś nazwę
  • -ex, -Exclude:FILE - jeśli jawnie nie podasz w wierszu poleceń listy plików do przetwarzania, możesz użyć tej opcji w celu pominięcia określonych plików z bieżącego folderu
  • -od, -OutputDirectory:DIR - użyj tej opcji, aby określić nazwę folderu, gdzie wygenerowana dokumentacja zostanie zapisana
  • -fd, -ForceDelete - ImmDoc.NET sam nie usunie folderu wyjściowego, jeśli już istnieje, chyba, że użyjesz tej opcji
  • -vl, -VerboseLevel:LEVEL - opcja ta pozwala Ci zdecydować, jak wiele komunikatów program będzie wyświetlał; im wyższa wartość, tym więcej informacji na wyjściu; LEVEL może przyjmować wartości od 0 (brak informacji) do 3 (maksymalna ilość informacji)

Jest jeszcze jedna funkcjonalność, o której, tak sądzę, warto wspomnieć. Ze względu na to, że nie da się umieszczać komentarzy XML do przestrzeni nazw, a także możesz chcieć wygenerować dokumentację dla wielu zestawów jednocześnie, ImmDoc pozwala Ci dostarczyć takie dodatkowe komentarze. W tym celu musisz utworzyć plik XML z rozszerzeniem .docs. Składnia tego pliku jest bardzo prosta, ale zwróć uwagę, że będzie on walidowany za pomocą schematu XSD — AdditionalDocumentation.xsd — który znajduje się w archiwum ZIP z kodem źródłowym projektu. Jak już stworzysz taki plik, wystarczy, że umieścisz go w tym samym folderze co zestawy i pliki XML z komentarzami, które chcesz przetworzyć albo jawnie podasz jego nazwę w wierszu poleceń. Przykład:

<?xml version="1.0" encoding="utf-8"?>
<summaries>
    <assembly name="SomeAssembly">
        <summary>
            Tutaj jest opis zestawu.
        </summary>
        <namespace name="Some.Namespace">
            <summary>
                Tutaj jest opis przestrzeni nazw.
            </summary>
        </namespace>
        ...
    </assembly>
    ...
</summaries>

Projekt

ImmDoc.NET składa się z dwóch zestawów, które na końcu procesu budowania, łączone są za pomocą narzędzia ILMerge. Dzięki temu cały program zamknięty jest w jednym pliku EXE. Pierwszym ze wspomnianych zestawów jest ImmDocNetLib, który zajmuje się wszystkim tym, co związane jest z analizą i przetwarzaniem przekazanych zestawów i plików XML z komentarzami. Drugi zestaw to ImmDocNet, który jest aplikacją konsolową korzystającą z ImmDocNetLib i stanowiącą interfejs użytkownika.

Jak już wspomniałem, ImmDocNetLib posiada dwie główne odpowiedzialności: analizuje przekazane pliki wejściowe oraz generuje dokumentację. Przyjrzyjmy się bliżej pierwszemu z tych zadań, który w zasadzie sprowadza się do skorzystania z refleksji w celu zebrania szczegółowych informacji na temat zestawów i modułów, które te zestawy zawierają.

Najważniejsze klasy znajdują się w przestrzeni nazw Imm.ImmDocNetLib.MyReflection.MetaClasses. W uproszczeniu można powiedzieć, że klasy te są lżejszymi odpowiednikami klas z przestrzeni nazw System.Reflection. Generalnie, praktycznie wszystkie z nich dziedziczą po klasie MetaClas, która, między innymi, zawiera takie właściwości jak Name, Summary and Remarks. Inne klasy dodatkowo zawierają właściwości specyficzne dla encji, którą opisują. Przykładowo, klasa MyMethodInfo przechowuje informacje na temat typu zwracanego przez określoną metodę a także jej parametry. W ImmDoc.NET tego typu informacje są często trzymane w innych klasach dziedziczących z MetaClass, jak np.: MyParameterInfo, MyFieldInfo itp.

W pierwszym kroku przetwarzania informacje zebrane za pomocą standardowych mechanizmów refleksji łączone są z komentarzami wyciągniętymi z plików XML, aby utworzyć wewnętrzną reprezentację analizowanych zestawów. Poniżej zamieszczam diagram UML, który powinien dać Ci ogólny pogląd na tę reprezentację. Zwróć uwagę, że na diagramie nie ma wszystkich klas, żeby nie zaciemniać samej idei:

Diagram klas obrazujący wewnętrzną reprezentację zestawów .NET

Po zebraniu wszystkich potrzebnych informacji, nadchodzi w końcu czas na rozpoczęcie generowania dokumentacji. Używane do tego celu klasy znajdują się w przestrzeni nazw Imm.ImmDocNetLib.Documenters. Znajdziemy tam prostą klasę abstrakcyjną Documenter, z której możemy wyprowadzić klasy potomne implementujące generowanie dokumentacji w określonym formacie. Ja zaimplementowałem klasę HTMLDocumenter, która tworzy zbiór plików HTML, CSS i JavaScript, które wspólnie składają się na dokumentację zestawów przypominającą tę znaną nam wszystkim z MSDNa.

HTMLDocumenter jest raczej sporą klasą, więc gdyby ktoś chciał ponownie wykorzystać część jej funkcjonalności przy implementacji własnego generatora dokumentacji w innym formacie, wskazana by była jakaś wysokopoziomowa refaktoryzacja. Ogólne spojrzenie na odpowiedzialności klasy HTMLDocumenter powinien dać Ci poniższy diagram, na którym umieściłem sygnatury kilku metod tejże klasy.

Kilka klas z przestrzeni nazw Imm.ImmDocNetLib.Documenters oraz Imm.ImmDocNetLib.Documenters.HTMLDocumenter

Poniżej krótkie opisy powyższych metod:

bool GenerateDocumentation(
  string outputDirectory,
  DocumentationGenerationOptions options)

Inicjuje proces generowania dokumentacji. Wywołuje takie metody jak PrepareOutputDirectory(), GenerateMainIndex(), ExtractStyleSheets(), ProcessAssemblies(), GenerateTableOfContents() itp.

void CreateInvokableMembersOverloadsIndex(
    MyInvokableMembersOverloadsInfo myInvokableMembersOverloadsInfo,
    MyClassInfo declaringType,
    string dirName)

Ta metoda tworzy stronę HTML, która będzie zawierała indeks wszystkich przeciążeń określonej metody bądź konstruktora.

string CreateNamespaceMemberSyntaxString(MyClassInfo namespaceMember)

Ta metoda odpowiedzialna jest za tworzenie napisu z deklaracją jakiegoś typu. Taki napis, przykładowo, składa się z modyfikatora widoczności, klasy bazowej, implementowanych interfejsów, więzów nałożonych na parametry generyczne itp.

void ExtractBinaryResourceToFile(string resourceName, string fileName)

Generowane strony HTML korzystają z kilku plików graficznych. Ta metoda zajmuje się wyciąganiem osadzonych zasobów i zapisywaniem ich do plików.

string ProcessComment(string contents)

Ta metoda przetwarza każdy komentarz znaleziony w przekazanych plikach XML w celu zastąpienia znaczników takich jak <code>, <c>, <see> itd. odpowiednimi znacznikami HTML.

void WriteIndexHeader(
  StreamWriter sw,
  string pageTitle, 
  string[] sectionsNamesAndIndices)

Ta metoda wypisuje standardowy nagłówek HTML wykorzystywany przez prawie wszystkie wygenerowane strony.

string ResolveLink(MetaClass metaClass)

Użyteczność dokumentacji elektronicznych w dużej mierze zależy od możliwości nawigacji pomiędzy poszczególnymi stronami. Ta metoda pomaga podczas generowania odnośników, na przykład do określonej składowej klasy.

Podsumowanie

Mam nadzieję, że ImmDoc.NET komuś się przyda. Ja sam korzystam z niego często, aby bezboleśnie i szybko generować dokumentację dla projektów, które tworzę. Chętnie dowiem się, co o nim sądzicie i czy jest z Waszej strony jakieś zainteresowanie, żeby zdecydować, czy warto go rozwijać ;) Wciąż brakuje mu kilku pomniejszych funkcjonalności; również bardziej przyjazny dla użytkownika interfejs fajnie by było mieć. Tak więc, jeśli macie jakieś sugestie, pytania albo być może znaleźliście jakiś błąd, będę wdzięczny za kontakt. Mój adres e-mail to: marek.stoj@gmail.com. Strona domowa projektu ImmDoc.NET utrzymywana jest w serwisie CodePlex — to właśnie stamtąd możecie pobrać zarówno sam program, jak i jego pełny kod źródłowy.

środa, 13 października 2010

Właściwości kontekstowe log4net a ASP.NET

Zapewne wielu z Was zna i korzysta z biblioteki log4net, służącej do logowania na platformie .NET. log4net świetnie się sprawdza w praktyce, jest stosunkowo proste w użyciu a jednocześnie na tyle elastyczne, że przy jego pomocy da się zrealizować również bardziej zaawansowane scenariusze logowania. Większość użytkowników tej biblioteki prędzej czy później zaznajamia się z możliwością przechowywania danych kontekstowych, które następnie używane są podczas logowania konkretnych komunikatów. Takimi danymi mogą być np: nazwa aktualnie zalogowanego użytkownika czy też URL żądania.

Jak się jednak często w naszej branży okazuje, nie wszystko jest tak proste, na jakie wygląda. Kolekcja właściwości kontekstowych powiązanych z aktualnie wykonującym się wątkiem, czyli ThreadContext.Properties, bo o niej tutaj mowa, tak naprawdę jest zupełnie nieprzydatna w środowisku aplikacji webowych, jeśli korzystamy z niej w sposób standardowy. Poprzez standardowy sposób rozumiem tutaj najzwyklejsze w świecie dodawanie elementów do kolekcji:

log4net.ThreadContext.Properties["UserName"] = Thread.CurrentPrincipal.Identity.Name;

Dokonując takiego przypisania np. w zdarzeniu Application_BeginRequest, spodziewamy się, że w późniejszych etapach przetwarzania żądania, wszelkie odwołania do loggera tę informację nam zapiszą i, co więcej, ta informacja będzie przypisana do tego konkretnego żądania. Niestety semantyka tej kolekcji nam tego nie zagwarantuje w środowisku aplikacji webowej ze względu na tzw. zwinne zarządzanie wątkami (ang. thread agility) przez środowisko uruchomieniowe ASP.NET. Na czym to polega? Myślę, że podobnie jak dla mnie, również dla wielu z Was zaskoczeniem będzie informacja, że pojedyncze żądanie do aplikacji ASP.NET może w trakcie swojego istnienia być obsługiwane przez więcej niż jeden wątek (!).

W kontekście biblioteki log4net, przełączanie wątków w trakcie obsługi jednego żądania oznacza, że nie możemy polegać na wartościach zapisanych w kolekcji ThreadContext.Properties. Sposób implementacji tej kolekcji w połączeniu z ASP.NET nie daje nam gwarancji, że odpowiednie wartości zostaną przepisane w momencie przełączenia wątków. W rezultacie, gdy na produkcji trafi nam się jakiś błąd systemu i będziemy chcieli przeanalizować czynności wykonywane przez konkretnego użytkownika, z dużym prawdopodobieństwem informacje na temat aktualnie zalogowanego użytkownika będą przekłamane.

Istnieje jednak sposób na obejście tego problemu. Otóż w ASP.NET istnieje jedna kolekcja, dla której mamy gwarancję, że będzie zachowana nawet po przejściu na inny wątek. Jest to kolekcja HttpContext.Items. Chciałbym Wam przedstawić gotową implementację rozwiązania opartego właśnie na tej kolekcji, którą bez większych problemów możecie zastosować w swoich aplikacjach.

Główną ideą tego rozwiązania jest zastosowanie klasy opakowywującej wartości, które chcemy trzymać w kolekcji ThreadContext.Properties. Robimy to po to, żeby w momencie, gdy log4net będzie te wartości pobierał, aby zalogować komunikat, nasz obiekt będzie mógł zdecydować, czy tę wartość należy pobrać z kolekcji HttpContext.Items (jeśli kod wykonuje się w środowisku webowym), czy też po prostu zwrócić wartość oryginalną.

Klasę tę nazwałem AdaptivePropertyProvider<T> a jej kod zamieszczam poniżej:

public class AdaptivePropertyProvider<T>
{
  private readonly string _propertyName;
  private readonly T _propertyValue;

  #region Constructor(s)

  protected internal AdaptivePropertyProvider(string propertyName, T propertyValue)
  {
    if (string.IsNullOrEmpty(propertyName))
    {
      throw new ArgumentNullException("propertyName");
    }

    _propertyName = propertyName;
    _propertyValue = propertyValue;

    if (HttpContext.Current != null)
    {
      HttpContext.Current.Items[GetPropertyName()] = propertyValue;
    }
  }

  #endregion

  #region Overrides of object

  public override string ToString()
  {
    if (HttpContext.Current != null)
    {
      var item = HttpContext.Current.Items[GetPropertyName()];

      return item != null ? item.ToString() : null;
    }

    if (!ReferenceEquals(_propertyValue, null))
    {
      return _propertyValue.ToString();
    }

    return null;
  }

  #endregion

  #region Private helper methods

  private string GetPropertyName()
  {
    return string.Format("{0}{1}", AdaptivePropertyProvider.PropertyNamePrefix, _propertyName);
  }

  #endregion
}

Zwróćcie uwagę, iż logika pobierania wartości znajduje się w metodzie ToString(). Korzystamy tutaj z faktu, że sam log4net podczas logowania komunikatów używa właśnie tej metody w celu sformatowania obiektów przechowywanych w kolekcji Properties.

Dla wygody możemy zdefiniować drugą klasę, która udostępni nam metodę fabryczną konstruującą obiekty klasy AdaptivePropertyProvider<T>:

public class AdaptivePropertyProvider
{
  public const string PropertyNamePrefix = "log4net_app_";

  #region Factory methods

  public static AdaptivePropertyProvider<T> Create<T>(string propertyName, T propertyValue)
  {
    return new AdaptivePropertyProvider<T>(propertyName, propertyValue);
  }

  #endregion
}

Użycie takiej metody fabrycznej pozwoli nam korzystać z inferencji typów wykonywanej przez kompilator (w wypadku zwykłego konstruktora nie możemy pominąć generycznego parametru):

log4net.ThreadContext.Properties["UserName"] =
  AdaptivePropertyProvider.Create("UserName", Thread.CurrentPrincipal.Identity.Name);

Pamiętając o tym, żeby właściwości kontekstowe przechowywać właśnie w ten sposób, ustrzeżemy się przed niemiłymi niespodziankami podczas analizowania logów z produkcji w sytuacjach kryzysowych :]


Do pobrania:

Odniesienia:

wtorek, 12 października 2010

Rusza devBlog programistów KRD!

KRD Witamy na oficjalnym blogu prowadzonym przez programistów Krajowego Rejestru Długów! W naszej firmie jest sporo programistów-pasjonatów, którzy swoją profesję traktują jako hobby i chcieliby robić coś w tym temacie również poza pracą. Zdajemy sobie sprawę, że prowadzenie własnego bloga może być dla programisty ciekawym i pouczającym doświadczeniem, jednakże czasem brakuje motywacji do robienia tego w pojedynkę. Wpadliśmy więc pewnego razu na genialny pomysł, żeby poprowadzić wspólnego bloga, dzięki czemu moglibyśmy wzajemnie wspierać się i zachęcać do pisania. Co ciekawe naszym przełożonym ten pomysł się spodobał i oto mamy oficjalnego bloga działu informatycznego KRD, a dokładniej — sekcji programistów.

Dział informatyczny KRD

Firma KRD nie jest rozpoznawalna na rynku jako firma dla programistów. Niemniej jednak pracuje w niej duży zespół developerów, którzy rozwijają oprogramowanie w najnowszych technologiach Microsoftu. Na co dzień każdy z nas napotyka na zadania i problemy, których rozwiązania niejednokrotnie mogłyby przydać się innym programistom. Właśnie tego typu wiedzę chcielibyśmy gromadzić na tym blogu.

Poza artykułami czysto technicznymi chcielibyśmy również pisać na lżejsze tematy, jak na przykład o środowisku pracy, procesach wytwarzania oprogramowania, zarządzaniu, przeglądach kodu, programowaniu w parach itp. W związku z tym mamy nadzieję, że już wkrótce każdy pasjonat branży informatycznej będzie mógł tutaj znaleźć coś ciekawego dla siebie.

Zapraszamy do lektury!
Zespół devBlog.KRD.pl