Category Archives: Uncategorized

PrimeHotel – dodanie Entity Framework Core 5 w .NET 5

Przyjrzyjmy się, jak wprowadzić Entity Framework Core 5 w projekcie ASP.NET Core w .NET 5. Zaczniemy od pustej bazy danych, w której mamy pełne pole do popisu i możemy dodawać tabele tak, jak chcemy.

W tym poście będziemy pracować z projektem PrimeHotel, który stworzyłem do celów edukacyjnych. Wszystko to jest dostępne w moim GitHub, więc możesz go pobrać bezpłatnie. Zobacz także ten post, aby dowiedzieć się jak uruchomić ten projekt: PrimeHotel – jak uruchomić projekt.

Czym jest Entity Framework Core

Entity Framework Core 5 to lekka, rozszerzalna, otwarta i wieloplatformowa wersja popularnej technologii dostępu do danych Entity Framework. EF Core jest obiektowym mapperem (O/RM), umożliwia programistom pracę z bazą danych przy użyciu obiektów .NET i eliminuje potrzebę korzystania z większości kodu dostępu do danych, który zwykle muszą pisać. Oznacza to koniec pisania komend w SQL. Sprawdza się doskonale w większości przypadków, jednak gdy trzeba pracować z dużą ilością danych, prawdopodobnie lepiej będzie napisać własne polecenia SQL.

Dodanie Entity Framework Core

Dodawanie EF Core 5 do projektu ASP.NET Core jest bardzo proste. Zacznij od instalacji pakietów NuGet:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.SqlServer

Po zakończeniu dodaj PrimeDbContext do folderu Models, który wyglądałby następująco:

    public class PrimeDbContext : DbContext
    {
        public PrimeDbContext(DbContextOptions<PrimeDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Room> Rooms { get; set; }
    }

Teraz musimy dodać klasę Room, która reprezentowałaby encję pokoju, z tabeli Rooms.

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

        public int Number { get; set; }

        public string Description { get; set; }

        public DateTime LastBooked { get; set; }

        public int Level { get; set; }

        public RoomType RoomType { get; set; }

        public int NumberOfPlacesToSleep { get; set; }
    }

    public enum RoomType
    {
        Standard,
        Suite
    }

Teraz zajmiemy się częścią konfiguracyjną. Użyjmy pustej bazy danych, która jest hostowana przez nas lokalnie. Najłatwiejszym sposobem jest zainstalowanie SQL Server w wersji Express i skonfigurowanie jej lokalnie. Możesz jednak skonfigurować serwer bazy danych w kontenerze docker. Sprawdź mój post, jak to zrobić: Set up a SQL Server in a docker container.

W pliku appsettings.json musimy wpisać nasz connection string. Powinien on wyglądać podobnie jak tutaj:

A teraz przejdźmy do pliku Startup.cs, gdzie musimy skonfigurować EF Core, aby używał naszego connection stringa. W metodzie ConfigureServices dodaj następujący wiersz:

    // Entity Framework
    services.AddDbContext<PrimeDbContext>(options =>
         options.UseSqlServer(Configuration.GetConnectionString("HotelDB")));

Zauważ, że użyłem nazwy HotelDB, która jest nazwą mojego connection stringa z pliku appsettings.json. To ważne, żeby te dwie wartości do siebie pasowały.

Wykonaliśmy już większość kluczowych rzeczy, ale potrzebujemy czegoś, co stworzyłoby tabele w bazie danych z naszego PrimeDbContext. Do tej pory mamy tylko jedną, ale nadszedł właściwy czas na wprowadzenie mechanizmów migracji.

Dodanie migracji w EF Core

Migracje w EF Core bazy danch pozwalają wprowadzać zmiany w bazie danych tak, aby była ona aktualna z kodem aplikacji, które ją używa. Jest to bardzo istotny mechanizm, ponieważ zmiany w strukturze bazy danych wprowadzane są dość często, nawet przez wielu programistów, więc potrzebujemy uniwersalnego mechanizmu, żeby te zmiany śledzić i wprowadzać. 

Dodawanie pierwszej migracji niewiele różni się od dodawania kolejnych. Musisz otworzyć okno terminala w lokalizacji projektu i wykonać polecenie:

dotnet ef migrations add InitMigration

Po pomyślnym wykonaniu tego polecenia zostanie wygenerowany plik migracji, w którym można sprawdzić, jakie zmiany zostaną zastosowane.

W tym pliku znajdziesz dwie metody: Up i Down. Reprezentują zmiany, kiedy migracja zostanie zastosowana i kiedy zostanie wycofana.

    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Rooms",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Number = table.Column<int>(nullable: false),
                    Description = table.Column<string>(nullable: true),
                    LastBooked = table.Column<DateTime>(nullable: false),
                    Level = table.Column<int>(nullable: false),
                    RoomType = table.Column<int>(nullable: false),
                    NumberOfPlacesToSleep = table.Column<int>(nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Rooms", x => x.Id);
                });
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "Rooms");
        }
    }

Ostatnią rzeczą do zrobienia jest uruchomienie aktualizacji bazy danych. Byłoby wspaniale, gdyby nasze zmiany były sprawdzane i stosowane przy każdym uruchomieniu projektu. Zobaczmy zatem, jak możemy to osiągnąć. Przede wszystkim przejdźmy do pliku Startup.cs i utwórzmy metodę.

    private void UpgradeDatabase(IApplicationBuilder app)
    {
        using (var serviceScope = app.ApplicationServices.CreateScope())
        {
            var context = serviceScope.ServiceProvider.GetService<PrimeDbContext>();
            if (context != null && context.Database != null)
            {
                context.Database.Migrate();
            }
        }
    }

Ta metoda wykorzysta wbudowany mechanizm wstrzykiwania zależności do pobrania instancji naszego PrimeDbContext i użycia jej do uruchomienia migracji bazy danych. Uruchomione zostaną tylko te, które nie zostały jeszcze zastosowane.

Pozostaje jeszcze jedna linia do dodania w metodzieConfigure, na samym dole.

    UpgradeDatabase(app);

Jeśli spojrzymy na bazę danych, zobaczymy, jakie migracje zostały zastosowane. Tabela __EFMigrationsHistory zostanie utworzona automatycznie przez Entity Framework Core.

Używanie EF Core 5

Gdy mamy już wszystko na miejscu, odpowiednią konfigurację i migracje baz danych, możemy zacząć korzystać z mechanizmu O/RM.

