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.


12/02/2012

Problem z AssemblyResolve

Home

Niedawno miałem okazję pracować z aplikacją, w której zaimplementowano obsługę zdarzenia AppDomain.AssemblyResolve. Zdarzenie to pozwala załadować assembly jeśli standardowy mechanizm platformy .NET nie poradzi sobie z tym zadaniem. Aplikacja działała poprawnie aż do migracji na platformę .NET 4.0 Czemu? O tym właśnie będzie post. Zwrócę w nim uwagę na dość istotną różnicę pomiędzy platformą .NET 4.0, a jej wcześniejszymi wersjami jeśli chodzi o wspomniane zdarzenie. Różnica ta, w określonych warunkach, może napsuć krwi.

Przechodząc do meritum chodzi o to, że począwszy od .NET 4.0 zdarzenie AppDomain.AssemblyResolve generowane jest dla wszystkich assemblies, również tych z zasobami. Poniżej cytat z dokumentacji z MSDN'a.

Beginning with the .NET Framework 4, the ResolveEventHandler event is raised for all assemblies, including resource assemblies. In earlier versions, the event was not raised for resource assemblies. If the operating system is localized, the handler might be called multiple times: once for each culture in the fallback chain.

Oznacza to, że jeśli biblioteka SomeLibrary zawiera zasoby, to zdarzenie AppDomain.AssemblyResolve zostanie wygenerowane dla nie dwa razy. Raz z żądaniem załadowania assembly o nazwie SomeLibrary, a drugi raz z żądaniem załadowania SomeLibrary.resources. Fizycznie oba assembly znajdują się natomiast w dll'ce o nazwie SomeLibrary.dll.

Jeśli obsługa zdarzenia AppDomain.AssemblyResolve zaimplementowana została w uproszczony albo raczej błędy sposób mamy problem. Mam tu na myśli implementację, w której nie przewidziano, że nie uda się znaleźć dll'ki o nazwie zgodnej z nazwą assembly. W omawianym przypadku wyglądało to mniej więcej tak:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 Assembly asm = Assembly.LoadFrom(path);
 return asm;
}
Łatwo się domyślić, że wywołanie Assembly.LoadFrom(path) dla ścieżki SOME_PATH\SomeLibrary.resources.dll zakończy się błędem bo taka ścieżka nie istnieje. W aplikacji objawi się to wyjątkiem FileNotFoundException przy próbie odczytania czegokolwiek z zasobów. Powinno to zostać zrobione wcześniej ale łatwo to naprawić:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 try
 {
  return Assembly.LoadFrom(path);
 }
 catch(Exception ex)
 {
  //Log exception
  
  return null;
 }
}
Albo w taki sposób, jeśli chcemy obsłużyć tylko scenariusz z zasobami:
private static Assembly MyResolveEventHandler(object sender, ResolveEventArgs args)
{
 //Determine assembly path based on the configuration
 //...
 
 if(args.Name.Contains(".resources,"))
  return null;
  
 Assembly asm = Assembly.LoadFrom(path);
 return asm;
}
Zwrócenie null powoduje, że zostanie użyty standardowy mechanizm wyszukiwania i ładowania assemblies platformy .NET, a ponieważ dll'ka SomeLibrary.dll zostanie załadowana przy pierwszym żądaniu to poradzi sobie on z załadowaniem assembly z zasobami.

07/02/2012

RavenDB (cz. 3) - zapytania

Home

Wstęp

W tej części serii poświęconej Raven DB napiszę o zapytaniach, pobieraniu danych. Temat sam w sobie jest bardzo obszerny i to, co napiszę, to tylko szczyt góry lodowej. Większość tematów po prostu zasygnalizuje, ale sądzę, że dobrze pokaże, jak to wygląda z Raven DB.

Podstawy

Podstawowe zapytania zadajemy w bardzo prosty sposób i robimy to otwierając najpierw sesję pracy z bazą np.:
using (var session = Store.OpenSession())
{
 var res = from ex in session.Query<ExpressionEntity>()
    select ex;
 ...
}
Aby pobrać listę dokumentów użyłem metody Query określając jaki typ dokumentów mnie interesuje. Dla przypomnienia klasa ExpressionEntity reprezentuje wyrażenie i jego tłumaczenia. Jest to ta sama klasa, która wcześniej posłużyła mi do umieszczenia danych w bazie.

Jeszcze jeden przykład. Tym razem pobieram listę dokumentów by na jej podstawie określić listę różnych kategorii, jakie zdefiniowano dla wszystkich wyrażeń.
var res = (from e in session.Query<ExpressionEntity>()
    where e.Category != null
    orderby e.Category
    select e.Category).Distinct();
Jak widać podstawowe zapytania wykonuje się bardzo łatwo.

Stronicowanie

Przy dużej liczbie dokumentów przydatne okaże się stronicowanie. To też nie jest trudne do zrealizowania:
var res = (from ex in session.Query<ExpressionEntity>()
    orderby ex.Expression
    select ex).Skip(index * pageSize).Take(pageSize).ToList();
Użyta w kodzie zmienna index to indeks strony do pobrania (w numerowaniu od zera), a pageSize to oczywiście wielkość strony liczona w liczbie dokumentów. Metoda Skip pozwala więc na przeskoczenie do konkretnej strony, a metoda Take na pobranie takiej liczby dokumentów jaka mieści się na stronie.

Robi się trudniej

Jedną z funkcjonalności, jakie chciałem mieć w swoim programie LanguageTrainer, było zliczanie liczby wyrażeń posiadających tłumaczenie w danym języku. Brzmi prosto, prawda? Pierwsza moja próba wyglądała tak:
 var count = 
 (from ex in session.Query<ExpressionEntity>()
 from t in ex.Translations
 where t.Language == selectedLang && !String.IsNullOrEmpty(t.Translation) 
 select 1).Count();
Zmienna selectedLang zawiera interesujący nas język. Wykonanie takiego zapytania zakończy się wyjątkiem NotSupportedException z komunikatem Method not supported: SelectMany. A więc może coś takiego:
var count =
 (from ex in session.Query<ExpressionEntity>()
 where ex.Translations.Any(t => t.Language == selectedLang && !String.IsNullOrEmpty(t.Translation))
 select 1).Count();
Tym razem zakończy się wyjątkiem z komunikatem Method not supported: IsNullOrEmpty. No cóż IsNullOrEmpty łatwo zastapić zwykłym porównaniem. Spróbujmy więc jeszcze raz:
var count = 
 (from ex in session.Query<ExpressionEntity>()
 where ex.Translations.Any(t => t.Language == selectedLang && t.Translation != null && t.Translation != String.Empty)
 select 1).Count();
To też nie zadziała i znowu zakończy sie błędem, tym razem z komunikatem Node not supported: Constant. Jeszcze jednak próba i w końcu zadziałało:
var count = 
 (from ex in session.Query<ExpressionEntity>()
 where ex.Translations.Any(t => t.Language == selectedLang && t.Translation != null && t.Translation != String.Empty)
 select ex).Count();
Nie jest to skomplikowane ale wymaga znajomości kilku "trików", nie jest do końca intuicyjne.

MapReduce

Do opisanego powyżej problemu można też podejście w Raven DB w inny sposób, a mianowicie stosując algorytm MapReduce. W ten sposób wykonując jedno zapytanie otrzymamy wyniki dla wszystkich języków za jednym razem. W Raven DB robimy to definiując indeks (jeszcze o tym napiszę):
public class TranslationsCounter : AbstractIndexCreationTask<ExpressionEntity, TranslationsCounter.ReduceResult>
{
 public class ReduceResult
 {
  public Languages Lang { get; set; }
  public int Count { get; set; }
 }

