W poprzednich artykułach opisywałem MSBuilda oraz przykładowy system do automatyzacji wdrożeń, czyli Cake:
Powyższe rozwiązania nie wyczerpują tematu automatyzacji, integracji usług, narzędzi oraz prostoty, za którą idzie maksyma DRY (Don’t Repeat Yourself). Pierwsze na tapet bierzemy zaawansowane funkcjonalności MSBuilda oraz rozbudowywanie procesu.
ItemGroup
Grupa elementów, w której dla każdego elementu (dajmy na to – pliku) jesteśmy w stanie zdefiniować odpowiednie atrybuty. Przykładowo: wszystkie pliki .cs są zdefiniowane jako Compile:
PropertyGroup
Grupa właściwości, w której każda właściwość pozwala przetrzymywać wartości pod daną nazwą, można nadpisywać i sklejać wiele takich zmiennych w jedną:
Odwołanie do właściwości:
Condition
Prosty „if”, dzięki niemu powyższe elementy można włączać/wyłączać wedle ustalonych reguł. Na przykład:
Target
Naprzemiennie będę wykorzystywał target i element docelowy jako jedno i to samo. Jest to coś w rodzaju metody, którą wpinamy w proces MSBuilda. Dzięki takiemu podejściu możemy po procesie budowania powiedzieć, że chcemy, aby uruchomił się proces paczkowania:
Służą do tego atrybuty BeforeTargets oraz AfterTargets. Jeśli jako atrybut Name podamy istniejący już element docelowy, to zastąpimy jego działanie, więc nie warto nazywać targetów tak samo, chyba że jesteśmy pewni konsekwencji:
Sam proces, jakim jest Build, ma również elementy docelowe BeforeBuild i AfterBuild. Tyczy się to także innych podstawowych procesów, takich jak Clean, Rebuild, czyli:
Directory.Build.props
Gdy chcemy rozszerzyć funkcjonalność projektów w .NET, mamy dwie możliwości. Pierwsza – kopiowanie do każdego projektu potrzebnych targetów oraz właściwości. Druga – agregacja targetów oraz właściwości w „jakimś” miejscu oraz powiedzenie projektowi: użyj tego.
Pierwszym z agregatów jest Directory.Build.props. Plik powinien definiować tylko podstawowe elementy oraz właściwości, z których będą korzystać targety, elementy czy warunki. Zasada szukania za plikiem jest prosta – szuka go w katalogu, jak nie znajdzie, to przeskoczy katalog wyżej i powtórzy szukanie. Takie podejście powoduje, że możemy definiować ten rodzaj pliku dla pojedynczego projektu, solucji lub wszystkich systemów.
Ważne jest to, że gdy plik zostanie znaleziony, poszukiwania się kończą, chyba że w pliku najbliższym projektowi dodamy linię:
Kiedy mamy trzy takie pliki, to dwa poprzednie muszą mieć tę linię.
W procesie MSBuilda załączany jest wcześnie, więc wiele predefiniowanych właściwości może być pustych, gdy będziemy chcieli z nich skorzystać.
Jako że definiowany jest na początkowym etapie, możemy sami zaimplementować proces szukania za konkretnym plikiem, a potem wykonać na nim akcje. Przykładowo: chcemy, aby w momencie znalezienia pliku stylecop.json załączył go w projekcie:
Przy takim podejściu mamy pewność, że załączy pierwszy znaleziony plik, dzięki temu istnieje możliwości, że projekt będzie miał swoją własną implementację rozwiązania, a reszta – ogólną.
Directory.Build.targets
Rozszerzenie sugeruje, że w jakiś sposób ten plik agreguje targety, i jest to prawdą. Gdy MSBuild wykryje ten plik w katalogu, to do każdego projektu, jaki znajdzie, doda wszystkie zdefiniowane przez niego elementy docelowe oraz właściwości. Zasada szukania jest taka sama jak za Directory.Build.props.
W całym procesie jest załączany prawie na samym końcu, tuż po podłączeniu plików .target z paczek NuGet, więc jest w stanie nadpisać większość właściwości zdefiniowanych w MSBuildzie i Directory.Build.props.
Oba pliki wprowadzają podstawową agregację elementów docelowych oraz właściwości. Jednak gdy chcemy powielać te same rozwiązania, to kopiowanie i utrzymywanie 70 kopii tego samego rozwiązania jest zatrważająco ciężkie. Mówimy tutaj o MSBuildzie i sytuacji, w której jedna literówka będzie miała wpływ na to, jak system będzie się zachowywał, bo większość rzeczy została napisana w XML-u.
SDK
SDK zostało wprowadzone w MSBuild v15 i jego zadaniem jest agregować nasze właściwości oraz targety w jednym miejscu – projekcie – i dodawać jako SDK:
Podstawowa struktura projektu:
Folder w paczce musi nazywać się Sdk oraz pliki kolejno Sdk.props, Sdk.targets.
UsingTask
Wprawne oko zauważy, że w strukturze można załączać pliki oparte na C#. MSBuild pozwala na definiowanie własnych zadań, które możemy wpiąć w proces za pomocą elementu docelowego.
UsingTask jest informacją, że z takiej biblioteki o takiej nazwie i takim zadaniu będziemy korzystać. Tworzenie zadania wygląda następująco:
a użycie w taki sposób:
W przykładzie wyżej mamy właściwość XeinaemmSdkPath, czyli miejsce, gdzie ma szukać SDK mojego projektu. Jak nie znajdzie, to zaczyna szukać w folderze, w którym znajduje się Sdk.targets, tj. MSBuildThisFileDirectory. W większości wypadków oznacza to ścieżkę do folderu lib w paczce NuGet.
Gdy mam już zdefiniowane zadanie, jego użycie jest proste. Odwołujemy się do niego w elemencie docelowym. W przypadku sekcji „runtime” w app.config lub web.config najlepiej taką akcję wykonać przed elementem docelowym ResolveAssemblyReferences, bo kolejnym etapem jest generowanie tejże sekcji przez MSBuilda.
Nowy format projektu
Wokół funkcjonalności SDK został zbudowany nowy format projektów. Biblioteki używają Microsoft.NET.Sdk, a ASP.NET Core – Microsoft.NET.Sdk.Web. Obecnie z tego procesu wyłączony jest ASP.NET ze względu na brak pełnej kompatybilności (stan na dzień 15.11.2019).
Podstawowa struktura nowego formatu dla biblioteki:
TargetFramework jest respektowany zgodnie z wytycznymi SDK. Przykładowo: można wykorzystać net48, netstandard2.1 czy netcoreapp3.0, aby mieć w pełni funkcjonalną bibliotekę bez definiowania większej liczby opcji.
Warto nadmienić, że – w przeciwieństwie do starego – obecny format jest dynamiczny. Oznacza to, że zmiana właściwości, dodanie pliku, usunięcie zostaną od razu zauważone przez projekt. Domyślnie pliki takie jak .cs są rozpoznawane i automatycznie ustawiany jest dla nich element Compile. Podobnie wykrywane są pliki .ts.
Koncept All i App
Wraz z pojawieniem się .NET Core 2.0 wprowadzono koncept agregacji paczek rozbity na dwa podejścia:
- App – agregacja rozwiązań związanych z .NET Core jako całościowej biblioteki (frameworka).
- All – App z rozwiązaniami budowanymi przez firmy trzecie, na przykład Json.NET (Newtonsoft).
W wersjach ASP .NET Core 2.0–2.2 można było wybrać paczkę, z jakiej korzystamy, jednak z dużym naciskiem na App, ponieważ All wycofano w wersji 3.0. App natomiast został zakorzeniony jako „by design”.
Ten sam koncept można wykorzystać w rozwiązaniach wewnętrznych, budując własne lokalne biblioteki, wykorzystywane w wielu systemach:
Powyżej mamy przykład takiej biblioteki. Poza tym, że zbiera kilkanaście bibliotek do jednego worka, nie robi nic innego.
Takie podejście pozwala na:
- Zebranie rozwiązań najczęściej wykorzystywanych w systemach i załączenie ich jako jedną paczkę.
- Upewnienie się, że taka paczka zawiera wszystkie potrzebne zmiany do działania, czyli jest bardzo blisko koherentności.
Zarazem:
- Powoduje dołączenie niepotrzebnych bibliotek.
- Agreguje wszystkie zmiany, więc podbicie wersji takiego agregatu wymusza na nas wprowadzenie wszystkich poprawek „breaking changes”.
- Wykorzystywanie zewnętrznych rozwiązań może powodować niekoherentność, zwłaszcza w dużych agregatach.
Koherencja rozwiązania
Brak koherentności oznacza, że system jest niespójny i prawdopodobnie w błędnym stanie, co w naszym wypadku najczęściej wiąże się z posiadaniem wielu wersji tej samej paczki. W systemach .NET i NuGet przy zarządzaniu paczkami przez Visual Studio mamy zakładkę „Konsolidacji”. Listuje ona wszystkie paczki, które są w różnych wersjach – to brak koherencji.
Przy wprowadzaniu zmian w bibliotekach współdzielonych często jedna zmiana oddziałuje na inne paczki przez co, gdy zaktualizujemy jeden system to inne systemy dalej posiadają ten sam błąd. Często zdarza się, że nawet w jednym systemie możemy wywołać brak spójności, wystarczy podbić jedną paczkę, która znajduje się najniżej w zależnościach(inne paczki z niej korzystają) i zawiera zmiany, które spowodują, że system się wysypie bo jedna z paczek nie zawiera potrzebnych zmian. To również jest brak koherencji.
Agregacja wielu paczek w jedną pozwala nam do pewnego stopnia zaradzić temu problemowi, jednak jeśli wykorzystujemy zewnętrzne rozwiązania, której mają swoje własne zależności, problem się rozrasta. Warto więc uważać na takie zależności.
Prostym rozwiązaniem na wiele zależności może być wykorzystanie SDK zbudowanego przez Microsoft:
Za pomocą pliku Packages.props definiujemy listę paczek oraz wersji, a SDK będzie dbał o to, aby projekt nie definiował wersji oraz paczek, których nie ma na liście, poprzez blokowanie kompilacji.
Podsumowanie
.NET Core bazuje na systemie zarządzania znanym pod nazwą Arcade, z którego można czerpać inspirację. W dokumentacji problem koherencji można spotkać pod nazwą Release Shutdown.
Niezależnie od wersji MSBuilda w pliku Microsoft.Common.CurrentVersion.targets mamy zdefiniowanie elementy docelowe, w które możemy się wpiąć. Zatem: sky is the limit.
Przykłady kodu wziąłem z bibliotek Jarvis oraz Xeinaemm.Sdk.
Do następnego!