Transakcyjność w EfCore

Transakcyjność w EfCore

Wstęp

Transakcje są nieodłącznym elementem baz danych SQL. Z tego artykułu dowiesz się, po co się je stosuje, a także jak je obsłużyć w EfCore.

Ten artykuł ma jedynie w prosty sposób wyjaśnić czym jest transakcja, po co stosować i jak ją ogarniać w EfCore. O transakcyjności jako takiej napisano niejedną książkę, więc ramy prostego artykułu zdecydowanie nie pozwalają na wejście w temat głębiej. Osoby znające zagadnienie nie wyniosą z artykułu raczej niczego nowego (może poza tym, że w EfCore właściwie nie trzeba stosować transakcji – o tym niżej).

Czym jest transakcja w bazie danych?

Przedstawię Ci oklepany przykład przelewu bankowego. Jednak nie myl transakcji bankowej z transakcją bazodanową. To dwa różne pojęcia.

Co system musi zrobić, jeśli przelewam Ci pieniądze? Musi wykonać przynajmniej dwie operacje:

  • odjąć sumę przelewu z mojego konta
  • dodać sumę przelewu do Twojego konta

System przy tym musi cały czas pracować. Co się stanie, jeśli wykona tylko jedną operację, a potem serwer straci zasilanie albo po prostu, po ludzku się spali? Z mojego konta znikną pieniądze, a na Twoim się nie pojawią. Jak się przed czymś takim zabezpieczyć? Transakcją.

Transakcja powoduje to, że wszystkie zmiany (UPDATE, INSERT itd.) w niej zawarte wykonają się w całości albo nie wykonają się w ogóle. Czyli w powyższym przykładzie, jeśli sprzęt straci zasilanie po wykonaniu pierwszej instrukcji, nic złego się nie stanie. Na moim koncie wciąż będą pieniądze – transakcja się nie powiodła, więc została (zostanie) wycofana.

Jak to działa? O tym można napisać książkę, ponadto różne systemy bazodanowe obsługują transakcje na różne sposoby. Najważniejsze jest to, że transakcja zapewnia atomowość operacji – czyli wszystkie operacje wykonują się jako jedna – czyli albo wszystko zostanie zmienione, albo nic.

Rozpoczętą transakcję możesz zakończyć na dwa sposoby. Albo ją zaakceptować (COMMIT) albo wycofać (ROLLBACK).

Transakcja musi być szybka – to NIE jest backup danych.

Transakcje jawne i niejawne

W świecie SQL każde zapytanie INSERT, UPDATE, DELETE itd. wykonuje się w transakcji niejawnej. Jeśli robisz UPDATE, możesz zmienić kilka pól. A co jeśli w pewnym momencie, podczas tej operacji coś się stanie? Zmienisz tylko pół rekordu? No nie. Dlatego, że nad całością czuwa transakcja niejawna.

Gdzieś tam na niskim poziomie, gdy wykonujesz instrukcję SQL, najpierw system uruchamia transakcję i jeśli wszystko przejdzie ok, transakcja jest akceptowana.

Transakcja jawna to taka, którą Ty sam wywołujesz. Jak w powyższym przykładzie z przelewem.

Transakcje przyspieszają

Z powyższego akapitu wyłania się pewien wniosek. Jeśli masz do zrobienia 100 insertów, każdy z nich musi utworzyć nową transakcję, zmienić dane i zakończyć transakcję. Czyli tak naprawdę wykonywane są 3 operacje dla jednego insertu. To znaczy, że przy 100 insertach wykona się na niższym poziomie 300 operacji.

Jeśli jednak te wszystkie 100 insertów wykonasz w transakcji jawnej, wykonają się tylko 102 operacje – rozpoczęcie transakcji jawnej, wykonanie insertów (bez transakcji niejawnych, ponieważ transakcja już istnieje) no i zakończenie Twojej transakcji.

I faktycznie – transakcja tak użyta powoduje, że cała operacja wykona się szybciej.

Kiedy używać transakcji?

W tym momencie artykułu sam powinieneś być w stanie odpowiedzieć na to pytanie. Generalnie, podsumowując, transakcji używamy głównie w dwóch przypadkach:

  1. Gdy wykonujemy operacje w bazie danych, które w jakiś sposób są od siebie zależne. Tzn. jedna operacja bez drugiej nie ma sensu albo spowoduje błędy w danych. Przykład – przelew bankowy. Albo bardziej przyziemny – system fakturowy. Dodajesz rekord faktury do jednej tabeli, a do drugiej jej elementy. Faktura bez zawartości nie ma sensu. Poza tym, gdyby coś poszło nie tak, do bazy trafiło by tylko część zawartości faktury. Widzisz, że te dane muszą być spójne.
  2. Gdy wykonujemy operacje w pętli (nie muszą być ze sobą powiązane), wtedy transakcja może przyspieszyć całość.