 public TranslationsCounter()
 {
  Map = docs => from doc in docs
   from t in doc.Translations
   select new { Lang = t.Language, Count = String.IsNullOrEmpty(t.Translation) ? 0 : 1 };

  Reduce = results => from t in results
   group t by t.Lang
   into g
   select new { Lang = g.Key, Count = g.Sum(x => x.Count) };
 }
}
i zadanie zapytania przy jego użyciu np.:
public IDictionary<Languages,int> CountExpressionsByLanguage()
{
 using (var session = Store.OpenSession())
 {
  var dict = new Dictionary<Languages, int>();

  foreach(var res in session.Query<TranslationsCounter.ReduceResult, TranslationsCounter>())
  {
   dict.Add(res.Lang, res.Count);
  }

  return dict;
 }
}


To jeszcze nie koniec. Do tematu wrócę w kolejnym poście.

22/01/2012

RavenDB (cz. 2) - podstawowe operacje na dokumentach

Home

Wstęp

W drugim poście poświęconym Raven DB opiszę jak wykonywać podstawowe operacje (zapisz/usuń/zmień) na dokumentach. Ale czym jest dokument? Nie wiem jaka jest formalna definicja ale ja na dokumenty patrzę po prostu jak na obiekty zapisane (zserializowane) w formacie JSON. Poniżej przykład takiego dokumentu z mojego pet project, który odpowiada obiektowi klasy reprezentującej wyrażenie i jego tłumaczenia.
{
  "Category": "Geografia",
  "Expression": "wioska",
  "Translations": [
    {
      "Language": "Language1",
      "Translation": "Dorf",
      "GoodAnswers": 0,
      "BadAnswers": 1,
      "WasLastAnswerGood": false
    },
    {
      "Language": "Language2",
      "Translation": "village",
      "GoodAnswers": 1,
      "BadAnswers": 0,
      "WasLastAnswerGood": true
    }
  ]
}
Klasa, której obiekty chcemy wrzucić do Raven DB może być zwykłym POCO z dokładnością do jednej rzeczy. Musi posiadać taką właściwość:
public int Id { get; set; }
To przykład konwencji. Jeśli chcemy zmienić sposób przechowywania identyfikatora, należy skorzystać z DocumentStore.Conventions.FindIdentityProperty. Użyty przeze mnie EmbeddableDocumentStore dziedziczy z DocumentStore, a więc jeśli znudzi mi się Id to zawsze mogę to zmienić.

Jeśli chcemy aby Raven DB ignorował niektóre właściwości w czasie zapisywania obiektu to powinniśmy oznaczyć je atrybutem Newtonsoft.Json.JsonIgnore, który działa w taki sam sposób jak XmlIgnore dla XmlSerializer.

Zapisz/Zmień

Operację wstawienia nowego dokumentu do bazy lub aktualizacji już istniejącego wykonuję w taki sam prosty sposób np.:
public void Save(ExpressionEntity ex)
{
 using (var session = Store.OpenSession())
 {
  session.Store(ex);
  session.SaveChanges();
 }
}
Zaczynam, więc od otworzenia sesji pracy z RavenDB. Następnie przy pomocy metody Store wrzucam do bazy nowy dokument lub aktualizuję już istniejący. Jak widać nie przejmuję się, którą operację właściwie wykonuję. Jeśli obiekt będzie miał pusty/zerowy Id to zostanie utworzony nowy dokument, a właściwość Id zostanie uaktualniona. Jeśli Id zostanie wypełnione Raven DB spróbuje odszukać istniejący dokument i go uaktualnić. Jeśli nie znajdzie dokumentu o podanym identyfikatorze utworzy go. Metoda SaveChanges to coś w rodzaju Commit'a: wysyła zmiany do serwera.

Usuń

Usuwanie dokumentów z bazy również jest bardzo proste np.:
public void Remove(ExpressionEntity ex)
{
 using (var session = Store.OpenSession())
 {
  session.Delete<ExpressionEntity>(session.Load<ExpressionEntity>(ex.Id));
  session.SaveChanges();
 }
}
Jak widać przed usunięciem ładuję obiekt z bazy na podstawie identyfikatora. Jest to potrzebne ponieważ inaczej dostaniemy błąd z informacją, że obiekt nie jest doczepiony do sesji (is not associated with the session, cannot delete unknown entity instance). Nie znalazłem innego sposobu rozwiązania problemu.

Ważna uwaga

Powyżej przedstawiłem dwie przykładowe metody Save oraz Remove, które odpowiednio tworzą/aktualizują jeden dokument oraz usuwają jeden dokument z bazy danych. W przypadku wykonywania operacji na wielu dokumentach takie podejście będzie zbyt kosztowne. Jeśli zachodzi potrzeba pracy z wieloma dokumentami to powinniśmy zrezygnować z takiego mikro zarządzania i jak najwięcej operacji wykonywać w ramach jednej sesji.

Podsumowanie

Dzisiaj pokazałem jak wykonywać podstawowe operacje na dokumentach. Jak widać jest to bardzo proste. Właściwie nie musimy nic robić aby nakłonić nasze obiekty do współpracy z Raven DB. Nie potrzebujemy również mappera relacyjno-obiektowego. To jest właśnie to co bardzo mi się spodobało w Raven DB.

Definiuję sobie klasę, tworzę jej instancje i po prostu wrzucam je do bazy danych. Nie piszę komend INSERT, UPDATE lub DELETE. Nie instaluję dodatkowych komponentów. Nie konfiguruję mapowań pomiędzy tabelami i obiektami. Nie definiuję schematu bazy danych. Jest prosto, szybko i przyjemnie czyli tak jak powinno być!

Na koniec znowu podsumujmy co już mamy/umiemy:
  • Bazę Raven DB osadzoną w aplikacji hostującej.
  • Kod inicjalizujący Raven DB.
  • Dostęp do Raven Studio i API REST'owego.
  • Obiekty POCO jakie mogą zostać umieszczone w Raven DB.
  • Kod dodający/zmieniający/usuwający dokumenty.

19/01/2012

RavenDB (cz. 1)

Home

Raven DB to dokumentowa baza danych, przedstawiciel trendu NoSQL, opracowana przez Ayende i jego zespół. Całkiem niedawno bo w październiku 2011 Ayende był zresztą w Polsce i opowiadał o swoim dziecku. Kilka miesięcy temu w jednym ze swoich pet project postanowiłem zmienić technologię dostępu do danych i wybór padł właśnie na Raven DB.

Kilka słów wstępu

Projekt ten to program LanguageTrainer wspomagający naukę słówek. Pomysł jego napisania narodził się ponad rok temu kiedy "ponownie" rozpocząłem naukę języka niemieckiego. W sieci nie znalazłem programu, który spełniałby moje oczekiwania. Zdecydowałem więc, że napiszę coś szytego na miarę. Początkowo rozpoczęło się niewinnie: listę słówek i ich tłumaczeń trzymałem w pliku. Dokładniej mówiąc był to po prostu wynik serializacji XML'owej. Podejście to przestało się sprawdzać kiedy do głowy zaczęły mi przychodzić kolejne pomysły: A może dodać bardziej zaawansowane wyszukiwanie, jakieś statystyki itd.

Początkowo pomyślałem o przejściu na bazę relacyjną, ale stwierdziłem, że tutaj nie nauczę się niczego nowego. Pomyślałem, że fajnie będzie wypróbować coś zupełnie nowego, z czym nie miałem jeszcze do czynienia, a ponieważ wcześniej czytałem trochę o Raven DB i wiedziałem, że ma .NET'owe API, wybrałem właśnie ją.

Z bazą tą już trochę pracuję, z braku czasu niezbyt intensywnie, ale zebrałem już trochę doświadczeń i postanowiłem sie nimi podzielić. Planuję serię postów, w których opiszę jak pracuje się z Raven DB na przykładzie swojego pet project. Nie będzie to jednak typowy tutorial omawiający wszystkie zagadnienia. Poruszę tylko te, z którymi miałem okazję się zapoznać.

