Własny szablon dla dotnet new

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 rubryce tags.
  • 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, to dotnet 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ć?

  1. Użytkownik ma mieć opcję do podania nazwy klienta – to już nam załatwił parametr varApiClientName.
  2. Użytkownik ma mieć możliwość wyłączenia lub włączenia rejestracji serwisów – czyli domyślnej konfiguracji klienta.
  3. 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.jsonsources – 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:

  1. 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.
  2. 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.
Zobacz, jak sprytnie Visual Studio rozkminił wersje

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 poziomu dotnet 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 nazwie icon.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.

  1. 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)

  1. Dodawaj parametr Framework do template.json.
  2. 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
  3. Jeśli chcesz wejść mocniej w temat, koniecznie odwiedź:

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 🙂

Podziel się artykułem na: