Na początku czerwca odbyła się największa konferencja programistyczna w Polsce, o czym pisałem tutaj.
A było tak…
Maskotka konferencji, złowieszczy TRex zwiastujący brak Internetów…
Prelekcje
Jeśli chodzi o kwestie merytoryczne to trochę szkoda, że nie było rzeczy związanych typowo z .NETem. Jednak prelekcje były naprawdę bardzo rozwijające i otwierające (zwłaszcza ta o NFT i Metaverse).
Ja tu się naprawdę uśmiecham! 😉
Większość wykładów była w języku angielskim, ale było też kilku prowadzących Polaków, którzy posługiwali się naszym rodzimym językiem. Więc nawet jeśli ktoś ma problem z angielskim (to powinien to nadrobić), mógł wyciągnąć sporo.
Strefa retro gier – tutaj Doom. Kieeeedyś to się strzelało…
Najbardziej podobały mi się dwie prelekcje, na które trafiłem:
pisanie własnego systemu operacyjnego – prowadzący naprawdę świetnie przedstawił temat i pokazał bardzo prosty system operacyjny. Aż mnie naszła ochota, żeby pisać własny… 😉
NFT i Metaverse – ta prelekcja była niesamowita. Prowadzący opowiadał o tym, jak to wszystko MOŻE wyglądać w przyszłości. Przedstawiał różne use case’y, które są możliwe zarówno w metaverse, jak i w świecie rzeczywistym z użyciem NFT. Chociaż całość zagadnienia jest na bardzo wczesnym poziomie, to ma to odmienić świat w taki sam sposób jak kiedyś Internet. Warto się tym zainteresować.
Jednym z prowadzących był Bjarne Stroustrup. Jak na gwiazdę przystało wystąpił na samym końcu. Jednak nie mogłem tak długo zostać i musiałem zwinąć się jakieś 2 godziny przed jego prelekcją. Trochę tego żałuję. Może następnym razem.
A w międzyczasie…
A, że nie samą nauką człowiek żyje, można było sobie pograć w retro gierki. Dostępnych dla nas było kilka starych maszyn jak C64, Atari ST, Pegasus, jakieś inne konsole Atari, ale i starsze pecety. Oczywiście zagrałem w kilka tytułów, które przypomniały mi smak dzieciństwa i jak świetnym rozwiązaniem był joystick 😉
Nieodzownym elementem była też strefa rekruterska. Niektóre stoiska oferowały dodatkowe atrakcje. Np. przy jednym robili popcorn, przy innym można było sobie napędzić rowerkiem prąd, który zasilił maszynę do wyciskania soku z pomarańczy.
Na szczęście rekruterki nie były nachalne i właściwie same czekały aż się do nich podejdzie. A zainteresowanie naprawdę było spore. Widziałem tylko większe firmy, ale też coś związanego z lotnictwem i nowymi technologiami.
Świetnym elementem była też strefa VR, gdzie można było się pobawić tym sprzętem. Naprawdę super sprawa.
Niestety, jak się okazało, nie jestem mistrzem w robieniu zdjęć. Obiecuję, że następnym razem zrobię więcej i ciekawszych 🙂
W prawdziwym świecie raczej nie usuwa się rekordów z bazy na stałe. Często są one tylko oznaczane jako „rekordy usunięte”. Później te rekordy można archiwizować lub faktycznie usunąć po jakimś czasie. Dlaczego tak się robi?
Czasem są to powody biznesowe, a czasem nawet prawne. Wyobraź sobie chociażby sytuację, w której tworzysz forum internetowe. Ktoś napisał bardzo obraźliwy komentarz lub nawet komuś groził, po czym komentarz został przez autora usunięty. Jednak sprawa została zgłoszona na policję. W tym momencie masz możliwość sprawdzenia czy i jaki komentarz został utworzony, a nawet kiedy został „oznaczony do usunięcia”.
To oczywiście prosty przykład, ale w świecie bardzo ważnych danych, które można usunąć jednym kliknięciem, takie usuwanie na stałe nie jest raczej pożądane. W końcu to użytkownik jest najgorszą częścią systemu, a my niejako musimy chronić ten system przed użytkownikami.
Mechanizm oznaczania rekordów jako usuniętych nazywa się Soft Delete (miękkie usuwanie).
Implementacja
Flaga w modelu
Implementacja soft delete w EfCore jest raczej prosta. Przede wszystkim musisz posiadać w swoim modelu bazodanowym jakieś pole, które będzie trzymało informację o tym, czy rekord jest usunięty, czy nie. Ja używam zawsze bazowej klasy do wszystkich modeli bazodanowych, która wygląda mniej więcej tak:
public abstract class DbItem
{
public Guid Id { get; set; }
public bool IsDeleted { get; set; }
}
Tutaj rolę tej flagi pełni oczywiście właściwość IsDeleted.
Pobieranie danych nieusuniętych
Następny krok to konfiguracja EfCore w taki sposób, żeby pobierać dane tylko rekordów nieusuniętych. Tak jakbyś do każdego zapytania dodał warunek WHERE isDeleted = 0.
Oczywiście można to zrobić automatycznie. Podczas konfiguracji modelu w metodzie OnModelCreating dodaj filtr, używając HasQueryFilter, np:
Ja do konfiguracji modeli używam interfejsu IEntityTypeConfiguration i bazowej klasy, więc jest to łatwiejsze. Opiszę to w innym artykule.
W każdym razie HasQueryFilter na modelu robi tyle, że do każdego zapytania dla tego modelu dodaje właśnie taki warunek WHERE.
Jeśli jednak będziesz chciał w jakimś konkretnym przypadku pobrać dane również oznaczone do usunięcia, możesz zignorować QueryFilters w zapytaniu Linq:
var data = dbContext.MyModels
.Where(x => x.Name == "blabla")
.IgnoreQueryFilters(); //to zignoruje wszystkie QueryFilters skonfigurowane na modelu
Modyfikacja danych zamiast usuwania
W ostatnim kroku trzeba troszkę zmienić działanie EfCore. Jeśli ktoś chce usunąć rekord, napisze:
Domyślnie EfCore utworzy zapytanie „DELETE”. Trzeba to zmienić w taki sposób, żeby usuwanie rekordu tak naprawdę modyfikowało flagę IsDeleted. Dlatego dodajemy zdarzenia do DbContext. Kod się sam opisuje:
public AppDbContext(DbContextOptions options)
:base(options)
{
SavingChanges += AppDbContext_SavingChanges; //dodajemy handlery do zdarzeń
SavedChanges += AppDbContext_SavedChanges;
SaveChangesFailed += AppDbContext_SaveChangesFailed;
}
private void AppDbContext_SaveChangesFailed(object sender, SaveChangesFailedEventArgs e)
{
ChangeTracker.AutoDetectChangesEnabled = true;
}
private void AppDbContext_SavedChanges(object sender, SavedChangesEventArgs e)
{
ChangeTracker.AutoDetectChangesEnabled = true;
}
//to się dzieje podczas dbContext.SaveChanges()
private void AppDbContext_SavingChanges(object sender, SavingChangesEventArgs e)
{
ChangeTracker.DetectChanges(); //wykrywamy zmiany w modelu
foreach(var entry in ChangeTracker.Entries()) //przechodzimy przez wszystkie zmienione modele
{
switch(entry.State)
{
case EntityState.Deleted: //jeśli model ma zostać usunięty
entry.CurrentValues[nameof(DbItem.IsDeleted)] = true; //zmień pole IsDeleted na true
entry.State = EntityState.Modified; //i oznacz go jako zmodyfikowany, a nie usunięty
break;
}
}
ChangeTracker.AutoDetectChangesEnabled = false;
}
Dzięki takiej operacji zmodyfikujesz rekord zamiast go usuwać.
Być może w pewnych sytuacjach będziesz chciał usunąć rekord za pomocą zapytania SQL. Tutaj już musisz sam pamiętać o tym, żeby zamiast DELETE zrobić UPDATE odpowiedniego pola.
To tyle. Jeśli czegoś nie zrozumiałeś, potrzebujesz dodatkowych wyjaśnień albo znalazłeś błąd w artykule, koniecznie podziel się w komentarzu 🙂
„Po co interfejsy, skoro mamy klasy po których można dziedziczyć” – wielu młodych programistów zadaje takie pytanie. Sam też kiedyś o to pytałem, nie rozumiejąc w ogóle istoty interfejsów. No bo po co używać tworu, który niczego nie robi i niczego nie potrafi?
Zrozumienie tego może zabrać trochę czasu. Przyjmuję wyzwanie i postaram Ci się to wszystko wyjaśnić w tym artykule.
Co to interfejs?
Prawdopodobnie już wiesz, że interfejs to taki twór, który niczego nie potrafi. Ma tylko nagłówki metod i tyle. W C# interfejs może mieć też zdefiniowane właściwości (a i od jakiegoś czasu nieco więcej…)
Interfejs a klasa abstrakcyjna
„Czym się różni interfejs od klasy abstrakcyjnej” – to częste pytanie, które pada w rozmowach o pracę dla juniorów, ale uwaga… też i dla seniorów. Okazuje się, że zbyt dużo programistów (co mnie osobiście bardzo szokuje) nie jest w stanie przedstawić jasnych różnic między interfejsem a klasą abstrakcyjną. Zacznijmy więc od podobieństw:
Podobieństwa:
nie można utworzyć instancji interfejsu ani instancji klasy abstrakcyjnej
klasa abstrakcyjna i interfejs zawierają nagłówki metod – bez ciał
I to właściwie tyle jeśli chodzi o podobieństwa. Różnic jest znacznie więcej:
Różnice:
klasa może dziedziczyć po jednej klasie abstrakcyjnej, ale może implementować wiele interfejsów
klasa abstrakcyjna może zawierać metody nieabstrakcyjne (z ciałami) – interfejs nie może*
klasa abstrakcyjna może zawierać pola i oprogramowane właściwości – interfejs nie może
klasa abstrakcyjna może dziedziczyć po innej klasie, a także implementować interfejsy; interfejs może jedynie rozszerzać inny interfejs
*UWAGA! Od C# 8.0 interfejs może zawierać domyślną implementację metod. Nie jest to jednak oczywiste działanie i nie zajmujemy się tym w tym artykule. Wspominam o tym z poczucia obowiązku.
Definiowanie interfejsu
W C# interfejs jest definiowany za pomocą słówka interface:
Utworzyliśmy sobie interfejs IFlyable. Konwencja mówi tak, że nazwy interfejsów zaczynają się literką I (i jak igła).
Interfejs sam w sobie może mieć (tak jak klasa) określoną widoczność. W tym przypadku IFlyable jest publiczny. Natomiast jego składniki nie mogą mieć określanych widoczności. Wszystkie właściwości i metody są publiczne.
Stara konwencja, która właściwie już nie obowiązuje (ale pomoże Ci zrozumieć interfejs), mówi że nazwa interfejsu powinna OPISYWAĆ cechę (kończyć się na -able). Np: IFlyable, ITalkable, IWalkable, IEnumerable…
Interfejs w roli opisywacza
Końcówka -able w nazwie interfejsu powinna dać Ci do myślenia… „Interfejs OPISUJE jakieś zachowanie.” – tak. Interfejs opisuje zachowanie, a właściwie cechę. Klasa, która implementuje dany interfejs, musi też utworzyć dane zachowanie.
Z pomocą przyjdzie przykład. Załóżmy, że tworzysz świat. I masz taką fantazję, że tworzysz organizmy żywe. Podzieliłeś je na jakieś grupy – ssaki, ptaki, gady, owady.
I utworzyłeś analogiczne klasy abstrakcyjne:
public abstract class Mammal
{
}
public abstract class Reptile
{
}
public abstract class Bird
{
}
itd. Każda z tych klas abstrakcyjnych ma jakieś elementy wspólne dla całej grupy. I teraz zaczynasz tworzyć sobie konkretnych osobników:
public class Human: Mammal
{
}
public class Bat: Mammal
{
}
public class Pigeon: Bird
{
}
public class Pingeuin: Bird
{
}
I już coś Ci przestaje działać. Dlaczego?
No spójrz. Pewnie na początku wyszedłeś z założenia, że ptaki latają. Okazuje się, że latanie nie jest domeną ptaków. Pingwin nie lata, kura nie lata, struś nie lata… Oczywiście większość ptaków jednak lata ale nie wszystkie. I co dalej? Mamy nietoperza. Lata, ale nie jest ptakiem. Jednak większość ssaków nie lata.
Skoro większość ssaków nie lata, klasa Mammal nie może w żaden sposób zaimplementować latania. Klasa Bird też nie może zaimplementować latania, ponieważ nie wszystkie ptaki latają. No a nie będziesz przecież latania implementował od nowa w każdym gatunku, no bo to jednak jakaś cecha grupy. A my nie chcemy dublować kodu.
Ok, idźmy dalej tą drogą i zróbmy latające ssaki i latające ptaki:
public abstract class Mammal
{
}
public abstract class FlyingMammal: Mammal
{
public abstract void Fly();
}
public class Human: Mammal
{
}
public class Bat: FlyingMammal
{
public override void Fly()
{
}
}
public abstract class Bird
{
}
public abstract class FlyingBird
{
public abstract void Fly();
}
public class Pigeon: FlyingBird
{
public override void Fly()
{
}
}
public class Pingeuin: Bird
{
}
Uff, udało się napisać abstrakcyjne klasy i nawet po nich dziedziczyć. Super.
I teraz przychodzi Ci napisać metodę, która w parametrze przyjmuje zwierzęta latające… BUM. Wszystko wybuchło…
public void StartFlying(FlyingBird bird)
{
bird.Fly();
}
public void StartFlying(FlyingMammal mammal)
{
mammal.Fly();
}
Zamiast jednej prostej metody, masz ich wiele (tyle, ile masz klas zwierząt latających). Sam widzisz, że nie tędy droga. I teraz na scenę wkraczają interfejsy. Wróćmy do naszego interfejsu, który napisaliśmy na początku:
Okazuje się, że interfejs pełni rolę cechy. W tym przypadku tą cechą jest latanie. Więc zmieńmy teraz nasze klasy w taki sposób, żeby pozbyć się latających klas abstrakcyjnych na rzecz interfejsu:
public abstract class Mammal
{
}
public class Human: Mammal
{
}
public class Bat: Mammal, IFlyable
{
public bool IsFlying { get; private set; }
public void Fly()
{
}
}
public abstract class Bird
{
}
public class Pigeon: Bird, IFlyable
{
public bool IsFlying { get; private set; }
public void Fly()
{
}
}
public class Pingeuin: Bird
{
}
I co się okazuje? Niektóre ssaki (ale nie wszystkie) potrafią latać. Niektóre ptaki (ale nie wszystkie) potrafią latać. I jak teraz będzie wyglądała metoda, która w parametrze przyjmuje latające zwierzę?
public void StartFlying(IFlyable f)
{
if (!f.IsFlying)
f.Fly();
}
Mamy tutaj polimorfizm w najczystszej postaci. Metoda StartFlying nie wie jakie zwierzę dostanie w parametrze. Wie natomiast, że to co dostanie – na pewno umie latać.
Skoro wiemy, że jednak większość ptaków lata, możemy w tym momencie pokusić się o stworzenie dodatkowej klasy – latające ptaki:
public abstract class FlyingBird: Bird, IFlyable
{
public bool IsFlying { get; private set; }
public void Fly()
{
}
}
Zauważ, że metoda StartFlying w ogóle się nie zmieni, ponieważ latające ptaki implementują interfejs IFlyable. Klasa FlyingBird jest taką trochę pomocniczą. Ona wie jak ptak powinien latać. A chyba wszystkie latające ptaki robią to w ten sam sposób.
Można napisać taką klasę, ponieważ większość ptaków lata. Jednak nie tworzyłbym klasy FlyingMammal, ponieważ latanie wśród ssaków jest wyjątkową cechą. Zatem zachowałbym ją dla konkretnych gatunków (chociaż to wymaga przemyślenia).
Zróbmy trochę bardziej śmieszny świat. Ssaki… Niektóre są żyworodne, niektóre są jajorodne (kolczatka). Jeśli chodzi o ptaki… to chyba wszystkie są jajorodne (chociaż aż tak się nie znam ;)). Ale widzisz, że tutaj żyworodność, czy też jajorodność nie jest domeną całej grupy, więc idealnie nadaje się na interfejs. Tak samo jak umiejętność pływania. Są ptaki, ssaki, owady i inne, które potrafią pływać, ale są też takie, które tego nie potrafią. Tak jak z lataniem.
Klasa abstrakcyjna zamiast interfejsu
Czasem możesz mieć taką pokusę, żeby zastosować klasę abstrakcyjną zamiast interfejsu. Zwłaszcza jak w przykładzie ze zwierzętami latającymi. I czasem będzie to dobre rozwiązanie. Jednak w powyższym przykładzie widzisz, że nie zadziała. Zarówno latające ptaki, jak i latające ssaki powinny dziedziczyć po takiej klasie – a to się nie da. W C# nie mamy wielodziedziczenia.
W niektórych językach (np. C++) jest możliwość dziedziczenia po wielu klasach. Wtedy takie rozwiązanie jest jak najbardziej ok. Ale nie w C#. Czy to ograniczenie? Może i tak. Ale takie samo jak to, które zabrania Ci jechać na czerwonym świetle. Wielodziedziczenie bardzo łatwo może stać się powodem problemów. Do tego stopnia, że niektórzy programiści uważają, że jeśli musisz dziedziczyć po wielu klasach, to coś pewnie zaprojektowałeś źle.
I dlatego mamy też interfejsy. Żeby zaimplementować cechy z różnych „światów”.
Interfejs i wstrzykiwanie zależności
Jeśli nie wiesz, czym jest wstrzykiwanie zależności, koniecznie przeczytaj ten artykuł.
Jeśli chodzi o DI, to interfejsy jakoś tak samoczynnie stały się standardem. Do obiektu nie wstrzykujemy klasy, tylko interfejs. Chociaż czasem wstrzykiwanie klasy abstrakcyjnej jest jak najbardziej ok. Jednak wstrzykiwanie interfejsu czasem jest po prostu szybsze do zaimplementowania. Załóżmy, że masz taką klasę:
public class ConsoleWriter
{
public void Write(string msg)
{
Console.WriteLine(msg);
}
}
I chcesz ją wstrzyknąć do innej klasy. Więc z ConsoleWriter wyekstrahujesz albo interfejs IWriter, albo klasę abstrakcyjną AbstractWriter. Interfejs w tym momencie jest zdecydowanie bardziej naturalnym podejściem. Daje Ci pewność, że w przyszłości niczego Ci nie popsuje. Interfejs żyje trochę z boku wszystkiego.
Jeśli wyekstrahowałbyś klasę abstrakcyjną, musiałbyś się zastanowić, czy to dobre rozwiązanie. Czy nagle nie pojawi się kiedyś potrzeba, żeby jakaś klasa dziedziczyła po takim writerze, np:
public class MySuperStringList: List<string>//, AbstractWriter???
{
}
Oczywiście nie jesteś w stanie przewidzieć przyszłości. Ale jesteś w stanie przewidzieć, że w DI klasa abstrakcyjna może w pewnym momencie coś zblokować. Natomiast interfejs nigdy niczego nie zablokuje.
Interfejs w roli pluginu / adaptera
W takim przypadku wybór interfejsu jest raczej jednoznaczny. Załóżmy, że piszesz odtwarzacz mp3. I chcesz, żeby taki odtwarzacz mógł być rozszerzany przez pluginy. W pierwszym kroku musisz jakoś zaprojektować taki plugin:
W taki sposób Twoja aplikacja będzie mogła poinformować pluginy, że piosenka się zaczęła lub skończyła. Pluginy będą implementować ten interfejs i odpowiednio reagować na zmiany. Np. może być plugin, który napisze post na FB o tym, że po raz piąty w tym dniu słuchasz tej samej piosenki. Może być inny plugin, który będzie zapisywał do bazy danych historie słuchanych przez Ciebie utworów i analizował ją. Itd.
Ty z poziomu swojej aplikacji mp3 musisz tylko wywołać metodę z interfejsu.
public void PlayMp3(string fileName)
{
//w jakiś sposób odtwórz piosenkę, a potem poinformuj pluginy
foreach(IMyMp3Plugin plugin in plugins)
{
plugin.Mp3Started(fileName);
}
}
No to tyle jeśli chodzi o interfejsy. Mam nadzieję, że wszystko udało mi się wyjaśnić. Jeśli jednak nadal czegoś nie rozumiesz lub też znalazłeś błąd w tekście, koniecznie daj znać w komentarzu.
Osoby niezdające sobie sprawy, jak pod kapeluszem działa HttpClient, często używają go źle. Ja sam, zaczynając używać go kilka lat temu, robiłem to źle – traktowałem go jako typową klasę IDisposable. Skoro utworzyłem, to muszę usunąć. W końcu ma metodę Dispose, a więc trzeba jej użyć. Jednak HttpClient jest nieco wyjątkowy pod tym względem i należy do niego podjeść troszkę inaczej.
Raz a dobrze!
HttpClient powinieneś stworzyć tylko raz na cały system. No, jeśli strzelasz do różnych serwerów, możesz utworzyć kilka klientów – po jednym na serwer. Oczywiście przy założeniu, że nie komunikujesz się z kilkuset różnymi serwerami, bo to zupełnie inna sprawa 😉
Przeglądając tutoriale, zbyt często widać taki kod:
using var client = new HttpClient();
To jest ZŁY sposób utworzenia HttpClient, a autorzy takich tutoriali nigdy (albo bardzo rzadko) o tym nie wspominają albo po prostu sami nie wiedzą.
Dlaczego nie mogę ciągle tworzyć i usuwać?
To jest akapit głównie dla ciekawskich.
Każdy request powoduje otwarcie nowego socketu. Spójrz na ten kod:
for(int i = 0; i < 10; i++)
{
using var httpClient = new HttpClient();
//puszczenie requestu
}
On utworzy 10 obiektów HttpClient, każdy z tych obiektów otworzy własny socket. Jednak, gdy zwolnimy obiekt HttpClient, socket nie zostanie od razu zamknięty. On będzie sobie czekał na jakieś zagubione pakiety (czas oczekiwania na zamknięcie socketu ustawia się w systemie). W końcu zostanie zwolniony, ale w efekcie możesz uzyskać coś, co nazywa się socket exhaustion (wyczerpanie socketów). Ilość socketów w systemie jest ograniczona i jeśli wciąż otwierasz nowe, to w końcu – jak to mawiał klasyk – nie będzie niczego.
Z drugiej strony, jeśli masz tylko jeden HttpClient (singleton), to tutaj pojawia się inny problem. Odświeżania DNSów. Raz utworzony HttpClient po prostu nie będzie widział odświeżenia DNSów.
Te problemy właściwie nie istnieją jeśli masz aplikację desktopową, którą użytkownik włącza na chwilę. Wtedy hulaj dusza, piekła nie ma. Ale nikt nie zagwarantuje Ci takiego używania Twojej apki. Poza tym, coraz więcej rzeczy przenosi się do Internetu. Wyobraź sobie teraz kontroler, który tworzy HttpClient przy żądaniu i pobiera jakieś dane z innego API. Tutaj katastrofa jest murowana.
Poprawne tworzenie HttpClient
Zarówno użytkownicy jak i Microsoft zorientowali się w pewnym momencie, że ten HttpClient nie jest idealny. Od jakiegoś czasu mamy dostęp do HttpClientFactory. Jak nazwa wskazuje jest to fabryka dla HttpClienta. I to przez nią powinniśmy sobie tego klienta tworzyć.
Ta fabryka naprawia oba opisane problemy. Robi to przez odpowiednie zarządzanie cyklem życia HttpMessageHandler, który to bezpośrednio jest odpowiedzialny za całe zamieszanie i jest składnikiem HttpClient.
Jest kilka możliwości utworzenia HttpClient za pomocą fabryki i wszystkie działają z Dependency Injection. Teraz je sobie omówimy. Zakładam, że wiesz czym jest dependency injection i jak z niego korzystać w .Net.
Podstawowe użycie
Podczas rejestracji serwisów, zarejestruj HttpClient w taki sposób:
services.AddHttpClient();
Następnie, w swoim serwisie, możesz pobrać HttpClient w taki sposób:
class MyService
{
IHttpClientFactory factory;
public MyService(IHttpClientFactory factory)
{
this.factory = factory;
}
public void Foo()
{
var httpClient = factory.CreateClient();
}
}
Przy wywołaniu CreateClient powstanie wprawdzie nowy HttpClient, ale może korzystać z istniejącego HttpMessageHandler’a, który odpowiada za wszystkie problemy. Fabryka jest na tyle mądra, że wie czy powinna stworzyć nowego handlera, czy posłużyć się już istniejącym.
Takie użycie świetnie nadaje się do refaktoru starego kodu, gdzie tworzenie HttpClient’a zastępujemy eleganckim CreateClient z fabryki.
Klient nazwany (named client)
Taki sposób tworzenia klienta wydaje się być dobrym pomysłem w momencie, gdy używasz różnych HttpClientów na różnych serwerach z różną konfiguracją. Możesz wtedy rozróżnić poszczególne „klasy” HttpClient po nazwie. Rejestracja może wyglądać tak:
services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
services.AddHttpClient("MyWebApi", httpClient =>
{
httpClient.BaseAddress = new Uri("https://example.com/api");
httpClient.RequestHeaders.Add("x-login-data", config["ApiKey"]);
});
Zarejestrowaliśmy tutaj dwóch klientów. Jeden, który będzie używany do połączenia z GitHubem i drugi do jakiegoś własnego API, które wymaga klucza do logowania.
A jak to pobrać?
class MyService
{
IHttpClientFactory factory;
public MyService(IHttpClientFactory factory)
{
this.factory = factory;
}
public void Foo()
{
var gitHubClient = factory.CreateClient("GitHub");
}
}
Analogicznie jak przy podstawowym użyciu. W metodzie CreateClient podajesz tylko nazwę klienta, którego chcesz utworzyć. Z każdym wywołaniem CreateClient idzie też kod z Twoją konfiguracją.
Klient typowany (typed client)
Jeśli Twoja aplikacja jest zorientowana serwisowo, możesz wstrzyknąć klienta bezpośrednio do serwisu. Skonfigurować go możesz zarówno w serwisie jak i podczas rejestracji.
Kod powie więcej. Rejestracja:
services.AddHttpClient<MyService>(client =>
{
client.BaseAddress = new Uri("https://api.services.com");
});
Taki klient zostanie wstrzyknięty do Twojego serwisu:
class MyService
{
private readonly HttpClient _client;
public MyService(HttpClient client)
{
_client = client;
}
}
Tutaj możesz dodatkowo klienta skonfigurować. HttpClient używany w taki sposób jest rejestrowany jako Transient.
Zabij tego HttpMessageHandler’a!
Jak już pisałem wcześniej, to właśnie HttpMessageHandler jest odpowiedzialny za całe zamieszanie. I to fabryka decyduje o tym, kiedy utworzyć nowego handlera, a kiedy wykorzystać istniejącego.
Jednak domyślna długość życia handlera jest określona na dwie minuty. Po tym czasie handler jest usuwany.
Jeśli pobierasz dane lub pliki większe niż 50 MB powinieneś sam je buforować zamiast korzystać z domyślnych mechanizmów. One mogę mocno obniżyć wydajność Twojej aplikacji. I wydawać by się mogło, że poniższy kod jest super:
Niestety nie jest. Przede wszystkim zajmuje taką ilość RAMu, jak wielki jest plik. RAM jest zajmowany na cały czas pobierania. Ponadto przy pliku testowym (około 1,7 GB) nie działa. Task, w którym wykonywał się ten kod w pewnym momencie po prostu rzucił wyjątek TaskCancelledException.
Co więcej w żaden sposób nie możesz wznowić takiego pobierania, czy też pokazać progressu. Jak więc pobierać duże pliki HttpClientem? W taki sposób (to nie jest jedyna słuszna koncepcja, ale powinieneś iść w tę stronę):
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Test", "1.0"));
var httpRequestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = uri
};
using var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead);
if (!httpResponseMessage.IsSuccessStatusCode)
return;
var fileSize = httpResponseMessage.Content.Headers.ContentLength;
using Stream sourceStream = await httpResponseMessage.Content.ReadAsStreamAsync();
using Stream destStream = File.Open("D:\\test.avi", FileMode.Create);
var buffer = new byte[8192];
ulong bytesRead = 0;
int bytesInBuffer = 0;
while((bytesInBuffer = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
bytesRead += (ulong)bytesInBuffer;
Downloaded = bytesRead;
await destStream.WriteAsync(buffer);
await Dispatcher.InvokeAsync(() =>
{
NotifyPropertyChanged(nameof(Progress));
NotifyPropertyChanged(nameof(Division));
});
}
W pierwszej linijce ustawiam przykładową nazwę UserAgenta. Na niektórych serwerach przy połączeniach SSL jest to wymagane.
Następnie wołam GET na adresie pliku (uri to dokładny adres pliku, np: https://example.com/files/big.avi).
Potem już czytam w pętli poszczególne bajty. To mi umożliwia pokazanie progressu pobierania pliku, a także wznowienie tego pobierania.
Możesz poeksperymentować z wielkością bufora. Jednak z moich testów wynika, że 8192 jest ok. Z jednej strony jego wielkość ma wpływ na szybkość pobierania danych. Z drugiej strony, jeśli bufor będzie zbyt duży, to może nie zdążyć się zapełnić w jednej iteracji i nie zyskasz na prędkości.
Koniec
No, to tyle co chciałem powiedzieć o HttpClient. To są bardzo ważne rzeczy, o których trzeba pamiętać. W głowie mam jeszcze jeden artykuł, ale to będą nieco bardziej… może nie tyle zaawansowane, co wysublimowane techniki korzystania z klienta.
Dzięki za przeczytanie artykułu. Jeśli znalazłeś w nim błąd lub czegoś nie zrozumiałeś, koniecznie podziel się w komentarzu.
Języki programowania – biblioteki, narzędzia i lepsze praktyki. Również dla .NET
Dane & AI – uczenie maszynowe, big data, analiza, sieci neuronowe i inne.
Oprócz merytoryki na najwyższym, światowym poziomie, podczas konferencji IT zapewniona zostanie przestrzeń do networkingu oraz relaksu, dodatkowe atrakcje, a także food truck zone.
Gdzie i kiedy?
Trójmiasto – 30 maj 2022, ul. Elektryków
Kraków – 1 czerwca 2022, ICE Kraków
Warszawa – 2 czerwca 2022, PGE Narodowy
Ja się wybieram na konferencję w Warszawie. Mam nadzieję, że się spotkamy i pogadamy 🙂
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