Zwracanie więcej niż jednego wyniku z metody – tuple

Czy zmierzyłeś się kiedyś z programem, w którym trzeba było zwrócić więcej niż jeden wynik z metody? Jest kilka sposobów, aby sobie z tym poradzić, a tuple może być tym, czego potrzebujesz.

Problem

Spójrzmy na kod, w którym analizujemy dane transakcji, aby pokazać je na ekranie. Mamy na to oddzielną metodę: GetTransactionData

string Data = "Michał;Breakfast;59;2021-02-13";

var value = new TupleExample().GetTransactionData(Data, out string customer);
Console.WriteLine($"Customer {customer} bought an item for {value}");

public class TupleExample
{
    public decimal GetTransactionData(string data, out string customer)
    {
        var chunks = data.Split(';');
        customer = chunks.First();

        return decimal.Parse(chunks[2]);
    }
}

 

A oto rezultat aplikacji konsolowej:

Możliwe rozwiązania

Możemy do tego problemu podejść na kilka sposobów. Najgorszy wydaje się ten, który zaproponowałem na początku, zwracając dodatkowe wyniki jako parametry wyjściowe. Niemniej jednak widziałem tę konstrukcję więcej niż raz i są przypadki, w których może być ona użyteczna. Na przykład:

var isSuccess = decimal.TryParse("59,95", out decimal result);

Innym podejściem będzie stworzenie nowej klasy, która zwróci rezultat jaki chcemy. Kod będzie wyglądał następująco:

var transactionData = new TupleExample().GetTransactionData(Data);
Console.WriteLine($"Customer {transactionData.Customer} bought an item for {transactionData.Value}");

public class TransactionData
{
    public string Customer { get; set; }
    public decimal Value { get; set; }
}

public class TupleExample
{
    public TransactionData GetTransactionData(string data)
    {
        var chunks = data.Split(';');

        return new TransactionData
        {
            Customer = chunks.First(),
            Value = decimal.Parse(chunks[2])
        };
    }
}

To rozwiązanie jest poprawne pod każdym względem. Stworzyliśmy klasę reprezentującą to, co zwracamy z metody. Właściwości mają prawidłowe nazwy.
A co by było, gdybyśmy mogli osiągnąć ten sam rezultat bez tworzenia nowej klasy?

Poznaj tuple

Tuple(krotka) to typ wartościowy, który może przechowywać zestaw danych. Jest to bardzo elastyczna struktura, podobna do typów dynamicznych, dzięki czemu możesz nadawać nazwy swoim właściwościom. Skończmy mówić i przejdźmy do kodu:

var transactionData = new TupleExample().GetTransactionData(Data);
Console.WriteLine($"Customer {transactionData.Customer} bought an item for {transactionData.Value}");

public class TupleExample
{
    public (string Customer, decimal Value) GetTransactionData(string data)
    {
        var chunks = data.Split(';');

        return (chunks.First(), decimal.Parse(chunks[2]));
    }
}

Czy zauważyłeś, że w ogóle nie zmieniłem kodu, który wywołuje metodę GetTransactionData? 😲 Jest to zmiana, która wpływa tylko na samą metodę, ale czyni ją prostszą i bardziej zwięzłą.

Zwróć uwagę, jak można zdefiniować tuple:

Możesz stworzyć tuple przy pomocy operatora new lub przy pomocy nawiasów.

var tuple = new Tuple<string, decimal>("John", 20);
(string, decimal) tuple2 = ("Anna", 30);
(string Name, decimal Value) tuple3 = ("Cathrine", 40);

Console.WriteLine($"Customer {tuple2.Item1} bought an item for {tuple2.Item2}");
Console.WriteLine($"Customer {tuple3.Name} bought an item for {tuple3.Value}");

Możesz określić nazwy właściwości jeśli chcesz, a jeżeli nie, to będą miały swoje domyślne nazwy: Item1, Item2, itd.

Podsumowanie

Tuple to prosty, dynamiczny typ, który świetnie nadaje się w momencie, gdy nie chcemy tworzyć klasy tylko do jednego użycia. Jest elastyczny i łatwy w tworzeniu.

Czy tuple sprawdzi się w Twoim kodzie? Ja zdecydowanie dam mu szanse 😀

 

 

Returning more than one result from a method – tuple

Have you ever had a case, when you needed to return more than one parameter from a method? There are a couple of ways to cope with that and tuple might just be what you need.

The problem

Let’s look at the code, where we parse transaction data just to show it on the screen. We have a separate method for that GetTransactionData

string Data = "Michał;Breakfast;59;2021-02-13";

var value = new TupleExample().GetTransactionData(Data, out string customer);
Console.WriteLine($"Customer {customer} bought an item for {value}");

public class TupleExample
{
    public decimal GetTransactionData(string data, out string customer)
    {
        var chunks = data.Split(';');
        customer = chunks.First();

        return decimal.Parse(chunks[2]);
    }
}

 

And here is the result:

Possible solutions

There are a couple of ways we can approach this. The one that I proposed in the beginning, passing additional return values as out parameters seems to be the worst one. Nevertheless, I saw this construction more than once and there are certain cases it might be useful. For example:

var isSuccess = decimal.TryParse("59,95", out decimal result);

Another approach would be creating a new class to return the result, that might look like this:

var transactionData = new TupleExample().GetTransactionData(Data);
Console.WriteLine($"Customer {transactionData.Customer} bought an item for {transactionData.Value}");

public class TransactionData
{
    public string Customer { get; set; }
    public decimal Value { get; set; }
}

public class TupleExample
{
    public TransactionData GetTransactionData(string data)
    {
        var chunks = data.Split(';');

        return new TransactionData
        {
            Customer = chunks.First(),
            Value = decimal.Parse(chunks[2])
        };
    }
}