Zaczynamy

LanguageTrainer to typowa aplikacja grubego klienta (WPF + MVVM). Nie chciałem jednak uzależniać się od połączenia z serwerem. Dlatego zdecydowałem się na hostowanie Raven DB w procesie aplikacji. Po ściągnięciu paczki z binariami na swoje potrzeby skopiowałem więc katalog EmbeddedClient. W tym momencie zaznaczę jeszcze, że warstwę dostępu do danych mam ukrytą za dobrze zdefiniowanym interfejsem, a więc aplikacja nie wie z jakim źródłem danych pracuje. Poniżej fragment kodu z klasy implementującej ten interfejs, odpowiedzialny za zainicjowanie Raven DB.

public EmbeddableDocumentStore Store { get; private set; }

...

public void Init(string dir)
{
 Store = new EmbeddableDocumentStore
 {
  DataDirectory = dir,
  UseEmbeddedHttpServer = true,
 };

 Store.Initialize();
}
 


Instancja EmbeddableDocumentStore posłuży nam później do otwierania sesji pracy z Raven DB, wykonywania zapytań itd. W kodzie tym ustawiam tylko dwie właściwości. DataDirectory wskazuje katalog roboczy, w którym Raven DB utworzy odpowiednią strukturę katalogów i gdzie będzie trzymał dane. Za pierwszym razem inicjalizacja zajmie więc trochę więcej czasu.

Istotna jest też właściwość UseEmbeddedHttpServer. Dzięki ustawieniu jej na true mam dostęp do napisanej w Silverlight aplikacji Raven Studio do zarządzania dokumentami oraz, co mniej ważne z mojej perspektywy, dostęp REST'owy do dokumentów. Oczywiście ponieważ Raven DB jest hostowany przez mój program, aplikacja Raven Studio będzie dostępna tylko wtedy, kiedy uruchomiony jest LanguageTrainer.

Numer portu z jakiego będzie korzystał serwer HTTP ustawiamy w pliku konfiguracyjnym aplikacji dodając następujący wpis:
<appSettings>
    <add key="Raven/Port" value="8888" />
</appSettings>

To jednak nie wszystko. Jeśli w tym momencie spróbujemy uruchomić Raven Studio otrzymamy od serwera HTTP taki błąd:

Could not find file Raven.Studio.xap, which contains the Raven DB Studio functionality. Please copy the Raven.Studio.xap file to the base directory of RavenDB and try again.

Plik Raven.Studio.xap znajdziemy w dystrybucji Raven DB i musimy umieścić go w katalogu z jakiego uruchamiamy aplikację. Ja zrobiłem to w ten sposób, że dodałem go do projektu, Build Action ustawiłem na Content, a Copy to Output Directory na Copy if newer. Testowałem w przeglądarkach Chrome, Firefox oraz IE i działa.

Podsumowanie

Tyle na dzisiaj. Podsumujmy co już mamy/umiemy:
  • Bazę Raven DB osadzoną w aplikacji hostującej.
  • Kod inicjalizujący Raven DB.
  • Dostęp do Raven Studio i API REST'owego.
W następnej części opiszę jak wykonywać podstawowe operacji na dokumentach.

24/12/2011

Życzenia świąteczne

Home



Z okazji Świąt Bożego Narodzenia życzę czytelnikom i czytelniczkom wszystkiego dobrego, żeby najbliższe dni spędzili w wymarzony sobie sposób, z bliskimi sobie ludźmi, a w nowym roku pomyślności i wielu ciekawych wpisów na tym blogu :)

Serdecznie pozdrawiam,
Michał Komorowski

01/12/2011

Wczytywanie podzespołów do domeny aplikacyjnej

Home

Platforma .NET, dzięki mechanizmowi refleksji, pozwala na dynamiczne wczytywanie do programu podzespołów (ang. assembly). Pozwala to w łatwy sposób pisać rozszerzane przy pomocy pluginów aplikacje i na wiele innych rzeczy. Ostatnio potrzebowałem wykorzystać ten mechanizm do własnych celów. Aby zwiększyć bezpieczeństwo, postanowiłem ładować podzespoły do odzielnych domen aplikacyjnych. W ten sposób, jeśli po załadowaniu podzespołu i wykonaniu jego kodu pojawi się błąd, główna domena aplikacyjna pozostaje nienaruszona.

Użycie osobnej domeny aplikacyjnej przydaje się również kiedy chcemy wczytywać i usuwać załadowane assembly z pamięci. Problem polega na tym, że po załadowaniu podzespołu do domeny nie ma możliwości aby go z niej usunąć. Można jednak osiągnąć podobny rezultat ładując podzespoły do oddzielnych "roboczych" domen, a potem skorzystać z metody AppDomain.Unload, która usuwa z pamięci domenę i wszystkie wczytane do niej podzespoły. (To pewne uproszczenie. Jeśli assembly zostało załadowane do kilku domen to zostanie usunięte dopiero jeśli usuniemy wszystkie domeny ją używające.)

Jak to zrobić? W sieci można znaleźć kilka podejść, ja użyłem w uproszczeniu następującego sposobu:
public static class SeperateDomainAssemblyLoader
{
  [Serializable]
  private class InternalLoader
  {
    public void LoadAndProcess(string assemblyPath)
    {
      Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);

      var assembly = Assembly.LoadFrom(assemblyPath);
      //...
    }
  }

  private static AppDomain _domain = AppDomain.CreateDomain("SeperateDomainAssemblyLoader");

  public static void LoadAndProcess(string assemblyPath)
  {
    InternalLoader internalLoader = (InternalLoader)(_domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(InternalLoader).FullName));
    internalLoader.LoadAndProcess(assemblyPath);
   }
}
i kod testujący:
...
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
SeperateDomainAssemblyLoader.LoadAndProcess(somePath);
...
Niestety ku mojemu zdziwieniu program wypisał na ekran dwa razy tą samą nazwę domeny. Jak to możliwe, przecież jak wół stoi, że instancja klasy InternalLoader została stworzona w osobnej domenie. Uważni czytelnicy już pewnie widzą błąd. Ja też go znalazłem, ale chwilę zajęło mi uzmysłowienie sobie, co robię nie tak.

Zapomniałem o tym, że obiekty pomiędzy domenami aplikacyjnymi przekazywana są domyślnie przez wartość. Co z tego, że utworzyłem obiekt w osobnej domenie, skoro i tak pracowałem z jego kopią. Jeśli InternalLoader dziedziczyłby z MarshalByRefObject to pracowałabym nie z prawdziwym obiektem ale z proxy i wszystko byłoby dobrze. Poprawka jest więc bardzo prosta:
...
private class InternalLoader : MarshalByRefObject
{
  ...
}
...

Problem z półką

Home

Jakiś czas temu próbując wykonać operację merge w TFS napotkałem na bardzo irytujący problem pod tytułem:

TF203015 The Item '' has an incompatible pending change.

Nie robiłem nic bardzo skomplikowanego. Najpierw pobrałem do gałęzi A zmiany umieszczone na półce (ang. shelve). Następnie, przy pomocy polecenia merge, chciałem do nich dodać zmiany z changeset'a z gałęzi B i w tym momencie pojawił się powyższy komunikat. Sprawdziłem też odwrotną kolejność czyli najpierw merge z gałęzi B do A, a potem pobranie kodu z półki ale błąd również wystąpił. Innymi słowy, zamiast zgłosić konflikt i umożliwić jego rozwiązanie TFS wypiął się i rzucił błędem.

