Showing posts with label programowanie. Show all posts
Showing posts with label programowanie. Show all posts

16/12/2012

Code Retreat

Home

O Code Retreat, czyli swego rodzaju warsztatach programistycznych, usłyszałem po raz pierwszy całkiem niedawno, kiedy dyrektor działu, w którym pracuję, rzucił pomysł zorganizowania czegoś takiego w firmie. Od pomysłu do realizacji nie minęło dużo czasu i w ostatnią sobotę wraz z kolegami i koleżankami z pracy (około 20 osób) wzięliśmy udział w takich warsztatach prowadzonych przez Michała Taszyckiego.

Z czym to się je? Celem Code Retreat jest wymiana wiedzy pomiędzy uczestnikami warsztatów oraz nauka nowych rzeczy, których na co dzień nie używa się w pracy. Warsztaty takie składają się z kilku krótkich sesji, w moim przypadku było to 6 sesji po 45 minut, podczas których rozwiązuje się jakieś problemy programistyczne. Pracowaliśmy w parach i co jakiś czas wymienialiśmy się klawiaturą. W czasie każdej z sesji pary były różne, a ponieważ w wydarzeniu brali udział programiści C# i Java, czasami trzeba było programować w języku, którego nie używa się na co dzień.

Tematem przewodnim tego Code Retreat była gra w życie. W czasie każdej z sesji mieliśmy za zadanie zaprogramować taką grę, ale za każdym razem wymagania na metodykę pracy i użyte techniki programistyczne były inne. Co istotne, każda sesja kończy się usunięciem całego kody. Pogrubienie oznacza fizyczne, trwałe usunięcie kodu, a więc za każdym razem pracę rozpoczynaliśmy się od początku.

Należy podkreślić, że celem nie jest tak naprawdę napisanie działającej gry. Temat nie jest trudny, ale 45 minut to nie jest dużo czasu zważywszy na to, że przy okazji uczymy się nowych rzeczy. Główny cel to nauka, nauka i jeszcze raz nauka, w sprzyjających warunkach i bez stresu, a gra w życie czy inne zadanie to tylko pretekst.

Czego się nauczyłem? W czasie jednej z sesji mieliśmy na przykład zastosować czysto purystyczne podeście to Test Driven Development, czyli nie mogliśmy napisać linijki kodu bez testów. W innej sesji programując nie mogliśmy w ogóle używać myszki, no ewentualnie po to aby raz sprawdzić skrót i potem już go używać. W jeszcze innej sesji pisany kod powinien zawierać minimalną liczbę klauzul sterujących if, a najlepiej w ogóle itd. Zadania mogą być najróżniejsze.

Czy purystyczne trzymanie się TDD jest dobre? Uważam, że nie. Czy używanie tylko i wyłącznie klawiatury nie jest przesadę? Uważam, że dobrze znać skróty klawiaturowe i starać się minimalizować używanie myszki ale ona się też się przydaje. Czy programowanie bez if'ów ma sens, czy tak w ogóle się da? Ano da się, nie jest to może proste, ani oczywiste ale da się.

Zadania z takimi skrajnymi wymaganiami wyrywają jednak z rutyny, pokazują, że coś można zrobić inaczej, pobudzają do myślenia, rozszerzają horyzonty i to jest moim zdaniem SUPER. Niewątpliwą zaletą jest również integracja zespołu. Co by nie mówić, Code Retreat to po prostu świetna zabawa.

Podsumowując jestem bardzo zadowolony z udziału w Code Retreat. Przyznam, że początkowo byłem trochę sceptyczny, nie wiedziałem czego tak naprawdę się spodziewać, ale nie żałuję ani minuty spędzonej w sobotę w biurze :), w czym niewątpliwą zasługę ma również prowadzący Michał Taszycki.

Polecam Code Retreat!!!

19/11/2012

StackOverflowException - jak sobie z nim radzić

Home

Każdy programista, czy to C#, C++ czy Java, musiał się kiedyś spotkać z przepełnieniem stosu, który, w przypadku platformy .NET, objawia się wyjątkiem StackOverflowException. Najłatwiej do takiego błędu doprowadzić pisząc nieskończoną rekursję. W tym poście zajmę się jednak tym jak poradzić sobie z przepełnieniem stosu, kiedy napisana metoda rekurencyjna wcale nie jest nieskończona. Zacznijmy od przykładu. Załóżmy, że mamy klasę Node i przy jej pomocy tworzymy drzewo binarne, w którym dla uproszczenia będziemy przechowywać liczby. Drzewa, które rozpatrujemy w tym przypadku nie są drzewami przeszukiwań i mogą być niezrównoważone.

public class Node
{
 public uint Data { get; set; }

 public Node Left { get; set; }
 public Node Right { get; set; }
}

Napisaliśmy również metodę rekurencyjną przeszukującą zadane drzewo w poszukiwaniu węzła przechowującego określoną wartość.

public static Node FindRec(Node root, uint i)
{
 if (root == null)
  return null;

 if (root.Data == i)
  return root;

 var res = FindRec(root.Right, i);

 if (res != null)
  return res;

 return FindRec(root.Left, i);
}

Metoda ta będzie działać, ale do czasu. Utwórzmy zdegenerowane drzewo czyli takie, które w rzeczywistości jest listą. Do wygenerowania takiego drzewa użyjemy następującego kodu:

public static Node CreateDegeneratedTree(uint noOfNodes)
{
 Node root = new Node() { Data = 0 };
 Node temp = root;

 for (uint i = 0; i <= noOfNodes; ++i)
 {
  temp.Right = new Node() { Data = i };
  temp = temp.Right;
 }

 return root;
}

W przypadku wystarczająco dużej liczby węzłów w zdegenerowanym drzewie (patrz parametr noOfNodes) wywołanie FindRec zakończy się wyjątkiem StackOverflowException. Wbrew pozorom, nie musi to być jakaś ogromna wartość. Na komputerze, na którym testowałem ten kod, wystarczyło aby drzewo zawierało 10 tyś węzłów. Trochę mało, czyż nie?

W takiej sytuacji musimy zrezygnować z rekursji na rzecz poczciwej pętli. Ja bym napisał taki kod:

public static Node Find(Node root, uint i)
{
 Queue<Node> toCheck = new Queue<Node>();
 toCheck.Enqueue(root);

 while (toCheck.Count > 0)
 {
  var node = toCheck.Dequeue();

  if (node != null)
  {
   if (node.Data == i)
    return node;
    
   toCheck.Enqueue(node.Right);
   toCheck.Enqueue(node.Left);
  }
 }

    return null;
}

Korzystam w nim z pomocniczej kolekcji, do której wrzucam kolejne węzły do sprawdzenia. Pętla kręci się dopóki nie zostanie znaleziony szukany węzeł albo dopóki wszystkie węzły nie zostaną sprawdzone. Podobne podejście możemy zastosować dla innych dla innych zagadnień tego typu.

Problem z StackOverflowException można również rozwiązać w sposób nie algorytmiczny, poprzez zwiększenie domyślnego rozmiaru stosu (1MB) przy pomocy narzędzia editbin albo tworząc nowy wątek o odpowiedniej wielkości stosu. Ja był jednak tego nie robił. Jeśli można osiągnąć jakiś efekt pisząc kod w odpowiedni sposób, to należy tak zrobić. W swojej karierze nie spotkałem jeszcze przypadku kiedy musiałbym zwiększyć wielkość stosu.

Na koniec załączam jeszcze kod metody Main. Cały program pozwala pobawić się pokazanymi powyżej metodami do wyszukiwania (iteracyjną i rekurencyjną) dla zdegenerowanego drzewa o zadanej liczbie węzłów. Program umożliwia również ustalenie wielkości stosu (w celach edukacyjnych :)). Kod może i jest trochę pokraczny, ale dla celów edukacyjnych się nada. Uwaga! Podanie zbyt dużej wartości jako Stack size in kilobytes (0 for default): albo Number of nodes in tree: może zakończyć się OutOfMemoryException.

