Monthly Archives: May 2020

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 – sending a request

Sending a request in ASP.NET Core in .NET 5 is a standard operation that can be achieved pretty easily. However, details matter in this case and I’ll show you the best practice available. We will also take a look at some advanced features to get the full scope.

Using a real available API

In this article, I will be using 3rd party free service for fetching weather forecasts – http://weatherstack.com. To be able to use it, just register on their website and you can use it as well. 1000 requests in a month are available for a free account and that should be more than enough to fulfill our needs.

First, let’s have a look at the requests we are going to make. To test the API, I’m using a Postman app, which is very powerful, yet intuitive. I strongly encourage you to read my article about it here: Postman the right way

Here is how a request to fetch current weather in Poznań looks like:

This is a GET request to http://api.weatherstack.com/current with two parameters:

  • access_key which you get when registering on the website
  • query that can be a city name

The response that we got is a 200 OK with JSON content.

 To make this request, I’ll create a WeatherStackClient class.

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;
        }
    }
}

There are a few things to notice here:

  • AccessKey which is hardcoded for now, but in a real-life API should be moved to configuration
  • IWeatherStackClient interface that is introduced for Dependency Injection support
  • HttpClient class is passed in a constructor. It will be automatically created and maintained by the framework

Now let’s create the logic.

    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;
    }

Let’s go through this code and explain what’s going on:

  • _client.GetStreamAsync is an asynchronous method that takes a URL an returns a stream. There are more methods, like: GetAsync, PostAsync, PutAsync, PatchAsync, DeleteAsync for all CRUD operations. There is also GetStringAsync that serializes a response content to string – just like GetStreamAsync does
  • GetWeatherStackUrl is merging a service URL with query parameters, returning a full URL address
  • JsonSerializer.DeserializeAsync<WeatherStackResponse>(responseStream) deserializes a stream and format output as a WeatherStackResponse class

The WeatherStackResponse class looks like this:

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; }
        }
    }
}

Notice that I used JsonPropertyName attribute to identify what JSON property is each property matching. Here is the structure that we are going to map.

One last thing – we need to register our WeatherStackClient in a Dependency Injection container. In order to do so, we need to go to Startup class and add the following line in ConfigureServices method.

services.AddHttpClient<IWeatherStackClient, WeatherStackClient>();

We are using a dedicated method for registering classes using HttpClient. Underneath it’s using IHttpClientFactory that helps to maintain the pooling and lifetime of clients. You have a limited number of HTTP connections that you can maintain on your machine and if you create too much clients each blocking a connection, you will end up failing some of your requests. It also adds a configurable logging experience (via ILogger) for all requests sent through. All in all, it makes a developer’s life easier and allows you to do some smart stuff too.

Does it work? Yes it does! The response was correctly mapped into my class and I can return it.

Do you wonder what was logged when making this request? Let’s have a quick look.

As I mentioned earlier IHttpClientFactory also provides a logging mechanism, so that every request is logged. Here you can see that not only address was logged, but also HTTP method and time of execution. This can be pretty useful for debugging.

Adding a retry mechanism

In a micro-services world, every micro-service can have a bad day once in a while. Therefore, we need to have a retry mechanism for services we call and we know that they fail from time to time. In ASP.NET Core for .NET 5 there is a third-party library integrated just that purpose – it’s Polly. Polly is a comprehensive resilience and transient fault-handling library for .NET. It allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.

For our scenario let’s add a retry mechanism, that will call WeatherStack service and retry 3 times after an initial failure. With Polly, a number of retries and delays between then can be easily set. Let’s have a look at the example – it’s in the Startup method where we configure DI container.

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

With this code we will retry the same request after 1, 5, and 10 seconds delay. There is no additional code needed. Polly will do everything for us. We will see logs that something failed, only after all retries will fail, we will get the exception.

Adding a cancellation token

A cancellation token is a mechanism that can stop the execution of an async call. Let’s say that our request shouldn’t take more than 3 seconds, because if it does, we know that something isn’t right and there is no point to wait.

To implement that we need to create a cancellation token and provide that when making a call with an HTTP client.

    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;
        }
    }