Wszystkie operacje na tabelach w EF Core muszą przejść przez PrimeDbContext. Korzystanie z niego jest bardzo proste, gdy zarejestrujemy go w klasie Startup.cs, będzie on dostępny w dowolnej klasie do wstrzyknięcia i użycia. Spójrz na ten przykład prostych operacji CRUD w RoomController.

    [ApiController]
    [Route("[controller]")]
    public class RoomController : ControllerBase
    {
        private readonly PrimeDbContext primeDbContext;

        public RoomController(PrimeDbContext _primeDbContext)
        {
            primeDbContext = _primeDbContext;
        }

        [HttpGet]
        public async Task<IEnumerable<Room>> Get()
        {
            return await primeDbContext.Rooms.ToListAsync();
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> Get(int id)
        {
            var room = await primeDbContext.Rooms.FindAsync(id);
            if (room == null)
            {
                return NotFound();
            }

            return Ok(room);
        }

        [HttpPost]
        public async Task<IActionResult> Post([FromBody] Room room)
        {
            var createdRoom = await primeDbContext.Rooms.AddAsync(room);
            await primeDbContext.SaveChangesAsync();

            return Ok(createdRoom.Entity);
        }

        [HttpPut]
        public async Task<IActionResult> Put([FromBody] Room room)
        {
            var existingRoom = await primeDbContext.Rooms.FindAsync(room.Id);
            if (existingRoom == null)
            {
                return NotFound();
            }

            existingRoom.Number = room.Number;
            existingRoom.Description = room.Description;
            existingRoom.LastBooked = room.LastBooked;
            existingRoom.Level = room.Level;
            existingRoom.RoomType = room.RoomType;
            existingRoom.NumberOfPlacesToSleep = room.NumberOfPlacesToSleep;

            var updatedRoom = primeDbContext.Update(existingRoom);
            await primeDbContext.SaveChangesAsync();
            return Ok(updatedRoom.Entity);
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            var existingRoom = await primeDbContext.Rooms.FindAsync(id);
            if (existingRoom == null)
            {
                return NotFound();
            }

            var removedRoom = primeDbContext.Rooms.Remove(existingRoom);
            await primeDbContext.SaveChangesAsync();

            return Ok(removedRoom.Entity);
        }
    }

Zauważ, że w EF Core każda metoda ma wersję asynchroniczną. Wykorzystanie asynchroniczności jest dobrym pomysłem. W ten sposób Twój kod będzie szybszy i może być uruchamiany wydajniej z wieloma innymi żądaniami równolegle.

Ważne rzeczy, o których należy pamiętać:

  • możemy wyszukiwać kolekcje encji w sposób, w jaki chcemy z LINQ, używając Where, Select i innych metod, które na koniec wygenerują SQL ze wszystkich tych warunków
  • Jeśli tylko filtrujesz encje do wyświetlenia, możesz użyć AsNoTracking() w celu poprawy wydajności
  • Wywołanie bazy danych zostanie wykonane dopiero wtedy, gdy kod, który piszemy, wymaga wyników. Dzieje się tak na przykład, gdy używamy ToListAsync
  • Wszystkie zmiany, które wprowadzamy, należy zapisać za pomocą SaveChangesAsync, aby je zapisać w bazie danych

To tylko kilka punktów, o których należy pamiętać, ale jest jeszcze wiele rzeczy dziejących się pod spodem, o których warto wiedzieć. To wprowadzenie wystarczy jednak na początek i jest więcej niż wystarczające, aby samodzielnie zacząć pracę z Entity Framework Core.

Podsumowanie

Schludnie i wygodnie! Entity Framework Core jest idealny do prawie każdego prostego użycia bazy danych. Jestem pewien, że uznasz jego możliwości za przydatne i intuicyjne.

Cały kod opublikowany w tym poście jest dostępny na moim GitHub, więc możesz go dowolnie ściągać i modyfikować. Zerknij także na post jak uruchomić projekt PrimeHotel: PrimeHotel – jak uruchomić projekt

Dzięki za przeczytanie, daj mi znać, jeśli podoba Ci się ten post 🙂

PrimeHotel – przekazywanie parametrów do akcji – zadania

To jest post z zadaniami do zrobienia, jeśli chcesz sprawdzić swoją wiedzę na temat ASP.NET Core w .NET 5. Ten wpis dotyczy przekazywania parametrów do akcji kontrolera, co jest niezwykle ważne do opanowania podczas tworzenia mikro-serwisów.

Nie musisz zaczynać od zera, możesz bazować na projekcie PrimeHotel, stworzonym specjalnie do celów edukacyjnych. Możesz pobrać go na mojej stronie GitHub. Zobacz także post dotyczący tego projektu: PrimeHotel – jak uruchomić projekt

Pełny artykuł na temat przekazywania parametrów do akcji w ASP.NET Core w .NET 5 można znaleźć tutaj: ASP.NET Core w .NET 5 – przekazywanie parametrów do akcji

Zadanie 1

Trudność: łatwa

Kontekst

Dowiedz się, jak pisać operacje CRUD. Powiedzmy, że chcielibyśmy obsługiwać faktury – dodaj wszystkie metody, które pozwolą na obsługę faktur.

Co musi być zrobione

  • utwórz nową klasę o nazwie Invoice
  • utwórz metody Add, Get, Update oraz Delete do obsługi faktur
  • lista faktur może być zakodowana na stałe w klasie kontrolera
  • dodaj opcjonalne filtrowanie w metodzie Get z parametrami ciągu zapytania

Wskazówka

Spójrz na WeatherForecastController i jak tam obsługujemy prognozy pogody.

Zadanie 2

Trudność: średnia

Kontekst

Dowiedz się, jak przekazać tablicę w ciągu zapytania. Ten sposób może być przydatny, gdy chcesz przekazać kolekcję obiektów, ale musisz użyć do tego ciągu zapytania

Co musi być zrobione

  • utwórz metodę GET
  • metoda musi zaakceptować tablicę liczb całkowitych z ciągu zapytania
  • jak wywołałbyś tę metodę i przekazał parametry?

Gdzie zamieszczać odpowiedzi?

Po prostu napisz komentarz do tego posta, spojrzę.

Lub, jeśli masz na to ochotę, zgłoś pull request do repozytorium PrimeHotel.

Powodzenia! 🙂

 

PrimeHotel – jak uruchomić projekt

W tym poście poprowadzę Cię przez proces pobierania i uruchamiania mojego projektu – PrimeHotel. Jest to projekt stworzony do celów edukacyjnych, dzięki czemu masz swobodę wprowadzania zmian, klonowania projektu i wykorzystywania go do wszelkich działań niekomercyjnych.

Jest to średniej wielkości serwis reprezentujący system do zarządzania hotelem. Przechowuje rezerwacje użytkowników, profile użytkowników i zapisuje je w bazie danych MS SQL. Może także połączyć się z zewnętrzną usługą pogodową, aby pobrać bieżącą pogodę. Z czasem pojawi się jeszcze więcej funkcji, które pokażą więcej integracji i możliwości platformy .NET, dobre praktyki i niesamowite możliwości.

Jeżeli dopiero zaczynasz swoją przygodę z .NET, zerknij proszę na ten post: .NET 5 – Jak zacząć. Pomoże Ci w skonfigurowaniu twojego środowiska pracy i pokrótce opowie jakie są możliwości .NET.

Pobieranie projektu

PrimeHotel to projekt .NET 5, który wykorzystuje ASP.NET Core i Entity Framework Core 5. Jest on hostowany na GitHub, dzięki czemu możesz przejść do tego linku: https://github.com/mikuam/PrimeHotel

Upewnij się, że masz zainstalowany Git na swoim komputerze. Następnie możesz sklonować projekt za pomocą polecenia w terminalu konsoli.

git clone https://github.com/mikuam/PrimeHotel.git

Teraz otwórz projekt w swoim ulubionym IDE, takim jak Visual Studio lub Visual Studio Code.

Korzystam z programu Visual Studio 2019 i na moim komputerze wygląda to tak.

Rzućmy teraz okiem na strukturę projektu:

  • Clients – tutaj są klasy komunikujące się z innymi usługami za pomocą HttpClient
  • Controllers – tutaj są wszystkie klasy kontrolerów zawierające wszystkie punkty końcowe w tej usłudze
  • Data – klasy repozytoriów, które zawierają polecenia SQL – obecnie Dapper
  • Migrations – migracje Entity Framework Core 5, które utrzymują zmiany schematu bazy danych
  • Models – klasy Entity Framework Core 5, które reprezentują tabele w bazie danych

Konfiguracja bazy danych

Do celów edukacyjnych najlepiej skonfigurować SQL Server na komputerze. Właśnie to robię. Jeśli jednak chcesz ustawić go jako obraz dokera, możesz przeczytać o nim mój post: Set up a SQL Server in a docker container.

Możesz pobrać wersję SQL Server Express za darmo ze strony Microsoft. Po zainstalowaniu i skonfigurowaniu serwera będzie on prawdopodobnie dostępny z adresem localhost. Spróbuj połączyć się z serwerem SQL Server i sprawdź, czy masz bazę danych PrimeHotel. Do łączenia używam Azure Data Studio – niesamowite i szybkie narzędzie, idealne do prostych zadań.

Jeżeli nie masz bazy PrimeHotel, to po prostu ją stwórz i pozostaw pustą.

Ostatnią częścią jest zmiana connection stringa w projekcie. Aby to zrobić przejdź do edycji pliku appsettings.json. Wygląda on następująco:

Connection string może się nieco różnić, jednak mój wygląda tak:

Data Source=localhost;Initial Catalog=PrimeHotel;Integrated Security=True

Zamiast członu localhost możesz mieć coś podobnego do localhost\SQLEXPRESS, jednak jaki dokładnie będzie adres zależy od konfiguracji SQL Servera.

Uruchomienie projektu

Kiedy wszystko jest już gotowe, możesz już uruchomić projekt. Pamiętaj, że potrzebujesz także mieć zainstalowane środowisko uruchomieniowe .NET 5(.NET 5 runtime) oraz pakiet SDK.

W Visual Studio możesz po prostu nacisnąć F5. W Visual Studio Code otwórz okno terminala i wpisz dwie komendy: dotnet build oraz dotnet run. Następnie przejdź w przeglądarce do linku, który wyświetli się na konsoli. Powinieneś zobaczyć coś takiego:

Proste, prawda? Gratulacje, właśnie uruchomiłeś PrimeHotel. Teraz możesz zacząć kodować i ulepszać ten serwis.

Powodzenia!

.NET 5 – jak zacząć

Chciałbyś nauczyć się programować w .NET i dowiedzieć się co potrafi platforma od Microsoft? Jakich narzędzi użyć i od czego zacząć? Dobrze trafiłeś! Wyjaśnię Ci wszystko krok po kroku.

Co to jest .NET?

.NET jest to platforma programistyczna stworzona przez Microsoft. A oto najważniejsze jej cechy:

  • można pisać w wielu językach: C#, F# oraz VB.NET
  • biblioteki napisane w różnych językach w .NET mogą ze sobą współpracować, ponieważ są kompilowane do kodu pośredniego IL
  • .NET 5 i technologie mu towarzyszące są otwarte, a ich źródła są dostępne na platformie GitHub 
  • w .NET 5 można budować aplikacje konsolowe, strony internetowe, API, gry, aplikacje na komórkę oraz na komputery stacjonarne
  • .NET jest ogromnie popularny, dlatego posiada wiele gotowych integracji z technologiami Amazon, czy Google, jednak najłatwiej będzie się nam pracowało z produktami Microsoft oraz chmurą Azure
  • program napisany w .NET 5 możemy bez problemu uruchomić na Windows, Linux oraz MacOS

Oczywiście te kilka punktów w żadnym stopniu nie wyczerpuje tematu, bo można by spokojnie napisać kilka książek do czego zdolny jest .NET, jednak na sam początek to szybkie podsumowanie wystarczy.

Co muszę zainstalować na początek?

Aby budować aplikacje musisz zainstalować pakiet narzędzi programistycznych, czyli SDK. Aby je uruchomić – pakiet uruchomieniowy.

Oba znajdziesz tutaj: https://dotnet.microsoft.com/download/dotnet/5.0

Jeżeli chodzi o Runtime, to wybierz to co potrzebujesz:

  • ASP.NET Core – jeżeli chcesz buować aplikacje webowe
  • Desktop – dla aplikacji desktopowych pod Windows
  • .NET Runtime – dla aplikacji konsolowych

Jeden z powyższych na początek zdecydowanie wystarczy. Ja polecałbym ASP.NET Core.

W czym pisać programy?

Podobno dobry programista i w notatniku sobie poradzi, ale myślę, że ta era już dawno się skończyła. Tutaj są tak naprawdę dwie opcje do wyboru.

Visual Studio

Ogromny i bardzo popularny edytor kodu. Jest to prawdziwy kombajn, jest świetnie zintegrowany ze wszystkimi technologiami Microsoft, zwłaszcza z tymi starszymi. Używam go od wielu lat i nie wyobrażam sobie pracy bez niego.

Najważniejsze cechy:

  • wygodny edytor, gdzie wiele rzeczy możemy wyklikać
  • wsparcie dla starszych technologii Microsoft
  • dostępny na Windows oraz MacOS
  • płatny, jednak istnieje wersja darmowa, okrojona – Community

Visual Studio Code

Prosty, wieloplatformowy edytor kodu, rozwijany open-source. Nie posiada tak wielu integracji jak Visual Studio, jednak jest lekki i dzięki darmowym rozszerzeniom, można go łatwo dostosować pod swoje potrzeby. Z pewnością jednak pracuje się na nim wygodnie z mniejszymi projektami oraz ze stronami internetowymi. Używam go w momencie kiedy pracuję np. z React.js

Najważniejsze cechy:

  • jest szybki i lekki
  • jest darmowy i łatwo można go dostosować
  • działa pod Windows, Linux oraz MacOS
  • integracja ze starszymi technologiami Microsoft nie jest najlepsza, jednak z .NET Core i .NET 5 działa świetnie

Pierwszy program

Kreator w Visual Studio

Najprostszym sposobem, aby stworzyć swój pierwszy program w .NET 5 jest użycie jednego z gotowców w Visual Studio. To środowisko oferuje nam wiele opcji do wyboru:

Najprostszy projekt, który możesz wybrać na początek to aplikacja konsolowa w języku C#. Jeżeli natomiast wolisz zacząć od projektu webowego, wybierz ASP.NET Core application.

 

Aby upewnić się, że używasz .NET 5, edytuj plik projektu i sprawdź jaka wartość jest wpisana w TargetFramework, powinna mieć wartość net5.0.

 

Aby uruchomić projekt, naciśnij po prostu F5. W tym momencie aplikacja konsolowa zostanie uruchomiona i pojawi się czarne okienko z napisem Hello World!.

Nowy projekt w Visual Studio Code

.NET Core jak i .NET 5 jest wydawany razem z .NET CLI, czyli wieloplatformowym zbiorem poleceń, który pozwala na tworzenie, budowanie i publikowanie projektów .NET. Ważnym słowem jest tutaj wieloplatformowość – to właśnie dzięki CLI możemy budować i uruchamiać programy w .NET 5 nie tylko na Windows, ale także na Linux i MacOS.

Stwórzmy zatem nowy projekt. Kiedy otworzymy Visual Studio Code, należy otworzyć nowy terminal i wpisać polecenie dotnet new --list, dzięki któremu zobaczymy jakie są obecnie dostępne gotowce projektów.

 Aby stworzyć aplikację konsolową o nazwie ConsoleApp2 należy wpisać polecenie dotnet new console -n ConsoleApp2

Świetnie, mamy już aplikację konsolową z pojedynczym plikiem w języku C# o nazwie Program.cs. Potrzebna nam będzie znajomość jeszcze dwóch komend:

  • dotnet build–  aby zbudować projekt
  • dotnet run – aby go uruchomić

Po kilku sekundach Twoim oczom ukaże się czarne okienko z programem.

Podsumowanie

Programowanie w .NET 5 można zacząć bardzo szybko i już po chwili pisać własne programy. Dodatkowo zadziałają one nie tylko na Windows, ale także na Linux i MacOS. Mogą także działać w chmurze, np. w kontenerach. Możliwości są naprawdę ogromne.

Jeżeli chciałbyś dowiedzieć się czegoś więcej o programowaniu w konsoli, zerknij na serię moich artykułów: https://www.michalbialecki.com/2018/05/25/how-to-make-you-console-app-look-cool/

Jeżeli zaczynasz swoją przygodę z .NET, to dobrze trafiłeś. Zasubskrybuj mój blog i dostawaj informacje o nowych postach – na pewno dowiesz się z nich czegoś ciekawego.

Pozdrawiam i powodzenia 🙂

ASP.NET Core in .NET 5 – wysyłanie żądania

Wysłanie żądania w programie ASP.NET Core w .NET 5 jest standardową operacją, którą można dość łatwo wykonać. Jednak szczegóły mają znaczenie w tym przypadku i pokażę Wam najlepsze praktyki. Przyjrzymy się również niektórym zaawansowanym funkcjom, aby uzyskać pełny ogląd sytuacji.

Żądania do prawdziwego API

W tym artykule będę korzystać z bezpłatnej usługi dostępnej w Internecie do pobierania prognoz pogody – http://weatherstack.com. Aby móc z niego korzystać, po prostu zarejestruj się na stronie internetowej i możesz go również użyć. W ciągu miesiąca dostępnych jest 1000 żądań dla darmowego konta, które powinno wystarczyć do zaspokojenia naszych potrzeb.

Najpierw rzućmy okiem na żądania, które zamierzamy wysyłać. Aby przetestować API, używam aplikacji Postman, która jest bardzo wydajna, ale intuicyjna. Gorąco zachęcam do przeczytania mojego artykułu na ten temat tutaj: Postman the right way

Oto jak wygląda żądanie pobrania aktualnej pogody w Poznaniu:

Jest to żądanie typu GET wysłane na adres http://api.weatherstack.com/current z dwoma parametrami:

  • access_key który otrzymasz kiedy zarejestrujesz konto
  • query które jest nazwą miasta

Odpowiedź, którą otrzymaliśmy to 200 OK z zawartości w formacie JSON.

 Żeby wykonać to żądanie w kodzie, zaimplementuję klasę WeatherStackClient.

using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace PrimeHotel.Web.Clients
{
    public class WeatherStackClient : IWeatherStackClient
    {
        private const string AccessKey = "3a1223ae4a4e14277e657f6729cfbdef";
        private const string WeatherStackUrl = "http://api.weatherstack.com/current";

        private HttpClient _client;
        private readonly ILogger<WeatherStackClient> _logger;

        public WeatherStackClient(HttpClient client, ILogger<WeatherStackClient> logger)
        {
            _client = client;
            _logger = logger;
        }
    }
}

Kilka rzeczy w tym kodzie warto wyjaśnić:

  • AccessKey jest na razie wpisany na sztywno, ale w prawdziwym produkcyjnym API powinien być pobrany z pliku konfiguracyjnego
  • IWeatherStackClient interfejs, który został dodany, aby wykorzystać go przy wstrzykiwaniu zależności (Dependency Injection)
  • HttpClient jest przekazywany przez konstruktor. Będzie on automatycznie stworzony i zarządzany przez framework

A teraz utwórzmy kod odpowiedzialny za logikę:

    public async Task<WeatherStackResponse> GetCurrentWeather(string city)
    {
        try
        {
            using var responseStream = await _client.GetStreamAsync(GetWeatherStackUrl(city));
            var currentForecast = await JsonSerializer.DeserializeAsync<WeatherStackResponse>(responseStream);
            
            return currentForecast;
        }
        catch (Exception e)
        {
            _logger.LogError(e, $"Something went wrong when calling WeatherStack.com");
            return null;
        }
    }

    private string GetWeatherStackUrl(string city)
    {
        return WeatherStackUrl + "?"
                + "access_key=" + AccessKey
                + "&query=" + city;
    }

Przeanalizujmy ten kod po kolei i zobaczmy co się w nim dzieje:

  • _client.GetStreamAsync jest metodą asynchroniczną, której przekazujemy URL i zwraca treść odpowiedzi w postaci strumienia. Jest więcej metod do wyboru: GetAsync, PostAsync, PutAsync, PatchAsync, DeleteAsync aby wykonać wszystkie operacje CRUD. Jest także metoda GetStringAsync które działa podobnie do GetStreamAsync
  • GetWeatherStackUrl tworzy URL poprzez dodanie do siebie URL serwisu oraz parametrów
  • JsonSerializer.DeserializeAsync<WeatherStackResponse>(responseStream) deserializuje strumień danych do obiektu typu WeatherStackResponse

A klasa WeatherStackResponse wygląda następująco:

using System.Text.Json.Serialization;

namespace PrimeHotel.Web.Clients
{
    public class WeatherStackResponse
    {
        [JsonPropertyName("current")]
        public Current CurrentWeather { get; set; }

        public class Current
        {
            [JsonPropertyName("temperature")]
            public int Temperature { get; set; }

            [JsonPropertyName("weather_descriptions")]
            public string[] WeatherDescriptions { get; set; }

            [JsonPropertyName("wind_speed")]
            public int WindSpeed { get; set; }

            [JsonPropertyName("pressure")]
            public int Pressure { get; set; }

            [JsonPropertyName("humidity")]
            public int Humidity { get; set; }

            [JsonPropertyName("feelslike")]
            public int FeelsLike { get; set; }
        }
    }
}

Zauważ, że użyłem atrybutu JsonPropertyName, aby określić które elementy w JSON odpowiadają właściwościom w mojej klasie. A oto struktura, którą chcę zmapować.

Zauważ, że nie muszę mapować wszystkich właściwości, a jedynie te, których potrzebuję. Ostatnia rzeczy, czyli zarejestrowanie klasy WeatherStackClient w kontenerze IoC. Aby tego dokonać, muszę przejść do klasy Startup i w metodzie ConfigureServices dodać następującą linię.

services.AddHttpClient<IWeatherStackClient, WeatherStackClient>();

Używamy dedykowanej metody rejestracji klas używającycj HttpClient. Pod spodem korzystamy z IHttpClientFactory, który pomaga w utrzymaniu puli i żywotności klientów. Istnieje ograniczona liczba połączeń HTTP, która może być utrzymywana w jednym momencie, a jeśli utworzysz zbyt wielu klientów, którzy blokują połączenie, niektóre z twoich żądań nie zostaną zrealizowane. IHttpClientFactory dodaje także konfigurowalne środowisko logowanie (za pośrednictwem ILogger) dla wszystkich wysyłanych żądań. Podsumowując, wspomniany mechanizm ułatwia życie programiście i działa przy tym w sposób inteligentny.

Czy to działa? Tak! Odpowiedź została poprawnie odwzorowana na moją klasę i mogę ją zwrócić.

Zastanawiasz się może co zostało zalogowane podczas procesowania tego żądania? Sprawdźmy.

Jak wspomniałem wcześniej, IHttpClientFactory zapewnia również mechanizm logowania, dzięki czemu każde żądanie jest logowane. Tutaj widać, że zalogowano nie tylko adres, ale także metodę HTTP i czas wykonania. Może to być bardzo przydatne podczas debugowania.

Dodanie mechanizmu ponawiania próby

W świecie mikro-serwisów, każdy serwis może czasami mieć gorszy dzień. Dlatego musimy mieć mechanizm ponawiania prób dla usług, do których wysyłamy żądania, i wiemy, że od czasu do czasu zawodzą. W ASP.NET Core dla .NET 5 istnieje biblioteka zewnętrzna, ale zintegrowana we framework, właśnie w tym celu – to Polly. Polly to wszechstronna biblioteka do obsługi błędów dla platformy .NET. Pozwala programistom na płynne i bezpieczne tworzenie zasad, jak błędy mają być obsługiwane. Chodzi to o scenariusze takie jak: ponawianie próby, Circuit Breaker, Timeout, Bulkhead Isolation, oraz Fallback.

W naszym przypadku dodamy mechanizm ponownej próby, która wywoła usługę WeatherStack i ponowi próbę 3 razy po początkowej awarii. Dzięki Polly można łatwo ustawić wiele prób i opóźnień między nimi. Rzućmy okiem na przykład – jest to metoda w klasie Startup, w której konfigurujemy kontener DI.

    services.AddHttpClient<IWeatherStackClient, WeatherStackClient>()
        .AddTransientHttpErrorPolicy(
            p => p.WaitAndRetryAsync(new[]
            {
                TimeSpan.FromSeconds(1),
                TimeSpan.FromSeconds(5),
                TimeSpan.FromSeconds(10)
            }));

Za pomocą tego kodu ponowimy to samo żądanie z opóźnieniem 1, 5 i 10 sekund. Nie jest wymagany dodatkowy kod. Polly zrobi to wszystko za nas. Zobaczymy zalogowany błąd, że coś się nie powiedzie, ale dopiero gdy wszystkie próby zakończą się niepowodzeniem, otrzymamy wyjątek.

Dodanie cancellation token

Cancellation token to mechanizm, który może zatrzymać wykonywanie połączenia asynchronicznego. Powiedzmy, że nasze żądanie nie powinno zająć więcej niż 3 sekundy, ponieważ jeśli tak jest, to wiemy, że coś jest nie tak i nie ma sensu czekać.

Aby zaimplementować taki mechanizm, musimy utworzyć cancellation token i podać go podczas wysyłanie żądania w kliencie HTTP.

    public async Task<WeatherStackResponse> GetCurrentWeatherWithAuth(string city)
    {
        try
        {
            using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3));

            using var responseStream = await _client.GetStreamAsync(GetWeatherStackUrl(city), cancellationTokenSource.Token);
            var currentForecast = await JsonSerializer.DeserializeAsync<WeatherStackResponse>(responseStream);

            return currentForecast;
        }
        catch (TaskCanceledException ec)
        {
            _logger.LogError(ec, $"Call to WeatherStack.com took longer then 3 seconds and had timed out ");
            return null;
        }
        catch (Exception e)
        {
            _logger.LogError(e, $"Something went wrong when calling WeatherStack.com");
            return null;
        }
    }