Automatyczne transakcje w EfCore – czyli kiedy NIE używać jawnych transakcji

Metoda Save(Async) automatycznie robi wszystkie zmiany w transakcji. Czyli jeśli masz kod w stylu:

foreach(var item in list)
    _dbContext.Items.Add(item);

await _dbContext.SaveAsync();

to wszystkie zapytania wykonają się w transakcji. Metoda Add po prostu dodaje element do listy (i oznacza je jako dodane), ale w żaden sposób nie dotyka bazy danych. To dopiero robi SaveAsync. To tutaj jest tworzona transakcja i są wysyłane zapytania. Czyli nawet w sytuacji, w której dodajesz, usuwasz i modyfikujesz wiele rekordów, ale masz tylko jedno wywołanie SaveAsync – nie potrzebujesz transakcji – tą zapewnia Ci SaveAsync (chyba że masz naprawdę źle zrobioną konfigurację modeli, ale wtedy się raczej wszystko wywali).

No to teraz rodzi się pytanie – po co używać kilku SaveAsync?

Czasem nie ma innej możliwości. Np. masz dwa zależne od siebie rekordy, ale powiązane są jedynie za pomocą Id, np:

public class Parent
{
    public int Id { get; set; }
}

public class Child
{
    public int Id { get; set; }
    public int ParentId { get; set; }
}

W takim wypadku, jeśli chcesz dodać do bazy rekord Parent wraz z kilkoma Childami, musisz najpierw dodać Parenta, zrobić SaveAsync i dopiero w tym momencie dostaniesz jego Id. Po SaveAsync. W drugim zapytaniu możesz dodać Childa z konkretnym Id Parenta.

Tutaj pojawia się mała uwaga dygresyjna – w pewnych przypadkach Id rekordu możesz dostać już podczas dodawania do kolekcji, np: _dbContext.Parents.Add(parent) – jeśli masz własny generator Id lub Id jest tworzone z całkowitym pominięciem bazy danych.

Drugim powodem jest to, że nie zawsze masz wpływ na to, kiedy SaveAsync się wywoła. Spójrz na ten przykład z użyciem Identity:

IdentityUser user = new IdentityUser("test");
user.Email = "test@example.com";
await _userManager.CreateAsync(user);

Invoice inv = new Invoice();
inv.UserId = user.Id;
inv.Items.Add(new InvoiceItem());

_db.Invoices.Add(inv);

await _db.SaveChangesAsync();

Masz tutaj DWA wywołania SaveAsync. Jak to? Pierwsze jest ukryte gdzieś w środku CreateAsync z UserManagera. I pamiętaj, że wszystkie metody z UserManager lub RoleManager, które faktycznie wprowadzają jakąś zmianę, mają w pewnym miejscu wywołanie SaveAsync.

Transakcje w EfCore

Obsługa transakcji w EfCore jest banalnie prosta:

using(var tr = _dbContext.Database.BeginTransaction())
{
    try
    {
        _db.Dummies.Add(new Dummy { Name = "1" });
        await _db.SaveChangesAsync();          

        await tr.CommitAsync();
    }catch (Exception)
    {
        tr.Rollback();
    }
}

Na początku rozpoczynamy nową transakcję (przez właściwość Database z kontekstu EfCore), w której wykonujemy operacje na bazie danych, na koniec ją zatwierdzamy lub wycofujemy.

Musisz zwrócić uwagę na dwie rzeczy:

  • Transakcja implementuje IDisposable, to znaczy, że trzeba ją zniszczyć po użyciu. Najwygodniej używać jej wraz z using – tak jak w przykładzie powyżej. Staraj się tak działać.
  • Jeśli transakcja zostanie zniszczona, a wcześniej nie zostanie zaakceptowana, to automatycznie zostanie wycofana. Tzn., że powyższy kod można by zapisać równie dobrze w taki sposób:
using(var tr = _dbContext.Database.BeginTransaction())
{
    Dummy dummy = new Dummy { Name = "1" };
    _db.Dummies.Add(dummy);
    await _db.SaveChangesAsync();          

    await tr.CommitAsync();
 }

Działanie będzie dokładnie takie samo – transakcja zostanie wycofana, jeśli nie zostanie użyty Commit – czyli jeśli podczas dodawania rekordu do bazy danych wystąpi jakiś błąd.

Punkty kontrolne

EfCore w wersji 5 wprowadziło możliwość utworzenia punktu kontrolnego. Ale zadziała to tylko na połączeniach, które NIE używają Multiple Active Result Sets (popatrz w swoim connection stringu, czy masz ustawioną tą właściwość).