Nie chciałem wykonywać "ręcznego" łączenia plików ponieważ to błędogenne i niewygodne. Zacząłem szukać rozwiązania i znalazłem sposób na obejście problemu. Daleki od ideału, ale lepszy rydz niż nic. Postąpiłem w następujący sposób:
  • Zainstalowałem Team Foundation Server Power Tools
  • Najpierw wykonałem operację merge z gałęzi B do gałęzi A.
  • Uruchomiłem Visual Studio 2010 Command Prompt.
  • Przeszedłem do katalogu z gałęzią A.
  • Wpisałem komendę tfpt unshelve
  • Wybrałem swoja półkę.
  • Rozwiązałem konflikty.
Jak widać jeśli korzystamy z komendy tfpt to zamiast otrzymać błąd dostaniemy listę wykrytych konfliktów i możliwość ich rozwiązania. Można? Ano można.

16/11/2011

Hawkeye

Home

Hawkeye .NET Runtime Object Editor to program, który znalazłem w sieci dobre dwa lata temu. W tym czasie wielokrotnie mi się przysłużył, a jest przydatny w szczególności tym, którzy pracują z technologią Windows Forms. W skrócie, pozwala modyfikować UI działającej aplikacji. Jego użycie jest proste. Wskazujemy myszką interesujący nas fragment aplikacji, a Hawkeye oznacza wybraną kontrolkę przy pomocy czerwonej ramki i wyświetla listę właściwości i prywatnych pól klasy, których wartości możemy modyfikować. Jak by tego było mało, Hawkeye pozwala również dynamicznie wywoływać metody dla wybranych obiektów.

Zastanawiamy się jak nasza aplikacja będzie wyglądać z różowym tłem? Chcemy przesunąć na próbę kontrolkę o 2 piksele w prawo? A może chcemy nacisnąć przycisk, który jest nieaktywny? Te i inne rzeczy osiągamy zmieniając wartości odpowiednich właściwości, w tym przypadku odpowiednio: BackColor, Location, Enabled. W trzecim wypadku możemy też wywołać dynamicznie metodę PerformClick. Pozwala to w łatwy i przyjemny sposób zobaczyć, jak przy danych ustawieniach, będzie w runtime'ie będzie wyglądała nasza aplikacja, bez potrzeby jej rekompilacji.

Hawkeye pozwala również poruszać się w hierarchii kontrolek tworzących interfejs i sprawdzić kto jest rodzicem interesującej nas kontrolki. Podaje też pełną nazwę klasy dla aktualnie wybranego obiektu. Przydaje się to, kiedy pracujemy z dużą, skomplikowaną aplikacją i chcemy dowiedzieć się gdzie u licha znajduje się kod obsługujący aktualnie widoczną kontrolkę. Przykład z życia. Ostatnio kumpel potrzebował znaleźć kod odpowiedzialny za pewną część UI. W tym celu chciał wyświetlić designera dla interesującego go okna i na tej podstawie dojść do kontrolki. Niestety Visual Studio odmówiło posłuszeństwa i przy próbie wyświetlenia designera raportowało błąd. Hawkeye rozwiązał problem w kilka minut.

Uwaga! Na dzień dzisiejszy, na stronie programu znajdują się jego dwie wersje do pobrania. Jedna działa tylko z aplikacjami skompilowanymi na platformę .NET 4, a druga z aplikacjami skompilowanymi na starsze wersje platformy.

28/10/2011

Wiele usług w jednym procesie 2

Home

W poście tym wrócę jeszcze do tematu uruchamiania kilku usług w jednym procesie. Otóż, ciekawe jest to, że można konfigurować to zachowanie już po zainstalowaniu usługi. Służy do tego, i nie tylko tego, program wiersza poleceń o nazwie sc. Poniżej przedstawiam przykład jego użycia.

Zacznijmy od pobrania konfiguracji usługi ABC przy pomocy komendy sc query ABC. Przykładowy wynik pokazałem poniżej.
SERVICE_NAME: ABC
TYPE               : 20  WIN32_SHARE_PROCESS
STATE              : 1  STOPPED
  (NOT_STOPPABLE,NOT_PAUSABLE,IGNORES_SHUTDOWN)
WIN32_EXIT_CODE    : 1077       (0x435)
SERVICE_EXIT_CODE  : 0  (0x0)
CHECKPOINT         : 0x0
WAIT_HINT          : 0x0
Jak widać usługa ABC może współdzielić proces z jakąś inną usługą. Efekt działania bardzo ładnie widać w menadżerze zadań. Jeśli uruchomimy usługi: ABC oraz ABCDebug to menadżer zadań pokaże tylko jeden proces.



Aby to zmienić należy użyć komendy sc config ABC type= own. Teraz, po uruchomieniu obu usług, menadżer zadań pokaże dwa procesy.



Do stanu pierwotnego wracamy przy pomocy komendy sc config ABC type= share. Warto zwrócić uwagę na jeszcze jedną rzecz. Spójrzmy na ten scenariusz.
  • Mamy dwie usługi współdzielące proces: ABC oraz ABCDebug..
  • Uruchamiamy obie i menadżer zadań pokazuje jeden nowy proces: WindowsService1.exe.
  • Zatrzymujemy usługę ABC.
  • Usługa ABCDebug nadal działa.
  • sc config ABC type= own
  • Uruchamiamy usługę ABC i teraz menadżer zadań pokazuje dwa procesy WindowsService1.exe.
  • Zatrzymujemy usługę ABC
  • sc config ABC type= share
  • Ponownie uruchamiamy usługę ABC i menadżer zadań znowu pokazuje jeden proces WindowsService1.exe.
Czyli w czasie kiedy zmienialiśmy konfigurację usługi ABC usługa ABCDebug cały czas działała i w niczym to nie przeszkadzało.

26/10/2011

Wiele usług w jednym procesie

Home

Istnieje kilka podejść do debugowania usług systemowych. Jeśli chcemy debugować już uruchomioną usługę to możemy skorzystać z opcji Attach to process.... Sprawa jest trudniejsza jeśli chcemy podłączyć się do usługi w momencie jej uruchamiania. W takim wypadku można w kodzie usługi wywołać metodę Debugger.Brake. Są też inne sposoby, na przykład sztuczne opóźnienie startu usługi, tak aby zdążyć się do niej podpiąć.

Ostatnio poznałem nowe, bardzo ciekawe podejście. Polega ono na stworzeniu dodatkowej, pomocniczej "pustej" usługi i zainstalowaniu jej w odpowiedni sposób razem z właściwą usługą. Strukturę przykładowego projektu widać na poniższym rysunku.



Projekt musi również zawierać dwa instalatory (klasa ServiceInstaller), po jednym dla każdej z usług.



W kodzie przekłada się to na coś takiego.
...
this.Installers.AddRange(new System.Configuration.Install.Installer[] {
  this.serviceProcessInstaller1,
  this.serviceInstaller1,
  this.serviceInstaller2});
...
Instalację przeprowadzamy standardowo przy pomocy narzędzia InstallUtil. Zadba ono o to aby zainstalować obie usługi za jednym razem. Następnie, przy debugowaniu najpierw uruchamiamy tą drugą, dodatkową usługę, podczepiamy się do niej przy pomocy opcji Attach to process..., stawiamy pułapkę w kodzie pierwszej usługi i dopiero ją uruchamiamy. To zadziała ponieważ obie usługi zostaną uruchomione w jednym procesie, a co więcej otrzymujemy to za darmo. Odpowiada za to metoda ServiceIntaller.Install, której fragment przytaczam poniżej.
int serviceType = 0x10;
...
if (numberOfServices > 1)
{
  serviceType = 0x20;
}
...
NativeMethods.CreateService(databaseHandle, this.ServiceName, this.DisplayName, 0xf01ff, serviceType, (int) this.StartType, 1, str3, null, IntPtr.Zero, dependencies, servicesStartName, password);
...
CreateService to natywna metoda WinAPI, która dodaje usługę do bazy danych menadżera usług. Istotny jest jej piąty parametr serviceType, może przyjąć kilka wartości ale nas interesują dwie:
  • SERVICE_WIN32_OWN_PROCESS = 0x00000010 oznacza, że każda usługa działa w swoim procesie
  • SERVICE_WIN32_SHARE_PROCESS = 0x00000020 oznacza, że usługi mogą dzielić jeden proces