This solution is correct in every way. We created a class to represent what we return from a method. Properties are named correctly.
What if we could achieve the same without creating a new class?

Meet tuples

A tuple is a value type that can store a set of data elements. It is a very flexible structure, similar to dynamic types, so you can set names for your properties. Let’s finish talking and jump into code:

var transactionData = new TupleExample().GetTransactionData(Data);
Console.WriteLine($"Customer {transactionData.Customer} bought an item for {transactionData.Value}");

public class TupleExample
{
    public (string Customer, decimal Value) GetTransactionData(string data)
    {
        var chunks = data.Split(';');

        return (chunks.First(), decimal.Parse(chunks[2]));
    }
}

Have you noticed, that I haven’t changed the code that’s using a method GetTransactionData? 😲 It is the change that affects only the method itself, but it just makes it simpler and more concise.

Notice how tuple can be defined:

You can create a tuple with new keyword or with parenthesis.

var tuple = new Tuple<string, decimal>("John", 20);
(string, decimal) tuple2 = ("Anna", 30);
(string Name, decimal Value) tuple3 = ("Cathrine", 40);

Console.WriteLine($"Customer {tuple2.Item1} bought an item for {tuple2.Item2}");
Console.WriteLine($"Customer {tuple3.Name} bought an item for {tuple3.Value}");

You can specify property names, but if you don’t, you can use default names like Item1, Item2, etc.

Summary

A tuple is a simple, dynamic type when we don’t want to create a class for just a single usage. It is flexible and easy to create.

Would it be useful for you? I will give it a try 😀
 

 

 

Dodawanie dużej ilości elementów przy pomocy Entity Framework Core 5

MS SQL Server umożliwia szybkie wstawianie dużych ilości danych. Nazywa się ono kopią zbiorczą (ang. bulk copy) i jest wykonywane przez klasę SqlBulkCopy. Porównałem już, jak szybko działa ta klasa w porównaniu z EF Core 5 w tym poście: https://www.michalbialecki.com/2020/05/06/entity-framework-core-5-vs-sqlbulkcopy/, ale tym razem chcę sprawdzić coś innego – bibliotekę linq2db.

Czym jest Linq2db

Sprawdźmy, jak Linq2db jest opisane na swojej stronie internetowej:

LINQ to DB to najszybsza biblioteka dostępu do bazy danych LINQ oferująca prostą, lekką, szybką i bezpieczną dla typów warstwę między obiektami POCO a bazą danych.

LINQ to DB is the fastest LINQ database access library offering a simple, light, fast, and type-safe layer between your POCO objects and your database (wersja oryginalna)

Brzmi imponująco i niedawno odkryłem, że istnieje pakiet rozszerzeń EF Core linq2db.EntityFrameworkCore, który jest zintegrowany z EF Core i wzbogaca DbContext o kilka fajnych funkcji.

Najważniejsze możliwości linq2db to: 

  • Bulk copy (bulk insert)
  • szybkie wczesne ładowanie (nieporównywalnie szybsze niż wbudowane polecenieInclude)
  • wsparcie dla polecenia MERGE 
  • wsparcie dla tabel tymczasowych
  • rozszerzenia do wyszukiwania w tekście (Full-Text search)
  • oraz jeszcze kilka

Napiszmy kod

W projekcie PrimeHotel zaimplementowałem już metodę z SqlBulkCopy do wstawiania profili do bazy danych. Najpierw generuję profile za pomocą biblioteki Bogus, a następnie wstawiam je. W tym przykładzie użyję 3 metod z ProfileController:

  • GenerateAndInsert – zaiplementowana przy pomocy EF Core
  • GenerateAndInsertWithSqlCopy – zaimplementowana z użyciem klasy SqlBulkCopy
  • GenerateAndInsertWithLinq2db – zaimplementowana z użyciem Linq2db

Pokażę szybko, jak wyglądają wspomniane metody. Pierwszą z nich jest GenerateAndInsert, zaimplementowana przy pomocy czystego Entity Framework Core 5.

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

Używam klasy Stopwatch, aby zmierzyć, ile czasu zajmuje wygenerowanie profili metodą GenerateProfiles i ile czasu zajmuje ich wstawienie.

GenerateAndInsertWithSqlCopy jest zaimplementowana przy pomocy klasy SqlBulkCopy:

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

Zauważ, że ta implementacja jest znacznie dłuższa i musiałem utworzyć obiekt DataTable, aby przekazać moje dane w postaci tabeli.

I wreszcie implementacja GenerateAndInsertWithLinq2db, która wykorzystuje bibliotekę linq2db.

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

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

    using (var db = primeDbContext.CreateLinqToDbConnection())
    {
        await db.BulkCopyAsync(new BulkCopyOptions { TableName = "Profiles" }, profiles);
    }

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

Ta metoda jest prawie tak krótka jak implementacja z użyciem EF Core, ale tworzy obiekt DataConnection z metodą CreateLinqToDbConnection.

Wyniki

Porównałem te 3 metody z wstawianiem 1k, 10k, 50k, 100k i 500k rekordów. Jak myśliś, jak szybko udało się to osiągnąć? Sprawdźmy, poniższe dane są wyrażone w sekundach.

  EF Core Bulk insert linq2db bulk insert
1000 0.22 0.035 0.048
10000 1.96 0.2 0.318
50000 9.63 0.985 1.54
100000 19.35 1.79 3.18
500000 104 9.47 16.56

Oto tabela z wynikami, w sekundach. Sam EF Core nie jest wcale imponujący, ale w połączeniu z biblioteką Linq2db jest prawie tak szybki, jak klasa SqlBulkCopy.

A oto wykres, im niższa wartość, tym lepiej.

Zabawne – podczas testów zauważyłem, że generowanie danych testowych jest faktycznie wolniejsze niż wstawianie do bazy danych, wow 😀