static void Main(string[] args)
{
    Console.Write("Do you want to use recursion (Y for yes)?: ");
    
    string line = Console.ReadLine();
    bool useRecursion = String.Equals(line, "Y", StringComparison.OrdinalIgnoreCase);

    if(useRecursion)
        Console.WriteLine("Recursion search will be used");

    Console.Write("Stack size in kilobytes (0 for default): ");

    line = Console.ReadLine();
    uint stackSize;
    if (!UInt32.TryParse(line, out stackSize))
        stackSize = 1024

    Console.WriteLine("Stack size is {0} kilobytes ", stackSize);

    var th = new Thread(
  new ThreadStart(() =>
  {
   Console.Write("Number of nodes in tree: ");

   while ((line = Console.ReadLine()) != String.Empty)
   {
    uint noOfNodes;
    if (UInt32.TryParse(line, out noOfNodes))
    {
     Node root = CreateDegeneratedTree(noOfNodes);

     Stopwatch sw = new Stopwatch();

     if (useRecursion)
     {
      sw.Start();
      var res = FindRec(root, noOfNodes);
      sw.Stop();
     }
     else
     {
      sw.Start();
      var res = Find(root, noOfNodes);
      sw.Stop();
     }

     root = null;
     Console.Write("Processing time (ms): {0}\n\n", sw.ElapsedMilliseconds);
    }

    Console.Write("Number of nodes in tree: ");
   }
  }), (int)stackSize * 1024);

 th.Start();
}

29/10/2012

Trochę więcej o serializacji

Home

W poprzednim poście pisałem o serializacji XML'owej, a w tym wrócę jeszcze do tematu. Skorzystamy z tego samego kodu co poprzednio ale tym razem uruchomimy go korzystając z innych serializatorów. Zamieńmy, więc XmlSerializer na SoapFormatter lub BinaryFormatter i uruchommy przykładowy kod jeszcze raz (można użyć też DataContractSerializer ale z drobną modyfikacją kodu).

Efekt końcowy będzie inny niż dla serializatora XML'owego - po deserializacji właściwość Name będzie miała taką samą wartość jak przed serializacją. To nie jedyna różnica. Jeśli do konstruktora domyślnego dodamy linijkę:

Console.WriteLine("I'm in a constructor");

i popatrzymy na liczbę wypisanych na ekran napisów I'm in a constructor to okaże się, że w przypadku serializatora XML'owego konstruktor domyślny został wywołany dwa razy (przy tworzeniu obiektu oraz przy deserializacji), a w przypadku pozostałych klas tylko jeden raz (przy tworzeniu obiektu).

Należy postawić pytanie, jak w takim razie pozostałe serializatory tworzą obiekty przy deserializacji. Przecież muszą wołać jakiś konstruktor. Otóż nie koniecznie. O ile wiem taki BinaryFormatter, pozostałe serializatory korzystają zapewne z podobnych mechanizmów, używa metody FormatterServices.GetSafeUninitializedObject, która jest odpowiedzialna za zaalokowanie odpowiednio dużego fragmentu pamięci dla obiektu zadanego typu.

Po co to wszystko? Czy nie łatwiej wywołać konstruktor? Początkowo pomyślałem, że to kwestia wydajności. Ale szybki test pokazuje, że użycie FormatterServices.GetSafeUninitializedObject jest troszkę wolniejsze niż utworzenie obiektu za pomocą konstruktora.

var s = new Stopwatch();
s.Start();

for(int i = 0; i < 1000000; ++i)
 FormatterServices.GetSafeUninitializedObject(typeof(A));

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~130 ms

s.Reset();
s.Start();

for (int i = 0; i < 1000000; ++i)
    new A();

s.Stop();

Console.WriteLine(s.ElapsedMilliseconds); //Na mojej maszynie ~20 ms
Po chwili zastanowienia doszedłem do innego wniosku. Otóż, jeśli na proces deserializacji popatrzymy jak na proces "przywracania do życia" już raz utworzonych obiektów, to ponowne wywołanie konstruktora po prostu nie ma sensu albo wręcz jest niewskazane. Macie jakieś inne pomysły? Jak sądzicie czemu jedne serializatory wołają konstruktor, a inne nie?

27/10/2012

XmlSerializer- taka ciekawostka

Home

Serializator XML'owy platformy .NET jest bardzo łatwy i przyjemny w użyciu, ale czasami jego działanie może sprowadzić nas na manowce. Poniższy kod obrazuje o co mi chodzi. Zacznijmy od przykładowej, bardzo prostej klasy, którą będziemy serializować:

public class A
{
 public string Name { get; set; }
 
 public A()
 {
  Name = "Hello"; 
 }
}

Oraz kawałka kodu:

var obj = new A() { Name = null };

var stream = new MemoryStream();
var serializer = new XmlSerializer(typeof(A));

serializer.Serialize(stream, obj);

stream.Position = 0;

var obj2 = (A)serializer.Deserialize(stream);

Kod jest bardzo prosty. Tworzymy obiekt klasy A i ustawiamy właściwość Name na null. Następnie zapisujemy obiekt do strumienia. Po zapisaniu przesuwamy wskaźnik odczytu/zapisu strumienia na początek aby móc zdeserializować obiekt. W czym tkwi haczyk? Spróbujcie odpowiedzieć na to pytanie bez zaglądania do dalszej części posta:

Jaka będzie wartość właściwości Name po odczytaniu obiektu ze strumienia?

Otóż po zdeserializowaniu właściwość Name nie będzie pusta. Serializator XML'owy odczytując obiekt ze strumienia najpierw wywoła konstruktor domyślny. Potem zobaczy, że w strumieniu właściwość Name jest pusta i stwierdzi, że w takim wypadku nie trzeba niczego przypisywać do właściwości Name deserializowanego obiektu. W konstruktorze znajduje się natomiast kod, który ustawia tą właściwości. A więc po zdeserializowaniu właściwość Name będzie zawierała ciąg znaków "Hello", czyli inną niż przed serializacją.

Teraz wyobraźmy sobie trochę bardzie skomplikowaną sytuację. Załóżmy, że mamy kod pobierający jakieś dane z bazy danych i ładujący je do odpowiednich klas. Klasy te są napisane w podobny sposób jak klasa A powyżej, czyli w konstruktorze domyślnym niektóre właściwości są inicjowane wartościami innymi niż null. Dane (obiekty) są następnie udostępniane aplikacji klienckiej przez web service'y (stare dobre ASMX, usługi WCF jeśli użyto XmlSerializerFormatAttribute)  czyli muszą być najpierw zserializowane, a potem zdeserializowane. Kod działa poprawnie.

Teraz zabieramy się za pisanie testów integracyjnych i używamy tego samego kodu odpowiedzialnego za wczytywanie danych. W momencie, kiedy chcemy użyć tych samych obiektów (tak samo stworzonych i wypełnionych danymi) co aplikacja kliencka, nawet w ten sam sposób, okazuje się, że dostajemy wyjątek NullReferenceException.

Dlaczego? Ano dlatego, że w testach integracyjnych obiekty nie zostały przesłane przez sieć, czyli nie były serializowane i deserializowane, czyli konstruktor domyślny nie został wywołany drugi raz przy deserializacji i puste właściwości nie zostały ustawione tak jak opisano powyżej.

Jak w wielu innych podobnych sytuacjach, to żadna wiedza tajemna, ale jak się o tym nie wie, może napsuć trochę krwi. Pomijam tutaj fakt, że korzystanie z opisanej "funkcjonalności", świadomie czy nie, nie jest najlepszym pomysłem.

14/10/2012

Make Object ID

Home

Z komendy Make Object ID korzystam już od bardzo dawna, nie codziennie ale w niektórych sytuacjach jest ona nieodzowna. Ostatnio zorientowałem się jednak, że nawet doświadczeni użytkownicy VS mogą o niej wiedzieć i stąd pojawił się pomysł na ten post.