Jeśli liczba usług jest większa niż 1 to ServiceIntaller.Install używa flagi SERVICE_WIN32_SHARE_PROCESS, a w przeciwnym wypadku SERVICE_WIN32_OWN_PROCESS.

24/10/2011

PrintScreen

Home

Ostatnimi czasy zdarzyło się, że musiałem intensywnie korzystać z przycisku Print Scrn, tworzyć wiele zrzutów ekranu, a potem jeszcze edytować je w programie graficznym. W pewnym momencie stwierdziłem, że proces ten trzeba jakoś zautomatyzować. Z pomocą przyszedł program PrintScreen.

W wersji podstawowej jest bezpłatny. Reaguje na standardowy przycisk Print Scrn ale można to zmienić. Zrzuty ekranu wypluwa w wybranym formacie, do wskazanego katalogu. Nazwy plików mogą być generowane automatycznie np.: ScreenSchot001, ScreenSchot002 itd. Bardzo spodobało mi się to, że jeśli usuniemy np.: plik ScreenSchoot002 i w ten sposób powstanie luka w numeracji to zostanie ona zapełniona przez następny plik. Program umie również wyświetlić podgląd zrzutu, a przed zapisaniem na dysk umożliwia np.: zmianę wielkości obrazka. W zależności od konfiguracji robi zrzut całego ekranu, wybranego okna lub wskazanego fragmentu.

Nie jest to nic co przyprawia o zawrót głowy ale doskonale spełnia swoją funkcję - po prostu kawał dobrze wykonanej roboty. Program ten zagości na stale w mojej skrzynce narzędziowej i polecam go każdemu komu nie wystarcza standardowa funkcjonalność systemu operacyjnego dotycząca tworzenia zrzutów ekranu.

13/09/2011

Okazja do wzięcia udziału w ciekawych kursach

Home

Uniwersytet Stanforda organizuje zdalne kursy poświęcone uczeniu maszyn (ang. machine learning), sztucznej inteligencji oraz bazom danych. Zajęcia są bezpłatne i zaczynają się w październiku, a potrwają do grudnia. Kursy składają się nie tylko z wykładów, ale również z rożnych zadań do rozwiązania, będzie można również zadawać pytania. Moim zdaniem to wspaniała okazja, aby zdobyć albo odświeżyć dużo cennej wiedzy. Na każdy z tych wykładów zapisało się już ponad 50 tysięcy osób!

12/09/2011

IntelliTrace - problem ze zdarzeniem

Home

Jakiś czas temu pracując z IntelliTrace próbowałem zdefiniować zdarzenie diagnostyczne dla pewnej metody. Dla ustalenia uwagi niech jej sygnatura wygląda tak, jak poniżej.

string Flip(string s)

Moim celem było, aby opis zdarzenia zawierał wynik zwrócony przez metodę oraz wartość argumentu s. Inaczej mówiąc, aby w oknie IntelliTrace Events View w Visual Studio 2010 zdarzenie zarejestowane dla wywołania metody z argumentem s="Hello" i wynikiem "olleH" wyglądalo tak:

"olleH" Flip("Hello")

O definiowaniu zdarzeń IntelliTrace już pisałem (Własne zdarzenia IntelliTrace!, Własne zdarzenia IntelliTrace 2) dlatego nie będę opisywał całego procesu. Przytoczę już gotową definicję zdarzenia:
<DiagnosticEventSpecification enabled="true">
  <Bindings>
    <Binding>
      <ModuleSpecificationId>FibTest.exe</ModuleSpecificationId>
        <TypeName>Utilities</TypeName>
        <MethodName>Flip</MethodName>
        <MethodId>Utilities.Flip(System.String):System.String</MethodId>
        <ShortDescription _locID="shortDescription.Utilities.Flip">"{0}" Flip("{1}")</ShortDescription>
        <LongDescription _locID="longDescription.Utilities.Flip">"{0}" Flip("{1}")</LongDescription>
        <DataQueries>
          <DataQuery index="-1" maxSize="100" type="String" query="" />
          <DataQuery index="1" maxSize="100" type="String" query="" />
        </DataQueries>
        <ProgrammableDataQuery>
          <ModuleName></ModuleName>
          <TypeName></TypeName>
        </ProgrammableDataQuery>
    </Binding>
  </Bindings>
  ...
</DiagnosticEventSpecification>
Niestety ku moje zdziwieniu to nie zadziałało. Zdarzenie zostało zarejestrowane, ale w oknie IntelliTrace Events View zamiast zobaczyć upragniony wynik otrzymałem komunikat: An error occured while fetching the data for this event. Zajrzałem, więc do wcześniej zdefiniowanych przez siebie zdarzeń i przypomniałem sobie o jednej rzeczy. Aby odwołać się do wartości zwróconej przez metodę należy ustawić atrybut onReturn.
...
<Binding onReturn="true">
...
Niestety to też nie pomogło. Ponownie zajrzałem więc do wcześniej przygotowanych zdarzeń i na pierwszy rzut oka wszystko wyglądało tak samo. Po chwili zastanowienia doszedłem do wniosku, że przyczyną kłopotów może być to, że próbuję odwołać się zarówno do wartości argumentów jak i do wartości zwracanej przez metodę. Wcześniej czegoś takiego nie próbowałem. Zamiast jednego przygotowałem więc dwa zdarzenia. W jednym odczytuję wartość argumentu przekazanego do metody, a w drugim wynik zwrócony przez metodę.
...
<Binding onReturn="false">
  ...
  <ShortDescription _locID="shortDescription.Utilities.Flip">Flip("{0}")</ShortDescription>
  <LongDescription _locID="longDescription.Utilities.Flip">Flip("{0}")</LongDescription>
  <DataQueries>
    <DataQuery index="1" maxSize="100" type="String" query="" />
  </DataQueries>
  ...
</Binding>
...
...
<Binding onReturn="true">
  ...
  <ShortDescription _locID="shortDescription.Utilities.Flip">Flip returns "{0}"</ShortDescription>
  <LongDescription _locID="longDescription.Utilities.Flip">Flip returns "{0}"</LongDescription>
  <DataQueries>
    <DataQuery index="-1" maxSize="100" type="String" query="" />
  </DataQueries>
  ...
</Binding>
...
To zadziałało, zostały zarejestrowane dwa zdarzenia, jedno z opisem Flip("Hello"), a drugie z opisem Flip returns "olleh". Moim zdaniem to spore ograniczenie IntelliTrace, ale nie ma rady i trzeba o tym po prostu pamiętać.

11/09/2011

Londyn - trochę nietypowa wycieczka

Home

Londyn to miasto odwiedzane rokrocznie przez miliony turystów. Twierdza Tower, Tower Bridge, Pałac Buckingham to tylko z niektórych miejsc, które znajdują się na liście do zobaczenia dla wielu odwiedzających. Ja też nie omieszkałem pokazać się w tych i wielu innych miejscach, ale w tym poście chciałbym zachęcić do wizyty w innym, trochę bardziej nietypowym miejscu. Mam na myśli Thorpe Park czyli wesołe miasteczko zlokalizowane pod Londynem (ok. 40 minut pociągiem + 10 minut autobusem). Bilet kosztuje dla dorosłych 40 funtów, ale cenę można zbić o 35% przy zakupie online lub nawet 50% przy zakupie grupowym (7 biletów).