Podsumowanie

Linq2db to imponująca biblioteka, która oferuje już całkiem sporo. Z tego co można zobaczyć na GitHub wygląda na to, że jest to dobrze ugruntowany projekt z wieloma współtwórcami. Wiedząc to, jestem zdziwiony, że wcześniej na niego nie trafiłem.

Wstawianie zbiorcze z linq2db jest prawie tak szybkie, jak użycie klasy SqlBulkCopy, ale jest znacznie czystsze i krótsze. Jest też mniej podatne na błędy i na pewno użyłbym go w swoich projektach.

Cały zamieszczony tutaj kod jest dostępny na moim GitHub.

Mam nadzieję, że ci się przyda, pozdro! 😄

Bulk copy with Entity Framework Core 5

MS SQL Server provides functionality to quickly insert large amounts of data. It is called Bulk Copy and is performed by SqlBulkCopy class. I already compared how fast is it compared to EF Core 5 in this post: https://www.michalbialecki.com/2020/05/03/entity-framework-core-5-vs-sqlbulkcopy-2/, but this time I want to check something different – linq2db library.

What is Linq2db

Let’s check how Linq2db is described on its website:

LINQ to DB is the fastest LINQ database access library offering a simple, light, fast, and type-safe layer between your POCO objects and your database.

Sounds impressive and recently I discovered that there is an EF Core extensions package linq2db.EntityFrameworkCore, which is integrated with EF Core and enriches a DbContext with some cool features.

The most interesting are: 

  • Bulk copy (bulk insert)
  • Fast Eager Loading (incomparable faster on massive Include query)
  • MERGE statement support
  • Temporary Tables support
  • Full-Text Search extensions
  • and a few more

Let’s write some code

In the PrimeHotel project, I already implemented SqlBulkCopy method to insert profiles to db. First I generate profiles with Bogus library and then insert them. In this example, I’m going to use 3 methods from ProfileController:

  • GenerateAndInsert – implemented with pure EF Core
  • GenerateAndInsertWithSqlCopy – implemented with SqlBulkCopy class
  • GenerateAndInsertWithLinq2db – implemented with Linq2db

Let me show you quickly how those 3 methods look like. The first one is GenerateAndInsert, implemented with pure Entity Framework Core 5.

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

I use a Stopwatch class to measure how long does it take to generate profiles with the GenerateProfiles method and how long does it take to insert them. 

GenerateAndInsertWithSqlCopy is implemented with SqlBulkCopy class:

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

Notice that this implementation is much longer and I needed to create DataTable object, to pass my data as a table.

And finally, GenerateAndInsertWithLinq2db implementation, which uses linq2db library.

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

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

    using (var db = primeDbContext.CreateLinqToDbConnection())
    {
        await db.BulkCopyAsync(new BulkCopyOptions { TableName = "Profiles" }, profiles);
    }

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

This method is almost as short as EF Core one, but it creates a DataConnection with CreateLinqToDbConnection method.

The results

I compared those 3 methods with inserting 1k, 10k, 50k, 100k and 500k records. How fast would it be? Let’s check.

  EF Core Bulk insert linq2db bulk insert
1000 0.22 0.035 0.048
10000 1.96 0.2 0.318
50000 9.63 0.985 1.54
100000 19.35 1.79 3.18
500000 104 9.47 16.56

Here is a table with results in seconds. EF Core itself isn’t impressive at all, but combined with Linq2db library is almost as fast as a bulk copy.

And here is the chart, the lower value, the better.

Funny thing – while testing I noticed, that generating test data is actually slower than inserting to DB, wow 😀

The summary

Linq2db is an impressive library and already offers a lot. From GitHub, it seems that it is a well-established project with a bunch of contributors. Knowing that I’m surprised that I haven’t come across it earlier. 

A bulk insert with linq2db is almost as fast as using SqlBulkCopy class but is much cleaner and shorter. It is also less error-prone and I would definitely use it in my projects.

All code posted here is available on my GitHub.

Hope it will be useful for you as well, cheers 😄

Entity Framework Core – czy jest szybki?

Entity Framework Core to świetny ORM, który niedawno osiągnął wersję 5. Czy jest szybki? Czy jest szybszy niż swój poprzednik, Entity Framework 6, który nadal oferuje nieco więcej funkcji? Sprawdźmy to.

Jedno z ciekawszych porównań zostało wykonane przez Chada Goldena, który porównał wydajność dodawania, aktualizowania i usuwania 1000 obiektów. Dokładne dane i kod są dostępne na jego blogu: https://chadgolden.com/blog/comparing-performance-of-ef6-to-ef-core-3

Wnioski są oczywiste: w prawie każdym teście przeprowadzanym przez Chada, Entity Framework Core 3 jest szybszy niż Entity Framework 6 – dokładnie 2,25 do 4,15 razy szybszy! Dlatego jeśli wydajność jest ważna dla aplikacji i działa na dużych ilościach danych, EF Core powinien być naturalnym wyborem.

Czy jest szybszy niż Dapper?

Dapper jest bardzo popularnym maperem relacyjno-obiektowym i podobnie jak EF Core ułatwia pracę z bazą danych. Nazywany jest królem Micro ORM, ponieważ jest bardzo szybki i wykonuje część pracy za nas. Jeśli porównamy EF Core i Dapper, od razu zauważymy, że możliwości EF Core są znacznie większe. Technologia firmy Microsoft umożliwia śledzenie obiektów, migrację schematu bazy danych i interakcję z bazą danych bez pisania zapytań SQL. Z drugiej strony Dapper odwzorowuje obiekty zwracane przez bazę danych, ale wszystkie polecenia SQL muszą być napisane samodzielnie. Zapewnia to z pewnością większą swobodę w obsłudze bazy danych, ale istnieje większe ryzyko popełnienia błędu podczas pisania zapytania SQL. Podobnie jak w przypadku aktualizowania schematu bazy danych, EF Core może samodzielnie tworzyć zmiany i generować migrację, a w Dapper trzeba ręcznie edytować kod SQL.

