To ósmy odcinek serii, w której budujemy WP Importera — aplikację konsolową w .NET 6, która ściąga firmy z bazy, dogaduje się z Google Places API i publikuje gotowe ogłoszenia na WordPressie. Mamy już konfigurację bazy, dependency injection, moduł Google oraz mechanizm, który pobiera z WordPressa token JWT. Czas skleić to wszystko w całość i faktycznie wrzucić pierwszy wpis przez REST API, autoryzując się pobranym wcześniej Bearer tokenem.
Plan — serwis do publikacji
Zanim napiszesz jakąkolwiek linijkę, warto narysować sobie w głowie mały przepływ. Z bazy danych ciągniesz firmę. Na podstawie jej nazwy pytasz Google Places o szczegóły — dostajesz nazwę wizytówki, stronę internetową, adres. Następnie sklejasz z tego payload, który pasuje do kontraktu endpointu POST /wp-json/wp/v2/posts. Na końcu dokładasz nagłówek Authorization: Bearer <token> i strzelasz w WordPressa. W odpowiedzi dostajesz JSON z utworzonym zasobem, a z niego najczęściej interesuje Cię tylko jedno pole — id nowego wpisu.
Dlaczego to wszystko w osobnym serwisie, a nie w Program.cs? Bo Single Responsibility. Klasa PostPublisher ma robić jedną rzecz — wysłać wpis. Token i logowanie do WP to osobna odpowiedzialność — siedzi w IWordpressAuthService, który zrobiliśmy w poprzednim odcinku. PostPublisher tylko woła GetTokenAsync() i dostaje gotowego stringa.
Nie mieszaj autoryzacji z wysyłką treści. Jak zaczniesz w jednej klasie i pobierać token, i budować payload, i parsować response, to za trzy tygodnie nie będziesz wiedział, co gdzie zepsuć, kiedy WordPress zmieni wersję API.
Model payloadu
Najpierw prosta klasa DTO. WordPress w /wp/v2/posts oczekuje JSON-a z co najmniej trzema polami — title, content, status. Opcjonalnie doklejasz categories, tags, meta, excerpt czy featured_media. Na tym etapie zależy nam na publikacji minimalnego rekordu.
public class WpPostRequest
{
public string Title { get; set; } = default!;
public string Content { get; set; } = default!;
public string Status { get; set; } = "publish";
public int[]? Categories { get; set; }
}
Status publish u mnie jest hardcodowany — tak wynika z założenia biznesowego, bo importer dodaje od razu gotowe wpisy. U Ciebie może to być draft, jeśli wolisz jeszcze ręcznie przejrzeć efekt przed publikacją. Wystarczy podmienić default albo wystawić setter i ustawiać dynamicznie.
Teraz model odpowiedzi. Nie potrzebujemy odwzorowywać całego ciała, które zwraca WordPress — a zwraca go sporo, łącznie z URL-em, slugiem, datami, strukturą _links i całym polem content.rendered. Nas interesuje tylko id, więc robimy mały klasowy plaster:
public class WpPostResponse
{
public int Id { get; set; }
}
Serializer zignoruje resztę pól — o to właśnie chodzi. Dostaniesz ID, będziesz mógł zapisać je z powrotem do swojej bazy (kolumna wordpress_id) i przy kolejnym imporcie odfiltrować firmy, które już mają swój wpis.
Serwis PostPublisher
Samo wysłanie requestu w .NET 6 jest banalne — HttpClient, JsonContent.Create, SendAsync, ReadFromJsonAsync. Trudniejszy jest moment, w którym to wszystko składasz w jedną klasę i pamiętasz o nagłówku autoryzacyjnym.
public class PostPublisher : IPostPublisher
{
private readonly HttpClient _http;
private readonly IWordpressAuthService _auth;
public PostPublisher(HttpClient http, IWordpressAuthService auth)
{
_http = http;
_auth = auth;
}
public async Task<int> PublishAsync(WpPostRequest post)
{
var token = await _auth.GetTokenAsync();
var request = new HttpRequestMessage(HttpMethod.Post, "wp-json/wp/v2/posts")
{
Content = JsonContent.Create(post),
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<WpPostResponse>();
return created!.Id;
}
}
Kilka szczegółów, które łatwo przegapić. HttpClient wstrzykujemy przez konstruktor — skonfigurowany wcześniej przez AddHttpClient<IPostPublisher, PostPublisher>() w Program.cs, z ustawionym BaseAddress wskazującym na Twoją instalację WordPressa (u mnie to https://fotowoltaika.pl/, u Ciebie będzie coś innego). Dzięki temu w HttpRequestMessage podaję ścieżkę relatywną — wp-json/wp/v2/posts — a runtime sam dokleja resztę.
JsonContent.Create(post) serializuje obiekt do JSON-a i od razu ustawia Content-Type: application/json w request body. Nie musisz tego robić ręcznie przez StringContent i MediaTypeHeaderValue — jedna linia robi dobrze.
EnsureSuccessStatusCode to moja wygoda na etapie kursu. W produkcyjnym kodzie warto zrobić to ładniej — sprawdzić konkretne kody (201 Created, 401 Unauthorized, 400 Bad Request), zalogować body błędu, ewentualnie spróbować odświeżyć token, jeśli wygasł. Na razie jednak rzucamy wyjątkiem przy czymkolwiek innym niż 2xx i idziemy dalej.
Authorization header z tokenem
Tu siedzi cały sens tego odcinka. Bez nagłówka Authorization WordPress odpowie 401 i nie opublikuje niczego. Z tokenem wygląda to tak:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
AuthenticationHeaderValue to klasa z System.Net.Http.Headers. Pierwszy argument — schemat (Bearer), drugi — wartość. Zero ręcznego doklejania słowa „Bearer " przed tokenem, zero kombinowania z request.Headers.Add("Authorization", ...). Framework robi to za Ciebie.
Po wykonaniu request WordPress zobaczy, że zapytanie jest wykonane w imieniu konkretnego użytkownika — tego, dla którego pobrali śmy JWT w odcinku numer 7. Wszystkie operacje POST, PUT, DELETE pójdą z jego uprawnieniami. Jeśli user nie ma roli author/editor/administrator, WordPress odrzuci wpis z 403 Forbidden. Sprawdź to, zanim zaczniesz debugować godzinami.
Token JWT w nagłówku to standard. Jeżeli dzisiaj zrozumiesz schemat, zrozumiesz też integrację z dziesiątkami innych API — GitHub, Stripe, SendGrid, większość SaaS-ów działa tak samo.
Warto też wiedzieć, że HttpRequestMessage tworzysz na każde wywołanie osobno. Nie współdziel go między requestami — to obiekt jednorazowy. HttpClient odwrotnie: rób z niego singleton (albo typed client przez IHttpClientFactory), bo w przeciwnym wypadku w sandboxach produkcyjnych wyczerpiesz socket-y.
Składanie payloadu
Na koniec krótko o tym, jak sam WpPostRequest powstaje w Program.cs. Ciągniemy firmę z bazy, pytamy Google o PlaceDetails dla nazwy, a potem składamy obiekt:
var details = await _googlePlaces.GetDetailsAsync(listing.Name);
var post = new WpPostRequest
{
Title = details.Result.Name,
Content = $"Strona internetowa: {details.Result.Website}. ID w bazie: {listing.Id}.",
Categories = new[] { 5 } // ID kategorii w WP
};
var wpId = await _publisher.PublishAsync(post);
Zwróć uwagę na interpolację stringów z prefiksem $"..." — to najczystszy sposób budowania dynamicznych opisów, bez konkatenacji ze znakami +. W odcinku pokazuję, że podmieniam w opisie ID z bazy ({listing.Id}), żeby po publikacji móc w panelu WordPressa zweryfikować, czy faktycznie leci właściwy rekord. W produkcji zrobisz tu już prawdziwy opis firmy — z mapką, godzinami otwarcia, formularzem kontaktowym. Na razie chodzi o to, żeby dowieźć proof of concept.
Po uruchomieniu aplikacji pod debuggerem widzę w konsoli, że WP odpowiedział 201 Created, a z ciała wyciągam Id = 496. Loguję się do panelu WordPressa, wchodzę w Ogłoszenia — nowy wpis jest opublikowany, w treści widać stronę firmy i numer ID z bazy. Dokładnie o to chodziło.
Co dalej?
W kolejnych odcinkach rozbudujemy payload o resztę rzeczy, które normalnie idą na wizytówce — mapka z koordynatami, godziny otwarcia, kategorie niestandardowe, obraz wyróżniający. WordPress wystawia to przez meta oraz featured_media, więc zabawa będzie głównie w dobre zmapowanie pól. Wrzucimy też retry na poziomie HTTP (Polly) i logowanie body błędu, żeby 500-ki nie gubiły się bez śladu.
Jeżeli chcesz przejrzeć cały kod z odcinka — leży na github.com/jarmatys/WPImporter. Ściągnij, porównaj ze swoją wersją, sklonuj branch z tego odcinka. A jeśli masz pytania albo coś Ci nie zagrało — wal śmiało w komentarz pod filmem. Idziemy dalej.