Make Object ID to komenda (pokazana na powyższych zrzutach ekranu), która dostępna jest z poziomu menu kontekstowego w okienku Locals i Watch czy też z poziomu edytora kodu. Po jej wywołaniu dla danego obiektu zostanie wygenerowany identyfikator. Nadanie identyfikatora objawia się tym, że w oknie Locals czy też Watch w kolumnie Value zostanie dodany przyrostek {ID#}.



Mając taki identyfikator możemy w dowolnym momencie debugowania programu odwołać się do danego obiektu i podejrzeć jego zawartość wpisując w okienku Watch identyfikator w formacie ID#. Co najważniejsze możemy to zrobić nawet jeśli w danym momencie nie mamy dostępu (referencji) do danego obiektu:



Wyobraźmy sobie, że debugujemy problem i dochodzimy do wniosku, że stan jakiegoś obiektu zmienił się w niepowołany sposób pomiędzy jego utworzeniem/zainicjowaniem, a miejscem użycia. Niestety prześledzenie "drogi" jaką przebył obiekt krok po kroku jest bardzo trudne. W międzyczasie wywołanych zostało setki jeśli nie tysiące metod, kod jest skomplikowany, uzyskanie referencji do danego obiektu jest w wielu miejscach trudne lub niemożliwe itd. W tym przypadku Make Object ID ułatwi nam znalezienie miejsca gdzie obiekt został popsuty. W praktyce mogłoby wyglądać to tak:
  • Generujemy identyfikator dla obiektu w momencie jego inicjalizacji.
  • Stawiamy pułapkę w miejscu gdzie podejrzewamy, że obiekt jest już zmieniony.
  • Po zatrzymaniu się na pułapce sprawdzamy stan obiektu.
  • Jeśli obiekt jest zmieniony to przesuwamy pułapkę w kierunku miejsca gdzie został zainicjowany.
  • Jeśli obiekt jest niezmieniony to przesuwamy pułapkę w kierunku przeciwnym do miejsca gdzie został zainicjowany.
  • Powtarzamy procedurę.
Opisana procedura to coś w rodzaju przeszukiwania połówkowego, prosty ale skuteczny sposób. Make Object ID przyda się za każdym razem kiedy chcemy podejrzeć stan obiektu, do którego w danym momencie nie możemy się w łatwy sposób odwołać.

Na koniec dwie uwagi. Spotkałem się z informacją, że Make Object ID jest dostępne tylko w Visual Studio w wersji Ultimate. Nie mam teraz dostępu do innej wersji środowiska więc nie mogę tego zweryfikować. Po drugie, o ile wiem, nie istnieje sposób aby wylistować listę obiektów i nadanych im identyfikatorów. W praktyce trzeba więc albo zapamiętać albo zapisać sobie na kartce nadane identyfikatory.

14/09/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy 2

Home

Niedawno kolega opowiedział mi o jeszcze jednym przypadku kiedy opisane przeze mnie zachowanie konstruktora statycznego w środowiskach x86/x64 doprowadziło do kłopotów. Scenariusz był dość ciekawy, dlatego go opiszę na uproszczonym przykładzie. Zacznijmy od tego, że napisaliśmy zarządzany komponent COM. Komponent ten w konstruktorze statycznym czyta wartość jakiegoś parametru konfiguracyjnego z pliku i na tej podstawie coś robi. W poniższym przykładzie, żeby nie komplikować sprawy, po prostu zapisuje jego wartości do pliku:

[ComVisible(true)]
public class MySimpleCOM : ServicedComponent
{
 static MySimpleCOM()
 {
  File.AppendAllText(
   @"c:\log.txt",
   String.Format("Value of configuration parameter = '{0}' in the domain '{1}'\n.", ConfigurationManager.AppSettings["Test"], AppDomain.CurrentDomain.FriendlyName));
 }
 
 public MySimpleCOM() {}
 public void Fun() {}
}

Dodam jeszcze, że do pliku AssemblyInfo.cs w projekcie z naszym zarządzanym komponentem zostały dodane takie atrybuty:

[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(false)]

ApplicationActivation zapewnia, że nasz komponent będzie hostowany poza procesem, w którym się do niego odwołamy, dokładniej przez proces dllhost.exe. Drugi atrybut jest opcjonalny ale ułatwia testy i mówi, że nie jest wymagana żadna kontrola dostępu (uprawnień) kiedy ktoś będzie próbował użyć naszego komponentu. Kod testujący nasz komponent jest jeszcze prostszy:

class Program
{
 static void Main(string[] args)
 {
  using (var host = new MySimpleCOM())
  {
   host.Fun();
  }
 }
}

Warto zwrócić uwagę, że nie trzeba samemu rejestrować komponentu. Zostanie to zrobione za nas.

Na koniec dodajemy jeszcze do pliku konfiguracyjnych programu testującego (app.config) oraz komponentu COM (Application.config) sekcję appSettings z parametrem test. Dla rozróżnienia niech wartość tego parametru różni się w obu przypadkach np.:

<appSettings>
 <add key="test" value="Tester"/>
</appSettings>

oraz

<appSettings>
 <add key="test" value="COM"/>
</appSettings>

Aby zobaczyć na czym dokładnie polega problem najłatwiej postąpić w następujący sposób. Najpierw kompilujemy oba projekty z opcją x86, uruchamiamy i zaglądamy do pliku z log.txt. Dalej kompilujemy ponownie oba projekty z opcją x64 i znowu uruchamiamy pamiętając aby uprzednio odinstalować wersję x86 komponent COM. Można to zrobić na przykład przy pomocy narzędzia Usługi składowe (dcomcnfg) wbudowanego w system Windows.

W pierwrzym przypadku (x86) plik log.txt będzie wyglądał tak:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.

A w drugim (x64) w następujący sposób:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.
Value of configuration parameter = 'Tester' in the domain 'Tester.vshost.exe'.

Jak widać w przypadku środowiska x64 kod inicjalizujący w konstruktorze statycznym został wywołany dwa razy, do tego z różnymi wartościami parametru konfiguracyjnego. W prawdziwym systemie mogłoby to doprowadzić do powstania jakichś błędów i chyba sami przyznacie, że ich przyczyna nie byłaby trywialna do wykrycia.

PS.

Jeśli przy własnoręcznym odtwarzaniu tego scenariusza napotkacie problem, że komponent COM nie będzie czytał konfiguracji ze pliku Application.config, to odsyłam na przykład do tego wpisu, w którym wszystko jest wyjaśnione.

22/07/2012

Jeden dziwny znak, a kilka rzeczy do zapamiętania

Home

Niedawno zetknąłem się z raportem, który wypluwał dane do pliku. Przy czym jednym z wymagań było aby trim'ować pola, które zostały dopełnione do wymaganej długości. W tym celu użyto funkcji rtrim. Przeglądając wyprodukowane raporty, zauważyłem jednak, że w niektórych przypadkach zawartość jednego pola zawiera jakieś białe znaki. Na pierszy rzut oka wyglądało to na spacje. Na wszelki wypadek sprawdziłem czy napewno zastosowano rtrim i wszystko się zgadzało.

Rozpocząłem więc poszukiwania czemu rtrim mogło nie zadziałać i od razu nauczyłem się pierwszej rzeczy, do której nie przywiązywałem wcześniej uwagi. rtrim obsługuje tylko spacje i nie poradzi sobie z tabulatorem, znakiem powrotu karetki itd. Poniżej krótki przykład demonstrujący problem (char(9) to tabulator).

DECLARE @string char(10)
SET @string = 'abc' + char(9)

SELECT LEN(@string), LEN(RTRIM(@string))

Rozwiązaniem problemu może być na przykład użycie funckji replace:

SELECT LEN(@string), LEN(RTRIM(@string)), LEN(REPLACE(@string,char(9),SPACE(0)))

W tym przypadku to jednak nie pomogło. W pliku wyjściowym cały czas pojawiały się te "dziwne" spacje. Użyłem więc prostego skryptu aby zobaczyć kody znaków dla pola, które sprawiało problemy. Okazało się, że te dziwne "spacje" to znak o kodzie 255. Teraz w zależności od tego do jakiej tablicy znaków zajrzałem uzyskałem inną informację. Na przykład według ISO 8859-1 to znak łacińska mała litera Y z diarezą/umlautem.

Tutaj dochodzimy do kolejnej ważnej rzeczy, którą łatwo przeoczyć kiedy przez większość czasu pracujemy w dobrze określonym środowisku z takimi, a nie innymi ustawieniami regionalnymi itd. Otóż ten sam znak może wyglądać inaczej kiedy wykonamy zapytanie w Microsoft SQL Server Management Studio ze względu na collation, a inaczej po zapisaniu do pliku i otwarciu w jakimś edytorze ze względu na wybraną w programie stronę kodową itd. Poniższy skrypt pokazuje jak ten sam znak zostanie zaprezentowany dla różnych collation.

DECLARE @string char(10)
SET @string = 'abc' + char(255)
SELECT @string
Collate SQL_Latin1_General_CP1_CI_AS
SELECT @string
Collate SQL_Polish_CP1250_CI_AS

Nie ma w tym nic trudnego i przez większość czasu kwestie kodowania, tablicy znaków... nas nie obchodzą ale warto pamiętać o takich rzeczach bo potem mogą pojawić się cokolwiek zaskakujący efekty, nie zawsze oczywiste do wyjaśnienia.

Post ten dotyczy MSSQL 2008.

08/07/2012

WPF i opóźnione wykonanie (deferred execution)

Home

Najpierw spójrzmy na poniższy fragment XAML'a z prostą implementacją combobox'a z wieloma kolumnami:

...
<ComboBox ItemsSource="{Binding PerformedAnalysis}" SelectedValuePath="Id" SelectedValue="{Binding SelectedItem}">
 <ComboBox.ItemTemplate>
  <DataTemplate>
   <StackPanel Orientation="Horizontal">
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Id}" Width="100"></TextBlock>
    </Border>
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Name}" Width="500"></TextBlock>
    </Border>
   </StackPanel>
  </DataTemplate>
 </ComboBox.ItemTemplate>