Nie ulega jednak wątpliwości, że Dapper ma swoich zwolenników, głównie ze względu na swoje osiągi. Na blogu exceptionnotfound.net możemy znaleźć porównanie między Entity Framework Core 3 i Dapper w wersji 2.

Jak widać, porównujemy tutaj 3 odczyty bazy danych, gdzie Entity Framework Core ze śledzeniem obiektów to jeden przypadek, brak śledzenia to drugi, a Dapper to trzeci. Śledzenie zmian w encjach w EF Core można wyłączyć za pomocą opcji AsNoTracking(), co znacznie przyspiesza operacje odczytu. Więcej informacji na temat tego testu można znaleźć tutaj: https://exceptionnotfound.net/dapper-vs-entity-framework-core-query-performance-benchmarking-2019/

 

Podsumowanie

Podsumowując – Dapper znacznie szybciej odczytuje dane z bazy danych i na pewno będzie stosunkowo szybki podczas zapisu. Wymaga jednak pisania zapytań SQL, które mogą narazić programistę na błędy. Osobiście użyłem Dappera w kilku projektach i zasadniczo tylko w jednym miało to sens, ponieważ wymagał szybkich operacji na bazie danych. Dla prostej logiki zapisywania i pobierania danych z bazy danych użyłbym Entity Framework Core ze względu na jego prostotę i wygodę we wprowadzaniu zmian.

Entity Framework Core – is it fast?

Entity Framework Core is a great ORM, that recently reached version 5. Is it fast? Is it faster than it’s predecessor, Entity Framework 6, which still offers slightly more functionality? Let’s check that out.

This comparison was made by Chad Golden, comparing the performance of adding, updating, and deleting 1000 entities. The exact data and code are available on his blog: https://chadgolden.com/blog/comparing-performance-of-ef6-to-ef-core-3

The conclusions are obvious: in almost every test conducted by Chad, Entity Framework Core 3 is faster than Entity Framework 6 – exactly 2.25 to 4.15 times faster! So if performance is important to your application and it operates on large amounts of data, EF Core should be a natural choice.

Is it faster than Dapper?

Dapper is a very popular object-relational mapper and, like EF Core, it facilitates working with the database. It’s called the king of Micro ORM because it’s very fast and does some of the work for us. If we compare EF Core and Dapper, we immediately notice that the capabilities of EF Core are much greater. Microsoft technology allows you to track objects, migrate the database schema, and interact with the database without writing SQL queries. Dapper, on the other hand, maps the objects returned by the database, but all SQL commands have to be written yourself. This certainly allows more freedom in operating the database, but there is a greater risk of making a mistake when writing a SQL query. Similarly to updating the database schema, EF Core can create changes and generate a migration by itself, and in Dapper, you have to manually edit the SQL code.

There is no doubt, however, that Dapper has its supporters, mainly due to its performance. On the blog exceptionnotfound.net we can find a comparison between Entity Framework Core 3 and Dapper version 2.

As you can see, we compare 3 database reads here, where Entity Framework Core with object tracking in one case, non-tracking in the other, and Dapper’s third. Tracking changes to entities in EF Core can be turned off with the AsNoTracking() option, which makes reading operations significantly faster. More information on this test can be found here: https://exceptionnotfound.net/dapper-vs-entity-framework-core-query-performance-benchmarking-2019/

Summary

All in all – Dapper is much faster to read from the database and will certainly be comparatively fast when writing. However, it requires writing SQL queries, which can expose the developer to errors. I have personally used Dapper on several projects, and basically, only one has been dictated by performance. For the simple logic of saving and retrieving data from the database, I would use Entity Framework Core because of its simplicity and convenience in introducing changes.

 

Przydatne polecenia SQL podczas pisania migracji w EF Core 5

Entity Framework Core 5 to świetny ORM i uwielbiam jego wydajność oraz zwięzłość. Po włączeniu mechanizmu migracji można wygenerować następną migrację na podstawie zmian poczynionych w modelu. To bardzo użyteczne, ale jeśli chodzi o inne obiekty bazy danych, musisz poradzić sobie samemu. To znaczy – nadal możesz korzystać z migracji, ale musisz sam wymyślić instrukcję SQL. Rzućmy okiem na kilka fajnych instrukcji, których możesz użyć z bazą danych SQL Server.

CREATE OR ALTER

Jest to bardzo potężne polecenie, które dosłownie sprawdza, czy obiekt bazy danych istnieje i zmienia go lub tworzy nowy na podstawie tego faktu.

Więc zamiast pisać takie polecenie:

IF OBJECT_ID('UpdateProfilesCountry', 'P') IS NOT NULL
DROP PROC UpdateProfilesCountry
GO

CREATE PROCEDURE [dbo].[UpdateProfilesCountry]
    @StardId int
AS
BEGIN
    SET NOCOUNT ON;
    UPDATE Profiles SET Country = 'Poland' WHERE LEFT(TelNo, 2) = '48' AND Id > @StardId
END

Mogę użyć CREATE OR ALTER, w ten sposób:

CREATE OR ALTER PROCEDURE [dbo].[UpdateProfilesCountry]
    @StardId int
AS
BEGIN
    SET NOCOUNT ON;
    UPDATE Profiles SET Country = 'Poland' WHERE LEFT(TelNo, 2) = '48' AND Id > @StardId
END

Dodatkowo, CREATE OR ALTER nie resetuje uprawnień, więc nie musisz ich ponownie dodawać, co byłoby konieczne w przypadku pierwszego skryptu.