Jeśli żądanie potrwa zbyt długo, otrzymamy wyjątek TaskCancelledException, który możemy wychwycić i zareagować na niego inaczej, niż w przypadku nieoczekiwanego wyjątku.

Dodanie autoryzacji

Podstawowa autoryzacja jest zdecydowanie najprostsza i jest jedną z najpopularniejszych. Chodzi o to, że każde żądanie do określonej usługi musi być autoryzowane, więc razem z naszą zawartością, musimy wysyłać także informacje autoryzacyjne. Przy podstawowej autoryzacji musimy przekazać użytkownika i hasło zakodowane jako ciąg znaków w formacie base64 i umieścić go w nagłówku żądania. Zobaczmy, jak można to osiągnąć.

    private const string ApiKey = "3a1223ae4a4e14277e657f6729cfbdef";
    private const string Username = "Mik";
    private const string Password = "****";

    public WeatherStackClient(HttpClient client, ILogger<WeatherStackClient> logger)
    {
        _client = client;
        _logger = logger;

        var authToken = Encoding.ASCII.GetBytes($"{Username}:{Password}");
        _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Basic",
            Convert.ToBase64String(authToken));
    }

W ten sposób każde połączenie z WeatherStackClientbędzie zawierało informacje o autoryzacji i nie będziemy musieli niczego dodawać podczas wysyłania żądań. Jedynym miejscem, w którym musimy umieścić dodatkowy kod, jest konstruktor.