</ComboBox>
...

Oraz kod z view model'u zasilający tą kontrolkę. Kod ten używa Entity Framework, a Context to nic innego jak obiekt dziedziczący po ObjectContext.

...
private IEnumerable _PerformedAnalysis = null;

public IEnumerable PerformedAnalysis
{
    get
    {
        try
        {
            if (_PerformedAnalysis == null)
            {
                _PerformedAnalysis = 
                        from a in Context.Analyses
                        orderby a.Id
                        select new { Id = a.Id, Name = a.Name };
            }
        }
        catch (Exception ex)
        {
            ServiceProvider.GetService<IWindowService>().ShowError(ex);
            _PerformedAnalysis = new[] { new { Id = -1, Name = ex.Message } };
        }

        return _PerformedAnalysis;
    }
}
...

Kod ten zawiera dość poważną usterką. Jaką? Ktoś może powiedzieć, że błędem jest użycie bezpośrednio EF'a, zamiast ukryć dostęp do danych za warstwą interfejsów. W prostej aplikacji nie ma to moim zdaniem sensu, w bardziej rozbudowanej też można nad tym dyskutować. Ktoś może również powiedzieć, że przy bindowaniu należy użyć ObservableCollection. W tym jednak przypadku do kolekcji nie będą dodawane lub usuwane żadne elementy, więc nie jest to konieczne.

Błąd jest dużo poważniejszy i może spowodować, potocznie mówiąc, wywalenie się całej aplikacji. Odpowiedzmy na pytanie co się stanie jeśli nawiązanie połączenia z bazą danych jest niemożliwie? Oczywiście zostanie zgłoszony błąd ale czy zostanie obsłużony? Pozornie powyższy kod poradzi sobie z takim scenariusz, przecież zawiera blok try/catch.

Zapomniano jednak o tzw. opóźnionym wykonaniu (ang. deffered execution) czyli o tym, że właściwe zapytanie do bazy danych zostanie wykonane dopiero kiedy dane będą rzeczywiście potrzebne czyli kiedy silnik WPF wykona bindowanie. Inaczej mówiąc wyjątek spowodowany brakiem połączenia z bazą danych zostanie rzucony gdzieś wewnątrz silnika WPF. Będzie można go przechwycić korzystając z DispatcherUnhandledException ale z perspektywy działania aplikacji to już nic nie da.

Jak to często bywa naprawienie błędu jest bardzo proste. Należy zapewnić wykonanie zapytania jeszcze w naszym kodzie np.:

...
 _PerformedAnalysis = 
  (from a in Context.Analyses
  orderby a.Id
  select new { Id = a.Id, FileName = a.FileName, Start = a.StartTime }).ToList();
...

05/06/2012

CLR Profiling API - dobrego złe początki

Home

Jakiś czas temu odpowiadając na komentarz pod postem przebąknąłem, że przymierzam się do wzięcia byka za rogi i chcę napisać własny profiler korzystając z CLR Profiling API. Zagadnienie nie jest trywialne dlatego postanowiłem zacząć od zapoznania się z "literaturą". Na pierwszy ogień poszedł artykuł Write Profilers With Ease Using High-Level Wrapper Classes. Swoje lata ma, chociaż na liście mam jeszcze starsze artykuly, ale akurat w przypadku CLR  Profiling API lata biegną wolniej niż w przypadku innych technologii.

Do artykułu, jakby inaczej, dołączony jest kod, który postanowiłem jak najszybciej uruchomić aby zobaczyć czy to rzeczywiście działa. W tym momencie natknąłem się na pierwszy problem i o tym będzie ten post. Problem wynikał z tego, że chciałem "sprofilować" aplikację używającą .NET 4.0 kodem napisanym z myślą o .NET 2.0. Z biegu nie jest to możliwe i objawia się takim błędem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed. The profiler that was configured to load was designed for an older version of the CLR. You can use the COMPLUS_ProfAPI_ProfilerCompatibilitySetting environment variable to allow older profilers to be loaded by the current version of the CLR. Please consult the documentation for information on how to use this environment variable, and the risks associated with it. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. Process ID (decimal): 5128. Message ID: [0x2517].


Łatwo jednak sobie z tym poradzić, wystarczy ustawić zmienną środowiskową COMPLUS_ProfAPI_ProfilerCompatibilitySetting. Uruchomienie programu pod kontrolą profiler'a będzie więc wyglądało w następujący sposób:

rem Rejestrujemy dll'kę z profiler'em
regsvr32 /s MyProfiler.dll

rem Włączenie profilowania
set COR_ENABLE_PROFILING=1

rem Ustawiamy profiler, który chcemy użyć (zarejestrowaliśmy go 2 linijki wcześniej)
set COR_PROFILER={32E2F4DA-1BEA-47ea-88F9-C5DAF691C94A}

rem Włączamy kombatybilność wstecz
COMPLUS_ProfAPI_ProfilerCompatibilitySetting=EnableV2Profiler

rem Uruchamiamy program
MyProgram.exe

rem Profiler nie jest już potrzebny, więc go wyrejestrowujemy
regsvr32 /s /u MyProfiler.dll

Ale to nie wszystko. Drugi rodzaj błędu z jakim się spotkałem objawia się następującym wpisem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed during CoCreateInstance. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. HRESULT: 0x80070002. Process ID (decimal): 2620. Message ID: [0x2504].

I może wynikać z dwóch rzeczy. Po pierwsze jeśli nasz program jest programem 64 bitowym, do jego profilowania musimy użyć 64 bitowej wersji profilera. Sprawa ma się podobnie, jeśli program jest 32 bitowy. W przeciwnym wypadku środowisko nię będzie potrafiło załadować odpowiedniej biblioteki. Błąd pojawi się również jeśli spróbujemy uruchomić profiler nie posiadając praw administratora (Windows Vista z włączonym UAC). A więc uruchamiając skrypt należy skorzystać z opcji Uruchom jako administrator.