Punkty kontrolne czasem mogą się przydać, jeśli nie chcesz wycofać całej transakcji, np:

using(var tr = _db.Database.BeginTransaction())
{
    try
    {
        IdentityUser user = new IdentityUser("test");
        user.Email = "test@example.com";
        await _userMan.CreateAsync(user);

        tr.CreateSavepoint("BeforeInvoice");

        Invoice inv = new Invoice();
        inv.UserId = user.Id;
        inv.Items.Add(new InvoiceItem());

        _db.Invoices.Add(inv);

        await _db.SaveChangesAsync();

        await tr.CommitAsync();
    }
    catch (Exception)
    {
        tr.RollbackToSavepoint("BeforeInvoice");
    }                
}

Tutaj, jeśli nie powiedzie się dodanie faktury, transakcja zostanie wycofana do zdefiniowanego punktu kontrolnego – BeforeInvoice. Tzn., że użytkownik pozostanie w bazie. W tym momencie możesz spróbować jeszcze raz dodać faktury (lub wycofać całą transakcję). Kiedy to ma sens? Przykładowo, kiedy kilka osób pracuje na jakimś rekordzie i on aktualnie jest zablokowany. Po chwili może zostać odblokowany. To oczywiście może być dużo bardziej skomplikowane. Ale takie użycie save pointów samo się narzuca.

Ograniczenia

Musisz pamiętać o tym, że EfCore to jest ORM na dopalaczach. EfCore jest coraz mniej zależny od bazy danych, a właściwie od źródła danych. Niektóre źródła mogą nie obsługiwać transakcji i wtedy nawet dobry Boże nie pomoże. Wywołanie transakcji w takim przypadku może zakończyć się albo wyjątkiem, albo po prostu nic się nie stanie.

Na szczęście bazy danych SQL (MSSQL, MariaDb, MySQL, Oracle, SQLite itd) wspierają transakcje.


Dobrnęliśmy do końca. Dzięki za przeczytanie tego artykułu, jeśli był dla Ciebie pomocny, podziel się nim w swoich mediach społecznościowych.

Jeśli znalazłeś w artykule błąd lub czegoś nie rozumiesz, koniecznie daj znać 🙂

Obrazek wyróżniający: Biznes zdjęcie utworzone przez rawpixel.com – pl.freepik.com

Podziel się artykułem na:
Co się stało na Code Europe 2022?

Co się stało na Code Europe 2022?

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 🙂

Podziel się artykułem na:
Soft delete w EfCore

Soft delete w EfCore

Czyli jak oznaczyć rekord jako usunięty?

Wstęp

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:

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<MyDbModel>()
        .HasQueryFilter(x => x.IsDeleted == false);
}

Ten QueryFilter musisz dodać do każdego modelu.

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:

dbContext.MyModels.Remove(data);
dbContext.SaveChanges();

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 🙂

Obrazek dla artykułu dzięki rawpixel.com – pl.freepik.com

Podziel się artykułem na:
Po co te interfejsy?

Po co te interfejsy?

Wstęp

„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:

public interface IFlyable
{
    bool IsFlying { get; }
    void Fly();
}

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:

public interface IFlyable
{
    bool IsFlying { get; }
    void Fly();
}

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 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:

public interface IMyMp3Plugin
{
    void Mp3Started(string fileName);
    void Mp3Finished(string fileName);
}

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.

Podziel się artykułem na:
Jak poprawnie korzystać z HttpClient

Jak poprawnie korzystać z HttpClient

Wstęp

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.

Ale możesz mieć wpływ na czas jego życia:

services.AddHttpClient("c1", client =>
{
    client.BaseAddress = new Uri("http://api.c1.pl");
    client.DefaultRequestHeaders.Add("x-login", "asd");
}).SetHandlerLifetime(TimeSpan.FromMinutes(10));

Używając metody SetHandlerLifetime podczas konfiguracji, możesz określić maksymalny czas życia handlera.

Konfiguracja HttpMessageHandler

Czasem bywa tak, że musisz nieco skonfigurować tego handlera. Możesz to zrobić, używając metody ConfigurePrimaryHttpMessageHandler:

builder.Services.AddHttpClient("c1", client =>
{
    client.BaseAddress = new Uri("http://api.c1.pl");
    client.DefaultRequestHeaders.Add("x-login", "asd");
}).ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        PreAuthenticate = true,
        UseProxy = false
    };
};

Pobieranie dużych ilości danych

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:

byte[] fileBytes = await httpClient.GetByteArrayAsync(uri);
File.WriteAllBytes("D:\\test.avi", fileBytes);

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.

Podziel się artykułem na: