Składamy całość — od Google Places do posta WordPress

Łączysz DbContext, Google Places, Selenium i WordPressa w jeden worker. Pokazuję flow end-to-end, orkiestrator i pierwsze uruchomienie importera w praktyce.

JAJarosław Armatys 8 min czytania
Wersja wideo tego wpisu — obejrzyj na YouTube

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:

  1. Pobranie firm z bazy danych — z tabeli Companies wyciągasz listę rekordów, które mają zostać przeprocesowane. To źródło prawdy o tym, co importujesz.
  2. Odpytanie Google Places — dla każdej firmy po name wyciągasz place_id, a następnie szczegóły: średnia ocena, liczba recenzji, pełna lista opinii z autorami i ratingami.
  3. Publikacja posta na WordPress — składasz WpPostRequest z tytułem (nazwa firmy) i contentem (opis + dane z Google) i strzelasz do REST API WordPressa. Ten zwraca ci URL nowo utworzonego ogłoszenia.
  4. 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.
  5. Zapis powiązania w bazie — zapisujesz, że firmę X połączyłeś z postem Y, ż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 Importer robi 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 Nowak na Kamil 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-catch jest 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 AnyAsync i Add wystarczają, 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.

Tagi:#course-dotnet-wordpress#dotnet#architecture#integration

Masz pytanie do tego wpisu?

Napisz do mnie — chętnie porozmawiam o szczegółach technicznych, albo o tym jak zastosować to u Ciebie w projekcie.