Miasteczko oferuje bardzo dużo dla miłośników przeciążeń, chyba nawet więcej niż konkurencyjny park Alton Towers. Wspomnę tylko o Stealth, w którym osiągamy prędkość ok. 130 km/h w ciągu 1.8 sekundy, maksymalne przeciążenie to prawie 5G (wciska w fotel), a najwyższy punkt kolejki znajduje się 60m nad ziemią. Robi niesamowite wrażenie, do tego stopnia, że bez zastanowienia zdecydowałem się na drugą przejażdżkę.

Z rzeczy praktycznych to w parku jest tłoczno nawet poza sezonem. Mówię tutaj o weekendach, w ciągu tygodnia może być lepiej. W związku z tym należy przygotować się na długie czekanie, nawet do 2 godzin aby dostać się na kilkunasto sekundową przejażdżkę! Kolejki można ominąć na dwa sposoby. Pierwszy to tzw. single riders. Polega to na tym, że najpierw do wagoników ładowane są osoby z normalnej kolejki i jeśli zostają wolne miejsca (bo ktoś nie chce jechać sam tylko z przyjaciółmi) to wsiada osoba z kolejki dla single riders. Kolejka ta przeważnie jest dużo krótsza niż normalna, ale po pierwsze nie jest dostępna wszędzie, a po drugie dużo zależy od szczęścia. Równie dobrze może być tak, że czas oczekiwania będzie taki jak w normalnej kolejce.

Drugi sposób jest dużo skuteczniejszy, ale nie ma nic za darmo i to dosłownie. Można kupić bilety fast tracks, które pozwalają wejść na atrakcje bez czekania w kolejce. Pojedynczy bilet (jeden przejazd na jednej atrakcji) to koszt od 2 do 5 funtów. Czy warto? Niech każdy odpowie sobie samemu, czy woli czekać czy nie.

Wizytę w Thorper Park szczerze polecam wszystkim, którzy lubią takie atrakcje, szczególnie, że jest zlokalizowany blisko Londynu. Ja bawiłem się tam naprawdę dobrze. Linki do poprzednich postów z serii na temat życia w Londynie:

04/09/2011

IntelliTrace - schemat XSD

Home

Swego czasu w postach Własne zdarzenia IntelliTrace! oraz Własne zdarzenia IntelliTrace 2 opisałem jak zmodyfikować plik CollectionPlan.xml zawierający plan działania IntelliTrace (historycznego debuggera) tak, aby zdefiniować swoje własne zdarzenie IntelliTrace (ważny punkt w historii działania programu kiedy IntelliTrace nagrywa stan aplikacji). Ostatnio wróciłem do tego zagadnienia i "bawię się" testując różne możliwości IntelliTrace. Niestety czasami, po zmodyfikowaniu pliku CollectionPlan.xml, przy próbie uruchomienia debuggera otrzymywałem błąd np.:

error VSLG4001: The specified collection plan is invalid: 'C:\CollectionPlan.xml'. More information: The 'type' attribute is invalid - The value 'Object' is invalid according to its datatype 'urn:schemas-microsoft -com:visualstudio:tracelog:ClrType' - The Enumeration constraint failed.

Komunikat jest czytelny, wartość Object dla atrybutu type jest niedozwolona. Metodą prób i błędów można wywnioskować, jakie wartości są poprawne, ale to męczące i niewydajne. Pomyślałem więc, że skoro zawartość pliku CollectionPlan.xml to dokument XML, to musi on być walidowany przy pomocy odpowiedniego schematu XSD. Ale gdzie go szukać? Przejrzałem zawartość katalogu instalacyjnego Visual Studio 2010 i niczego nie znalazłem.

Stwierdziłem więc, że zajrzę do biblioteki Microsoft.VisualStudio.IntelliTrace (pisałem o niej w poście Poznaj swój program), która umożliwia programową analizę logów IntelliTrace, ale nie tylko. Biblioteka ta wykorzystywana jest również przez program IntelliTrace.exe (o tym też pisałem w poście Używanie IntelliTrace poza Visual Studio 2010!), który znajdziemy w katalogu instalacyjnym Visual Studio 2010. Program ten służy do uruchomienia historycznego debuggera i kiedy używamy IntelliTrace z poziomu Visual Studio, to korzystamy właśnie z tego programu. Skoro tak to doszedłem do wniosku, że walidacja konfiguracji i schemat XSD znajdują się właśnie w tej bibliotece.

Okazało się to strzałem w dziesiątkę. Bibliotekę załadowałem do .NET Reflector'a. Najpierw zlokalizowałem w zasobach komunikat z błędem. Następnie sprawdziłem gdzie jest używany i znalazłem tylko jedno takie miejsce, a stamtąd już szybko doszedłem do wywołania metody public static XmlSchema GenerateXmlSchema(). Niestety okazało się, że nie mogę jej wywołać z własnego kodu, ponieważ znajduje się w klasie internal class ConfigMessagePacker. Skopiowałem więc jej kod (ok. 1200 linii) do swojego programu i po chwili miałem już XSD.

31/08/2011

Krótko o instalowaniu ServicedComponent

Home

W dwóch poprzednich artykułach na temat zarządzanych komponentów COM+ pisałem, że instaluje się je przy użyciu narzędzia regsvcs.exe. Tak oczywiście jest, ale ostatnio ku swojemu zaskoczeniu zauważyłem, że jest to opcjonalne. Jeśli nie zainstalujemy takiego komponentu z poziomu konsoli (np.: regsvcs.exe MyComponent.dll) to zostanie on zainstalowany automatycznie przy pierwszym wywołaniu jego konstruktora.
[assembly: ApplicationName("MyComponent")]
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: System.Reflection.AssemblyKeyFile("MyComponent.snk")]
[assembly: ApplicationAccessControl(false)]
public class MyComponent: ServicedComponent
{
  ...
}

...

//Jeśli komponent nie został wcześniej zainstalowany to zostanie zainstalowany w tym momencie
using (MyComponent cmp = new MyComponent())
{
  ...
}


25/08/2011

Jeszcze o ServicedComponent

Home

W tym poście wrócę do tematu aplikacji modelu COM+ napisanych w kodzie zarządzanym, który to poruszyłem w poprzednim artykule. Tym razem chciałbym zwrócić uwagę na problem wersjonowanie takich komponentów. Upraszczając, chodzi o różnicę pomiędzy katalogiem, z jakiego komponent został zainstalowany w systemie, a katalogiem, w którym znajduje się biblioteka z komponentem jakiej używa dana aplikacja. W szczególności mogą to być inne katalogi np.: c:\Install oraz c:\bin.

W takim przypadku łatwo może dość do sytuacji, w której binaria w obu lokalizacjach będą się różnić. Objawy będą różne w zależności od trybu aktywacji (pisałem o nich poprzednio).W przypadku Aplikacji biblioteki tak długo jak w obu katalogach będą znajdować się biblioteki skompilowane dla tego samego środowiska (32 lub 64 bitowego) nie będzie żadnego problemu (poza bałaganem). Co ciekawe znaczenie ma tylko liczba bitów, wersje bibliotek mogą być inne, różna może być nawet liczba udostępnianych przez komponent metod. Natomiast w przypadku kiedy w jednym katalogu będzie wersja 32 bitowa, a w drugim 64 bitowa przy próbie skorzystania z komponentu dostaniemy wyjątek ComException o treści Klasa niezarejestrowana....

W trybie Aplikacji serwerowej liczba bitów nie ma znaczenia. Znaczenie ma jednak zawartość bibliotek w obu lokalizacjach. Jeśli będzie różna debugowanie nie będzie możliwe. Jeśli zmieni się interfejs, na przykład do biblioteki zostanie dodana nowa metoda, ale nowa wersja nie zostanie zainstalowana, to przy próbie jej użycia pojawi się wyjątek.