24/05/2012

Krótka lekcja na temat char i varchar

Home

Na początek prosty kawałek kodu w T-SQL, w którym sprawdzane jest, czy zadany ciąg znaków pasuje do podanego wzorca tj. czy zaczyna się dwoma cyframi:

declare @input char(10)
declare @pattern char(100)

SET @input = '12aaabbb'
SET @pattern = '[0-9][0-9]%'

if @input like @pattern
 print 'OK'
else
 print 'Fail'

Pomimo, że ciąg znaków pasuje do wzorca to warunek dopasowania nie jest spełniony i na ekran zostanie wypisany napis Fail. Dzieje się tak ponieważ kiedy przypisujemy wzorzec do zmiennej @pattern, która jest typu char(100) to zostanie on dopełniony spacjami. A więc przy testowaniu warunku tak naprawdę sprawdzamy czy zadany ciąg znaków zaczyna się dwoma cyframi i kończy przynajmniej 89 spacjami (100 - długość wzorca).

Można to naprawić przechowując wzorzec w zmiennej typu varchar albo stosując funkcję rtrim. Wszystko zależy od konkretnej sytuacji ale przy wykonywaniu różnych operacji na ciągach znaków należy zawsze pamiętać o różnicy pomiędzy char i varchar. Sprawa wydaje się prosta ale kiedy pracujemy z dużą ilością kodu bazodanowego łatwo można coś przeoczyć, a znalezienie takiego błędu może nie być wbrew pozorom proste.

13/05/2012

Podglądanie tabel tymczasowych

Home

Czasami zdarza się, że pracujemy z aplikacją, która tworzy tabelę tymczasową, a następnie woła serię procedur składowanych, które: wypełniają tę tabelę, modyfikują ją itd. W takim wypadku dość często np.: przy debugowaniu błędu, potrzeba podejrzeć zawartość takiej tabeli. Pół biedy kiedy jest to globalna tabela tymczasowa np.: ##SomeTempTable. Wtedy wystarczy zatrzymać aplikację na pułapce i wykonać zapytanie w SQL Server Management Studio np.: select * from ##SomeTempTable.

Niestety, jeśli globalna tabela tymczasowa została utworzona w transakcji, to już nie zadziała. Jeszcze gorzej jest w przypadku lokalnych tabel tymczasowych np.: #SomeTempTable, których nie podejrzymy nawet jeśli zostały utworzone poza transakcją. Co w takim wypadku?

Ja w takiej sytuacji korzystam z możliwości Visual Studio, a dokładniej z potęgi okna Immediate (Debug -> Windows -> Immediate Ctrl + D, I). Zakładam, że wykonując poniższe kroki aplikacja zatrzymana jest na jakiejś pułapce np.: przed uruchomieniem kolejnej procedury robiącej coś z tabelą tymczasową.
  • W oknie Immediate wpisuje DataTable dt = new DataTable();
  • Również w oknie Immediate, wpisuję dt = DataProvider.Instance.ExecuteDataTable("select * from #SomeTempTable");.
  • Przechodzę np.: do okna Watch, podaję nazwę utworzonej zmiennej czyli dt i cieszę się wbudowanym w VS wizualizatorem dla klasy DataTable.


Kilka słów wyjaśnienia w sprawie DataProvider.Instance.ExecuteDataTable. Zakładam tutaj, że jeśli ktoś pisze aplikację bazodanową i nie korzysta z ORM, to ma napisaną jakąś pomocnicza klaskę, która: zarządza połączeniem do bazy danych, pozwala łatwo wykonać zapytania bez potrzeby każdorazowego ręcznego tworzenia DbCommand itd. W tym przypadku jest to klasa DataProvider, która jest singleton'em. Jest to o tyle ważne, że kiedy odwołuję się do niej w oknie Immediate to korzystam z tego samego połączenia co aplikacja, a więc mogę podejrzeć tabele tymczasowe. Pod spodem wykonywane jest zwykłe zapytanie w stylu ADO.NET.

Ostatnio odkryłem również fajny projekt sp_select składający się z dwóch procedur składowanych, które w "magiczny" sposób pozwalają podejrzeć lokalną tabelę tymczasową korzystając z innego połączenia niż to, w którym tabela została utworzona.

09/05/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy

Home

Na początku marca pisałem o "dziwnym" zachowaniu konstruktora statycznego. W skrócie chodziło o to, że jeśli chcieliśmy wywołać metodę w domenie aplikacyjnej innej niż domyślna, konstruktor statyczny klasy, do której należała ta metoda, wołany był o jeden raz więcej w środowisku x64 niż w środowisku x86. Ponieważ sprawa mnie nurtowała postanowiłem zadać pytanie poprzez portal Microsoft Connect. Zajęło to trochę ale w końcu uzyskałem wyjaśnienie. Odpowiedź jest dość ciekawa, a więc zapraszam do lektury.

08/05/2012

RavenDB (cz. 7) - HttpListenerException

Home

Ten post będzie krótki i zwięzły. Jakiś czas temu kiedy włączyłem UAC (User Account Control) ze zdziwieniem zauważyłem, że moja aplikacja używająca Raven DB nie działa. Przy wywołaniu metody DocumentStore.Initialize rzucany był wyjątek HttpListenerException. Po wyłączeniu UAC błąd nie występował.

Z problem łatwo sobie poradzić nadając użytkownikowi, jaki uruchamia aplikację, uprawnienia do nasłuchiwania na porcie używanym przez bazę danych. Można to zrobić przy pomocy narzędzia httpcfg lub netsh tak jak to zostało opisane w tym dokumencie. Co jednak najlepsze Raven DB dostarcza gotowej klasy NonAdminHttp, w bibliotece Raven.Database.dll, która rozwiązuje ten problem. Poniżej przykład użycia.

...
NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(Store.Configuration.Port);

Store.Initialize();
...

A tak w ogóle to warto zajrzeć, przy pomocy jakiegoś deasemblera, do wnętrza metody EnsureCanListenToWhenInNonAdminContext i zobaczyć jak została zaimplementowana.

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.
  • Sterować tym, które właściwości zostaną zapisane do bazy danych.
  • Rozwiązać kłopoty związane z IntelliTrace i Raven DB.
  • Rozwiązać problem z HttpListenerException.

12/04/2012

Programmable Data Query (cz.2)

Home

W poprzednim poście opisałem czym są Programmable Data Query i jak zdefiniować wykorzystujące je zdarzenie IntelliTrace. Teraz pora na przedstawienie przykładowej implementacji PDQ. Zacznijmy jednak od przypomnienia do czego służą poszczególne metody interfejsu IProgrammbleDataQuery. Potrzebne informacje zamieściłem na poniższej mapie myśli:



Poniższe PDQ może zostać użyte z każdym zdarzeniem diagnostycznym. Jest proste ale nie znajdziemy analogicznego w bibliotece Microsoft.VisualStudio.DefaultDataQueries.dll. Bardzo dobrze nadaje się jako baza to tworzenia bardziej skomplikowanych rozwiązań.

Jego działanie polega na odczytaniu wartości parametrów aktualnych wywołania metody lub wartości zwróconej przez metodę i stworzeniu na tej postawie opisu zdarzenia. Dla przypomnienia opis zdarzenia wyświetlany jest przez Visual Studio w czasie przeglądania nagranego logu IntelliTrace.

