W poprzednim odcinku wygenerowaliśmy klucz do Google Places API. Teraz mamy problem, który prędzej czy później dotyka każdego developera — co zrobić z tym kluczem, żeby nie wylądował w repozytorium? I przy okazji: jak zorganizować to tak, żeby dodanie kolejnego klucza albo adresu URL nie kończyło się grzebaniem po całej aplikacji i wklejaniem stringów w kolejnych miejscach.
Rozwiązanie w ekosystemie .NET nazywa się Options pattern i opiera się o trzy klocki: plik appsettings.json, klasa konfiguracyjna mapowana z sekcji tego pliku oraz wstrzykiwany przez DI wrapper IOptions<T>. Pokażę Ci, jak to poskładać w aplikacji konsolowej — dokładnie tak, jak robimy to w naszym projekcie WPImporter (GitHub: Lekcja-4).
Dlaczego nie hardcodować w kodzie
Zacznijmy od pytania „po co się w ogóle męczyć”. Klucz do Google Places wklejony jako const string w Program.cs zadziała. Tylko ma trzy wady, z których każda w pewnym momencie Ci się odbije.
Sekretu, który znalazł się w historii Gita, nie usuniesz jednym commitem. On tam zostanie nawet po
git rm.
Po pierwsze — bezpieczeństwo. Jak tylko zrobisz git push, klucz siedzi w historii repozytorium. Jeśli repo jest publiczne, boty skanujące GitHuba znajdą go w kilka minut i wystawią Ci rachunek w Google Cloud. Jeśli jest prywatne — trafi do każdej osoby, która kiedykolwiek dostanie do niego dostęp.
Po drugie — środowiska. Klucz na developce, klucz na stagingu, klucz na produkcji. Connection string do lokalnego Postgresa i do tego w chmurze. Jeśli siedzą w kodzie, każda zmiana środowiska to zmiana kodu i nowy build. To nie jest konfiguracja, to jest kompilacja.
Po trzecie — porządek. Nawet jeśli jesteś sam i piszesz projekt do szuflady, wpięcie wartości konfiguracyjnych w appsettings.json daje Ci jedno miejsce, do którego zaglądasz, kiedy coś nie działa. Nie ganiasz po repozytorium i nie pytasz się pliku po pliku „gdzie ja to wpisałem”.
Rozwiązanie .NET-owe jest proste: wszystko, co może się zmienić między środowiskami albo jest sekretem, wynosimy do appsettings.json, a kod czyta to przez IConfiguration lub — lepiej — przez silnie typowaną klasę Options.
appsettings.json i struktury sekcji
Plik appsettings.json to zwykły JSON dorzucony do projektu (z Copy to Output Directory = Copy if newer, inaczej nie pojawi się w bin/). W szablonie projektu konsolowego musisz go dodać ręcznie, razem z paczką Microsoft.Extensions.Configuration.Json z NuGeta. Ta paczka daje Ci metodę rozszerzającą AddJsonFile(...), bez której builder konfiguracji nie umie odczytać JSON-a.
Strukturę pliku dzielisz na sekcje — logiczne grupy ustawień. Dla naszego WPImportera wygląda to mniej więcej tak:
{
"ConnectionStrings": {
"Default": "Host=localhost;Database=wpimporter;Username=postgres;Password=..."
},
"GoogleApi": {
"Key": "AIzaSy...",
"BaseUrl": "https://maps.googleapis.com/maps/api"
}
}
GoogleApi to nazwa sekcji — dokładnie tak samo nazwiesz potem klasę, na którą tę sekcję zmapujesz (bez sufiksu lub z Options, jak wolisz). Nazwa ma znaczenie tylko o tyle, że musi się zgadzać między JSON-em a kodem wywołującym GetSection("GoogleApi"). Możesz zagnieżdżać, dodawać tablice, mieszać typy — konfiguracja obsłuży wszystko, co da się serializować.
Jedno ważne: ten plik nigdy nie powinien trafić do repo z realnym kluczem. Albo trzymasz tam wartości puste / placeholdery i prawdziwe sekrety wkłada appsettings.Development.json (ignorowane przez Git), albo dotnet user-secrets, albo zmienne środowiskowe. Ja w projekcie kursowym zostawiam klucz lokalnie, a przed commitem go kasuję — to najprostsza dyscyplina i działa, jeśli pracujesz sam.
Klasa Options + Configure<T>
Mając sekcję w JSON-ie, potrzebujesz jej odpowiednika w kodzie. Tworzymy klasę, której pola nazywają się identycznie jak klucze w sekcji — ConfigurationBinder dopasowuje je po nazwie, case-insensitive.
public class GoogleApiOptions
{
public string Key { get; set; } = default!;
public string BaseUrl { get; set; } = default!;
}
Klasę trzymaj w osobnym projekcie Common (albo przynajmniej w folderze Common/Options) — to czysty POCO bez zależności, pasuje do współdzielonego kawałka aplikacji.
Teraz w Program.cs robisz dwie rzeczy. Najpierw budujesz IConfiguration z pliku JSON, potem rejestrujesz go w kontenerze DI i mapujesz sekcję na klasę:
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(configuration);
services.Configure<GoogleApiOptions>(configuration.GetSection("GoogleApi"));
Dwie linie, które robią całą magię. AddSingleton<IConfiguration> rejestruje samo IConfiguration, żeby w razie czego dało się je wstrzyknąć wprost (czasem wygodne do ad-hoc odczytu). Configure<GoogleApiOptions> mówi kontenerowi: weź sekcję GoogleApi, zbinduj ją na klasę i zarejestruj pod IOptions<GoogleApiOptions>.
Od tego momentu każdy serwis, który dostanie w konstruktorze IOptions<GoogleApiOptions>, dostanie gotowy, wypełniony obiekt:
public class PlaceImporter
{
private readonly GoogleApiOptions _options;
public PlaceImporter(IOptions<GoogleApiOptions> options)
{
_options = options.Value;
}
public void Run()
{
var url = $"{_options.BaseUrl}/place/textsearch/json?key={_options.Key}";
// ...
}
}
Silnie typowane. Bez magicznych stringów w logice biznesowej. Jak zmienisz strukturę sekcji, kompilator krzyknie, zanim dojdzie do runtime.
IOptions, IOptionsSnapshot, IOptionsMonitor — różnice
W filmie skupiamy się na IOptions<T>, bo w aplikacji konsolowej to zwykle wystarcza. Warto jednak wiedzieć, że framework oferuje trzy warianty i każdy ma inne zachowanie.
IOptions<T> — singleton. Wartości są odczytywane raz, przy pierwszym rozwiązaniu w kontenerze, i od tego momentu nie zmieniają się przez całe życie aplikacji. Jeżeli zmienisz appsettings.json w trakcie działania procesu, IOptions<T> tego nie zauważy. W praktyce: najprostszy i w 90% przypadków dokładnie to, czego chcesz — zwłaszcza w konsolówce, która i tak startuje, robi robotę i kończy.
IOptionsSnapshot<T> — scoped. Wartości są odczytywane na początku każdego scope'a. W aplikacji webowej scope to jeden request, więc w ramach jednego żądania masz spójny snapshot konfiguracji, a kolejne żądanie może zobaczyć inne wartości, jeśli plik się zmienił. W aplikacji konsolowej IOptionsSnapshot ma sens tylko wtedy, gdy sam definiujesz scope'y (np. jeden scope na jeden rekord importowany do WordPressa).
IOptionsMonitor<T> — singleton z reaktywną aktualizacją. Oprócz .CurrentValue daje metodę .OnChange(listener), która strzela callback, gdy konfiguracja się zmieni. To wariant dla długo żyjących usług (worker service, background job, daemon), które muszą reagować na zmianę np. feature flag bez restartu.
W aplikacji konsolowej
IOptions<T>to domyślny wybór. PoIOptionsMonitorsięgaj tylko wtedy, gdy faktycznie potrzebujesz reagować na zmiany pliku w locie.
W naszym importerze IOptions<GoogleApiOptions> w zupełności wystarczy — aplikacja uruchamia się, robi batch importu i kończy. Nie ma sensu komplikować kodu obsługą hot-reloadu, którego nikt nigdy nie użyje.
Co dalej?
Od tego momentu masz już solidny szkielet. Trzy rzeczy, które warto teraz zrobić, zanim pojedziesz dalej:
-
Dorzuć
appsettings.Development.jsoni wpisz go do.gitignore. To tam trzymasz realny klucz.appsettings.jsonw repo zostawiasz z pustym stringiem albo placeholderemYOUR_KEY_HERE. Builder konfiguracji automatycznie zaciąga oba pliki, jeśli dopiszesz.AddJsonFile("appsettings.Development.json", optional: true). -
Przejrzyj
user-secrets. To narzędzie .NET CLI (dotnet user-secrets set GoogleApi:Key "AIzaSy...") przechowuje sekrety poza projektem, w profilu użytkownika. W zespołowej pracy jest lepsze niżappsettings.Development.json, bo nie ma ryzyka, że ktoś przypadkiem zakomituje plik. Ja w kursie nie używam user-secrets, żeby nie rozmywać tematu, ale w realnym projekcie to standard. -
Rozszerz strukturę Options o walidację.
services.AddOptions<GoogleApiOptions>().Bind(...).ValidateDataAnnotations().ValidateOnStart()potrafi rzucić wyjątek przy starcie aplikacji, jeśli klucz jest pusty. Taniej jest złapać błąd konfiguracyjny w trzeciej sekundzie uruchomienia niż pół godziny później, kiedy aplikacja stuka do Google i dostaje 401.
W kolejnym odcinku wracamy do kodu biznesowego — będziemy wykonywać realne zapytania do Google Places API, używając właśnie tego, co dzisiaj zbudowaliśmy. Zobaczysz, jak IOptions<GoogleApiOptions> wstrzykuje się do serwisu HTTP, i jak wygodnie jest mieć wszystkie parametry integracji w jednej klasie zamiast przekazywać je po całej aplikacji jako luzem stringi.
Kod z tego odcinka znajdziesz w repo: github.com/jarmatys/WPImporter — szukaj taga/brancha Lekcja-4. Jak zawsze — jeśli coś nie działa albo masz pytanie, walnij w komentarz pod filmem. Do zobaczenia w kolejnym odcinku.