If the request takes too long, we will receive a TaskCancelledException, which we can catch and react to it differently, that when getting unexpected exception.

Provide authorization

Basic authorization is definitely the simplest and one of the most popular ones used. The idea is that every request to the specific service needs to be authorized, so that along with our content, we need to send authorization info. With basic authorization, we need to pass a user and password encoded as base64 string and put in a request header. Let’s see how that can be accomplished. 

    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));
    }

In this way every call from WeatherStackClient will have authorization info and we do not need to add anything when we make requests. The only place we need to put additional code is a constructor.

Note that authorization is not needed to call weatherstack.com and is added only to show how it can be done.

This article did not cover all of the possibilities of IHttpClientFactory so if you want to know more, just go to this Microsoft article.

Hope you like this post, all code posted in this article is available at my GitHub account:

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

ASP.NET Core in .NET 5 – pass parameters to actions

Passing parameters to actions is an essential part of building RESTful Web API. ASP.NET Core released as a part of .NET 5 offers multiple ways to pass parameters to methods, that represent your endpoints. Let’s see what they are.

Pass parameter as a part of an URL

When passing a parameter in a URL, you need to define a routing that would contain a parameter. Let’s have a look a the example:

    [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)]
        });
    }

This method returns a WeatherForecast for a single day in the future. DaysForward parameter represents how many days in advance weather forecast should be returned. Notice that daysForward is a part of the routing, so a valid URL to this endpoint will look like:

GET: weatherForecast/3

We can also use [FromRoute] attribute before variable type, but it will also work the same way by default.

   public IActionResult Get([FromRoute] int daysForward)

Pass parameter in a query string

This is a very common method for passing additional parameters, because it does not require us to change routing, so it is also backward compatible. It’s important if we were to change an existing solution.

Let’s look at a different method, that would return a collection of weather forecasts with a sorting option.

[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;
    }

In this example, we pass on sortByTemperature parameter which is optional. Notice that we use [FromQuery] attribute to indicate, that it’s a variable taken from the query string. A URL to this endpoint would look like this:

GET: weatherForecast?sortByTemperature=true

You can put many parameters like this:

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

Remember, that URL needs to be encoded properly to work right. If you were to pass a parameter like this:

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

It will need to be encoded into:

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

Pass an object in a query string

When you’re passing a lot of query parameters, it might be worth to handle them as an object. Take a look at the code below:

    // 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;
    }

As you can see, I’m passing WeatherForecastFilters class as a query parameter. Notice that I’m using [FromQuery] attribute before the class name.

And WeatherForecastFilters class looks like this:

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

It is a standard class, without any additional attributes. Let’s now make a request to the URL below:

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

The response would be:

So parameters can be passed-in by their names, even if they are inside the class. What’s more, they are accessible just by their names, without any class prefix and that means that all of the properties must have unique names.

Here is a screenshot from Visual Studio.

Handling query parameters as class properties will work, but only inside this class. It will not work with the nested objects – their properties will not be mapped.

Pass parameters with headers

Passing parameters in a request headers are less popular, but also widely used. It doesn’t show in a URL, so it’s less noticeable by the user. A common scenario for passing parameters in a header would be providing credentials or a parent request id to enable multi-application tracking. Let’s have a look at this example:

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

In order to send a POST request, we would need to use some kind of a tool, I’ll use Postman:

Here you see that I specified headers and parentRequestId is one of them.

Pass parameters in a request body

The most common way to pass the data is to include it in a request body. We can add a header Content-Type with value application/json and inform the receiver how to interpret this body. Let’s have a look at our example:

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

We use [FromBody] attribute to indicate, that forecast will be taken from request body. In ASP.NET Core for .NET 5 we don’t need and serialize to deserialize json body to WeatherForecast object, it will work automatically. To send POST request, let’s use Postman once again:

Have in mind, that size of the request body is limited by the server. It can be anywhere between 1MB to 2GB. In ASP.NET Core 5 maximum request body size is around 28MB, but that can be changed. What if I would like to send bigger files than that, over 2GB? Then you should look into sending content as a stream or sending it in chunks.

Pass parameters in a form