Pamiętaj, że autoryzacja nie jest wymagana, aby wysyłać żądania do weatherstack.com i jest dodana tylko w celach demonstracyjnych.

Ten artykuł nie obejmował wszystkich możliwości IHttpClientFactory, więc jeśli chcesz dowiedzieć się więcej, po prostu przejdź do tego artykułu Microsoft.

Mam nadzieję, że post Ci się spodobał. Cały kod z tego posta znajdziesz na moim koncie na GitHub:

https://github.com/mikuam/PrimeHotel

 

ASP.Net Core in .NET 5 – przekazywanie parametrów do akcji

Przekazywanie parametrów do akcji jest istotną częścią budowania RESTful Web API. ASP.NET Core, który został wydany jako część .NET 5 oferuje wiele sposobów przekazywania parametrów do metod reprezentujących punkty końcowe. Zobaczmy, jakie one są.

Przekazywanie parametrów jako część URL

Podczas przekazywania parametru w adresie URL musisz zdefiniować routing, który zawierałby parametr. Spójrzmy na przykład:

    [Route("{daysForward}")]
    [HttpGet]
    public IActionResult Get(int daysForward)
    {
        var rng = new Random();
        return new JsonResult(new WeatherForecast
        {
            Date = DateTime.Now.AddDays(daysForward),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        });
    }

