Przez ostatnie dziewięć odcinków budowałeś moduły w izolacji — DbContext z EF, GooglePlacesService, WordpressAuthService, PostPublisher i SeleniumBot. Każdy z nich działał sam. W tym odcinku robisz najważniejszą rzecz w projekcie — zamykasz je w jedną klasę, która orkiestruje całe flow od początku do końca, i pierwszy raz odpalasz importera jako normalną aplikację. To moment, w którym projekt przestaje być kursem i staje się działającym narzędziem.
Flow aplikacji — jak to się spina
Zanim napiszesz linijkę orkiestratora, musisz mieć w głowie sekwencję kroków. Całe flow da się zapisać w pięciu punktach:
- Pobranie firm z bazy danych — z tabeli
Companieswyciągasz listę rekordów, które mają zostać przeprocesowane. To źródło prawdy o tym, co importujesz. - Odpytanie Google Places — dla każdej firmy po
namewyciągaszplace_id, a następnie szczegóły: średnia ocena, liczba recenzji, pełna lista opinii z autorami i ratingami. - Publikacja posta na WordPress — składasz
WpPostRequestz tytułem (nazwa firmy) i contentem (opis + dane z Google) i strzelasz do REST API WordPressa. Ten zwraca ci URL nowo utworzonego ogłoszenia. - Dodanie komentarzy przez Selenium — bot dostaje URL i iteruje po liście recenzji, dla każdej otwiera stronę, wypełnia formularz i wysyła. Tutaj już nie ma API — jest bot, bo tego wymaga layout motywu.
- Zapis powiązania w bazie — zapisujesz, że firmę
Xpołączyłeś z postemY, żeby przy kolejnym uruchomieniu nie zrobić duplikatu.
Widzisz, że w praktyce punkty 2–5 wykonują się w pętli po firmach z punktu 1. Każda iteracja to pełny przepływ przez cztery serwisy.
Jak masz cztery integracje zewnętrzne w jednym flow, to nie myśl, że napiszesz to bezbłędnie za pierwszym razem. Ty tego po prostu jeszcze nie wiesz, bo dopiero widzisz, jak to się zachowuje w praktyce.
Orchestrator — klasa Importer
Potrzebujesz jednej klasy, która trzyma te wszystkie zależności w jednym miejscu i odpala ich metody w dobrej kolejności. Nazywam ją Importer i od razu wyciągam interfejs IImporter, żeby można ją było trywialnie zarejestrować w DI i podmienić w testach.
public class Importer : IImporter
{
private readonly IGooglePlacesService _places;
private readonly ISeleniumBot _bot;
private readonly IPostPublisher _publisher;
private readonly ImporterDbContext _db;
public Importer(IGooglePlacesService places, ISeleniumBot bot, IPostPublisher publisher, ImporterDbContext db)
{
_places = places;
_bot = bot;
_publisher = publisher;
_db = db;
}
public async Task RunAsync(double lat, double lng, int radius, string type)
{
var places = await _places.FindNearby(lat, lng, radius, type);
foreach (var place in places)
{
if (await _db.Places.AnyAsync(p => p.GooglePlaceId == place.Id))
continue;
var extra = await _bot.ScrapeDetails(place.Url);
var postId = await _publisher.PublishAsync(new WpPostRequest
{
Title = place.Name,
Content = BuildContent(place, extra),
});
_db.Places.Add(new Place
{
GooglePlaceId = place.Id,
WordpressPostId = postId,
Name = place.Name,
});
}
await _db.SaveChangesAsync();
}
}
Spójrz na konstruktor. Importer ma cztery zależności — IGooglePlacesService, ISeleniumBot, IPostPublisher i ImporterDbContext.
Klasa ma cztery zależności — to dużo, ale dalej sensowne, bo każda z nich to osobna odpowiedzialność. Gdyby było ich siedem, to byłby sygnał, że
Importerrobi za dużo i trzeba to rozbić.
Sama metoda RunAsync jest specjalnie napisana tak, żeby czytało się ją jak scenariusz. Pobierasz miejsca, iterujesz, dla każdego robisz skanowanie duplikatów w bazie (AnyAsync po GooglePlaceId), dociągasz szczegóły przez bota, publikujesz post na WP i dopisujesz rekord do lokalnej bazy. Na końcu jeden SaveChangesAsync zamyka transakcję — to celowe, bo albo przejdzie cała partia, albo żadna, i wtedy masz spójny stan.
W praktyce, poza tym co jest w snippecie, dodajesz jeszcze Console.WriteLine na każdym kroku, żeby wiedzieć, w którym momencie flow jesteś. To nie jest docelowe rozwiązanie — docelowo zamienisz to na ILogger — ale na etapie pierwszych testów przy zepsutym integracjach nie ma lepszego narzędzia niż zwykły console log.
Licznik iteracji — dlaczego to ma znaczenie
Jest jeden detal, który łatwo pominąć, a który w praktyce decyduje o tym, czy pod koniec miesiąca dostaniesz rachunek na 50 zł, czy na 5000 zł.
Google Places API jest płatne. Każdy GetDetails to pieniądze. Jeżeli w bazie masz 60 tysięcy firm (a tyle realnie miałem w tym projekcie), to odpalenie pełnego importu na raz wygeneruje rachunek, który zaboli. Dlatego w pętli trzymasz zmienną iteration, którą inkrementujesz przy każdym pełnym przejściu, i jeżeli przekroczy ustalony limit — robisz break.
Na potrzeby testów limit ustawiam na 1. Jedna firma, jeden post, pełne flow. Kiedy będę przetwarzał batch — podniosę go do 100 albo 500, ale nigdy nie zdejmuję tego bezpiecznika.
To jest wzorzec, który powinieneś mieć w głowie zawsze, kiedy piszesz kod dotykający płatnych API — zanim puścisz pętlę, zadaj sobie pytanie, co się stanie, jeżeli coś pójdzie nie tak i odpalisz to trzy razy. Z limitem: nic. Bez limitu: faktura.
DI i konfiguracja Host
Cała magia spinania zależności siedzi w Program.cs, w builderze hosta. Rejestrujesz tam każdy serwis jako AddScoped lub AddSingleton (w zależności od charakteru — HttpClient scoped, konfiguracja singleton), ImporterDbContext jako AddDbContext z connection stringiem z appsettings.json, a na końcu dodajesz samego Importera przez AddScoped<IImporter, Importer>().
W Main tworzysz scope, wyciągasz IImporter przez GetRequiredService i wołasz RunAsync. To tyle — DI załatwia za ciebie wszystkie zależności zejście w dół: kiedy tworzy Importer, automatycznie tworzy GooglePlacesService z jego HttpClientem i IOptions, PostPublisher z jego IOptions, SeleniumBot z driverem Chrome i ImporterDbContext z connection stringiem. Ty tylko prosisz o IImporter — reszta dzieje się sama.
Jeżeli na tym etapie kursu DI wydaje ci się nadmiarem, to wróć do niego za rok. Przy takich projektach, gdzie masz cztery integracje i konfigurację, DI nie jest ozdobnikiem — jest jedynym rozsądnym sposobem, żeby klasy nie wiedziały o sobie nawzajem.
Pierwsze uruchomienie end-to-end
Startuję z buildera, stawiam breakpoint na pierwszej linijce RunAsync i odpalam.
Pierwszy krok — z bazy wyciąga się lista 60 firm. Limit ustawiony na 1, więc przetworzy się tylko pierwsza: Skandia Construction Sp. z o.o.
Drugi krok — GooglePlacesService strzela po place_id. Dostaję go, strzela dalej po detale. W debuggerze widzę, że rating dla firmy to 3.29, liczba recenzji — 5. Pięć obiektów w tablicy, każdy z autorem, treścią i oceną. Super.
Trzeci krok — buduję WpPostRequest, tytuł to nazwa z Google, content to sklejony opis z danymi o średniej ocenie. Wysyłam do WordPressa. Zwraca mi URL nowo utworzonego ogłoszenia. W panelu widzę: ogłoszenie jest, dodane przez konto app-user, dokładnie tak jak chciałem (nie przez admina).
Czwarty krok — SeleniumBot dostaje ten URL i iteruje po pięciu recenzjach. Dla każdej: wchodzi na stronę, wypełnia formularz, klika „Dodaj opinię", zamyka okno. Widzę to na żywo w przeglądarce — hipnotyzujące patrzeć, jak bot sam wypełnia pola.
Po przetworzeniu wszystkich pięciu komentarzy w WordPressie widać trzy z pięciu — dwa czekają na moderację (pierwsza opinia od nowego autora musi być zatwierdzona). Licznik iteracji skacze na 1, break się wykonuje, SaveChangesAsync zapisuje powiązanie i aplikacja kończy pracę.
Przeszedłeś całe flow. Od zapytania do Google po post na WordPressie z komentarzami — w pełni automatycznie, jednym uruchomieniem.
Co dalej?
Ten odcinek był kamieniem milowym, ale kończy się z listą TODO, którą w kolejnych odcinkach trzeba posprzątać:
- Anonimizacja autorów — teraz w komentarzu leci pełne imię i nazwisko z Google. Trzeba zrobić serwis, który zamieni
Kamil NowaknaKamil N., bo bez tego można mieć problem z RODO. - Headless Selenium — w kolejnym odcinku pokażę, jak przełączyć bota na tryb bez otwierania przeglądarki (opcja, którą skonfigurowałem już w #9). Do testów wygodnie widzieć, co bot robi, ale produkcyjnie nikt nie chce mieć dwudziestu Chromów na serwerze.
- Obsługa błędów —
try-catchjest w tym kodzie nieobecny. Jak Google zwróci 429, jak WordPress zwróci 401, jak Selenium padnie na timeoucie — w obecnej wersji cała partia się wywali. To załatwimy w odcinku #14. - Bardziej zaawansowane operacje na bazie — na razie
AnyAsynciAddwystarczają, ale przy większej skali trzeba zająć się batch insertami i transakcjami per firma.
Jeśli masz pytania o orkiestrację, architekturę albo DI w aplikacjach konsolowych — zostaw komentarz pod filmem albo odezwij się przez armatys.me. W kolejnym odcinku bierzemy się za zaawansowane operacje w integracji z Google API.



