Regex.CompileToAssembly() – Prekompilacja wyrażeń regularnych i zautomatyzowanie procesu

By | December 19, 2016

W ostatnim poście opisałem i porównałem wydajność różnych sposobów wykorzystania regexów. Zdecydowanie najlepiej sprawowało się wyrażenie, które przygotowałem za pomocą metody Regex.CompileToAssembly(). Dziś omówię ją nieco dokładniej i podpowiem, jak zautomatyzować cały proces aktualizowania zebranych w oddzielnym assembly regexów.

By odnaleźć się w kontekście, zachęcam do przeczytania poprzedniego posta, bo w dużej mierze kontynuuję dziś wątki, które zacząłem wcześniej.

Zacznijmy więc od omówienia wspomnianej metody Regex.CompileToAssembly(). Cały kod odpowiedzialny za prekompilowanie wybranych, często używanych wyrażeń zebrałem w oddzielnym projekcie (w tej samej solucji). Cały projekt jest aplikacją konsolową i jeśli ktoś nie dba o jakość kodu w tego typu małych aplikacjach, to można go zamknąć wewnątrz metody Main (czego jednak nie polecam).

Co właściwie robi tytułowa metoda? Przyjmuje informacje o potrzebnych nam regexach i prekompiluje je, tworząc kilka klas dziedziczących po klasie Regex i umieszcza je w oddzielnym, zdefiniowanym przez nas pliku .dll. Po dodaniu referencji utworzonego .dll do głównego projektu, możemy korzystać z zestawu prekompilowanych klas zawierających nasze regexy. Korzysta się z nich niemalże tak samo jak z utworzonych instancji klasy Regex z kilkoma małymi różnicami. 

Po pierwsze nie możemy zmieniać wzoru naszego regexa, po drugie jego inicjalizacja trwa odrobinę dłużej niż zwykły new Regex(). Trzecią wyraźną różnicą jest prędkość z jaką działa, gdy już utworzymy jego instancję, o czym można poczytać we wspomnianym poprzednim poście.

Kod, który używany jest do wygenerowania nowego Assembly wygląda mniej więcej w ten sposób:

var pattern = @"(([\w-]+\.)+[\w-]+|([a-zA-Z]{1}|[\w-]{2,}))@((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\.([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])){1}|([a-zA-Z]+[\w-]+\.)+[a-zA-Z]{2,4})";

var info = new RegexCompilationInfo(pattern,
    RegexOptions.IgnoreCase | RegexOptions.CultureInvariant,
     "ExtractEmail",
     "My.Regex.Namespace",
     true);
var infoArray = new RegexCompilationInfo[] { info };
Regex.CompileToAssembly(infoArray, new System.Reflection.AssemblyName("RegexLib, Version = 1.0.0.1001, Culture = neutral, PublicKeyToken = null"));

Kluczowe tutaj są tak naprawdę dwie rzeczy. Pierwszą z nich jest instancja klasy RegexCompilationInfo, której w konstruktorze przekazujemy, jak sama nazwa wskazuje, informacje dotyczące tworzonego regexa. Przyjmowane parametry to w kolejności:

  • Pattern
  • Enumy RegexOption oddzielone znakiem ‘|’, które definiują szczegółowe ustawienia naszego wyrażenia
  • Nazwa klasy, która zostanie utworzona w assembly (w tym wypadku do utworzenia zbudowanego regexa użyjemy ‘new ExtractEmail()
  • Namespace w jakim będzie znajdowała się klasa z regexem i który najprawdopodobniej umieścimy w jakimś usingu
  • Bool, który określa czy klasa będzie dostępna publicznie
  • (nie użyty powyżej) TimeSpan, który określa domyślny timeout dla utworzonej klasy

Tak utworzone obiekty RegexCompilationInfo zberamy w tablicę i przekazujemy metodzie statycznej Regex.CompileToAssembly() wraz z nową instancją AssemblyName, która określa nazwę pod jaką utworzony zostanie plik .dll, samo Assembly i kilka innych właściwości, którymi tak naprawdę nie musimy się przejmować.

W powyższym przykładzie utworzony zostanie plik RegexLib.dll. Aby do tego doprowadzić musimy tylko zbudować i odpalić nasz projekt z aplikacją konsolową. I tutaj uwaga. Bardzo nietaktownie wali on dllkę pod siebie i gdy będziemy dodawać referencję do niej, musimy jej szukać w tym samym miejscu, w którym odpalaliśmy utworzony pliczek wykonywalny z aplikacją konsolową.

Jest to jednak najmniejszy problem, bo o wiele bardziej kłopotliwe jest dokładanie nowych regexów do Assembly. Bo jak to? Za każdym razem mam budować projekt, odpalać aplikację i dopiero budować główny projekt? Byłoby to trochę bez sensu.

I tutaj przychodzi na ratunek bardzo fajna funkcja, z której możemy skorzystać w Visual Studio – pre-build events. Wystarczy kliknąć prawym przyciskiem myszy na nasz główny projekt, odpalić jego właściwości i w menu odnaleźć Build Events, a naszym oczom ukażą się dwa okna. Jeden w którym możemy napisać mały skrypt, który będzie odpalany w konsoli przed buildem i drugi, który zostanie odpalony już po nim. Nas będzie interesował ten pierwszy.

W tym miejscu możemy umieścić kawałek skryptu identycznego jak w zwykłej, Windowsowej konsoli i zostanie od wykonany. Mamy nawet dostęp do małej listy zmiennych, które możemy umieścić w tekście, by ułatwić nam pracę. Co ja w nim umieściłem? Jedną linijkę, która odpala stworzony projekt konsolowy i dzięki której mam święty spokój.

$(SolutionDir)ConsoleApplication2\bin\Debug\ConsoleApplication2.exe

Dzięki temu przy każdym rebuildzie mam tworzone świeżutkie Assembly z regexami. Mało tego. Aplikacja uruchamiana jest w kontekście głównego projektu, co oznacza, że plik .dll umieszczany jest w lokalizacji głównego projektu, razem z innymi dllkami. Jest to bardzo przyjemne, ale jeśli dodawaliśmy wcześniej referencję do assembly wygenerowanego po ręcznym uruchomieniu projektu, to musimy pamiętać o zmianie ścieżki do załączonej referencji, by móc swobodnie korzystać z dodawanych na bieżąco, nowych regexów.