Ta metoda zwraca prognozę pogody dla jednego dnia w przyszłości. Parametr daysForward określa, ile dni z wyprzedzeniem należy zwrócić prognozę pogody. Zauważ, że daysForward jest częścią routingu, więc prawidłowy adres URL do tego punktu końcowego będzie wyglądał następująco:

GET: weatherForecast/3

Możemy również użyć atrybutu [FromRoute] przed deklaracją metody, ale domyślnie będzie również działał w ten sam sposób.

   public IActionResult Get([FromRoute] int daysForward)

Przekazywanie parametrów w parametrach żądania

Jest to bardzo powszechna metoda przekazywania dodatkowych parametrów, ponieważ nie wymaga od nas zmiany routingu, więc jest również kompatybilna wstecz. Kompatybilność jest ważna, w przypadku zmiany istniejącego rozwiązania.

Spójrzmy na inną metodę, która zwróciłaby kolekcję prognoz pogody z opcją sortowania.

[HttpGet]
    public IEnumerable<WeatherForecast> Get([FromQuery]bool sortByTemperature = false)
    {
        var rng = new Random();
        var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        });

        if (sortByTemperature)
        {
            forecasts = forecasts.OrderByDescending(f => f.TemperatureC);
        }

        return forecasts;
    }

W tym przykładzie przekazujemy parametr sortByTemperature, który jest opcjonalny. Zauważ, że używamy atrybutu [FromQuery], aby wskazać, że jest to zmienna pobrana z parametru zapytania. Adres URL tego punktu końcowego wygląda następująco:

GET: weatherForecast?sortByTemperature=true

Możesz także przekazać więcej parametrów rozdzielając je znakiem &:

GET: weatherForecast?key1=value1&key2=value2&key3=value3

Zwróć uwagę na to, że adres URL musi być odpowiednio zakodowany, aby działał poprawnie. Jeśli chcesz przekazać taki parametr:

https://www.michalbialecki.com/?name=Michał Białecki

To powinien być zakodowany jako:

https://www.michalbialecki.com/?name=Micha%C5%82%20Bia%C5%82ecki

Przekazywanie obiektu w parametrach żądania

Gdy podajesz wiele parametrów zapytania, warto traktować je jako obiekt. Spójrz na poniższy kod:

    // GET: weatherForecast/GetFiltered?SortByTemperature=true&City=Poznan
    [HttpGet("GetFiltered")]
    public IEnumerable<WeatherForecast> GetFiltered([FromQuery]WeatherForecastFilters filters)
    {
        var rng = new Random();
        var forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)],
            City = filters.City
        });
 
        if (filters.SortByTemperature)
        {
            forecasts = forecasts.OrderByDescending(f => f.TemperatureC);
        }
 
        return forecasts;
    }

Jak widać, przekazuję klasę WeatherForecastFilters jako parametr zapytania. Zauważ, że używam atrybutu [FromQuery] przed nazwą klasy.

Klasa WeatherForecastFilters wygląda następująco:

    public class WeatherForecastFilters
    {
        public bool SortByTemperature { get; set; }
 
        public string City { get; set; }
    }

Jest to standardowa klasa, bez żadnych dodatkowych atrybutów. Zróbmy teraz zapytanie na poniższy adres URL:

GET: /WeatherForecast/GetFiltered?SortByTemperature=true&City=Poznan

Odpowiedź będzie następująca:

Tak więc parametry mogą być przekazywane według ich nazw, nawet jeśli należą do klasy. Co więcej, są one dostępne tylko po ich nazwach, bez żadnego prefiksu klasy, a to oznacza, że wszystkie właściwości muszą mieć unikalne nazwy.

Oto zrzut ekranu z Visual Studio.

Obsługa parametrów zapytania jako właściwości klasy będzie działać, ale tylko wewnątrz tej klasy. Nie będzie działać z zagnieżdżonymi obiektami – ich właściwości nie będą mapowane. Jest to więc raczje zebranie parametrów w jeden obiekt, a nie przekazywanie skomplikowanego zagnieżdżonego obiektu. 

Przekazywanie parametrów w nagłówkach

Przekazywanie parametrów w nagłówkach żądania jest mniej popularne, ale również szeroko stosowane. Nie wyświetla się w adresie URL, więc jest mniej zauważalne przez użytkownika. Typowym scenariuszem przekazywania parametrów w nagłówku byłoby podanie parametrów autoryzacji lub identyfikatora żądania nadrzędnego, aby umożliwić jego śledzenie. Rzućmy okiem na ten przykład:

    [HttpPost]
    public IActionResult Post([FromHeader] string parentRequestId)
    {
        Console.WriteLine($"Got a header with parentRequestId: {parentRequestId}!");
        return new AcceptedResult();
    }

Aby wysłać żądanie POST, musimy skorzystać z jakiegoś narzędzia. Dobrym pomysłem jest wykorzystanie Postman-a:

W zakładce Headers można wprowadzić nagłówki żądania, w tym prypadku parentRequestId.

Przekazywanie parametrów w treści żądania

Najczęstszym sposobem przekazywania danych jest umieszczenie ich w treści żądania. Możemy dodać nagłówek Content-Type z wartością application/json i poinformować odbiorcę, jak interpretować tą treść. Rzućmy okiem na poniższy przykład:

    [HttpPost]
    public IActionResult Post([FromBody] WeatherForecast forecast)
    {
        Console.WriteLine($"Got a forecast for data: {forecast.Date}!");
        return new AcceptedResult();
    }