CREATE OR ALTER może być używany z następującymi obiektami:

  • STORED PROCEDURES (w tym skompilowane natywnie)
  • FUNCTIONS (Transact-SQL, w tym skompilowane natywnie)
  • TRIGGERS
  • VIEWS

Inne obiekty bazy danych, takie jak tabele lub indeksy, nie mogą być obsługiwane za pomocą tej instrukcji. Więcej informacji na ten temat można znaleźć w tym poście Microsoft-u.

Uwaga: CREATE OR ALTER jest dostępne w SQL Server od wersji 2016 SP1.

DROP IF EXISTS

DROP IF EXISTS to przydatna instrukcja, której można użyć w przypadku wielu obiektów bazy danych. Sprawdzi, czy obiekt istnieje, a jeśli tak, usunie go – wszystko w jednej instrukcji.

Zamiast pisać następujące polecenie:

IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL 
  DROP TABLE dbo.Products; 

Mogę napisać takie, jednolinijkowe

DROP TABLE IF EXISTS dbo.Products

Czyste i proste. DROP IF EXISTS będzie działać dla wielu obiektów bazy danych, takich jak:

  • AGGREGATE
  • ASSEMBLY
  • VIEW
  • DATABASE
  • DEFAULT
  • FUNCTION
  • INDEX
  • PROCEDURE
  • ROLE
  • RULE
  • SCHEMA
  • SECURITY POLICY
  • SEQUENCE
  • SYNONYM
  • TABLE
  • TRIGGER
  • TYPE
  • USER
  • VIEW

Możesz przeczytać o tym więcej w tym poście Microsoft-u. Zwróć uwagę także, że DROP IF EXISTS jest dostępna w SQL Server od wersji 2016.

Mam nadzieję, że ci się spodoba, może masz jakieś instrukcje SQL, które uznasz za przydatne w pracy? Daj mi znać lub zostaw komentarz. Pozdro! 😉 

Useful SQL statements when writing EF Core 5 migrations

Entity Framework Core 5 is a great ORM and I love how efficient and concise it is. With the migrations mechanism enabled, you can generate the next migration based on changes applied to your model. This is so cool, but when it comes to other database objects, you are on your own. I mean – you can still use migrations, but you have to figure out a SQL statement yourself. Let’s take a look at some cool statements you can use with the SQL Server database.

CREATE OR ALTER

This is a very powerful command that literally just check if a database object exists, and it alters it or creates a new one based on that fact.

So instead of writing a statement like this:

IF OBJECT_ID('UpdateProfilesCountry', 'P') IS NOT NULL
DROP PROC UpdateProfilesCountry
GO

CREATE PROCEDURE [dbo].[UpdateProfilesCountry]
    @StardId int
AS
BEGIN
    SET NOCOUNT ON;
    UPDATE Profiles SET Country = 'Poland' WHERE LEFT(TelNo, 2) = '48' AND Id > @StardId
END

I can use CREATE OR ALTER, like this:

CREATE OR ALTER PROCEDURE [dbo].[UpdateProfilesCountry]
    @StardId int
AS
BEGIN
    SET NOCOUNT ON;
    UPDATE Profiles SET Country = 'Poland' WHERE LEFT(TelNo, 2) = '48' AND Id > @StardId
END

Additionally, CREATE OR ALTER does not reset permissions, so you don’t need to re-apply them, which would be necessary with the first script.

CREATE OR ALTER can be used with the following objects:

  • STORED PROCEDURES (including natively compiled)
  • FUNCTIONS (Transact-SQL, including natively compiled)
  • TRIGGERS
  • VIEWS

Other database objects like tables or indexes cannot be handled with that statement. You can find more information about it in this Microsoft post.

Note: CREATE OR ALTER is available from SQL Server 2016 SP1.

DROP IF EXISTS

DROP IF EXISTS is a useful statement that can be used for many database objects. It will check if the object exists and if it does, it will drop it – all in one statement.

Instead of writing a statement like this:

IF OBJECT_ID('dbo.Products', 'U') IS NOT NULL 
  DROP TABLE dbo.Products; 

Now we can write a simple one-liner:

DROP TABLE IF EXISTS dbo.Products

Clean and simple. DROP IF EXISTS will work for many database objects, like:

  • AGGREGATE
  • ASSEMBLY
  • VIEW
  • DATABASE
  • DEFAULT
  • FUNCTION
  • INDEX
  • PROCEDURE
  • ROLE
  • RULE
  • SCHEMA
  • SECURITY POLICY
  • SEQUENCE
  • SYNONYM
  • TABLE
  • TRIGGER
  • TYPE
  • USER
  • VIEW

You can read more about it in this Microsoft post. Also Note: DROP IF EXISTS is available from SQL Server 2016.

Hope you like it, maybe you have some SQL statements you find useful at your work? Give me a shout or leave a comment. Cheers! 😉 

Testy jednostkowe w Entity Framework Core 5

Testy są nieodłączną częścią tworzenia oprogramowania. Są to oddzielne programy, które pozwalają sprawdzić, czy napisany przez nas kawałek program robi dokładnie to, co powinien. Testy jednostkowe są małymi fragmentami kodu, które testują pojedyncze elementy programu a w Entity Framework Core 5 pisze się je zaskakująco łatwo.

W pamięci, czy nie