public class Test : IProgrammableDataQuery
    {
        public object[] EntryQuery(object thisArg, object[] args)
        {
            return args;
        }

        public object[] ExitQuery(object returnValue)
        {
            return new object[] { returnValue };
        }

        public List<CollectedValueTuple> FormatCollectedValues(object[] results)
        {
            List<CollectedValueTuple> list = new List<CollectedValueTuple>();
            for (int i = 0; i < results.Length; ++i)
            {
                list.Add(new CollectedValueTuple(
                    String.Format("Result{0}", i), 
                    String.Format("'{0}'",results[i]), 
                    "string"));
            }
            return list;
        }

        public string FormatLongDescription(object[] results)
        {
            try
            {
                StringBuilder builder = new StringBuilder();
                builder.AppendFormat("Collected objects ({0}): ", results.Length);
                bool flag = true;
                foreach (var r in results)
                {
                    if (!flag)
                    {
                        builder.Append(", ");
                    }

                    flag = false;
                    builder.AppendFormat("'{0}'", r);
                }

                return builder.ToString();
            }
            catch (Exception ex)
            {
                return ex.ToString();
            }
        }

        public string FormatShortDescription(object[] results)
        {
            return FormatLongDescription(results);
        }

        public List<Location> GetAlternateLocations(object[] results)
        {
            return null;
        }
    }
Implementacja EntryQuery oraz ExitQuery jest trywialna. W żaden sposób nie modyfikuję danych wejściowych i po prostu przekazuje je dalej. FormatLongDescription wykorzystuje te dane do stworzenia opisu zdarzenia po prostu sklejając poszczególne elementy tablicy. Dla zdarzenia przeznaczonego do analizowania danych wejściowych będą to parametry aktualne, a dla zdarzenia przeznaczonego do analizowania danych wyjściowy wynik zwrócony przez metodę. FormatShortDescription po prostu wywołuje FormatLongDescription. Logika FormatCollectedValues też nie jest skomplikowana. Metoda ta po prostu zwraca dane jakie otrzymała na wejściu, dodatkowo nadając im etykiety Result0, Result1 itd.

07/04/2012

Programmable Data Query

Home

O zdarzeniach IntelliTrace pisałem już kilkakrotnie (Własne zdarzenia IntelliTrace!, Własne zdarzenia IntelliTrace 2). Każdy z tych postów dotyczył jednak zdarzeń definiowanych deklaratywnie w pliku XML. Jest to stosunkowo proste, nie potrzeba nic kodować ale co z tym związane ma to też swoje ograniczenia.

W takiej sytuacji z pomocą przychodzą nam Programmable Data Query (w skrócie PDQ) czyli klasy implementujące interfejs Microsoft.HistoricalDebuggerHost.IProgrammableDataQuery. Interfejs ten umożliwia programowe analizowanie zdarzeń IntelliTrace (wywołań metod), parametrów aktualnych wywołań, właściwości obiektów itd. Daje to bardzo duże pole do popisu, zacznijmy jednak od tego jak zdefiniować zdarzenie korzystające z PDQ w pliku z planem działania IntelliTrace (domyślnie CollectionPlan.xml):
<DiagnosticEventSpecification xmlns="urn:schemas-microsoft-com:visualstudio:tracelog" enabled="true">
 <Bindings>
  <Binding onReturn="false">
   <ModuleSpecificationId>TestApp.exe</ModuleSpecificationId>
   <TypeName>TestApp.A</TypeName>
   <MethodName>Fun</MethodName>
   <MethodId>TestApp.A.Fun(System.Int32):System.Void</MethodId>
   <ShortDescription _locID="shortDescription.TestApp.A.Fun(System.Int32):System.Void"></ShortDescription>
   <LongDescription _locID="longDescription.TestApp.A.Fun(System.Int32):System.Void"></LongDescription>
   <DataQueries>
   </DataQueries>
   <ProgrammableDataQuery>
    <ModuleName>IntelliTrace.ProgrammableDataQueries.dll</ModuleName>
    <TypeName>IntelliTrace.ProgrammableDataQueries.Test</TypeName>
   </ProgrammableDataQuery>
  </Binding>
 </Bindings>
 <CategoryId>Test</CategoryId>
 <SettingsName _locID="settingsName.TestApp.A.Fun(System.Int32):System.Void">Fun</SettingsName>
 <SettingsDescription _locID="settingsDescription.TestApp.A.Fun(System.Int32):System.Void">Fun</SettingsDescription>
</DiagnosticEventSpecification>
Nie będę dokładnie omawiał co oznaczają poszczególne węzły XML ponieważ, ponieważ zrobiłem to we wcześniejszych postach. W skrócie, powyższe zdarzenie zostało zdefiniowane dla metody o sygnaturze void Fun(int), której należy szukać w dll'ce TestApp.exe. Jedyna nowość to użycie węzła ProgrammableDataQuery zamiast DataQueries, który nie robi nic innego jak wskazuje PDQ. Zawiera on dwa podwęzły, których znaczenia łatwo się domyśleć. ModuleName to pełna nazwa dll'ki zawierającej klasę z implementacją interfejsu IProgrammableDataQuery, a TypeName definiuje pełną nazwę tej klasy.

Wróćmy do tego co najciekawsze czyli do implementacji interfejsu IProgrammableDataQuery. Deklarację tego interfejs znajdziemy w bibliotece Microsoft.VisualStudio.IntelliTrace.dll, który u mnie na komputerze leży w poniższym katalogu:

C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\PublicAssemblies\

Po dodaniu do projektu referencji do powyższej biblioteki i zaimportowaniu przestrzeni nazw nie pozostaje nam nic innego jak zabrać się do implementacji poszczególnych metod. Nie jest ich dużo. Pierwsza grupa metod wołana jest w czasie nagrywania logu IntelliTrace, w momencie pojawienia się zdarzenia - wywołania metody:
  • object[] EntryQuery(object thisArg, object[] args) - Metoda wołana jeśli mamy do czynienia ze zdarzeniem przeznaczonym do analizowania danych wejściowych (pisałem o tym w tym poście). thisArg to obiekt na rzecz, którego została wywołana metoda, a args to wartości parametrów przekazanych do metody. Tablica zwrócona przez EntryQuery zostanie następnie przekazana do FormatShortDescription, FormatLongDescription oraz FormatCollectedValues.
  • object[] ExitQuery(object returnValue) - Metoda wołana jeśli mamy do czynienia ze zdarzeniem przeznaczonym do analizowania danych wyjściowych. returnValue to wynik zwrócony przez metodę. Tablica zwrócona przez ExitQuery zostanie następnie przekazana do FormatShortDescription, FormatLongDescription oraz FormatCollectedValues.
Drugra grupa metod wołana jest w czasie przeglądania logu IntelliTrace na przyład w Visual Studio:
  • List<CollectedValueTuple> FormatCollectedValues(object[] results) - Metoda ta pozwala sformatować dane skojarzone ze zdarzeniem, zwrócone przez EntryQuery albo ExitQuery. Dane te będą potem wyświetlane w Visual Studio po wybraniu danego zdarzenia. Metoda ta powinna więc przynajmniej zwrócić to co otrzymała na wejściu tak aby Visual Studio miało co pokazać.
  • string FormatLongDescription(object[] results) - Ta metoda zwraca tzw. długi opis zdarzenia wyświetlany przez Visual Studio. Jako dane wejściowe przyjmuje tablicę zwróconą przez EntryQuery lub ExitQuery.
  • string FormatShortDescription(object[] results) - Ta metoda zwraca tzw. krótki opis zdarzenia wyświetlany przez Visual Studio. Jako dane wejściowe przyjmuje tablicę zwróconą przez EntryQuery lub ExitQuery.
  • List<Location> GetAlternateLocations(object[] results) - Szczerze mówiąc jeszcze dokładnie nie wiem jak użyć tej metody ale jak tylko się dowiem to o tym napiszę :)
Gotową dll'kę z naszą własną implementacją PDQ musimy umieścić w katalogu, w którym znajduje się program IntellITrace.exe. Domyślna lokalizacja to:

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools.

Uwaga! Tak jak pisałem dll'ka z PDQ potrzebna jest nie tylko w czasie nagrywania logu ale również w czasie jego przeglądania. Jeśli będzie jej brakować informacje na temat nagranych zdarzeń nie będą dostępne.