Generalnie problem dotyczy też natywnych komponentów COM+ (piekło COM+) ale z takimi komponentami nie pracowałem dlatego nie znam szczegółów.

23/08/2011

Debugowanie ServicedComponent

Home

ServicedComponent to klasa umożliwiająca tworzenie zarządzanych komponentów/klas, które mogą być użyte w aplikacjach COM+ oraz mogą korzystać z usług COM+. Jedną z takich usług jest na przykład pula obiektów, czyli coś podobnego do puli połączeń z tą różnicą, że możemy w niej umieścić instancje naszej własnej klasy.

Aby stworzyć taką specjalną klasę należy wydziedziczyć ją ze wspomnianej klasy ServicedComponent. Do tej pory nie miałem okazji z niej korzystać, dlatego napotkałem pewne kłopy przy debugowaniu takich zarządzanych komponentów COM+ (dalej będę używał po prostu pojęcia komponent).

Należy zacząć od tego, że są dwa tryby aktywacji komponentów COM+ (zarządzanych lub nie). W pierwszym (tzw. Aplikacja biblioteki/Library application) komponent aktywowany jest w procesie aplikacji, która z niego korzysta. W drugim trybie natomiast (tzw. Aplikacja serwera/Server application) aktywacja przeprowadzana jest przez dedykowany proces. Tworząc taki komponent możemy określić tryb aktywacji przy pomocy atrybutu ApplicationActivationAttribute. Tryb ten można również zmienić już po zainstalowaniu komponentu (przy pomocy narzędzia regsvcs.exe) w konsoli zarządzania w przystawce Usługi składowe (ang. Component services). Znajdziemy ją w lokalizacji C:\Windows\System32\comexp.msc.

Debugowanie takiego zarządzanego komponentu różni się w zależności od trybu aktywacji. Zacznijmy od pierwszego przypadku czyli tzw. Aplikacji biblioteki. Tutaj sprawa generalnie jest prosta. Skoro komponent aktywowany jest w procesie aplikacji, która z niego korzysta to wystarczy postawić pułapkę w odpowiednim miejscu np.: w kodzie komponentu i tyle. Jest jedno ale. To nie zadziała jeśli aplikacja korzystająca z komponentu została skompilowana na platformę .NET 4.0. Komponent będzie zwracał poprawne wyniki ale jak do tej pory nie udało mi się zmusić VS 2010 do zatrzymania się na pułapce ustawionej w kodzie komponentu albo przejść do tego kodu przy pomocy F11. Jeśli zmienimy platformę na przykład na .NET 3.5 to problem z debugowaniem nie wystąpi.

Trzeba również wiedzieć, że w przypadku tego trybu aktywacji, jeśli mamy zainstalowany komponent 32 bitowy, to proces, który chce z niego skorzystać również musi być 32 bitowy. Analogicznie dla 64 bitów. Informację o błędzie dostaniemy już przy próbie wywołania konstruktora komponentu.

W przypadku trybu "serwerowego" komponent aktywowany jest w innym procesie, więc wersja platformy czy nawet liczba bitów nie mają znaczenia (również przy debugowaniu). Z drugiej strony mamy inny problem ponieważ musimy doczepić się do tego procesu aby go zdebugować czyli skorzystać z funkcji Debug->Attach to Process.... Interesujący nas proces nazywa się dllhost.exe. Kłopot w tym, że na liście może znajdować się kilka procesów o tej nazwie. Pierwsze przybliżenie uzyskamy zawężając listę do tych procesów, które w kolumnie Type mają wartość Managed.... W przypadku gdy jest ich kilka można skorzystać z programu Process Explorer i sprawdzić identyfikator procesu, który korzysta z biblioteki z naszym komponentem.

W jednym z kolejnych postów wrócę jeszcze do tematu i napisze o problemach z wersjonowaniem omawianych komponentów.

22/08/2011

Londyn - metro (wskazówki)

Home

W ostatnim poście dotyczącym metra w Londynie obiecałem, że dam kilka wskazówek o tym jak z niego korzystać (o innych środkach transportu nie napiszę bo nie używam). Nie będzie to jakaś wiedza tajemna, ale kiedy jest się po raz pierwszy w Londynie, to nie wszystko jest oczywiste.
  • Nie opłaca się kupować biletów za gotówkę. Tak zakupiony bilet dla pierwszej strefy, w porównaniu do biletu elektronicznego, może być nawet 2 razy droższy. Dla dalszych stref różnica jest mniejsza, ale na przykład większość atrakcji turystycznych znajduje się w pierwszej strefie.
  • Osobiście korzystam z karty Oyster czyli odpowiednika Warszawskiej Karty Miejskiej z tą jednak różnicą, że Oyster oferuje więcej możliwości. Na taką kartę można załadować bilet okresowy, ale również określoną sumę pieniędzy do wydania na przejazdy tzw. pay as you go. Z metra korzystam raz na jakiś czas więc jest to dla mnie idealne rozwiązanie.
  • Kartę Oyster kupujemy (ładujemy) w automatach biletowych ustawionych w metrze lub w kasach. Przy zakupie zostanie pobrany zwrotny depozyt wysokosci 5 funtów.
  • Nie na każdej stacji znajdziemy kasy biletowe, automaty natomiast tak.
  • Ważne!!!Korzystając z karty Oyster trzeba koniecznie pamiętać żeby zbliżyć kartę do czytnika (oznaczone sa charakterystycznymi żółtymi kółkami) przy wejściu ale i przy wyjściu z metra. W innym wypadku zostaniemy obciążeni dodatkową opłatą (nie wiem ile dokładnie). W większości wypadków nie ma z tym problemu ponieważ nie przejdziemy przez barierki bez ważnego biletu. Niestety na niektórych stacjach nie ma barierek. Czasami zdarzy się również, że barierki są otwarte ale wtedy też trzeba pamiętać o czytniku.
  • Po zarejestrowaniu karty Oyster na tej stronie będziemy mogli śledzić historię naszych podróży oraz doładować kartę on-line. Jeśli chcemy kupić bilet okresowy na czas dłuższy niż tydzień rejestracja jest wymagana.
  • Ważne!!! Jeśli mamy kartę pay as you go to mamy zagwarantowane, że danego dnia nie wydamy na przejazdy więcej niż koszt biletu dziennego. Szczegóły można znaleźć tutaj.
  • Warto korzystać ze strony Transport for London. Znajdziemy tam bardzo dobrą wyszukiwarkę połączeń oraz informacje o planowanych remontach poszczególnych linii. Generalnie remonty przeprowadzane są w weekendy.
  • Nie wszystkie stacje metra mają windę.
  • Na stacjach metrach można znaleźć broszurki z planem metra - bardzo przydatne.
  • Jeśli ktoś źle znosi wysokie temperaturę lub duchotę, to schodząc do metra (szczególnie korzystając z linii głębinowych) warto zaopatrzyć się w butelkę wody.
  • Szczegółowy cennik znajdziemy tutaj
Linki do poprzednich postów z serii na temat życia w Londynie:

18/08/2011

Londyn - zamieszki

Home

Z publikacją tego posta czekałem aż zamieszki w Londynie ucichną i sprawa zostanie zapomniana przez media. No i się doczekałem. Obecnie w polskich mediach ciężko znaleźć jakąś wzmiankę na ten temat podczas gdy jeszcze jakiś czas temu można było odnieść wrażenie, że Londyn to strefa wojny.

O zamieszkach usłyszałam po raz pierwszy, kiedy spytała mnie o nie żona. Potem także rodzina i znajomi pytali mnie czy nic mi nie jest, czy jestem bezpieczny itd. Nie dziwię się temu, bo kiedy przeglądam artykuły na temat zamieszek, okraszone strasznymi zdjęciami, to przyznam, że nie wyglądało to fajnie. Osobiście oprócz większej liczby policjantów na ulicach nie widziałem innych skutków rozruchów.