Microsoft zaleca, żeby przy pisaniu testów używających EF Core, używać prawdziwej bazy danych, jeżeli to tylko możliwe. W zasadzie najlepiej używać bazy danych dokładnie w tej samej konfiguracji i na tym samym serwerze, na którym ma działać nasza aplikacja finalnie. Takie podejście może nie mieć sensu ze względu na koszty, co przyznaje również Microsoft. Testy wydajnościowe z pewnością powinny sprawdzać nasze rozwiązania na środowisku możliwie zbliżonym do produkcyjnego. Natomiast przy pisaniu testów jednostkowych wystarczy nam trzymanie bazy danych w pamięci. Entity Framework Core pozwala na działanie na wirtualnej bazie danych, tworzonej jedynie w pamięci. Możemy tez wykorzystać bazę SQLite, ponieważ działa szybko i nie potrzebuje serwera. Ma także tryb, w którym może działać w pamięci. W tym rozdziale nie będziemy się wgłębiali w użycie SQLite do testów, jednak mogę Cię zapewnić, że nie wymaga to wiele zachodu.

Pisanie testów jednostkowych

Entity Framework Core 5 bardzo łatwo skonfigurować do działania w pamięci i w projekcie z testami, wystarczy doinstalować pakiet NuGet o nazwie Microsoft.EntityFrameworkCore.InMemory. Przydadzą się też inne pakiety, a oto ich cała lista:

  • Microsoft.EntityFrameworkCore.InMemory – to run EF Core 5 in memory
  • NUnit – a framework to write and run unit tests
  • NUnit3TestAdapter – an adapter to run NUnit tests in Visual Studio
  • FluentAssertions – easy library to write nice and readable assertions

Do testów użyję klasy ReservationController, którą po części pokazywałem już wcześniej. Oto jej pełna treść:

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

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

    [HttpGet]
    public async Task<IEnumerable<Reservation>> Get()
    {
        return await primeDbContext.Reservations.Include(r => r.Room).AsNoTracking().ToListAsync();
    }

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

        await primeDbContext.Entry(reservation).Collection(r => r.Profiles).LoadAsync();
        await primeDbContext.Entry(reservation).Reference(r => r.Room).LoadAsync();

        return Ok(reservation);
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] NewReservation newReservation)
    {
        var room = await primeDbContext.Rooms.FirstOrDefaultAsync(r => r.Id == newReservation.RoomId);
        var guests = await primeDbContext.Profiles.Where(p => newReservation.GuestIds.Contains(p.Id)).ToListAsync();

        if (room == null || guests.Count != newReservation.GuestIds.Count)
        {
            return NotFound();
        }

        var reservation = new Reservation
        {
            Created = DateTime.UtcNow,
            From = newReservation.From.Value,
            To = newReservation.To.Value,
            Room = room,
            Profiles = guests
        };

        var createdReservation = await primeDbContext.Reservations.AddAsync(reservation);
        await primeDbContext.SaveChangesAsync();

        return Ok(createdReservation.Entity.Id);
    }
}

Klasę testową nazwałem ReservationControllerTests, czyli jest to nazwa klasy i końcówka Tests na końcu. W tych testach skupię się na sprawdzeniu jak podmienić dane w Entity Framework Core, a nie aby przetestować wszystkie możliwe przypadki.

Podstawą tutaj jest odpowiednie przygotowanie PrimeDbContext do testowania. Sama podstawa klasy z testami wygląda następująco:

public class ReservationsControllerTests
{
    private DbContextOptions<PrimeDbContext> dbContextOptions = new DbContextOptionsBuilder<PrimeDbContext>()
        .UseInMemoryDatabase(databaseName: "PrimeDb")
        .Options;
    private ReservationsController controller;

    [OneTimeSetUp]
    public void Setup()
    {
        SeedDb();

        controller = new ReservationsController(new PrimeDbContext(dbContextOptions));
    }

    private void SeedDb()
    {
        using var context = new PrimeDbContext(dbContextOptions);
        var rooms = new List<Room>
        {
            new Room { Id = 1, Description = "Room nr 1", Number = 1, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 2, Description = "Room nr 2", Number = 2, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 3, Description = "Room nr 3", Number = 3, Level = 2, RoomType = RoomType.Suite }
        };

        var profiles = new List<Profile>
        {
            new Profile { Id = 1, Ref = "Profile 1", Forename = "Michał", Surname = "Białecki" },
            new Profile { Id = 2, Ref = "Profile 2", Forename = "John", Surname = "Show" },
            new Profile { Id = 3, Ref = "Profile 3", Forename = "Daenerys", Surname = "Targaryen" }
        };

        context.AddRange(rooms);
        context.AddRange(profiles);

        context.AddRange(new List<Reservation>
        {
            new Reservation
            { 
                Id = 1,
                Room = rooms[0],
                Profiles = new List<Profile>{ profiles[0] },
                From = DateTime.Today,
                To = DateTime.Today.AddDays(2)
            },
            new Reservation
            {
                Id = 2,
                Room = rooms[2],
                Profiles = new List<Profile>{ profiles[1], profiles[2] },
                From = DateTime.Today.AddDays(1),
                To = DateTime.Today.AddDays(3)
            }
        });

        context.SaveChanges();
    }
}

Pierwsza rzecz, która od razu zwaraca naszą uwagę, to metoda SeedDb, która służy do dodania danych testowych do kontektsu EF Core. Dla tych testów, dane zostaną wprowadzone tylko raz, na samym początku dzięki użyciu atrybutu [OneTimeSetUp]. Stan bazy danych zostanie zachowany dopóki działa proces, który te testy wykonuje. Jednak ważniejszy fragment znajduje się na samej górze, czyli utworzenie dbContextOptions. Zauważ, że to właśnie tam używamy opcji UseInMemoryDatabase, a następnie przy pomocy tego obiektu tworzymy klasę PrimeDbContext. Przy tworzeniu podajemy nazwę bazy danych i używamy zawsze tej samej.

Kolejna bardzo istotna linia to:

using var context = new PrimeDbContext(dbContextOptions);

Na początku używamy słowa kluczowego using, ponieważ nie chcemy aby Garbage Collector usunął nam zmienną context z pamięci w trakcie wykonywania testu.

Skoro mamy już skonfigurowaną bazę oraz dane, to czas na testy:

[Test]
public async Task Get_FetchesReservationsWithoutRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var reservations = (await controller.Get()).ToList();

    reservations.Count.Should().Be(2);
    reservations.All(r => r.Room == null).Should().BeFalse();
    reservations.All(r => r.Profiles == null).Should().BeTrue();
}

W pierwszym teście pobieramy wszystkie rezerwacje i sprawdzamy, czy ich zależności są załadowane. W tym przypadku tak nie będzie, ponieważ metoda Get w kontrolerze, nie wymusza ładowania zależności. Przetestujmy kolejną metodę.

[Test]
public async Task GetById_WhenIdIsProvided_FetchesReservationWithRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.GetById(2);
    var okResult = result.As<OkObjectResult>();
    var reservation = okResult.Value.As<Reservation>();

    reservation.Should().NotBeNull();
    reservation.Profiles.Should().NotBeNull();
    reservation.Room.Should().NotBeNull();
}

W drugim teście pobieramy pojedynczą rezerwację i tutaj sprawdzamy, że zarówno pokój, jak i profile są załadowane. Dzieje się tak, ponieważ w metodzie GetById używamy metod Collection i Reference, aby te zależności załadować. Przetestujmy teraz metodę Post.

[Test]
public async Task Post_WithRoomAndProfiles_AddsReservation()
{
    var newReservation = new NewReservation
    {
        From = DateTime.Today.AddDays(3),
        To = DateTime.Today.AddDays(7),
        RoomId = 3,
        GuestIds = new List<int> { 2 }
    };

    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.Post(newReservation);

    var okResult = result.As<OkObjectResult>();
    var reservationId = okResult.Value.As<int>();
    var addedReservation = await context.Reservations
        .Include(p => p.Profiles)
        .Include(r => r.Room)
        .FirstOrDefaultAsync(r => r.Id == reservationId);

    addedReservation.Should().NotBeNull();
    addedReservation.Profiles.Should().NotBeNull();
    addedReservation.Profiles.Count.Should().Be(1);
    addedReservation.Profiles[0].Id.Should().Be(2);
    addedReservation.Room.Should().NotBeNull();
    addedReservation.Room.Id.Should().Be(3);
}

Ostatni test sprawdza, czy dodana rezerwacja, została dodana poprawnie. Sprawdzamy czy pokój i profil gościa, został odpowiednio przypisany do nowej rezerwacji.

Podsumowanie

Testy jednostkowe w Entity Framework Core piszę się naprawdę prosto i zrozumiale. Zaledwie kilka linii konfiguracji pozwala nam używać klasy dbContext do przygotowania żądanego stanu bazy danych. Nie musimy oddzielnie podmieniać poszczególnych kolekcji w PrimeDbContext jak to było w przypadku testów Entity Framework. Pod tym względem Entity Framework Core jest dopracowany, a testy jednostkowe z jego użyciem nie odstają znacząco od jakichkolwiek innych testów jednostkowych. Praca z nimi jest łatwa i przyjemna, czyli dokładnie tak, jak powinno być.

Cały zamieszczony tutaj kod dostępny jest ma moim GitHub, zerkaj do woli. Jest to projekt demonstracyjny co potrafi EF Core 5, więc zachęcam do eksploracji na własnę rękę!

Dzięki za przeczytanie i do zobaczenia 😊

 

Unit tests in Entity Framework Core 5

Tests are an integral part of software development. These are separate programs that allow you to check if a piece of the program written by us does exactly what it should. Unit tests are small pieces of code that test individual program elements and in Entity Framework Core 5 it’s surprisingly easy to write them.

In memory or not

Microsoft recommends that when writing tests that use EF Core, you should use a real database whenever possible. In fact, it is best to use the database in exactly the same configuration and on the same server on which our application is to run. This approach may not make sense when it comes to cost, as Microsoft also admits. Performance tests should certainly check our solutions in an environment as close to production as possible. However, when writing unit tests, it’s enough to keep the database in memory. Entity Framework Core allows you to run on a virtual database created only in memory. We can also use the SQLite database because it works fast and does not need a server. It also has a mode in which it can run in memory. In this chapter, we won’t go into detail about using SQLite for testing, but I can assure you that it doesn’t take much effort.

Writing unit tests

In Entity Framework Core 5 it’s very easy to configure the database to run in memory. In a test project, just install the NuGet package called Microsoft.EntityFrameworkCore.InMemory, but also a few more might come in handy. Let’s check the full list:

  • Microsoft.EntityFrameworkCore.InMemory – to run EF Core 5 in memory
  • NUnit – a framework to write and run unit tests
  • NUnit3TestAdapter – an adapter to run NUnit tests in Visual Studio
  • FluentAssertions – easy library to write nice and readable assertions

For testing, I will use the ReservationController class. Here is its full content:

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

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

    [HttpGet]
    public async Task<IEnumerable<Reservation>> Get()
    {
        return await primeDbContext.Reservations.Include(r => r.Room).AsNoTracking().ToListAsync();
    }

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

        await primeDbContext.Entry(reservation).Collection(r => r.Profiles).LoadAsync();
        await primeDbContext.Entry(reservation).Reference(r => r.Room).LoadAsync();

        return Ok(reservation);
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] NewReservation newReservation)
    {
        var room = await primeDbContext.Rooms.FirstOrDefaultAsync(r => r.Id == newReservation.RoomId);
        var guests = await primeDbContext.Profiles.Where(p => newReservation.GuestIds.Contains(p.Id)).ToListAsync();

        if (room == null || guests.Count != newReservation.GuestIds.Count)
        {
            return NotFound();
        }

        var reservation = new Reservation
        {
            Created = DateTime.UtcNow,
            From = newReservation.From.Value,
            To = newReservation.To.Value,
            Room = room,
            Profiles = guests
        };

        var createdReservation = await primeDbContext.Reservations.AddAsync(reservation);
        await primeDbContext.SaveChangesAsync();

        return Ok(createdReservation.Entity.Id);
    }
}