Powyższy katalog zawiera również bardzo ciekawą bibliotekę Microsoft.VisualStudio.DefaultDataQueries.dll, w której znajdziemy kilkadziesiąt przykładowych PDQ. Analizując ten kod można się dużo dowiedzieć. Na koniec jeszcze jedna informacja. PDQ zadziałają również jeśli uruchomimy IntelliTrace poza Visual Studio (o tej technice pracy z IntelliTrace pisałem w tym poście).

W następnym poście przedstawię przykładową implementację PDQ.

29/03/2012

RavenDB (cz. 6) - małe kłopoty z IntelliTrace

Home

Ten post będzie krótki ale poruszę w nim sprawę, o której dobrze wiedzieć aby potem nie kląć pod nosem i nie wołać o pomstę do nieba, bo coś nagle przestało działać.

Otóż Raven DB, z powodów opisanych dalej, nie współpracuje dobrze z historycznym debugerem IntelliTrace pracującym w trybie rozszerzonym (IntelliTrace events and call information). Jest to tryb, w którym IntelliTrace monitoruje wywołania metod, konstruktorów, dostęp do właściwości itd. oraz dodatkowo tzw. zdarzenia diagnostyczne, które są monitorowane również w trybie podstawowym (pisałem o tym w poście).

Tak naprawdę problem nie jest związany bezpośrednio z Raven DB ale z jedną z bibliotek z jakich korzysta. Nie współpracuje to zresztą eufemizm, bo powinienem napisać nie działa, wywala się... Jeśli uruchomimy aplikację korzystającą z Raven DB pod kontrolą IntelliTrace w pewnym momencie (próba zapisu, odczytu, utworzenia indeksu) otrzymamy wyjątek VerificationException z komunikatem Operation could destabilize the runtime.. Call stack zaprowadzi nas natomiast do biblioteki Newtonsoft.Json.

IntelliTrace wstrzykuje w kod monitorowanych programów własne instrukcje i to najpewniej w tym przypadku powoduje błąd. Z problemem można sobie jednak łatwo poradzić mówiąc IntelliTrace, aby ignorował tą bibliotekę. W tym celu otwieramy okno opcji Tools -> Options, wybieramy menu IntelliTrace i dalej Modules, klikamy przycisk Add..., i w polu tekstowym wpisujemy *Newtonsoft*, a na koniec zatwierdzamy.

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.
  • Sterować tym, które właściwości zostaną zapisane do bazy danych.
  • Rozwiązać kłopoty związane z IntelliTrace i Raven DB.

17/03/2012

RavenDB (cz. 5) - JsonIgnore

Home

Kiedy zapisujemy w Raven DB jakiś obiekt, to domyślnie w bazie zostaną zapisane wartości wszystkich jego właściwości, publicznych i prywatnych, a także tych tylko do odczytu. Nie zawsze jest to pożądane, niektóre rzeczy chcemy po prostu pominąć. W takiej sytuacji z pomocą przychodzi nam atrybut JsonIgnore. Właściwości oznaczone tym atrybutem będą pomijane przez silnik serializujący, a znajdziemy go w dll'ce Newtonsoft.Json.dll używanej przez Raven DB.
public class Test
{
 public int Id { get; set; }
 public int string WillBeSavedToRavenDB{ get; set; }
 [JsonIgnore]
 public int string WillBeIgnoredByRavenDB{ get; set; }
}
Użycie atrybutu JsonIgnore może być jednak problematyczne. W swoich projektach używam klasy BaseEntity, która jest klasą bazową dla innych encji np.: ExpressionEntity, TranslationEnity itd. Klasa ta zdefiniowana jest w osobnej bibliotece nie mającej niż wspólnego z Raven DB. W szczególności wykorzystuję ją w projektach, które korzystają z relacyjnej bazy danych.

Łatwo się domyślić, że klasa ta ma właściwości, których nie chcę zapisywać w dokumentowej, lub innej, bazie danych. Innymi słowy wymaga to abym oznaczył je atrybutem JsonIgnore czyli dodał do projektu zawierającego tą klasę referencję do biblioteki Newtonsoft.Json.dll. Nie chciałem jednak tego robić, bo jest to atrybut specyficzny dla Raven DB i biblioteka ta nie jest potrzebna we wszystkich moich projektach.

Problem rozwiązałem oznaczając interesujące właściwości jako virtual, umożliwiając tym samym ich przedefiniowanie (ang. override) i oznaczenie atrybutem JsonIgnore w projektach używających Raven DB.
public class BaseEntity
{
 public virtual string Name { get; set; }
}

public class TestEntity : BaseEntity
{
 public int Id { get; set; }
 public string Id { get; set; }
 [JsonIgnore]
 public override string Name
 {
  get { return base.Name; }
  set { base.Name = value; }
 }
}
Niestety z moich obserwacji wynika, że opisane podejście nie działa z właściwościami protected. Jeśli przedefiniowujemy taką właściwość i oznaczamy atrybutem JsonIgnore to zostanie to zignorowane, a jej wartość zostanie zapisana w bazie danych. Czyżby bug w Raven DB?

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.
  • Sterować tym, które właściwości zostaną zapisane do bazy danych.

03/03/2012

Domeny aplikacyjne, konstruktor statyczny, a platforma x86 vs x64

Home

Na początek trochę kodu. Zacznijmy od klasy testowej:
public class TestClass : MarshalByRefObject
{
    static TestClass()
    {
        Console.WriteLine(String.Format("I'm in the static constructor in the domain '{0}'.",
            AppDomain.CurrentDomain.FriendlyName));
    }

    public void Hello()
    {
        Console.WriteLine(String.Format("Hello from the domain '{0}'.", AppDomain.CurrentDomain.FriendlyName));
    }
}
Teraz kod testujący:
AppDomain domain = AppDomain.CreateDomain("Test");

TestClass t = (TestClass)domain.CreateInstanceAndUnwrap(typeof(TestClass).Assembly.FullName, typeof(TestClass).FullName);
t.Hello();
Oraz pytanie co zostanie wypisane na ekran? A w szczególności ile razy zostanie wywołany konstruktor statyczny? W głównej domenie aplikacyjnej? W domenie pomocniczej? A może w obu?

Skoro o to pytam to zapewne gdzieś tkwi haczyk. Otóż okazuje się, że wynik będzie zależał od tego czy program został skompilowany z opcję Platform target ustawioną na x86 czy x64. W przypadku x86 na ekran zostanie wypisany taki wynik:

I'm in the static constructor in the domain 'Test'.
Hello from the domain 'Test'
A w przypadku x64 taki:
I'm in the static constructor in the domain 'Test'.
I'm in the static constructor in the domain 'ConsoleApplication.vshost.exe'.
Hello from the domain 'Test'
Jeśli natomiast program zostanie skompilowany z opcją AnyCPU wynik będzie zależał od maszyny na jakiej go uruchomimy.

Konstruktor statyczny dla danej klasy wołany jest co najwyżej jeden raz w danej domenie aplikacyjnej. Jak jednak widać w zależności od platformy może zostać wywołany w jednej lub dwóch domenach. Może to mieć znaczenie kiedy jego kod będzie zawierał np.: jakiś kod inicjalizujący. Przedstawiony scenariusz nie jest zbyt częsty ale jeśli wystąpi, wykrycie błędu może być trudne, dlatego dobrze wiedzieć o tej różnicy.

Opisane zachowanie testowałem na trzech maszynach więc zakładam, że nie jest to coś lokalnego i przypadkowego.

19/02/2012

RavenDB (cz. 4) - zapytania 2

Home

Ten post to kontynuacja poprzedniego postu dotyczącego zapytań w Raven DB.

Contains == Equals !!!

