Własny szablon dla dotnet new
Wstęp
W poprzednim artykule pokazałem jak tworzyć klasyczny szablon projektów dla Visual Studio. Natomiast z tego artykułu dowiesz się jak stworzyć szablon dla dotnet new
. Taki szablon możesz zaimportować na każdym komputerze i systemie, w którym zainstalowany jest dotnet.
Klasyczne szablony (dla Visual Studio) są obsługiwane tylko przez VisualStudio dla Windows. Natomiast te nowe dla „dotnet new
” już nie mają tego ograniczenia. Ponadto pozwalają duuużo prościej zrobić bardziej skomplikowane rzeczy. Minus? Nie da się tego wyklikać…jeszcze…
Pamiętaj, że ten artykuł to nie jest kompletny podręcznik szablonów. O nie! To jest dość duży temat. Artykuł pokazuje jak zacząć i jak zrobić coś trudniejszego niż „hello world”. Na koniec jednak daję Ci kilka linków, które uzupełniają artykuł. Jeśli czegoś tutaj nie znajdziesz, być może znajdziesz w oficjalnej dokumentacji (podlinkowanej na końcu).
Z czego składa się szablon
Są dwie rzeczy:
- struktura projektu – wrzucasz tutaj dowolną zawartość (pliki, foldery, inne projekty), która ma się znaleźć na dzień dobry w projekcie wynikowym – od tego zaczynasz, po prostu stwórz nowy projekt, który będzie wyjściowym projektem dla Twojego szablonu
- plik konfiguracyjny
template.json
– opisuje parametry szablonu - opcjonalne pliki np. do obsługi Wizarda, czy też ikonka
W przeciwieństwie do klasycznych szablonów pod VisualStudio, nie musisz stosować specjalnych tokenów w formie parametrów w swoich projektach. Nie ma też problemu z kompilowaniem ich – właśnie ze względu na brak tych tokenów.
Musisz jednak zapewnić odpowiednią strukturę katalogów, np. taką:
Jak widzisz, w katalogu z projektem musisz utworzyć folder .template.config
, w którym umieścisz całą konfigurację swojego szablonu. Ja utworzyłem projekt WebApplication (RazorPages), jako projekt wyjściowy, któremu nadałem nazwę WebAppWithApiTemplate.
Tylko uwaga – będzie Cię korciło, żeby dodać folder .template.config
do projektu w Visual Studio. Nie rób tego. Wszystko, co zrobisz z projektem będzie miało skutek w Twoim szablonie. W prawdzie można potem zastosować pewien mechanizm, żeby pozbyć się tego folderu w wynikowym projekcie, ale moim zdaniem można to zrobić lepiej…
Dodawanie plików konfiguracyjnych do solucji
To nie znaczy jednak, że nie możesz pracować na plikach konfiguracyjnych w Visualu. Byłoby to mocno uciążliwe. Utwórz zatem ręcznie katalog .template.config
, a w nim plik o nazwie template.json
. A następnie w Visual Studio kliknij prawym klawiszem myszy na solucję i wybierz Add -> New Solution Folder:
To utworzy Ci folder (tzw. „filtr”) na poziomie solucji. Jednak żaden folder na dysku nie powstanie. Ja swój „filtr” nazwałem template-files
– to jak nazwiesz swój nie ma żadnego znaczenia.
Następnie kliknij go prawym klawiszem myszy i wybierz Add -> Existing Item:
W pickerze wybierz pliki konfiguracyjne szablonu, które utworzyłeś wcześniej. To doda je do solucji, ale nie zrobi żadnych zmian na poziomie projektów. Dzięki temu będziesz mógł pracować na plikach konfiguracyjnych w Visual Studio:
Plik template.json
To właściwie serce Twojego szablonu. Niestety na dzień dzisiejszy nie da się go wyklikać, ale dość łatwo się go tworzy. Zwłaszcza, jeśli wykorzystasz możliwości VisualStudio. Spójrz na okno z zawartością pliku:
Plik oczywiście jest pusty, ale w edytorze kodu widzisz combobox oznaczony jako Schema. Możesz w nim wybrać schemat pliku json, który chcesz tworzyć – dzięki temu cały Intellisense zadziała i będziesz mieć eleganckie podpowiedzi w kodzie. Odnajdź na tej liście https://json.schemastore.org/template.json
– to jest opisany schemat szablonu.
UWAGA! Widoczny na powyższym screenie schemat to jakiś przykładowy pierwszy lepszy z listy. Pamiętaj, żeby odnaleźć tam konkretny:
https://json.schemastore.org/template.json
VS dzięki temu opisowi może dawać Ci podpowiedzi. A teraz zacznijmy wypełniać plik template.json
. Na początek trzeba mu wskazać wybrany schemat. Robimy to za pomocą właściwości $schema
:
{
"$schema": "https://json.schemastore.org/template.json"
}
OK, teraz zupełnie podstawowa zawartość tego pliku może wyglądać tak:
{
"$schema": "https://json.schemastore.org/template.json",
"author": "Adam Jachocki",
"classifications": [ "WebApp", "ApiClient" ],
"identity": "Jachocki.WebAppWithApiTemplate",
"name": "Web application with API Client",
"shortName": "waapi",
"tags": {
"type": "project",
"language": "c#"
}
}
To są minimalne wymagane właściwości. Rozpoznasz je po tym, że w Intellisense są pogrubione. A teraz przelecimy je po kolei:
- author – to oczywiście autor szablonu
- classifications – to klasyfikuje szablon. Te wartości będą potem używane w filtrach np. Visual Studio. Podajesz tutaj tablicę wartości, które powinny kategoryzować Twój szablon. W VisualStudio to te właściwości podczas dodawania nowego projektu. W narzędziu
dotnet new --list
będą się pojawiały w rubrycetags
.
- identity – to jest coś w rodzaju ID Twojego szablonu. To musi być unikalne, dlatego daj tutaj jakąś unikalną wartość, ale niech to będzie coś opisowego a nie np. GUID – później się przyda
- name – pełna nazwa Twojego szablonu (przykłady istniejących: „Console App”, „ASP.NET Core Web App”)
- shortName – nazwa skrócona szablonu, której będziesz mógł używać, tworząc projekt na jego podstawie
- tags – podstawowy opis szablonu – wskazujesz, że szablon odnosi się do projektu (
type: project
) i jaki jest główny język projektu. Szablon może być jeszcze dla całej solucji (kilka projektów) lub nowym item’em – czyli poszczególnym typem pliku.
Dość istotną właściwością jest sourceName
. SourceName
to ciąg znaków zarówno w zawartości plików jak i w ich nazwach, który podczas tworzenia projektu zostanie zastąpiony nazwą projektu, którą poda użytkownik. Przykładowo, jeśli mój kod wygląda tak:
I jeśli we właściwości sourceName
podam: „WebAppWithApiTemplate
” wtedy ten ciąg zostanie zamieniony na to, co wpisze użytkownik podając nazwę swojego projektu.
Czyli zarówno namespace’y, jak i nazwa projektu głównego zostaną zamienione na ten żądany przez użytkownika. A więc dodajmy to:
{
"$schema": "https://json.schemastore.org/template.json",
"author": "Adam Jachocki",
"classifications": [ "WebApp", "ApiClient" ],
"identity": "Jachocki.WebAppWithApiTemplate",
"name": "Web application with API Client",
"shortName": "waapi",
"tags": {
"type": "project",
"language": "c#"
},
"sourceName": "WebAppWithApiTemplate"
}
Pewnie są sytuacje, w których nie chciałbyś takiej podmiany. Wtedy po prostu tego nie stosujesz, gdyż nie jest to właściwość wymagana.
To jest już pełnoprawny projekt szablonu. Teraz musisz go tylko zapakować w nuget i zainstalować. O tym później. Najpierw zrobimy inne fajne rzeczy.
Parametry
Szablon często będzie zawierał jakieś parametry, które mogą być ustawione przez użytkownika. W pliku template.json
lądują one we właściwości symbols
. Parametry są jednym z typów symboli.
Parametr składa się z nazwy (ID), a także typu danych. Może zawierać wartość domyślną, a także ciąg, który będzie zamieniony na wartość parametru.
Jak to działa? Silnik szablonów analizuje plik template.json
. Jeśli teraz znajdzie jakieś parametry (symbole) do podmiany, to każdy taki ciąg znaków w kodzie zamieni na konkretną wartość podaną przez użytkownika. Analogicznie do właściwości sourceName
.
Dodajmy do naszego szablonu folder ApiClient, a nim prostą klasę:
I teraz chcę, żeby taka klasa znalazła się w katalogu ApiClient
, ale to użytkownik będzie decydował o tym, jak ten klient ma się nazywać. Dlatego też posłużę się symbolem:
"symbols": {
"ApiClientName": { //nazwa parametru (jego ID)
"type": "parameter",
"datatype": "text",
"description": "Podaj nazwę klasy dla swojego klienta API",
"displayName": "Nazwa klasy dla Api Client",
"defaultValue": "MyApiClient",
"isRequired": true,
"replaces": "varApiClientName",
"fileRename": "varApiClientName"
}
}
Uznaj właściwość symbols
za klasę, która posiada inne właściwości – konkretne symbole. I teraz tak. Każdy symbol ma swoją nazwę (ID). Tutaj to APIClientName
. Nazwą symbolu możesz posługiwać się w kodzie swojego szablonu, o tym za chwilę.
Każdy symbol to też swego rodzaju klasa, która posiada konkretne właściwości:
- type – typ symbolu. To może być parametr, generator itd. Na razie skupmy się na parametrach
- datatype – typ danych, jakie ten parametr będzie przechowywał. Inne typy to np:
- bool – true/false – na GUI równoznaczne z checkboxem (np. Use Https)
- choice – lista wartości do wyboru – na GUI równoznaczne z combo (np. wybór Identity)
- float, hex, int, text – no te wartości same się opisują. Przy czym
text
jest wartością domyślną
- description – opis parametru. Pokaże się w konsoli w poleceniu
dotnet new
, ale też w VisualStudio jako dodatkowa informacja (chociaż dla IDE jest dodatkowy/osobny sposób pokazywania parametrów opisany niżej) - defaultValue – wartość domyślna
- isRequired – czy parametr jest wymagany. Jeśli podczas tworzenia projektu przez polecenie
dotnet new
użytkownik nie poda tej wartości, todotnet new
zwróci błąd - replaces – to jest ten magiczny string w plikach, który zostanie zamieniony na wartość wprowadzoną przez użytkownika. U mnie to
varApiClientName
. Ten prefix „var” jest tylko pewnym udogodnieniem dla mnie. Nie ma tutaj żadnych wytycznych ani obostrzeń. Ale pamiętaj, że zostaną zamienione WSZYSTKIE STRINGI w kodzie. Łącznie z tymi w plikach projektów, komentarze, a nawet wszystkie wartości tekstowe. Jeśli więc miałbym w kodzie coś takiego:
public string Id { get; set; } = "varApiClientName";
to też będzie zamienione. Dlatego ja posługuję się tym przedrostkiem var. To trochę chroni przed zrobieniem głupoty.
- fileRename – analogicznie jak replace z tą różnicą, że taki ciąg zostanie podmieniony w nazwach plików.
Stwórzmy teraz bardzo prostego i prymitywnego klienta dla API. To jest tylko bardzo prymitywny przykład. Jak fajnie zrobić takiego klienta pisałem w tym artykule. Nie chcę zaciemniać obrazu, dlatego zrobię najprościej jak się da. Ten klient powstał tylko dla przykładu. I naprawdę nie jest istotne, że nie ma większego sensu 😉
Sparametryzowany klient
Co chcemy uzyskać?
- Użytkownik ma mieć opcję do podania nazwy klienta – to już nam załatwił parametr
varApiClientName
. - Użytkownik ma mieć możliwość wyłączenia lub włączenia rejestracji serwisów – czyli domyślnej konfiguracji klienta.
- W przypadku włączenia rejestracji serwisów, użytkownik ma mieć możliwość podania
BaseAddress
dla API. Zarówno dla środowiska produkcyjnego jak i deweloperskiego.
Tworzenie pełnego kodu
Tak, czy inaczej musimy stworzyć cały kod. W pliku template.json
nie możemy dodawać plików ani linijek kodu. Dlatego też musimy wyjść od pełnego obrazu.
Przede wszystkim jest klasa, która umożliwia wysyłanie żądań – prosty klient w pliku varApiClientName.cs
:
namespace WebAppWithApiTemplate.ApiClient
{
public class varApiClientName
{
private readonly HttpClient _client;
public varApiClientName(HttpClient client)
{
_client = client;
}
public async Task<HttpResponseMessage> GetData(string endpoint)
{
return await _client.GetAsync(endpoint);
}
public async Task<HttpResponseMessage> PostData<TOut>(TOut data, string endpoint)
{
return await _client.PostAsJsonAsync(endpoint, data);
}
}
}
Do tego trzeba zrobić rejestrację HttpClienta
. Dlaczego w taki sposób? Opisywałem to w tym artykule.
W folderze ApiClient
dodałem plik ServiceCollectionsExtensions.cs
:
public class varApiClientNameOptions
{
public const string CONFIG_SECTION = "varApiClientName";
public string BaseAddress { get; set; }
}
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddvarApiClientNameIntegration(this IServiceCollection services,
IConfiguration config)
{
varApiClientNameOptions options = new varApiClientNameOptions();
config.Bind(varApiClientNameOptions.CONFIG_SECTION, options);
services.AddHttpClient<varApiClientName>(client =>
{
client.BaseAddress = new Uri(options.BaseAddress);
});
return services;
}
}
W tym kodzie po prostu rejestrujemy HttpClient
dla naszego klienta. Jeśli nie wiesz co robi AddHttpClient
, koniecznie przeczytaj ten artykuł.
Najpierw pobieramy ustawienia, w których będzie wpisany adres bazowy api. To nam zapewnia, że adres bazowy będzie pobrany w zależności od środowiska. Nasępnie konfigurujemy klienta.
Spójrz w jaki sposób przemycam wszędzie varApiClientName
– te wszystkie miejsca zostaną zamienione na faktyczną nazwę klienta.
Na koniec wywołamy jeszcze tę metodę podczas rejestracji serwisów w pliku Program.cs
:
using WebAppWithApiTemplate.ApiClient;
//...
builder.Services.AddRazorPages();
builder.Services.AddvarApiClientNameIntegration(builder.Configuration);
Dodawanie parametrów
W związku z tym, że chcemy aby użytkownik mógł podać adres bazowy dla API, musimy dodać takie parametry do template.json
. Oczywiście dodajemy to dalej w sekcji symbols
:
"ApiProdBaseAddress": {
"type": "parameter",
"datatype": "text",
"description": "Bazowy adres dla API dla środowiska produkcyjnego",
"replaces": "varApiProdBaseAddress",
"defaultValue": "https://api.example.com/"
},
"ApiDevBaseAddress": {
"type": "parameter",
"datatype": "text",
"description": "Bazowy adres dla API dla środowiska deweloperskiego",
"replaces": "varApiDevBaseAddress",
"defaultValue": "https://api.dev.example.com"
}
Dodałem dwa parametry – jeden to adres bazowy dla środowiska produkcyjnego, drugi dla deweloperskiego. Jeśli nie wiesz, czym jest appsettings.json
, czym się różni od appsettings.Development.json
i masz małe pojęcie o konfiguracji .NET, koniecznie przeczytaj ten artykuł.
Dodawanie ustawień w aplikacji
OK, możemy teraz te dane dodać do plików appsettings.
Teraz mój plik appsettings.json
wygląda tak:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"varApiClientName": {
"BaseAddress": "varApiProdBaseAddress"
}
}
Spójrz w jaki sposób przemycam tutaj znów nazwy tego klienta i parametr z bazowym adresem dla API. Analogicznie zrobimy w pliku appsettings.Development.json
:
"varApiClientName": {
"BaseAddress": "varApiDevBaseAddress"
}
Wycinanie fragmentów kodu
OK, teraz mamy ogarnięte punkty 1 i 3 z naszych założeń. Czyli użytkownik może podać nazwę klienta, a także adresy bazowe do API. To teraz musimy się zatroszczyć o punkt 2 – użytkownik ma mieć możliwość wyłączenia jakiejkolwiek konfiguracji klienta.
Najpierw dodajmy taki parametr w pliku template.json
:
"ConfigureApiClient": {
"type": "parameter",
"datatype": "bool",
"description": "Czy klient API ma być domyślnie skonfigurowany",
"defaultValue": "true"
}
Zauważ, że tutaj nie posługuję się właściwością replaces
, ponieważ niczego nie będziemy podmieniać. Albo wykonamy fragment kodu, albo nie.
Zatem wytnijmy warunkowo fragment kodu, który wywołuje rejestrację klienta, czyli to:
using WebAppWithApiTemplate.ApiClient;
//...
builder.Services.AddRazorPages();
builder.Services.AddvarApiClientNameIntegration(builder.Configuration);
Robimy to w bardzo prosty sposób – dyrektywami kompilatora:
#if ConfigureApiClient
using WebAppWithApiTemplate.ApiClient;
#endif
//...
builder.Services.AddRazorPages();
#if ConfigureApiClient
builder.Services.AddvarApiClientNameIntegration(builder.Configuration);
#endif
W taki sposób możesz sterować fragmentami kodu w plikach. Po prostu sprawdzasz, czy parametr o podanym ID ma wartość true.
Usuwanie plików
Jeśli chodzi jednak o plik ServiceCollectionExtensions.cs
nie jest on w ogóle potrzebny, gdy użytkownik nie chce automatycznej konfiguracji. Nie ma sensu wycinać kodu w tym pliku, bo zostałby nam zupełnie pusty. Dlatego warunkowo możemy go usunąć. Z pomocą przychodzi nowa sekcja w pliku template.json
– sources
– w niej możemy stosować modyfikatory plików źródłowych.
Sekcja sources zawiera modyfikatory. Każdy modyfikator może mieć warunek lub wykonać się bezwarunkowo (zawsze). Napiszmy więc taki modyfikator, który usunie plik ServiceCollectionsExtensions.cs
, gdy parametr ConfigureApiClient
nie będzie ustawiony (jego wartość będzie na false):
"sources": [
{
"modifiers": [
{
"condition": "(!ConfigureApiClient)",
"exclude": "ApiClient/ServiceCollectionExtensions.cs"
}
]
}
],
Jak widzisz, żeby sprawdzić wartość jakiegoś parametru, po prostu wpisujemy jego nazwę w nawias. Możemy też go zanegować wykrzyknikiem. Czyli w tym przypadku, gdybyśmy chcieli ten warunek przepisać na kod, wyglądałoby to akoś tak:
if(!ConfigureApiClient)
{
exclude("ApiClient/ServiceCollectionExtensions.cs");
}
Następnie musimy wskazać, co ma się zadziać, jeśli warunek będzie spełniony. A więc pozbywamy się pliku, wykluczamy go (exclude
) z całości.
Teraz może pojawić się pytanie – skąd silnik template’ów wie, gdzie jest konkretny plik. Ścieżką wyjściową (bazową) dla silnika jest folder, w którym znajduje się folder .template.config
.
I tutaj odnosimy się do folderu ApiClient
i pliku, który się w nim znajduje. Możesz stosować tutaj symbole wieloznaczne, np: „**/*Extensions.cs
” – usunęłoby wszystkie pliki z wszystkich podkatalogów, których nazwy kończą się na Extensions.cs
.
Jeśli jednak chciałbyś wykluczyć większość plików, a zostawić tylko jeden, łatwiej będzie posłużyć się modyfikatorem include
. Domyślnie include
włącza wszystkie pliki, które znajdują się w projekcie (oczywiście poza katalogiem .template.config
).
Możesz też chcieć warunkowo zmienić nazwy plików. Do tego możesz zastosować modyfikator rename
.
Warunkowe zawartości plików
Pliki *.cs
Jak już pisałem wcześniej, w plikach cs możemy posługować się dyrektywami kompilatora, żeby warunkowo mieć jakąś zawartość, np:
#if ConfigureApiClient
builder.Services.AddvarApiClientNameIntegration(builder.Configuration);
#endif
Możesz stosować również dyrektywę #elif. A co jeśli chcesz jednak, żeby jakaś dyrektywa była widoczna na koniec w pliku? Np:
#if DEBUG
logger.LogWarning("UWAGA! Tryb deweloperski!");
#endif
Jest i na to sposób. Nazywa się to processing flag. To już jednak wygląda jak czary:
//-:cnd:noEmit
#if DEBUG
logger.LogWarning("UWAGA! Tryb deweloperski!");
#endif
//+:cnd:noEmit
No cóż… Grunt, że jest taka opcja 😉 Pamiętaj – „processing flag”.
Pliki JSON
Tutaj nie możemy się posłużyć bezczelnie dyrektywą, ale możemy posłużyć się specjalnym komentarzem. W plikach json komentarz jest rozpoczynany znakami // i trwa do końca linii. To wytnijmy teraz ustawienia związane z konfiguracją api clienta z plików appsettings:
//#if ConfigureApiClient
"varApiClientName": {
"BaseAddress": "varApiProdBaseAddress"
}
//#endif
To spowoduje dokładnie to, czego się spodziewasz – sekcja varApiClientName
pojawi się w pliku appsettings
tylko wtedy, gdy parametr ConfigureApiClient
będzie miał wartość TRUE. Zrób analogiczną operację w pliku appsettings.Development.json
.
Zmiany w plikach projektów i innych XMLach
Tutaj analogicznie posłużymy się komentarzami XMLowymi. Możesz bez problemu je stosować w plikach projektów. Dodajmy do naszych wymagań jeszcze jedno – niech użytkownik wybierze, czy chce korzystać z biblioteki System.Text.Json
, czy ze starego dobrego Newtonsoft.Json
. Na początek dodajmy taki parametr do pliku template.json
. Niech to będzie combobox.
"JsonLibrary": {
"type": "parameter",
"datatype": "choice",
"choices": [
{
"choice": "Default",
"description": "Używaj domyślnej biblioteki do obsługi JSON",
"displayName": "Domyślny System.Text.Json"
},
{
"choice": "Newtonsoft",
"description": "Używaj starego, dobrego Newtonsoft",
"displayName": "Newtonsoft.Json"
}
],
"defaultValue": "Default"
}
Jak widzisz, choices składa się z tablicy obiektów (choice, description, displayName
). Ich właściwości same się opisują. I co ciekawe, przy choice
też możesz stosować właściwość replaces
, co później pokażę. Pamiętaj też, żeby wartością datatype
było ustawione na choice
.
To teraz zmieńmy plik projektu. Jak już wspomniałem robimy to specjalnym komentarzem:
<!--#if (JsonLibrary == 'Newtonsoft')-->
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<!--#endif -->
Musisz tutaj zwrócić uwagę na dwie rzeczy:
- Pomiędzy znakiem komentarza
<!--
a hashem nie może być spacji. Inaczej silnik szablonów nie uzna tego za dyrektywę, tylko za zwykły komentarz. - Wartość, którą porównujesz musi być w apostrofach. Inaczej warunek nie zostanie uznany jako spełniony.
Można to zrobić jeszcze inaczej – bez porównywania konkretnych wartości. Przydać się to może w sytuacji, gdzie miałbyś do sprawdzenia kilka tych samych warunków w kilku miejscach. Ale o tym za chwilę.
Trochę magii – czyli czego nie powie Ci dokumentacja
Dokumentacje szablonów są całkiem nieźle opisane, ale mają miejscami sporo braków. Jak np. magiczny parametr Framework
. Na jakimś video z Microsoftu słyszałem, że jest on zalecany, ale nigdzie nie jest opisany. Hurrra!
Generalnie parametr Framerowk
daje wybór frameworka, na którym ma być stworzony projekt oparty na szablonie. Tworzy się go analogicznie jak inne parametry:
"Framework": {
"type": "parameter",
"description": "The target framework for the project.",
"datatype": "choice",
"choices": [
{
"choice": "net6.0",
"description": "Target net6.0"
},
{
"choice": "netcoreapp3.1",
"description": ".NetCore 3.1"
},
{
"choice": "net7.0",
"description": ".NET 7.0"
}
],
"replaces": "net6.0",
"defaultValue": "net6.0"
}
Zwróć uwagę tutaj na dwie rzeczy:
- wartości w
choice
muszą być dokładnymi „monikerami” wersji .NET, np.:- net48 – .NetFramework 4.8
- netstandard2.1
- netcoreapp3.1
- net5.0
- net6.0
- net7.0
Jeśli wrzucisz inne wartości, to magiczny mechanizm nie zadziała.
- we właściwości
replaces
wstawiasz, jak to z innymi parametrami, string do podmiany. Pamiętaj, że to podmieni wszystkie znalezione stringi „net6.0” na wybraną przez użytkownika wartość – w szczególności w pliku projektu.
Symbole wyliczane
Innym typem symbolu są symbole wyliczane. To coś w rodzaju parametru. Tylko nie podaje go użytkownik, a wyliczamy go na jakiejś podstawie. Możesz to stosować gdy w kilku miejscach stosujesz jakieś warunki. Np:
"IsLTS": {
"type": "computed",
"value": "(Framework == net6.0 || Framework == netcoreapp3.1)"
}
Taki kodzik stworzy Ci coś w rodzaju zmiennej o nazwie IsLTS
, która przyjmie odpowiednią wartość na podstawie innych parametrów. W tym konkretnym przypadku możesz sprawdzić, czy wybrana wersja .net jest długowieczną (long time support), czy też nie. Później na tej podstawie możesz zadecydować o czymś innym, stosując w innych warunkach zmienną IsLTS
– dokładnie w taki sam sposób jak inne parametry. Np. możesz gdzieś w kodzie wpisać warning:
#if (!IsLTS)
#warning "Caution! Your framework is not LTS version!"
#endif
UWAGA! Symbole typu computed
mogą przyjmować jedynie wartości typu bool.
Instalowanie szablonu
Właściwie wszystkie wytyczne mamy już ogarnięte. Teraz możemy zainstalować taki szablon. Oczywiście nie można zrobić z VisualStudio tego automatem… jeszcze… za to można to zrobić na kilka sposobów. Najpierw podam Ci sposób lokalny – to jest wystarczające jeśli tworzysz jakiś szablon dla siebie i raczej nie będziesz w nim już grzebał.
Uruchom terminal i przejdź do katalogu głównego Twojej aplikacji. Tam, gdzie masz katalog z projektem. U mnie plik projektu znajduje się w projekty\MasterBranch\SingleNewTemplate\src\WebAppWithApiTemplate\WebAppWithApiTemplate.csproj
dlatego muszę ustawić się w katalogu src: projekty\MasterBranch\SingleNewTemplate\src\
Teraz wystarczy zainstalować szablon, wykonując polecenie:
dotnet new --install .\WebAppWithApiTemplate
czyli podajemy nazwę katalogu, w którym jest projekt z szablonem.
Następnie możemy przejrzeć sobie listę szablonów:
dotnet new --list
U mnie wygląda to tak:
Jak widzisz, mój szablon został zainstalowany.
Jeśli będziesz chciał go odinstalować, to dotnet new
podpowie Ci dokładnie co zrobić, ale zasadniczo powinieneś podać pełną ścieżkę dostępu do katalogu z projektem szablonu, np.:
dotnet new --uninstall d:\projekty\MasterBranch\SingleNewTemplate\src\WebAppWithApiTemplate
Jeśli chciałbyś utworzyć sobie projekt na podstawie tego szablonu, to możesz to zrobić podając króką nazwę szablonu. A wywołując instrukcję help
, dostaniesz pełną pomoc dla swojego szablonu:
dotnet new waapi --help
Jak widzisz, dotnet świetnie sobie poradził z Twoimi parametrami i pokazuje Ci dokładnie jak ich użyć. Przykładowe utworzenie projektu:
dotnet new waapi -J Default -A ExampleApiClient -o .\NewProject
Tutaj jednak pamiętaj, że parametry polecenia dotnet new mieszają się z Twoimi parametrami. Stąd np. -o (--output)
– parametr dotnet new
, który mówi gdzie stworzyć projekt.
Możesz też taki projekt stworzyć bezpośrednio z Visual Studio (zrestartuj Visual Studio po zainstalowaniu szablonu).
Instalowanie szablonu NuGetem
Ten sposób umożliwia Ci podzielenie się szablonem z innymi, a także łatwe jego wersjonowanie. Wymaga to utworzenia nugetowej paczki. Ale zadanie jest dość proste.
Utwórz gdzieś plik template.nuspec
. Możesz to zrobić w katalogu .template.config
, ale nie musisz. Utwórz go gdzieś, gdzie uznasz za słuszne. Jego przykładowa zawartość powinna wyglądać tak:
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>Jachocki.ApiClient</id>
<version>1.0.0</version>
<description>Przykładowy projekt szablonu</description>
<authors>Adam Jachocki</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<tags>dotnet templates apiclient webapi</tags>
<packageTypes>
<packageType name="Template" />
</packageTypes>
</metadata>
<files>
<file src="..\**\*.*" exclude="..\**\bin\**\*.*;..\**\obj\**\*.*;" />
</files>
</package>
Ten plik sam się opisuje. Musisz pamiętać jedynie o tym, żeby sekcja files odnosiła się do konkretnych plików Twojego szablonu. Ja akurat plik nuspec umieściłem w .template.config – dlatego też w sekcji files idę po pliki „piętro wyżej”.
Następnie musisz wywołać na nim:
nuget pack template.nuspec
Jeśli wszystko jest ok, to otrzymałeś plik z rozszerzeniem nupkg. I to jest Twój pakiet, którym możesz się już dzielić. Żeby teraz zainstalować taki szablon, wystarczy:
dotnet new install .\twoj-plik.nupkg
Odinstalowanie jest analogiczne. Tyle, że podajesz tylko nazwę pakietu – bez ścieżki do niego.
Przygotowanie szablonu dla środowisk IDE
dotnetcli.host.json
Oprócz pliku template.json, który jest podstawą, możesz mieć jeszcze dodatkowe pliki, w których swój szablon możesz podkręcić. Najprostszym jest dotnetcli.host.json
. Jego rolą jest ustawienie aliasów do parametrów w narzędziu dotnet new
. Domyślnie są one tworzone jakoś automagicznie. Jeśli wyświetlisz pomoc dla swojego szablonu, zobaczysz taki mniej więcej opis:
Parametr na długą nazwę, np. --ApiClientName
, ale może mieć też krótki alias, np: -a
. Zauważ, że pełna nazwa parametru rozpoczyna się podwójnym myślnikiem [–], natomiast aliast pojedynczym [-]. Jest to naturalne zachowanie. Jak widzisz powyżej, dla każdego parametru istnieją jakieś domyślne aliasy. Ale możesz je sam utworzyć, stosując plik dotnetcli.host.json
, np. tak:
{
"$schema": "https://json.schemastore.org/dotnetcli.host.json",
"symbolInfo": {
"ApiClientName": {
"longName": "ApiClientName",
"shortName": "cn"
}
}
}
Najpierw podajesz id symbolu takie jak w pliku template.json
. Następnie masz parametry:
- longName – długa nazwa – czyli ta prawilna cała nazwa parametru prefiksowana podwójnym myślnikiem
- shortName – alias – krótka nazwa parametru. Jeśli podasz pusty string „”, wtedy parametr nie będzie miał aliasu.
- isHidden – jeśli ustawisz na
true
, to ta opcja nie będzie widoczna z poziomudotnet new --help
. Jednak nadal będziesz mógł z niej korzystać (po prostu nie zobaczysz jej w podpowiedziach).
ide.host.json
Ten plik z kolei pomaga ogarnąć dodatkowe rzeczy w Visual Studio (i pewnie w innych IDE). We wcześniejszych wersjach (poniżej 2022) był wymagany. Teraz jest opcjonalny, jednak umożliwia dodatkowe czary mary.
Zacznijmy od zupełnej podstawy:
{
"$schema": "https://json.schemastore.org/ide.host.json",
"icon": "icon.png"
}
Tutaj ustawiamy ikonkę dla szablonu. To zakłada, że w katalogu .template.config
masz ikonkę o nazwie icon.png
. Żeby ikonka była dobrze widoczna, powinna być w rozmiarze 32×32 piksele o głębokości 32 bitów.
UWAGA! Jeśli tego nie zrobisz, ale w katalogu
.template.config
będziesz miał ikonkę o nazwieicon.png
, to VisualStudio od wersji 2022 też to ogarnie.
Parametry
Informacje o parametrach przechowywane są we właściwości symbolInfo
. To po prostu tablica parametrów, mówiących VisualStudio jak ma je obsługiwać. Podobnie jak w template.json
Podstawowa budowa obiektu jest taka:
"symbolInfo": [
{
"id": "ApiClientName",
"name": {
"text": "Nazwa klienta API"
},
"description": {
"text": "Podaj nazwę klienta dla swojego API. Tak będzie nazywać się wygenerowana klasa."
},
"isVisible": true
}
]
Id to nazwa parametru z pliku template.json
. Właściwości name
i description
pokażą się przy tym parametrze jako jego nazwa i opis, który będzie w hincie:
Jeśli nie wypełnisz właściwości name
i description
, zostanę one odczytane z pliku template.json
.
Parametr isVisible
określa, czy właściwość ma być widoczna.
Jeśli używasz pliku ide.host.json
, to pamiętaj że domyślnie wszystkie parametry są UKRYTE. Stąd właściwość isVisible
. Ustawiasz nią, które parametry mają być widoczne. Jeśli chcesz żeby wszystkie były widoczne, to jest od tego właściwość defaultSymbolVisibility
, np:
{
"$schema": "https://json.schemastore.org/ide.host.json",
"icon": "icon.png",
"supportsDocker": true,
"defaultSymbolVisibility": true
}
Ten kod sprawi, że wszystkie parametry z template.json będą widoczne podczas tworzenia nowego projektu w Visual Studio. Czyli zasadniczo w VisualStudio 2022 równie dobrze ten plik (ide.host.json
) mógłby nie istnieć.
Plik ide.host.json
ma jeszcze kilka parametrów:
- description – opis szablonu – jeśli go nie wypełnisz, zostanie wzięty z pliku
template.json
- name – nazwa szablonu – analogicznie jak wyżej
- order – kolejność w jakiej pojawi się szablon na liście tworzenia nowego projektu
- supportsDocker – jeśli ustawione na true, to podczas tworzenia projektu zobaczysz opcję (checkbox) pozwalającą zdokeryzować taki projekt. I tutaj uwaga! Jeśli chcesz żeby ta opcja była widoczna w VisualStudio, musisz do pliku
template.json
dodać parametr Framework (ten magiczny). Inaczej nie zadziała. - unsupportedHosts – możesz tutaj podać listę wersji, dla których ten szablon ma być niewidoczny na dialogu tworzenia nowego projektu, np. taka konfiguracja ukryje szablon w VisualStudio:
"unsupportedHosts": [
{
"id": "vs"
}
]
Zaawansowane
Specjalne operacje
Jeśli silnik szablonów nie potrafi czegoś zrobić standardowo, to być może da Ci taką możliwość za pomocą specjalnych operacji. Te operacje pozwalają na zdefiniowanie dodatkowych akcji podczas tworzenia projektu na podstawie szablonu. Akcje mogą być globalne (dla wszystkich plików) lub ograniczone tylko do niektórych plików.
{
"customOperations": { //odnosi się do wszystich plików - globalnie
},
"specialCustomOperations": { //tylko do niektórych plików
}
}
Ja w jednym swoim szablonie potrzebowałem mieć różne grupy usingów. Szablon został utworzony pod konkretną solucję, w której te specjalne projekty (namespacey) już są. Jednak z nimi projekt szablonu się nie kompilował. Owszem, mogłem utworzyć jakiś plik z konkretnymi namespaceami i potem go usuwać, ale wpadłem na inny pomysł. Wyobraź sobie taki plik *.cs:
/*add-usings
using Nerdolando.BS.Common.Abstractions;
using Nerdolando.BS.Integrations;
add-usings*/
namespace WebAppWithApiTemplate.ApiClient
{
public class SpecialOperations
{
}
}
Zauważ, że usingi mam tutaj wykomentowane. Ale chcę, żeby pojawiły się w wynikowym projekcie. Dlatego posłużyłem się customową operacją z pliku template.json
:
"SpecialCustomOperations": {
"**/*.cs": {
"flagPrefix": "/*add-usings",
"operations": [
{
"type": "replacement",
"configuration": {
"original": "/*add-usings",
"replacement": ""
}
},
{
"type": "replacement",
"configuration": {
"original": "add-usings*/",
"replacement": ""
}
}
]
}
}
Najpierw podaję do jakich plików odnoszą się akcje. Tutaj – wszystkie pliki z rozszerzeniem *.cs
. Następnie definuję operacje. Jest kilka predefiniowanych operacji, m.in. „replacement
„. Każda z takich operacji ma swoją konfigurację. Niestety na listopad 2022 w schemacie template.json
nie ma tego zdefiniowanego, więc nie będziesz miał podpowiedzi w Intellisense.
W związku z tym, że ten artykuł nie jest kompletnym przewodnikiem, masz tutaj stronę z oficjalnej dokumentacji z opisem tych operacji: https://github.com/dotnet/templating/wiki/Reference-for-template.json#global-custom-operations-and-special-custom-operations
Porady
W tym momencie kończę już ten nieco przydługi artykuł. Dam Ci jeszcze kilka porad na koniec.
- Przede wszystkim zainstaluj sobie narzędzie do analizy szablonów:
dotnet tool install --global sayedha.template.command
Następnie możesz przeanalizować swój szablon pod kątem problemów:
templates analyze -f <path-to-folder>
Gdzie <path-to-folder> to ścieżka do katalogu z szablonem (katalogu, w którym jest .template.config
)
- Dodawaj parametr
Framework
dotemplate.json
. - Jeśli masz jakiś problem ze swoim szablonem – Visual Studio widzi starą wersję albo nie widzi go wcale:
- odinstaluj szablon
- usuń zawartość katalogu
C:\Users\<nazwa użytkownika>\.templateengine\
- zainstaluj szablon na nowo
- jeśli to nie pomaga, czasem pomaga restart Visuala, a czasem restart komputera
- Jeśli chcesz wejść mocniej w temat, koniecznie odwiedź:
- https://github.com/dotnet/templating/wiki – oficjalna dokumentacja do szablonów
- https://github.com/dotnet/templating – repo z silnikiem szablonów
- https://github.com/sayedihashimi/template-sample – przykładowe szablony od człowieka z MS, który dość mocno w nich siedzi
- https://github.com/dotnet/aspnetcore/tree/main/src/ProjectTemplates/Web.ProjectTemplates – oficjalne szablony dla dotnet new
Dzięki za przeczytanie tego artykułu. Jeśli znalazłeś jakiś błąd albo czegoś nie rozumiesz, koniecznie daj znać w komentarzu. Daj też znać w komentarzu, jeśli uważasz, że taki artykuł jest za długi i powinien zostać podzielony na dwa 🙂