Tutoriale wszędzie pokazują jak świetnie umieszczać ustawienia aplikacji w pliku appsettings.json, a sekrety w secrets.json. To oczywiście pewne uproszczenie, bo w prawdziwym świecie sekretów raczej nie trzyma się w appsettings.json.
Jeśli jednak masz swój własny mały projekt i znalazłeś już hosting .NET (np. ten), może cię kusić wrzucenie sekretów do appsettings. I nawet wszystko działa. Raczej nie powinieneś tego robić (w zależności od wagi projektu i sekretów które udostępniasz), ale działa.
A co zrobić w aplikacji desktopowej (lub mobilnej)? Gdzie po prostu nie możesz trzymać sekretów na urządzeniu użytkownika?
Niestety nie mam dla ciebie dobrej wiadomości – prosto się nie da.
Kiedyś tworzyłem swoje własne rozwiązania – jakieś szyfrowania używające Data Protection API (DAPI), cuda na kiju i bum gdzieś do rejestru. To też nie jest dobre rozwiązanie do trzymania stałych sekretów. Jest lepsze – KeyVault.
W tym artykule opisuję jak trzymać sekrety w aplikacji webowej i jak się do nich dobrać z apki natywnej.
Czym jest Azure KeyVault?
To bardzo tania (serio, nawet student może sobie na to pozwolić) usługa Azure’owa, która służy do przechowywania danych wrażliwych. Dane są szyfrowane i trzymane na serwerach Microsoftu. Także bezpieczeństwo przede wszystkim.
Niestety nie można bezpiecznie dobrać się do KeyVault’a z aplikacji natywnej. Spędziłem naprawdę sporo czasu na szukaniu takiego rozwiązania, ale się nie da.
Musisz posłużyć się pośrednikiem. To może być Azure Function lub twoje własne, małe WebAPI. W tym artykule pokażę ci sposób z WebApi.
dostęp do serwera (spokojnie może to być współdzielony hosting) – .NETCore, php, cokolwiek.
Certyfikat SSL na serwerze (może być nawet darmowy Let’s Encrypt)
Scenariusz
Pobieranie sekretów z KeyVault przez aplikację natywną polega na:
wysłaniu żądania z aplikacji natywnej do twojego WebAPI
WebAPI uwierzytelnia się w KeyVault i pobiera z niego sekrety
WebAPI odsyła ci sekret
Oczywiście, ze względów bezpieczeństwa, komunikacja między aplikacją natywną i WebAPI musi być szyfrowana (SSL/TLS) i w jakiś sposób autoryzowana. Taka autoryzacja zależy w dużej mierze od konkretnego rozwiązania, więc pominę ten aspekt.
Niestety NIE MA innej drogi jeśli chodzi o aplikacje natywne. Zawsze musi być po drodze WebAPI (ewentualnie Azure Function).
Żeby WebAPI mogło się komunikować z KeyVaultem, musi się do niego uwierzytelnić. Można to zrobić na dwa sposoby:
przez certyfikat SSL – jeśli WebAPI jest hostowane POZA Azure WebService
przez Managed Identity, jeśli WebAPI jest hostowane na Azure WebService
Pokażę ci obie opcje.
Tworzenie infrastruktury w Azure
Jak już wspominałem wcześniej, musisz posiadać konto i subskrypcję na Azure.
Jeśli znasz ten portal, to utwórz sobie zasób KeyVault i zarejestruj swoje WebApi w AAD. Jeśli twoje WebApi ma być hostowane na Azure, utwórz dodatkowo WebService dla niego.
Dla nieobeznanych z Azure…
Rejestracja WebApi w Azure
Niezależnie od tego, czy twoje WebApi będzie na serwerze zewnętrznym, czy hostowane w Azure, musisz je zarejestrować w AAD (Azure Active Directory). AAD to darmowa usługa, która pozwala m.in. na rejestrowanie aplikacji. Przydaje się to do wielu rzeczy, m.in. przy używaniu logowania Microsoft. W naszym przypadku rejestracja WebApi umożliwi korzystanie z zasobów KeyVault.
Nie logując się do subskrypcji, wybierz Azure Active Directory
Z lewego menu wybierz opcję App Registrations
Następnie New Registration
W wyświetlonym oknie podaj nazwę aplikacji, a w Supported Account Types wybierz Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox). Pozwoli to na dostęp do aplikacji wszystkim użytkownikom. Jeśli wybrałbyś np. Accounts in this organizational directory only, tylko osoby z Twojej organizacji (zarejestrowanej w Azure) mogłyby z aplikacji korzystać
Kliknij przycisk Register
Tutaj się zatrzymamy i w następnych krokach utworzymy KeyVault.
Tworzenie ResourceGroup
W Azure właściwie wszystko jest zasobem. Zasoby można łączyć w grupy (Resource Groups). Bardzo dobrym nawykiem jest zamykanie w grupach konkretnych rozwiązań – projektów. Np. stworzenie Resource Group dla aplikacji A i zupełnie odrębnej Resource Group dla aplikacji B. Potraktuj ResourceGroup jak folder, w którym znajdują się wszystkie zasoby Azure potrzebne przy danym rozwiązaniu
z listy subskrypcji wybierz tę, na której chcesz pracować
znajdź takie menu po lewej stronie, kliknij, a następnie kliknij Create na górze ekranu:
Grupa musi mieć jakąś nazwę. Zwyczajowo nazwy zasobów tworzy się przez określenie rodzaju zasobu i jakąś nazwę, np: rg-test – rg od resource group, a test, no bo to moja grupa testowa na potrzeby tego artykułu:
Region możesz zupełnie olać na tym etapie. Ja zawsze jednak wybieram Niemcy – bo są blisko Polski. Region mówi o tym, gdzie informacje na temat twojego zasobu będą trzymane fizycznie.
Zatwierdź tworzenie grupy, klikając Review and create.
Po utworzeniu grupy wejdź do niej.
Tworzenie KeyVault
Teraz, będąc w Resource Group, możesz utworzyć nowy zasób – KeyVault, czyli miejsce do trzymania sekretów.
Z górnego menu wybierz Create aby utworzyć nowy zasób.
W okienku wyszukiwania zacznij wpisywać „key vault”
Powinieneś zobaczyć zasób KeyVault. Kliknij na niego aby go utworzyć.
Na kolejnym ekranie wybierz plan subskrypcji. Przy KeyVault jest tylko jeden, więc po prostu wciśnij guzik Create.
Teraz uzupełnij dane:
Resource Group, do której ma być przypisany Twój Key Vault
Nazwę KeyVault (np. kv-moja-aplikacja)
Region jaki chcesz
Pricing Tier ustaw na Standard, zapewni ci to minimalne ceny
Zatwierdź tworzenie KeyVault przyciskiem Review and create
Żeby Twoja apka mogła korzystać z tego KeyVault, musisz dać jej dostęp, ustawiając polityki dostępu.
Przejdź do swojego KeyVaulta
Z menu po lewej wybierz Access Policies, a następnie Add Access Policy
Aby móc poprawnie odczytywać sekrety, musisz ustawić dostęp do Secret Permissions na Get i List
Następnie dodaj principala, którym będzie twoja zarejestrowana wcześniej aplikacja (WebAPI zarejestrowane w AAD)
Potwierdź tę konfigurację, wciskając przycisk Add
Super, teraz możesz zapisać jakiś sekret.
Tworzenie sekretów w KeyVault
Żeby umieścić jakiś sekret w KeyVault, wybierz z lewego menu Secrets, a następnie Generate/Import
Teraz możesz utworzyć swoje sekrety, do których nikt nie powinien mieć dostępu
Przy okazji, każdemu z sekretów możesz nadać daty, w których ma być aktywny. Ja umieściłem 5 sekretów bez żadnych dat. Jeśli się przyjrzysz obrazkowi wyżej, zobaczysz przy MailSettings dwie kreski. Te kreski oddzielają sekcję od wartości. To tak, jakbyś w pliku appsettings.json umieścił:
To nie jest kurs obsługi KeyVault, ale muszę w tym momencie wspomnieć, że tutaj nie modyfikujesz swoich sekretów. Możesz dodać po prostu nową wersję. Jeśli klikniesz na jakiś sekret, w górnym menu zobaczysz opcję New Version – to pozwoli ci na dodanie nowej wartości tego sekretu.
Konfiguracja WebApi
Stwórz teraz WebAPI, które będzie pobierało dane z KeyVault. W tym celu, w Visual Studio utwórz standardowy projekt WebAPI.
Posłużymy się standardowym mechanizmem konfiguracji w .NetCore. Jeśli nie wiesz, jak działa konfiguracja w .NetCore, koniecznie przeczytaj ten artykuł.
Jak wspomniałem gdzieś na początku tego artykułu, możesz do KeyVault dobrać się na dwa sposoby. Z użyciem certyfikatu lub Managed Identity. Certyfikatem musisz się posłużyć, jeśli WebApi jest hostowane na serwerze zewnętrznym (poza Azure). Poniżej opisuję oba sposoby.
Łączenie z KeyVault za pomocą certyfikatu
Nikt ci nie zabroni użyć do tego najprostszego certyfikatu nawet self-signed. Ale dla bezpieczeństwa użyj tego, który masz na serwerze (to może być darmowy Let’s Encrypt). Przede wszystkim musisz uzyskać certyfikat w formacie CRT, CER lub PEM. Na szczęście możesz pobrać go ze swojej domeny.
Pokażę ci jak to zrobić skryptowo, żebyś mógł sobie ewentualnie ten proces zautomatyzować. Pamiętaj, że Let’s Encrypt jest ważny tylko 6 miesięcy. Po tym czasie zazwyczaj automatycznie się odnawia.
Pobieranie certyfikatu ze strony
Użyjemy aplikacji openssl, którą pewnie i tak masz już zainstalowaną. Jeśli nie, możesz pobrać ją stąd.
Aby pobrać certyfikat w formacie PEM, wykonaj poniższy skrypt:
Pamiętasz jak rejestrowaliśmy WebAPI w Azure AD? Teraz dodamy tam certyfikat.
Wejdź znów do AAD w taki sam sposób jak podczas rejestrowania aplikacji i przejdź do App Registrations. Wejdź do aplikacji, którą zarejestrowałeś na początku artykułu i z lewego menu wybierz Certificates & Secrets.
Wciśnij przycisk Upload certificate i wskaż plik certyfikat.pem, który pobrałeś w poprzednim kroku.
Zobaczysz dodany certyfikat.
Teraz zostało już tylko uwierzytelnienie aplikacji WebAPI w KeyVault.
Uwierzytelnienie aplikacji za pomocą certyfikatu
Przede wszystkim musisz zainstalować dwie paczki NuGet:
Azure.Extensions.AspNetCore.Configuration.Secrets
Azure.Identity
Teraz wystarczy to lekko skonfigurować. Dodaj taki kod podczas konfiguracji aplikacji WebAPI:
// using System.Linq;
// using System.Security.Cryptography.X509Certificates;
// using Azure.Extensions.AspNetCore.Configuration.Secrets;
// using Azure.Identity;
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
if (context.HostingEnvironment.IsProduction())
{
var builtConfig = config.Build();
using var store = new X509Store(StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var certs = store.Certificates.Find(
X509FindType.FindByThumbprint,
builtConfig["CertInfo:Thumbprint"], false);
config.AddAzureKeyVault(new Uri($"https://{builtConfig["Azure:KeyVaultName"]}.vault.azure.net/"),
new ClientCertificateCredential(builtConfig["Azure:AADDirectoryId"], builtConfig["Azure:ApplicationId"], certs.OfType<X509Certificate2>().Single()),
new KeyVaultSecretManager());
store.Close();
}
})
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
Najpierw musisz pobrać certyfikat o podanym odcisku palca. Ja tutaj to robię z magazynu certyfikatów – w zależności od tego, gdzie certyfikat masz zainstalowany, użyjesz StoreLocation.CurrentUser lub StoreLocation.LocalMachine.
UWAGA! Na niektórych serwerach współdzielonych możesz mieć problem do dobrania się do certyfikatu z poziomu aplikacji. Być może aplikacja będzie wymagała wyższych uprawnień. Jeśli tak będzie, porozmawiaj z supportem swojego hostingu.
Następnie dodajesz swojego KeyVaulta do konfiguracji .NetCore i to tyle. Od tego momentu wszystkie sekrety możesz pobierać używając IConfiguration.
Oczywiście ja w tym kodzie nie podaję żadnych danych na sztywno – one są brane z konfiguracji, co zapewne widzisz. Czyli z pliku appsettings.json, dlatego w linijce 12 buduję wstępną konfigurację. Rzeczy, które się tu znajdują nie są sekretami i dostęp osób trzecich do tych informacji niczym nie grozi. Przykładowy appsettings.json może wyglądać tak:
To teraz, skąd wziąć te dane? Nazwa KeyVault to wiadomo – taką nazwę podałeś podczas tworzenia KeyVault.
Jeśli chodzi o Id tenanta i Id aplikacji… Wejdź znów na Azure do Azure Active Directory, następnie wejdź w AppRegistrations i aplikację, którą rejestrowałeś:
Application (client) ID to Id twojej aplikacji. Natomiast Directory (tenant) Id to Id twojego tenanta (tenant czyli subskrybent).
A skąd wziąć odcisk palca certyfikatu? Z rejestracji aplikacji na AAD, tam gdzie dodawałeś certyfikat. To jest kolumna Thumbprint.
Te literki i cyferki to jest właśnie odcisk palca Twojego certyfikatu.
Łączenie z KeyVault z pomocą Managed Identity
Jeśli hostujesz swoją aplikację na Azure, możesz do KeyVault dobrać się za pomocą ManagedIdentity. To jest druga opcja. Tylko zaznaczam – wymaga hostowania Twojego WebApi na Azure.
Managed Identity pozwala aplikacji na łączenie się z innymi zasobami Azure. Nie tylko KeyVault.
Dodanie Managed Identity
Skoro jesteś w tym miejscu, zakładam że masz już utworzony AppService na Azure. Jeśli nie, to utwórz sobie w swojej resource group zasób o nazwe Web App (to utworzy tzw. AppService).
Następnie na poziomie tej aplikacji (App Service) kliknij w menu Identity po lewej stronie.
Następnie zmień Status na On i zapisz to. Właśnie dodałeś Managed Identity systemowe w swojej aplikacji. Teraz tylko zwróć uwagę na Object (principal) ID. Skopiuj ten identyfikator.
Kilka akapitów wyżej dodawałeś Access Policy do swojego KeyVault. Teraz dodaj nową właśnie dla tej aplikacji. Zamiast nazwy wklej po prostu ten identyfikator.
Uwierzytelnienie aplikacji w KeyVault za pomocą ManagedIdentity
Pokrótce, cały flow polega na wysłaniu żądania do Azure w celu otrzymania tokenu. Otrzymany token przekazujemy dalej w kolejnych żądaniach do chronionych zasobów. Oczywiście w .NET możemy to ogarnąć gotową biblioteką i tak też zrobimy.
Najpierw pobierz Nuget:
Azure.Identity
Teraz na scenę wchodzi klasa DefaultAzureCredential. Jest to cudo, które w środowisku deweloperskim uwierzytelnia cię w Azure za pomocą danych, które masz wklepane w zmienne środowiskowe. Jeśli apka znajduje się już na Azure, wtedy jest uwierzytelniana kontem Azurowym.
W środowisku deweloperskim musisz się zalogować do Azure, żeby sobie wszystko poustawiać. W Visual Studio wejdź do Tools -> Options i w oknie opcji znajdź Azure Service Authentication. Upewnij się, że jestem tam zalogowany na odpowiednim koncie:
To spowoduje, że DefaultAzureCredential pobierze odpowiednie dane.
Teraz już tylko zostało połączenie apki z KeyVaultem. Zrobisz to podczas jej konfiguracji dokładając taki kod:
// using Azure.Identity;
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
if (context.HostingEnvironment.IsProduction())
{
var builtConfig = config.Build();
config.AddAzureKeyVault(new Uri($"https://{builtConfig["KeyVaultName"]}.vault.azure.net/"),
new DefaultAzureCredentials());
store.Close();
}
})
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
Kod jest banalny. Najpierw wstępnie budujemy konfigurację, żeby móc odczytać dane z appsettings. Następnie dodajemy konfigurację KeyVault, przekazując nazwę twojego KeyVaulta. Nazwa ta jest brana z appsettings, żeby nie trzymać jej na sztywno w kodzie. Ten fragment w appsettings może wyglądać tak:
{
"KeyVaultName": "{nazwa twojego KeyVault}"
}
Od tego momentu możesz odczytywać sekrety normalnie przez IConfiguration jak gdyby były częścią pliku appsettings.json. I robisz to bezpiecznie, nie pokazując światu żadnych tajemnic.
Koniec!
Uff, to koniec. Wiem, że dużo pojawiło się zagadnień i nie wszystko może być od razu jasne. Jeśli masz jakieś pytania lub wątpliwości, koniecznie daj znać w komentarzu. Jeśli znalazłeś błąd w artykule, również się podziel 🙂
Również napisz, jeśli znasz bezpośredni bezpieczny sposób na odczytanie danych z KeyVault przez aplikację desktopową.
Jak już raz wejdziesz w internetowy świat .NET i później przyjdzie ci coś zrobić na konsoli albo innej aplikacji natywnej (Wpf, Winforms, Xamarin), to nagle się okazuje, że brakuje rzeczy. Nie ma dependency injection (trzeba pobierać np. starego, dobrego Autofaca), konfiguracji z appsettings i wielu innych mechanizmów, do których nas internetowy .NetCore przyzwyczaił.
Ale to nie znaczy, że nie można ich tam w prosty sposób umieścić.
Większość przykładów zrobimy na konsoli, bo jest najprościej. Jednak na koniec pokażę też przykłady w innych typach aplikacji.
W artykule o wstrzykiwaniu zależności pokazałem jak dodać DI do aplikacji konsolowej. Dzisiaj zrobimy pełen pakiet.
Co to Host?
Na początku stwórz nową aplikację konsolową. Po staremu to klasa Program ze statyczną metodą Main. W .NET6 to po prostu jedna linijka w pliku Program.cs:
Console.WriteLine("Hello, World!");
I tutaj dzieje się wszystko. Możesz wypisywać komunikaty na konsoli, możesz pobierać dane od użytkownika, tworzyć obiekty, zwalniać je itd. Jednym zdaniem – to jest serce całej aplikacji (po staremu – metoda Main z klasy Program). Można powiedzieć, że metoda Main jest w pewnym sensie hostem dla Twojej aplikacji.
A gdyby teraz przenieść zarządzanie tym wszystkim do innej klasy? I tak powstała klasa Host, która implementuje interfejs IHost. Host zajmuje się całym cyklem życia aplikacji. Zajmuje się konfiguracją, wstrzykiwaniem zależności, zwalnianiem zasobów, tworzeniem ich itd.
Żeby jej użyć, przede wszystkim musisz pobrać pakiet z NuGet: Microsoft.Extensions.Hosting.
Jednak, żeby utworzyć hosta, musisz posłużyć się HostBuilderem
Co to HostBuilder?
To budowniczy (w sensie wzorca projektowego) dla klasy Host. Najprostszy sposób na utworzenie Hosta to:
using Microsoft.Extensions.Hosting;
var hostBuilder = new HostBuilder();
var host = hostBuilder.Build();
host.Start();
Mamy tutaj utworzenie hosta i uruchomienie aplikacji (Start).
Jednak czym by był budowniczy, gdyby budował tylko takiego prymitywnego hosta? Zwykłym pijakiem spod bramy…
Budowanie hosta
Możesz zbudować domyślnego hosta na skróty w taki sposób:
using Microsoft.Extensions.Hosting;
var hostBuilder = Host.CreateDefaultBuilder();
var host = hostBuilder.Build();
await host.Run();
Cała magia zadzieje się w metodzie CreateDefaultBuilder. Ta metoda utworzy i zwróci ci domyślnego buildera, który ma już oprogramowane czytanie konfiguracji z appsettings, zmiennych środowiskowych i linii poleceń, utworzenie dependency injection i rejestrację podstawowych klas. W tym – domyślnej klasy Host.
Można powiedzieć, że to jest cała podstawa tego, co byś chciał mieć. Nic więcej, nic mniej.
Teraz możesz zadać pytanie – co stanie się w momencie wywołania host.Run()?
Obiekt Host poszuka zarejestrowanych obiektów implementujących IHostedService. I po kolei na każdym z nich wywoła metodę StartAsync. W tym przypadku oczywiście nie znajdzie takich obiektów, ponieważ nie utworzyłeś ich. Zatem praca od razu się zakończy. Wniosek z tego taki, że musisz stworzyć przynajmniej jedną klasę implementującą IHostedService.
IHostedService
Od tej pory pomyśl o swojej aplikacji konsolowej jako o „hoście”. Twoja główna aplikacja (po staremu metoda Main) ma za zadanie utworzyć i uruchomić obiekty IHostedService. I to w tych obiektach będzie dział się cały „prawdziwy” program. Stwórzmy taką klasę:
internal class MainApplication : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
Console.Write("Jak masz na imię? ");
string name = Console.ReadLine();
Console.WriteLine($"Cześć {name}!");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
//ta metoda zostanie wykonana na zakończenie Twojego programu
}
}
Teraz musimy zarejestrować gdzieś, ten IHostedService:
var hostBuilder = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<IHostedService, MainApplication>();
});
var host = hostBuilder.Build();
host.Run();
Jeśli chciałbyś ten kod porównać do starej, dobrej konsoli, to wyglądałoby to mniej więcej tak:
public static void Main(string[] args)
{
var app = new MainApplication();
app.StartAsync(CancellationToken.None).Wait();
app.StopAsync(CancellationToken.None).Wait();
}
Po takiej prostej konfiguracji masz już:
ustawienie ContentRootPath na katalog aplikacji
wczytanie konfiguracji hosta ze zmiennych środowiskowych z prefixem DOTNET_
wczytanie konfiguracji z linii poleceń
wczytanie konfiguracji z plików appsettings i sekretów
wczytanie konfiguracji ze zmiennych środowiskowych i linii poleceń
dodanie domyślnego mechanizmu logowania
no i rzecz jasna Dependency Injection (w tym z automatu zarejestrowane IHostEnvironment)
Run, RunAsync, Start, StartAsync? WTF?
Jak się zapewne zdążyłeś zorientować, Host (a właściwie interfejs IHost) zawiera 4 metody do startowania aplikacji: Run, RunAsync, Start, StartAsync. Nie rozróżniając na synchroniczne i asynchroniczne mamy dwie – Run i Start. Czym się różnią? Poniżej „aplikacja” zrozum jako „wszystkie zarejestrowane klasy IHostedServices„.
Run
To jest extension method. W kodzie frameworka wygląda ona tak:
host.RunAsync().GetAwaiter().GetResult();
Uruchamia aplikację i blokuje aktualny wątek do momentu zakończenia aplikacji (wszystkie IHostedServices się skończą).
RunAsync
To również jest extension method. Ona wywołuje StartAsync i czeka na zakończenie aplikacji:
Na koniec wywołuje Dispose (lub DisposeAsync) na rzecz hosta.
Start
Wywołuje StartAsync analogicznie jak Run wywołuje RunAsync:
host.StartAsync().GetAwaiter().GetResult();
StartAsync
Startuje hosta asynchronicznie. Działa w taki sposób:
loguje na konsoli start aplikacji
wywołuje metodę WaitForAsync na rzecz IHostLifetime
wywołuje asynchronicznie StartAsync na wszystkich zarejestrowanych obiektach IHostedService
powiadamia IHostLifetime, że aplikacja się rozpoczęła
Czyli widać z tego, że wszystko sprowadza się i tak do StartAsync. Metody Start i Run są tylko pomocnicze. RunAsync dodatkowo czeka na zakończenie aplikacji przez IHostLifetime. Dokładną różnicę między StartAsync, a RunAsync, zobaczysz po zakończeniu aplikacji konsolowej.
Jeśli użyjesz RunAsync, to domyślny IHostLifetime zakończy aplikację tylko przy zamknięciu okna konsoli lub wciśnięciu Ctrl+C.
Jeśli użyjesz StartAsync, aplikacja zostanie zakończona normalnie, ale w oknie konsoli będziesz mógł przeczytać dodatkowe informacje zanim je zamkniesz.
IHostLifetime jest domyślnie ustawiany w CreateDefaultBuilder. Domyślnie jest to ConsoleLifetime. Oczywiście nic nie stoi na przeszkodzie, żebyś stworzył własną implementację IHostLifetime i np. odłożył faktyczne uruchomienie programu do jakiegoś momentu. Jednak to nie jest o tym artykuł 🙂
Dodatki w .NET8
W .NET8 doszedł interfejs, który daje Ci nieco większą kontrolę nad cyklem życia Twojego hosta. IHostedLifecycleService wygląda w taki sposób:
Jak widzisz, IHostedLifecycleService implementuje już IHostedService. Więc jeśli chcesz mieć tę dodatkową kontrolę, Twój host powinien implementować IHostedLifecycleService.
To teraz przypatrzmy się jaki jest konkretny cykl życia takiego hosta. Te metody są wykonywane po kolei:
IHostLifetime.WaitForStartAsync
IHostedLifecycleService.StartingAsync
IHostedService.Start
IHostedLifecycleService.StartedAsync
IHostApplicationLifetime.ApplicationStarted
IHostedLifecycleService.StoppingAsync
IHostApplicationLifetime.ApplicationStopping
IHostedService.Stop
IHostedLifecycleService.StoppedAsync
IHostApplicationLifetime.ApplicationStopped
IHostLifetime.StopAsync
Środowisko
Pamiętaj, że jeśli w taki sposób tworzysz hosta, środowiskiem będzie Production. .NET uzna to za środowisko produkcyjne, ponieważ nigdzie nie znalazł zmiennej ASPNETCORE_ENVIRONMENT. Oczywiście, jeśli taką zmienną środowiskową masz wpisaną na swojej maszynie lub przekażesz ją w parametrach, to zadziała i środowisko będzie takie jakie sobie wpiszesz. Możesz też na sztywno posłużyć się metodą UseEnvironment:
w takim przypadku w launchSettings.json zmienne środowiskowe nie powinny być prefixowane. Dlatego jest ENVIRONMENT, zamiast ASPNETCORE_ENVIRONMENT
plik launchSettings.json powinien się znaleźć w katalogu wynikowym aplikacji. A więc upewnij się, że w jego właściwościach zaznaczysz opcję Copy to Output Directory na Copy always lub Copy if newer.
Teraz możesz sterować swoim środowiskiem z tego pliku. Możesz oczywiście wpisywać tam wszelkie zmienne środowiskowe, jakie Ci się zamarzą.
Konfiguracja
W powyższym kodzie cała konfiguracja aplikacji powinna odbyć się w ConfigureServices. Tak jak w przykładzie rejestruję MainApplication:
Jeśli jednak tak jak ja – w kodzie musisz mieć porządek, przenieś to do innej klasy lub metody. Na początek stwórz prostą klasę Startup – powinieneś znać ją z internetowej wersji .NETCore. Różnica jest taka, że w tej klasie nie definiujemy middleware pipeline, bo go po prostu nie ma:
internal class Startup
{
IConfiguration config;
ILogger<Startup> logger;
public Startup(IConfiguration config, ILogger<Startup> logger)
{
this.config = config;
this.logger = logger;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHostedService, MainApplication>();
}
}
W konstruktorze wstrzykuję konfigurację i loggera. Zwróć uwagę, że te obiekty zostały zarejestrowane w pierwszym kroku budowania hosta – ConfigureHostConfiguration w metodzie CreateDefaultBuilder.
A teraz swojego hosta stwórz w taki sposób:
var hostBuilder = Host.CreateDefaultBuilder()
.ConfigureServices(services =>
{
services.AddSingleton<Startup>();
var provider = services.BuildServiceProvider();
var startup = provider.GetRequiredService<Startup>();
startup.ConfigureServices(services);
})
.ConfigureHostConfiguration(config =>
{
config.AddJsonFile("launchSettings.json");
});
Spójrz, co się stało w ConfigureServices.
Najpierw zarejestrowałem naszą klasę Startup. Równie dobrze mógłbym ją utworzyć przez new, ale tak jest bardziej elegancko i możesz wstrzyknąć do konstruktora klasy Startup wszystko, co zostało zarejestrowane przed BuildServiceProvider (5 linijka)
Następnie w linijce 5 budujemy service providera. Powinieneś już wiedzieć (przynajmniej z tego artykułu), że to jest obiekt, który zwróci zarejestrowane wcześniej serwisy.
Teraz małe wyjaśnienie. ServiceProvider może być budowany kilka razy na podstawie jednego IServiceCollection. Dlatego w tym momencie otrzymamy dostęp do serwisów zarejestrowanych wcześniej i możemy pobrać obiekt klasy Startup. Co więcej, możemy dalej posługiwać się IServiceCollection i przekazać go jako parametr do metody ConfigureServices (linijka 8).
Na sam koniec ServiceProvider zostanie znowu utworzony (to się dzieje podczas tworzenia hosta) i wszystkie serwisy rejestrowanie w klasie Startup również będą dostępne.
Jak dodać appsettings?
Domyślna konfiguracja odczytuje pliki appsettings z katalogu aplikacji. Czyli teoretycznie niczego nie musisz robić. Zwłaszcza jeśli masz tylko jeden plik appsettings.json – do tego powinieneś dążyć na środowisku produkcyjnym. Upewnij się tylko, że plik znajduje się w katalogu wynikowym aplikacji (właściwość pliku Copy to Output Directory ustaw na Copy always lub Copy if newer).
Jest też druga możliwość. Dodanie appsettings do zasobów aplikacji i wczytywanie ich stamtąd. Ale to nie jest artykuł o tym, niedługo coś na ten temat skrobnę.
UWAGA!
Pamiętaj, że nie powinieneś w pliku appsettings przechowywać ŻADNYCH sekretów aplikacji. Wszelkie hasła, dostępy do kont i inne wrażliwe dane muszą być przechowywane w sposób bezpieczny. Pamiętaj, że nawet jeśli umieścisz plik appsettings.json w zasobach aplikacji, to nie ma problemu dla użytkownika, żeby sobie go wyekstrahować i przeczytać.
Przykład w WPF
Teraz, jak obiecałem na początku, pokażę Ci kilka przykładów innych niż aplikacja konsolowa. Na początek aplikacja WPF.
O ile w aplikacji konsolowej musiałeś stworzyć implementację IHostedService (bo gdzieś ten program musi być), to w WPF nie musisz już tego robić. Wystarczy, że odpalisz główne okno aplikacji.
Oczywiście przede wszystkim musisz zacząć od małej konfiguracji aplikacji – żeby nie uruchamiała okna głównego, tylko zdarzenie OnStartup w klasie Application. W tym celu w pliku App.xaml ustaw:
Metoda Application_Startup zostanie uruchomiona podczas uruchamiania aplikacji. Jeśli tego nie zrobisz, automatycznie uruchomione zostanie okno główne.
Jeśli chodzi o ShutdownMode to gdy nie ustawisz tej wartości, aplikacja po zamknięciu okna głównego nigdy się nie zakończy. Dlatego też ustaw to. Następnie skonfiguruj .NET.
W konstruktorze klasy App utwórz hosta:
IHost host;
public App()
{
host = Host.CreateDefaultBuilder(Environment.GetCommandLineArgs()) //przekaż linię poleceń
.ConfigureServices((ctx, services) =>
{
s.AddSingleton<Startup>();
var tempServices = services.BuildServiceProvider();
var startup = tempServices.GetRequiredService<Startup>();
startup.ConfigureServices(services);
}).Build();
}
W następnym kroku możesz uruchomić hosta w Application_Startup:
Najpierw uruchamiamy hosta. Potem pobieramy zarejestrowany serwis MainWindow -> w taki sposób, mając hosta, możesz pobrać każdy zarejestrowany serwis (łącznie z konfiguracją).
Gdy masz już MainWindow – po prostu pokazujesz je.
A jak rejestrujesz MainWindow? W tym przykładzie użyłem analogicznej klasy Startup jak w sekcji wyżej, po prostu:
internal class Startup
{
IConfiguration config;
ILogger<Startup> logger;
public Startup(IConfiguration config, ILogger<Startup> logger)
{
this.config = config;
this.logger = logger;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<MainWindow>();
}
}
W aplikacji WinForms zrobisz to analogicznie.
Przykład w Xamarin
Oczywiście z Xamarin jest trochę więcej roboty. Chociaż w MAUI ma to już być w standardzie. Yeah!
Ale póki co, spójrzmy na Xamarin.
W związku z tym, że Xamarin może mieć tak naprawdę kilka aplikacji (np. Android i iPhone) jest trochę więcej kombinacji.
Na początek spójrz do projektu Xamarin (tego współdzielonego) do pliku App.xaml.cs. To jest klasa aplikacji. Dodaj tam konstruktor:
public readonly IHost host;
public App(Action<HostBuilderContext, IServiceCollection> configureNativeServicesAction)
{
host = Host.CreateDefaultBuilder(Environment.GetCommandLineArgs())
.ConfigureServices((ctx, b) =>
{
b.AddSingleton<Startup>();
b.AddSingleton<App>(this);
configureNativeServicesAction?.Invoke(ctx, b);
})
.ConfigureServices((ctx, b) =>
{
var tempServices = b.BuildServiceProvider();
var startup = tempServices.GetRequiredService<Startup>();
startup.ConfigureServices(this, b);
})
.Build();
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
}
W konstruktorze przyjmujesz akcję do rejestracji pewnych natywnych serwisów, których być może używasz. Pisząc „natywne” mam na myśli takie, że na aplikacji Androidowej i na aplikacji iPhone się różnią. Ich implementacje są związane z urządzeniem.
W linii 10 wywołujesz tę akcję. Następnie wszystko jest analogiczne jak przy WPF z wyjątkiem tworzenia głównego okna.
Następnie w tym samym projekcie (Xamarin) stwórz sobie klasę do inicjalizacji aplikacji:
public static class AppInitializer
{
static App theApp;
static bool isInitialized = false;
public static void Init(Action<HostBuilderContext, IServiceCollection> configureNativeServicesAction)
{
if (isInitialized)
return;
theApp = new App(startupAssembly, configureNativeServicesAction);
}
public static App GetApplication()
{
return theApp;
}
}
Klasa App to oczywiście klasa z pliku App.xaml.cs z projektu Xamarina z odpowiednim konstruktorem.
Na koniec użyj tego w aplikacji natywnej, np. Android. W metodzie OnCreate klasy MainActivity uruchom to wszystko:
Zachęcam Cię do poeksperymentowania z HostBuilderem jak i samym Hostem. Możesz naprawdę sporo tym wyciągnąć. I wcale nie musisz posługiwać się metodą CreateDefaultBuilder. Możesz skonfigurować wszystko sam krok po kroku, używając dostępnych metod.
Dziękuję Ci za przeczytanie tego tekstu. Jeśli czegoś nie rozumiesz lub znalazłeś błąd w artykule, koniecznie daj znać w komentarzu. No i jak zwykle zachęcam do zostawienia swojego adresu poniżej – dzięki temu nie ominie Cię żadna dawka wiedzy 🙂
Super! Dostałeś nowy monitor 4k! Nic tylko szaleć. Odpalasz swój program napisany w WPF i… gówno widzisz. Wszystko jest za małe. Jak to się dzieje? Przecież obiecali, że WPF ogarnia DPI, czy coś tam i nie trzeba już nic robić…
No tak. Niby ogarnia, ale nie ogarnia zmiany DPI przy kilku monitorach. To automatycznie robią aplikacje pisane w UWP. Natomiast w WPF trzeba zrobić mały, prosty myk. Ale spokojnie, nie musisz przepisywać aplikacji, stosować jakiś ViewBoxów, czy skomplikowanych obliczeń. Wszystko zostanie załatwione w pliku manifestu.
Windows 10 od wersji 1703 używa czegoś takiego jak Per-Monitor v2 awarness mode. Po krótce chodzi o to, że potrafi rozpoznać, kiedy okno aplikacji jest przesuwane na monitor z inną rozdzielczością. Teraz musimy poinformować naszą aplikację WPF, że też to potrafi:
Rozwiązanie
1. Utwórz plik manifestu (jeśli używasz domyślnego) lub otwórz jeśli już go masz.
Aby utworzyć plik manifestu:
kliknij prawym klawiszem myszy na projekt
wybierz Add -> New Item
odnajdź plik manifestu (wpisz w okienko do szukania: manifest)
2. Jeśli utworzyłeś nowy plik manifestu, to prawdopodobnie już wszystko masz. Wystarczy znaleźć fragment <application xmlns="urn:schemas-microsoft-com:asm.v3"> i go odkomentować.
3. Powinieneś w pliku manifestu mieć taki fragment:
Obsługujemy pliki cookies. Jeśli uważasz, że to jest ok, po prostu kliknij "Akceptuj wszystko". Możesz też wybrać, jakie chcesz ciasteczka, klikając "Ustawienia".
Przeczytaj naszą politykę cookie