Jedną z funkcjonalności jaką chciałem zaimplementować w swoim programie było wyszukiwanie wyrażeń/tłumaczeń zawierających podany ciąg znaków. Brzmi prosto ale nie obyło się bez problemów. Otóż okazało się, że Raven DB traktuje wywołanie metody String.Contains jako String.Equals, chyba że do poszukiwanego ciągu znaków dodamy gwiazdki. Na przykład zamiast s.Contains("kot") użyjemy s.Contains("kot*"), s.Contains("*kot") lub s.Contains("*kot*") w zależności czy poszukiwane wyrażenie ma znajdować się na początku, na końcu lub w środku.
string textToFind = String.Format("*{0}*", textToFind);

var res = 
 from ex in session.Query<ExpressionEntity>()
 where ex.Expression!= null && ex.Expression.Contains(textToFind) || ex.Translations.Any(t => t.Translation != null && t.Translation.Contains(textToFind))
 orderby ex.Expression
 select ex
Pozostaje jeden problem, którego niestety nie udało mi się rozwiązać. Ten kod nie zadziała jeśli szukany tekst zawiera białe znaki. Załóżmy, że na liście naszych wyrażenie mamy wyrażenie Ala ma kota. Jeśli spróbujemy wyszukać "*Ala*", "*ma*", "*kota*" otrzymamy poprawny wynik. Jeśli natomiast spróbujemy wyszukać całe wyrażenie "*Ala ma kota*" zapytanie zwróci nam pustą listę. Co dziwniejsze jeśli nie użyjemy gwiazdek czyli zlecimy wyszukanie "Ala ma kota" wyrażenie zostanie znalezione.

Ale co, jeśli szukam podwyrażenia "Ala ma" lub "ma kota"? Nie jest to duże pocieszenie ale Raven DB poradzi sobie z "Ala ma*" (z "*ma kota" już nie). Moim zdaniem sugeruje to, że wewnętrznie Raven DB do wyszukiwania używa jakiejś struktury opartej o prefixy.

Małe ostrzeżenie

W jednym z zapytań chciałem użyć metody String.Equals ale zakończyło się to wyjątkiem NullReferenceException. Sądzę, że jest to związany z użyciem metod statycznych bo kiedy użyłem metody instancyjnej np.: ex.Expression.Equals(textToFind, StringComparison.InvariantCultureIgnoreCase) obyło się bez błędów.

Stale Data

Jedną z głównych idei przyświecających twórcom Raven DB było zapewnić aby pytający o dane jak najszybciej uzyskał odpowiedź nawet jeśli oznaczałoby to zwrócenie nieaktualnych, przestarzałych danych. Używając Raven DB trzeba być na to przygotowanym nawet pracując z małą ilością danych. W moim przypadku baza danych zawiera ok. 600 dokumentów, nie całe 0.5 MB po wyeksportowaniu do pliku XML i obserwuję ten efekt.

Na przykład na początku moja aplikacja wyświetlała liczbę wyrażeń obok etykiety z nazwą języka. Jeśli jednak użytkownik importował do aplikacji wiele wyrażeń to operacja zapisywania danych, odświeżania indeksów itd. po stronie bazy danych trwała na tyle długo, że wyliczona liczba wyrażeń nie zgadzała się ze stanem faktycznym.
  • Użytkownik rozpoczyna import wyrażeń.
  • Zakończenie importu z perspektywy aplikacji/użytkownika.
  • Obliczenie liczby wyrażeń w poszczególnych językach.
  • ...
  • Faktyczne zakończenie importu po stronie bazy danych.
W tej chwili do problemu podchodzę tak, że liczbę słów w danym języku wyświetlam na życzenie. Jeśli takie zachowanie Raven DB jednak nam przeszkadza to należy explicite powiedzieć, że nie śpieszy się nam i możemy poczekać na aktualne dane np.:

var res = 
 from ex in session.Query<ExpressionEntity>().Customize(a => a.WaitForNonStaleResults())
 select ex;

Lucene

Raven DB jako silnika indeksujący używa technologii Lucene .NET. Co ciekawe umożliwia zdefiniowanie zapytań w składni jaką posługuje się ten silnik. Bezpośrednie użycie Lucene może wyglądać tak:
var res = session.Advanced.LuceneQuery<ExpressionEntity>().Where(luceneQuery);
Gdzie luceneQuery to po prostu ciąg znaków zawierający zapytanie w składni zrozumiałej dla Lucene. Może to się przydać kiedy będziemy chcieli zadać zapytanie nie obsługiwane przez "LINQ to Raven DB".

Indeksy

Jeszcze parę słów o indeksach. Termin ten przewinął sie już kilka razy. Pracując z Raven DB trzeba sobie przede wszystkim uświadomić, że tutaj indeksy różnią się od indeksów używanych w bazach relacyjnych. Moim zdaniem dwie różnice są fundamentalne. Po pierwsze Raven DB indeksuje zapytania, a nie dane. Indeks definiujemy definiując zapytanie, a nie wskazując na przykład atrybut dokumentu, który ma zostać zaindeksowany.

Po drugie Raven DB używa indeksów przy wykonywaniu każdego zapytania nawet jeśli taki indeks nie został zdefiniowany. W takiej sytuacji tworzony jest indeks dynamiczny. Druga grupa indeksów to indeksy statyczne, zdefiniowane explicite przez użytkownika na podstawie jego wiedzy o zadawanych pytaniach. Tymczasowe indeksy dynamiczne mogą zostać zamienione na stałe jeśli Raven DB zauważy, że taki indeks jest często używany.

Podsumowanie

O ile sposób realizacji podstawowych operacji dodaj/usuń/zmodyfikuj na dokumentach bardzo chwaliłem ze względu na jego prostotę i niski koszt wejścia to o raportowaniu/zapytaniach nie mogę już tego powiedzieć. Z jednej strony nie jest to żaden rocket science ale z drugiej nie jest to już "bułka z masłem" i wymaga znajomości rożnych sztuczek i specyfiki Raven DB. Jeśli uwzględnić, że zapytania jakich użyłem były bardzo proste, a pomimo to sprawiły mi sporo kłopotów to obawiam się, chociaż są to tylko moje przypuszczenia, że realizacja skomplikowanych raportów może być w Raven DB bardzo nietrywialna.

Podsumujmy co już umiemy:
  • Osadzić Raven DB w aplikacji hostującej.
  • Zainicjować Raven DB.
  • Skonfigurować dostęp do Raven Studio i API REST'owego.
  • Tworzyć obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Dodawać/usuwać/modyfikować dokumenty.
  • Zadawać proste i te trochę bardziej skomplikowane zapytania.
  • Utworzyć indeks.
  • Skorzystać z algorytmu Map/Reduce.
  • Skorzystać z zapytań Lucene.
  • Wymusić zwrócenie przez zapytanie aktualnych danych.

13/02/2012

Problem z linkerem

Home

Na blogu nie poruszałem jeszcze tematyki C++ ale skoro nadarzyła się okazja to czemu nie. Otóż, przez mocno już zirytowanego programistę, zostałem poproszony o pomoc w rozwiązanie problemu z kompilacją programu napisanego w C++.

Rzeczony programista chciał w swojej aplikacji użyć znalezionej w sieci biblioteki kryptograficznej. W ustawieniach projektu wskazał, więc lokalizację pliku lib z bibliotekę oraz lokalizację potrzebnych plików nagłówkowych. Niestety, pomimo wielokrotnych prób, sprawdzaniu ustawień, rwania włosów z głowy itd. próba kompilacji zawsze kończyła się długą listą błędów.



Rozwiązanie okazało się tyle proste, co nieintuicyjne i trudne do znalezienia jeśli sie o tym nie wie. Otóż biblioteka kryptograficzna została skompilowana z inną wartością parametru Runtime Library tj. Multi-threaded Debug (/MTd) niż aplikacja tj. Multi-threaded Debug DLL (/MDd). Po zmianie ustawień parametru Runtime Library dla aplikacji na wartość Multi-threaded Debug (/MTd) proces kompilacji wreszcie zakończył się sukcesem.