Używamy atrybutu [FromBody], aby wskazać, że prognoza zostanie pobrana z treści żądania. W ASP.NET Core w .NET 5 nie musimy deserializować treści żądania, aby przekształcić z typu JSON w obiekt WeatherForecast, będzie to zrobione za nas automatycznie. Aby wysłać żądanie POST, użyjmy ponownie Postman-a:

Pamiętaj, że rozmiar treści żądania jest ograniczony przez serwer. Może mieć dowolną maksymalną wartość od 1 MB do 2 GB. W ASP.NET Core 5 domyślny maksymalny rozmiar ciała żądania wynosi około 28 MB, ale można to zmienić. A co jeśli chciałbym wysłać większe pliki, ponad 2 GB? Aby to osiągnąć powinieneś rozważyć wysyłanie treści w postaci strumienia lub wysyłanie jej w częściach.

Przekazywanie parametrów w formularzu

Wysyłanie treści w formularzu nie jest zbyt często stosowane, ale jest to najlepsze rozwiązanie, jeśli chcesz np. przesłać plik. Rzućmy okiem na przykład:

    [HttpPost]
    public IActionResult SaveFile([FromForm] string fileName, [FromForm] IFormFile file)
    {
        Console.WriteLine($"Got a file with name: {fileName} and size: {file.Length}");
        return new AcceptedResult();
    }

Ta metoda tak naprawdę nie wysyła pliku, ale pomyślnie otrzymuje plik z żądania. Interfejs IFormFile służy specjalnie do obsługi pliku.

Wysyłając żądanie, musimy ustawić Content-Type na application/x-www-form-urlencoded i w zakładce Body, musimy wybrać plik:

Zobaczmy, co otrzymamy po zdebugowaniu tego kodu:

Plik został poprawnie odczytany. Ciekawostką jest to, że dzięki IFormFile otrzymujemy nie tylko dane binarne, ale także typ i nazwę pliku. Możesz więc zapytać, dlaczego wysyłam nazwę pliku osobno? Wynika to z tego, że możesz chcieć inaczej nazwać plik na serwerze, niż ten, który wysyłasz.

Mam nadzieję, że podobał Ci się ten post, możesz rzucić okiem na kod opublikowany tutaj na moim Github:

https://github.com/mikuam/PrimeHotel

Entity Framework Core 5 vs SQLBulkCopy

Entity Framework Core 5 to świetny ORM do używania i łączenia się z bazą danych. Jest łatwy w użyciu i łatwy do zrozumienia. Oferuje wszystko co potrzebne aby poradzić sobie z większością wyzwań programistycznych. A co z wstawieniem dużej ilości danych za jednym razem? Czy byłoby to wystarczająco szybkie?

Zerknijmy na kod

Jako przykładu użyję prostej encji – Profile oraz repozytorium PrimeHotel dostępne na moim koncie na GitHub

Mój DbContext jest bardzo prosty i wygląda tak:

    public class PrimeDbContext : DbContext
    {
        public PrimeDbContext(DbContextOptions<PrimeDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<Room> Rooms { get; set; }

        public virtual DbSet<Profile> Profiles { get; set; }

        public virtual DbSet<Reservation> Reservations { get; set; }
    }

A encja Profile prezentuje się następująco:

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

        public string Ref { get; set; }

        public string Forename { get; set; }

        public string Surname { get; set; }

        public string TelNo { get; set; }

        public string Email { get; set; }

        public DateTime? DateOfBirth { get; set; }
    }

W tym przykładzie użyję aplikacji typu WebApi, aby w jak najprosztszy sposób wywołać kod. Aby to zrobić stworzyłem ProfileController.

    [ApiController]
    [Route("[controller]")]
    public class ProfileController : ControllerBase
    {
        private readonly PrimeDbContext primeDbContext;
        private readonly string connectionString;

        public ProfileController(PrimeDbContext _primeDbContext, IConfiguration _configuration)
        {
            connectionString = _configuration.GetConnectionString("HotelDB");
            primeDbContext = _primeDbContext;
        }
    }

Na razie jest dość pusty, ale będzie to dobra baza, od której możemy zacząć.

Stwórzmy zatem profile – dużo! 

Aby przetestować dodawanie wielu encji naraz, musimy wygenerować wiele danych testowych. Lubię mieć moje dane testowe zbliżone do prawdziwych jak to możliwe, więc aby je uzyskać, użyję pakietu NuGet Bogus.

Bogus to rozbudowany i bardzo łatwy w użyciu generator fałszywych danych. Wygeneruje losowe wartości, które pasują do danego kontekstu, takie jak nazwisko, wiek, adres, adres e-mail, nazwa firmy i tak dalej. Istnieją dziesiątki opcji. Idź i przekonaj się sam w dokumentacji

Generowanie dowolnej liczby profili, będzie wyglądało następująco:

    private IEnumerable<Profile> GenerateProfiles(int count)
    {
        var profileGenerator = new Faker<Profile>()
            .RuleFor(p => p.Ref, v => v.Person.UserName)
            .RuleFor(p => p.Forename, v => v.Person.FirstName)
            .RuleFor(p => p.Surname, v => v.Person.LastName)
            .RuleFor(p => p.Email, v => v.Person.Email)
            .RuleFor(p => p.TelNo, v => v.Person.Phone)
            .RuleFor(p => p.DateOfBirth, v => v.Person.DateOfBirth);

        return profileGenerator.Generate(count);
    }

Dodawanie profili z Entity Framework Core 5

Nie chcę wysyłać wszystkich tych profili w żądaniu, ponieważ byłaby to ogromna ilość danych. Przeniesienie tego do kontrolera i deserializacja po stronie ASP.NET Core 5 może trochę potrwać i nie jest to tak naprawdę część, którą chcę przetestować. Właśnie dlatego zdecydowałem się wygenerować moje profile w kontrolerze i wstawić je zaraz potem.

Kod całego rozwiązania jest naprawdę prosty:

    [HttpPost("GenerateAndInsert")]
    public async Task<IActionResult> GenerateAndInsert([FromBody] int count = 1000)
    {
        Stopwatch s = new Stopwatch();
        s.Start();

        var profiles = GenerateProfiles(count);
        var gererationTime = s.Elapsed.ToString();
        s.Restart();

        primeDbContext.Profiles.AddRange(profiles);
        var insertedCount = await primeDbContext.SaveChangesAsync();

        return Ok(new {
                inserted = insertedCount,
                generationTime = gererationTime,
                insertTime = s.Elapsed.ToString()
            });
    }

Dodatkowo dodałem klasę Stopwatch, aby zmierzyć, jak długo trwa generowanie profili, a także ich wstawianie. W końcu zwracam anonimowy typ, aby łatwo zwrócić więcej niż jeden wynik na raz.