I named the test class ReservationControllerTests, which is the name of the class and the Tests ending at the end. In these tests, I will focus on checking how to replace data in Entity Framework Core, and not to test all possible cases.

The basis here is the appropriate preparation of PrimeDbContext for testing. The very base of the class with tests looks like this:

public class ReservationsControllerTests
{
    private DbContextOptions<PrimeDbContext> dbContextOptions = new DbContextOptionsBuilder<PrimeDbContext>()
        .UseInMemoryDatabase(databaseName: "PrimeDb")
        .Options;
    private ReservationsController controller;

    [OneTimeSetUp]
    public void Setup()
    {
        SeedDb();

        controller = new ReservationsController(new PrimeDbContext(dbContextOptions));
    }

    private void SeedDb()
    {
        using var context = new PrimeDbContext(dbContextOptions);
        var rooms = new List<Room>
        {
            new Room { Id = 1, Description = "Room nr 1", Number = 1, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 2, Description = "Room nr 2", Number = 2, Level = 1, RoomType = RoomType.Standard },
            new Room { Id = 3, Description = "Room nr 3", Number = 3, Level = 2, RoomType = RoomType.Suite }
        };

        var profiles = new List<Profile>
        {
            new Profile { Id = 1, Ref = "Profile 1", Forename = "Michał", Surname = "Białecki" },
            new Profile { Id = 2, Ref = "Profile 2", Forename = "John", Surname = "Show" },
            new Profile { Id = 3, Ref = "Profile 3", Forename = "Daenerys", Surname = "Targaryen" }
        };

        context.AddRange(rooms);
        context.AddRange(profiles);

        context.AddRange(new List<Reservation>
        {
            new Reservation
            { 
                Id = 1,
                Room = rooms[0],
                Profiles = new List<Profile>{ profiles[0] },
                From = DateTime.Today,
                To = DateTime.Today.AddDays(2)
            },
            new Reservation
            {
                Id = 2,
                Room = rooms[2],
                Profiles = new List<Profile>{ profiles[1], profiles[2] },
                From = DateTime.Today.AddDays(1),
                To = DateTime.Today.AddDays(3)
            }
        });

        context.SaveChanges();
    }
}

The first thing that immediately catches our attention is the SeedDb method, which is used to add test data to the EF Core context. For these tests, the data will be entered only once, at the very beginning thanks to the [OneTimeSetUp] attribute. The state of the database will be preserved as long as the process that performs these tests is running. However, the more important part is at the top, which is creating a dbContextOptions. Note that this is where we use the UseInMemoryDatabase option, and then create the PrimeDbContext class using this object. When creating, we give the name of the database and always use the same one. Another very important line is:

using var context = new PrimeDbContext(dbContextOptions);

At first, we use the using keyword because we don’t want Garbage Collector to remove the context variable from memory while the test is running.

Since we already have a configured database and data, it’s time to test:

[Test]
public async Task Get_FetchesReservationsWithoutRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var reservations = (await controller.Get()).ToList();

    reservations.Count.Should().Be(2);
    reservations.All(r => r.Room == null).Should().BeFalse();
    reservations.All(r => r.Profiles == null).Should().BeTrue();
}

In the first test, we get all reservations and check if their dependencies are loaded. In this case, it won’t, because the Get method in the controller doesn’t force dependencies to be loaded. Let’s check another method.

[Test]
public async Task GetById_WhenIdIsProvided_FetchesReservationWithRoomsAndGuests()
{
    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.GetById(2);
    var okResult = result.As<OkObjectResult>();
    var reservation = okResult.Value.As<Reservation>();

    reservation.Should().NotBeNull();
    reservation.Profiles.Should().NotBeNull();
    reservation.Room.Should().NotBeNull();
}

In the second test, we take a single booking and here we check that both the room and the profiles are loaded. This is because in the GetById method we use the Collection and Reference methods to load these dependencies. Now let’s test the Post method.

[Test]
public async Task Post_WithRoomAndProfiles_AddsReservation()
{
    var newReservation = new NewReservation
    {
        From = DateTime.Today.AddDays(3),
        To = DateTime.Today.AddDays(7),
        RoomId = 3,
        GuestIds = new List<int> { 2 }
    };

    using var context = new PrimeDbContext(dbContextOptions);
    var result = await controller.Post(newReservation);

    var okResult = result.As<OkObjectResult>();
    var reservationId = okResult.Value.As<int>();
    var addedReservation = await context.Reservations
        .Include(p => p.Profiles)
        .Include(r => r.Room)
        .FirstOrDefaultAsync(r => r.Id == reservationId);

    addedReservation.Should().NotBeNull();
    addedReservation.Profiles.Should().NotBeNull();
    addedReservation.Profiles.Count.Should().Be(1);
    addedReservation.Profiles[0].Id.Should().Be(2);
    addedReservation.Room.Should().NotBeNull();
    addedReservation.Room.Id.Should().Be(3);
}

The last test checks if the added reservation was added correctly. We check whether the room and the guest’s profile have been properly assigned to the new booking.

The summary

Unit testing in Entity Framework Core is really simple and understandable. Only a few lines of configuration allow us to use the dbContext class to prepare the desired database state. We do not have to replace individual collections in PrimeDbContext separately, as was the case with the Entity Framework tests. In this respect, Entity Framework Core is refined, and unit testing using it does not differ significantly from any other unit tests. Working with them is easy and fun, which is exactly as it should be.

All code mentioned here is available on my GitHub, feel free to take a look at how EF Core can be used in other parts of the project.

Thanks for reading! 😊