Sending content in a form is not very common, but it is the best solution if you want to upload a file. Let’s have a look at the example:

    [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();
    }

This method doesn’t really send a file, but it will successfully receive a file from the request. The interface IFormFile is used specifically for handling a file.

When sending a request we need to set Content-Type to application/x-www-form-urlencoded and it the Body part, we need to choose a file:

Let’s see what do we get when we debug this code:

And the file is correctly read. An interesting fact is, that with IFormFile we get not only binary data but also a file type and name. So you might ask why I send a file name separately? This is because you might want to name file differently on the server, then the one you are sending.

Hope you enjoyed this post, you can have a look at the code posted here on my 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!

 

Entity Framework Core 5 vs SQLBulkCopy

Entity Framework Core 5 is a great ORM to use and connect to the database with. It is easy to use and easy to understand. It offers just enough for the most common scenarios. So what about inserting big amounts of data in a one go? Would it be fast enough?

Let’s have a look at the code

As my example, I’ll take a very simple entity – a Profile and PrimeHotel repository available here at my GitHub

My DbContext is very simple and it looks like this:

    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; }
    }

And Profile entity looks like this:

    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; }
    }

Because I’ll be using WebApi for the ease of demonstration, I’ll create a 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;
        }
    }

For now, it’s pretty empty, but now you get the base that we will start with.

Let’s get profiles… lots of them! 

To test an insert of many entities at once we need to generate a lot of testing data. I like to have my test data as close to real values as possible, so to get those, I’ll use a Bogus nugget package.

Bogus is robust and very easy to use fake data generator. It will generate random values, that will fit a given context, like a surname, age, address, e-mail, company name, and so on. There are dozens of options. Go see for yourself in its documentation

Generating any number of profiles will look like this:

    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);
    }

Inserting profiles with Entity Framework Core 5

I don’t want to send all those profiles in a request, because that would be a huge amount of data. Transferring that to a controller and deserialization on the ASP.NET Core 5 side can take a while, and it’s not really the part I want to test. This is why I choose to generate my profiles in the controller method and insert it right after that.

The code for the whole thing is really straightforward:

    [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()
            });
    }

Additionally, I added a Stopwatch to measure how long does it take to generate profiles as well as insert them. In the end, I’m returning an anonymous type to easily return more than one result at a time.

Finally, let’s test it out. For 1000 profiles I got:

But wait, let’s try something bigger, like 100000 entities:

 

25 seconds? Really? Not that impressive.

What it does underneath? Let’s check with 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 to the rescue

SqlBulkCopy is a class that was introduced a while ago, specifically in .Net Framework 2.0 – 18 years ago! SqlBulkCopy will only work to save data in a SQL Server database, but its source can be anything, as long as it’s results can be loaded to DataTable or read by IDataReader.

Let’s have a look at how we can use it in our example.

    [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()
        });
    }

First, we need to define a DataTable. It needs to represent the Profiles table because this is our destination table that we are going to use.

Then with WriteToServerAsync we are loading profiles to the database. Let’s have a look at how does it looks like in SQL.

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

How does it work internally? StackOverflow comes with an answer:

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.

Two totally different approaches. What about the performance? Let’s compare those two.

The performance

The outcome of a performance check is not a huge surprise. SqlBulkCopy is meant to insert data fast and it is extremely performant. 

Big differences start to show when you insert more than 10 thousand entities at a time. Over that number, it might be wort to reimplement your code to use SqlBulkCopy instead of Entity Framework Core 5.

What about other operations?

When it comes to handling big amounts of data things are starting to be a little more tricky. You might want to take a look at the database improvements and what data you actually need to operate on. Have in mind that operations on big chunks of data are much faster when done on the database side. 

I had a case once where I needed to perform and update on around a million entities, once a day. I combined a few things and it worked out pretty good.

  • create a temporary table, i.e. T1
  • insert data with SqlBulkCopy
  • perform an update on the database side
  • select any data you might need, i.e. for logging
  • delete a temporary table

I know this is moving business logic inside the database, but some sacrifices are required if this update operation needs to be fast.

All code posted here is available on my GitHub.

Cheers!