Na koniec przetestujmy to. Wstawiając 1000 profili dostałem odpowiedź:

Nieźle, ale spróbujmy czegoś większego, np. 100000 profili:

 

Aż 25 sekund? Serio? Nie zwala z nóg.

Co w takim razie dzieje się pod spodem? Sprawdźmy przy użyciu SQL Server Profiler:

exec sp_executesql N'SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]);
MERGE [Profiles] USING (
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, 0),
(@p7, @p8, @p9, @p10, @p11, @p12, @p13, 1),
(@p14, @p15, @p16, @p17, @p18, @p19, @p20, 2),
(@p21, @p22, @p23, @p24, @p25, @p26, @p27, 3),
...
) AS i ([DateOfBirth], [Email], [Forename], [Ref], [ReservationId], [Surname], [TelNo], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([DateOfBirth], [Email], [Forename], [Ref], [ReservationId], [Surname], [TelNo])
VALUES (i.[DateOfBirth], i.[Email], i.[Forename], i.[Ref], i.[ReservationId], i.[Surname], i.[TelNo])
OUTPUT INSERTED.[Id], i._Position
INTO @inserted0;

SELECT [t].[Id] FROM [Profiles] t
INNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id])
ORDER BY [i].[_Position];

',N'@p0 datetime2(7),
@p1 nvarchar(4000),
@p2 nvarchar(4000),
@p3 nvarchar(4000),
@p4 int,
@p5 nvarchar(4000),
...
@p0='1995-02-22 09:40:44.0952799',
@p1=N'Sherri_Orn@gmail.com',
@p2=N'Sherri',
...

SqlBulkCopy na ratunek

SqlBulkCopy to klasa, która została wprowadzona jakiś czas temu, a dokładnie w .Net Framework 2.0 – 18 lat temu! SqlBulkCopy będzie działać tylko w celu zapisywania danych w bazie danych SQL Server, ale jego źródłem może być wszystko, o ile wyniki mogą być ładowane do DataTable lub odczytywane przez IDataReader.

A jak zastosować SqlBulkCopy do dodawania profili? Zerknijmy na kod.

    [HttpPost("GenerateAndInsertWithSqlCopy")]
    public async Task<IActionResult> GenerateAndInsertWithSqlCopy([FromBody] int count = 1000)
    {
        Stopwatch s = new Stopwatch();
        s.Start();

        var profiles = GenerateProfiles(count);
        var gererationTime = s.Elapsed.ToString();
        s.Restart();

        var dt = new DataTable();
        dt.Columns.Add("Id");
        dt.Columns.Add("Ref");
        dt.Columns.Add("Forename");
        dt.Columns.Add("Surname");
        dt.Columns.Add("Email");
        dt.Columns.Add("TelNo");
        dt.Columns.Add("DateOfBirth");

        foreach (var profile in profiles)
        {
            dt.Rows.Add(string.Empty, profile.Ref, profile.Forename, profile.Surname, profile.Email, profile.TelNo, profile.DateOfBirth);
        }

        using var sqlBulk = new SqlBulkCopy(connectionString);
        sqlBulk.DestinationTableName = "Profiles";
        await sqlBulk.WriteToServerAsync(dt);

        return Ok(new
        {
            inserted = dt.Rows.Count,
            generationTime = gererationTime,
            insertTime = s.Elapsed.ToString()
        });
    }

Najpierw musimy zdefiniować DataTable. Musi ona reprezentować tabelę profili, ponieważ jest to nasza tabela docelowa, której będziemy używać.

Następnie za pomocą metody WriteToServerAsync ładujemy profile do bazy danych. A co się dzieje od strony bazy danych? Zerknijmy na wyniki w SQL Server Profiler.

select @@trancount; 
SET FMTONLY ON select * from [Profiles] 
SET FMTONLY OFF exec ..sp_tablecollations_100 N'.[Profiles]'

insert bulk [Profiles] (
   [Ref] NVarChar(max) COLLATE SQL_Latin1_General_CP1_CI_AS, 
   [Forename] NVarChar(max) COLLATE SQL_Latin1_General_CP1_CI_AS, 
   [Surname] NVarChar(max) COLLATE SQL_Latin1_General_CP1_CI_AS, 
   [TelNo] NVarChar(max) COLLATE SQL_Latin1_General_CP1_CI_AS, 
   [Email] NVarChar(max) COLLATE SQL_Latin1_General_CP1_CI_AS, 
   [DateOfBirth] DateTime2(7))

Wygenerowanych SQL jest minimalny. Co w takim razie dzieje się pod spodem? Na StackOverflow znalazłem następującą odpowiedź:

SqlBulkCopy does not create a data file. It streams the data table directly from the .Net DataTable object to the server using the available communication protocol (Named Pipes, TCP/IP, etc…) and insert the data to the destination table in bulk using the same technique used by BCP.

 W skrócie: SqlBulkCopy streamuje dane do servera i wpisuje je do wskazanej tabeli. Dwa zupełnie inne podejścia. A jak w tym przypadku wygląda wydajność? Porównajmy oba podejścia.

Wydajność

Wynik porównania może być dużym zaskoczeniem. SqlBulkCopy jest stworzone do szybkiego wstawiania danych do SQL Servera i jest przy tym niewiarygodnie wydajne. 

Duże różnice zaczynają się pojawiać, gdy wstawia się jednocześnie ponad 10 tysięcy encji. Powyżej tej liczby może być konieczne ponowne zaimplementowanie kodu w celu użycia SqlBulkCopy zamiast Entity Framework Core 5.

A co z innymi operacjami?

Jeśli chodzi o obsługę dużych ilości danych, sprawy zaczynają być nieco trudniejsze. Warto zapoznać się z ulepszeniami po stronie bazy danych i danymi, na których faktycznie trzeba operować. Pamiętaj, że operacje na dużych porcjach danych są znacznie szybsze, gdy są wykonywane po stronie bazy danych.

Raz implementowałem zadanie, w którym musiałem czytać i aktualizować około miliona encji, raz dziennie. Aktualizacja po stronie .Net nie była wystarcająco wydajna, więc cały proces przeniosłem do bazy danych. Połączyłem kilka rzeczy i wyszło całkiem nieźle.

  • stworzenie tabeli tymczasowej, i.e. T1
  • przesłanie danych za pomocą SqlBulkCopy
  • wykonanie aktualizacji po stronie bazy danych
  • pobranie danych zwrotnych, których możesz potrzebować, np. do logowania
  • usunięcie tabeli tymczasowej

Zdaję sobie sprawę, że przenoszenie logiki biznesowej do bazy danych to anty wzorzec, ale jeśli ta operacja aktualizacji musi być szybka, musimy zdecydować się na pewne ustępstwa.

Cały cytowany kod znajdziedzi na moim GitHub.

Do zobaczenia!