To pokazuje jaka może być różnica pomiędzy rzeczywistością, a obrazem świata przedstawianym przez prasę. Nie twierdzę, że problemu nie było bo problem oczywiście był (jest) i to duży. Wielu ludzi zostało poszkodowanych i przeżyło ciężkie chwile. Ale tak nie było w całym mieście. W tym samym czasie, wbrew temu co pokazywały media, w Londynie toczyło się normalne życie.

Linki do poprzednich postów z serii na temat życia w Londynie:

07/08/2011

Kłopoty z Client Profile

Home

The type or namespace name 'SomeType' does not exist in the namespace 'SomeNamespace' (are you missing an assembly reference?)

Sądzę, że każdy programista .NET spotkał się z powyższym błędem kompilacji. Nie jest to nic wyjątkowego i rozwiązanie problemu jest bardzo łatwe, wystarczy dodać do projektu referencję do brakującej biblioteki. Czy oby na pewno?

Kilka dni temu napotkałem powyższym błąd kompilatora i rozwiązanie problemu zajęło mi sporo więcej czasu niż normalne kilkanaście sekund. Na początku zdziwiłem się ponieważ niekompilujący się projekt aplikacji zawierał potrzebną referencję, kilka minut wcześniej sam ją dodałem. Na wszelki wypadek podpiąłem ją jednak jeszcze raz i przekompilowałem wszystkie potrzebne projekty ale nic to nie dało. Restart Visual Studio również nie pomógł. Kilka kolejnych prób kompilacji również spełzło na niczym.

Trochę zrezygnowany zabrałem się do przeglądania ustawień felernego projektu i zwróciłem uwagę na to, że korzysta on .NET Framework 4 Client Profile. Wcześniej nie miałem z tym problemu ale przypomniałem sobie, że w projektach tego typu nie można korzystać z bibliotek, których nie ma w NET Framework 4 Client Profile. Na próbę zmieniłem opcję Target framework na .NET Framework 4 i okazało się to strzałem w dziesiątkę.

W ramach testu zacząłem zmieniać różnym projektom opcję Target framework na .NET Framework 4 Client Profile i sprawdzać czy się kompilują. Okazało się, że prawie wszystkie skompilowały się bez żadnego problemu. Dalsze eksperymenty doprowadziły mnie do następującego wniosku:

Błąd zostanie zgłoszony jeśli w projekcie korzystającym z NET Framework 4 Client Profile użyjemy biblioteki, która bezpośrednio lub pośrednio korzysta z czegoś co nie znajduje się w NET Framework 4 Client Profile.

Przez użyjemy rozumiem np.: stworzenie instancji klasy. Samo dodanie referencji do biblioteki czy nawet zaimportowanie przestrzeni nazw przy pomocy using nie spowoduje błędu. Ważne jest również to, że biblioteka powodująca błąd może znajdować się gdzieś daleko w łańcuchu referencji, co jeszcze utrudnia sprawę. Należy również wiedzieć, że o przynależności lub nie do NET Framework 4 Client Profile nie decydują opcje kompilacji ale to z czego korzystamy w danej bibliotece. Listę "zabronionych" rzeczy można znaleźć tutaj. W moim przypadku błąd powodowała "zabroniona" biblioteka System.Data.OracleClient, z której korzysta Microsoft.Practices.EnterpriseLibrary.Data.dll, którą z kolei używam ja.

Sądzę, że warto o tym pamiętać aby zaoszczędzić sobie nerwów szczególnie, że kiedy dodajemy nowy projekt aplikacji (WPF, WinForms, konsola) to domyślnie będzie on korzystał .NET Framework 4 Client Profile. W większości przypadków jest to dla nas przezroczyste ale zawsze może trafić się ten jeden raz.

01/08/2011

Londyn - metro (trochę narzekania)

Home

Dzisiaj napiszę o metrze londyńskim, które należy do najstarszych i najbardziej rozbudowanych na świecie, a z którego w porównaniu do innych środków transportu korzystam najczęściej. Dwanaście linii wijących się pod miastem w porównaniu do jednej budowanej 25 lat w Warszawie robi wrażenie i budzi zazdrość. Ja chciałbym jednak pokazać, że jeśli choć na chwilę zapomnimy o liczbie linii to metro warszawskie zacznie wygrywać z londyńskim pod wieloma względami.

Zacznę od tego, że w londyńskim metrze jest bardzo duszno i gorąco. Nawet jeśli na dworze jest zimno i założyliśmy kurtkę to po zejściu do metra z pewnością ją zdejmiemy. Na przewarzającej liczbie stacji i w pociągach niestety brakuje klimatyzacji. Czytałem, że w najbliższym czasie na jednej z linii mają zostać wprowadzone pociągi z klimatyzacją ale to kropla w morzu potrzeb. Póki co przemieszczanie się metrem to przyjemnych nie należy, a zarząd metra zaleca aby w podróż wybrać się z butelką wody.

Komfortu nie poprawia fakt, że stacje w londyńskim metrze są bardzo małe, szczególnie te głębinowe. Jest akurat tyle miejsca aby zmieścił się pociąg i niewiele więcej. Biorąc pod uwagę to co widziałem, tak na oko przeciętna szerokość peronu to 3-4 metry. Pod tym względem Warszawa bije Londyn na głowę. Mniejsze są również wagony (z ciekawostek to na poszczególnych liniach mają różne rozmiary). W Warszawie bez problemu stoję wyprostowany, w Londynie mieszczę się po środku wagonu.

W londyńskim metrze brakuje również tzw. komór rozprężających powietrze. Powoduje to, że pociąg wjeżdżający na stację wpycha przed sobą ogromną masę powietrza, która uderza w pasażerów. Może to i drobiazg ale pokazuje, że metro w Warszawie należy do nowoczesnych. Potwierdza to, również fakt, że wszystkie stacje metra w Warszawie przystosowane są dla niepełnosprawnych (mam na myśli windy), a w Londynie nie.

Z innych rzeczy to jadąc metrem w Londynie mam wrażenie, że tory do najprostszych nie należą. Obserwując wagon jadący przed nami albo za nami można zauważyć jak nim rzuca, czasami jest jakby trochę wyżej, a czasami niżej :). Metro w Londynie jest również po prostu drugie, nawet biorąc pod uwagę wyższe zarobki. Koszt jednego przejazdy w ramach pierwsze strefy to na tą chwilę 1.9 funta szterlinga i to przy założeniu, że korzystamy z karty magnetycznej (tzw. Oyster). Tradycyjne bilety są jeszcze droższe.

Tyle narzekania. Ideałem byłoby metro nowoczesne jak w Warszawie ale z taką liczbą linii jak w Londynie (może moje wnuki tego doczekają). W kolejnych postach chciałbym dać kilka wskazówek jak korzystać z metra w Londynie.

Linki do poprzednich postów z serii na temat życia w Londynie:

29/07/2011

Londyn - kosze na śmieci

Home

Z ciekawostek. W Londynie jest bardzo mało koszy. W gruncie rzeczy jeśli mamy do wyrzucenia jakiś papierek, niedopałek papierosa itp. do mamy dwa wyjścia: schować do kieszeni lub wyrzucić na ulicę. Wiele osób, a w szczególności palacze wybierają drugą opcję. Służby sprzątające są jednak bardzo sprawne dlatego miasto nie jest zaśmiecone.

Ciekawe jest natomiast dlaczego tych koszy jest tak mało. Kolega stwierdził, że to pozostałość po czasach kiedy aktywna była Irlandzka Armia Republikańska. Kosz na śmieci to doskonałe miejsce na podłożenie bomby i dlatego usunięto je z ulic tylko, że potem "zapomniano" je przywrócić.

Linki do poprzednich postów z serii na temat życia w Londynie: