Category Archives: Uncategorized

5 darmowych gier online, w które możesz zagrać w pracy

Większość programistów uwielbia grać w gry, a niektórzy z przyjemnością  grają po pracy. Niektóre firmy organizują regularne imprezy towarzyskie i spotkania. Jednak kiedy wybuchła pandemia, pracowaliśmy głównie zdalnie, a zorganizowanie imprezy towarzyskiej było po prostu niemożliwe.

W mojej firmie rozpoczęliśmy coś, co nazywa się fun hour, podczas której regularnie gramy razem w gry online. We wszystkie można grać za darmo, ale niektóre z nich oferują płatną subskrypcję z większą ilością opcji. Jestem pewien, że je polubisz!

Gratic.io

Gratic.io to zabawna i całkowicie darmowa gra online. Na zmianę losujemy jedno z dwóch zaproponowanych przez grę angielskich słów. Inni gracze muszą wpisać je ręcznie i odgadnąć, co przedstawia obraz.

Słowa mogą być bardzo proste, jak papuga w tym przypadku, albo naprawdę trudne, jak listwa przypodłogowa. Możesz stworzyć prywatny pokój dla swojej drużyny i grać razem. Zazwyczaj mamy też w tle rozmowę na Teams, dzięki czemu możemy wspólnie komentować rysunki i śmiać się.

Link: https://gartic.io/

Soldat

Soldat to bardzo intensywna, wieloosobowa strzelanka w 2D. Wybierasz jedną z ogólnie dostępnych map i grasz w jednym z dostępnych trybów gry (przechwytywanie flagi, deatmatch itp.). Grafika jest bardzo prosta, ale to fizyka gry sprawia, że wydaje się ona bardzo realistyczna. Gra jest wciągająca, ale krew i latające części ciała mogą nie wszystkim przypaść do gustu.

W moim zespole zwykle dzielimy się na dwie podgrupy i gramy w trybie przechwytywania flagi. Dzięki rozmowie na Teams w tle, możemy swobodnie komunikować się i dzielić obowiązki i zadania między członków zespołu.

Bardzo lubię Soldat, ale są dwie wady tej gry. Pierwszą z nich jest to, że musisz zainstalować go na swoim komputerze, a to może być problemem na formowym laptopie. Po drugie, nie możesz stworzyć prywatnej mapy dla swojej drużyny (przynajmniej w darmowej licencji), więc mogą dołączać inni gracze.

Link: https://soldat.pl/en/

Among Us

Jest to prosta gra, w którą może grać wiele osób, ale tylko dwóch graczy jest oszustami, a pozostali członkowie drużyny muszą się dowiedzieć, którzy to. Zespół musi naprawić statek kosmiczny wykonując proste zadania umieszczone na mapie. Oszuści muszą zabijać członków zespołu jeden po drugim, nie dając się złapać. Za każdym razem, gdy zostaną znalezione zwłoki, zespół decyduje, kto jest oszustem i jeśli się zgodzą – zostaje wyrzucony ze statku.

 

Najbardziej zabawną częścią tej gry jest dyskusja, która prowadzi do wyrzucenia kogoś. Musisz albo udowodnić, że nie jesteś oszustem, albo bardzo dobrze udawać.

Link: Among Us

Curve Fever

Jest to bardzo prosta gra zręcznościowa w 2D. Przypomina mi węża, ale w wersji wieloosobowej. Każdy gracz buduje ścianę i musi jechać ostrożnie, aby ominąć inne ściany.
 
 
Myślę, że może powiedzieć, że w tej grze praktyka czyni mistrza. 
 

GeoGuessr

 
Jest to oryginalna gra, w której musisz znaleźć miejsce na mapie świata, po prostu eksplorując je w trybie Google Street View. Na każde zdjęcie masz maksymalnie 2 minuty, aby wybrać najbliższą lokalizację. Gdy wszyscy skończą, zostaniesz uszeregowany według tego, jak dobrze wskazałeś to miejsce.
 
 
To wciągająca, ale powolna gra. Muszę przyznać, że to jedna z moich ulubionych gier, bo lubię podróżować i poznawać nowe miejsca. Jakoś się nie nudzi i gramy w nią prawie za każdym razem.
 
 
 

Podsumowanie

Mam nadzieję, że spodobał Ci się mój wybór gier online, w które możesz grać z kolegami z zespołu. Jak ci się podobało? Może gracie w inne gry? Proszę daj mi znać 🙂
 
 

Praktyczne różnice pomiędzy C# a Vb.Net

Od około półtora roku pracuję w projekcie, który początkowo był napisany w Vb.Net, ale teraz nowszy kod jest tworzony w C#. To świetny przykład tego, jak jeden projekt można napisać w dwóch językach, a kod w Vb.Net może odwoływać się do C# i na odwrót.

Vb.Net i C# są bardzo podobne i są kompilowane do tego samego języka pośredniego, ale niektóre różnice są bardziej zaskakujące niż inne.

Proste różnice

Zacznijmy od rzeczy oczywistych. Vb.Net używa prostego języka angielskiego w swojej składni, podczas gdy C# używa składni opartej na C. Spójrzmy na przykład.

    private string ReplaceAnd(string s)
    {
        if (s != null && s.Contains(":"))
            return s.Replace(":", "?");

        return s;
    }

A teraz ten sam przykład w Vb.Net:

    Private Function ReplaceAnd(s As String) As String
        If s IsNot Nothing And s.Contains(":") Then
            Return s.Replace(":", "?")
        End If

        Return s
    End Function

Przyzwyczajenie się do deklarowania typów za pomocą słowa kluczowego As zajęło mi trochę czasu. Także If potrzebuje słowa Then, ale to są rzeczy, które piszesz raz lub dwa razy i staje się to nawykiem.

Moim zdaniem największe zmiany przy porównaniu C # z Vb.Net to:

  • nie używamy nawiasów do bloków kodu
  • nie ma potrzeby umieszczania średników na końcu instrukcji
  • nazwy zmiennych nie uwzględniają wielkości liter
  • i kilka innych 😃

&& vs And vs AndAlso

Wszystkie te proste różnice to tylko różnice w składni języka, ale zasadniczo kod pozostaje taki sam. Jednym z moich największych błędów podczas pisania kodu Vb.Net było użycie operatora And jako && z C#. Chodzi o to, że te dwa operatory nie są równoznaczne.

Spójrzmy na przykład. Użyję dwóch metod, które sprawdzają wartość null w instrukcji If i dokonają podmiany ciągu znaków.

    Private Function ReplaceAndAlso(s As String) As String
        If s IsNot Nothing AndAlso s.Contains(":") Then
            Return s.Replace(":", "?")
        End If

        Return s
    End Function

    Private Function ReplaceAnd(s As String) As String
        If s IsNot Nothing And s.Contains(":") Then
            Return s.Replace(":", "?")
        End If

        Return s
    End Function

Różnica pomiędzy And i AndAlso jest następująca:

  • And sprawdzi warunek po prawej stronie, nawet jeśli warunek po lewej stronie jest fałszywy
  • AndAlso nie sprawdzi warunku po prawej stronie, jeśli ten po lewej jest fałszywy

Aby to zobrazować, napisałem testy jednostkowe:

    <TestCase("abc:d", "abc?d")>
    <TestCase(Nothing, Nothing)>
    Public Sub AndAlsoTest(toReplace As String, expected As String)
        ReplaceAndAlso(toReplace).Should().Be(expected)
    End Sub

    <TestCase("abc:d", "abc?d")>
    <TestCase(Nothing, Nothing)>
    Public Sub AndTest(toReplace As String, expected As String)
        ReplaceAnd(toReplace).Should().Be(expected)
    End Sub

A rezultaty są następujące:

AndTest kończy się niepowodzeniem, ponieważ operator Andsprawdza zarówno warunek po lewej, jak i po prawej stronie, co powoduje wyjątek NullReferenceException. Więc jeśli chcesz sprawdzić wartość null w instrukcji If i zrobić coś innego, po prostu użyj AndAlso.

Wymuszenie przekazania zmiennej przez wartość

Może nie jest to duża różnica, ale byłem zaskoczony, że da się to zrobić.

Przyjrzyjmy się prostemu kodowi i dwóm testom:

    <Test>
    Public Sub AddTest()
        Dim a As Integer = 5

        Dim result = Add(a)
        a.Should().Be(6)
        result.Should().Be(6)
    End Sub

    <Test>
    Public Sub AddByValueTest()
        Dim a As Integer = 5

        Dim result = Add((a))
        a.Should().Be(5)
        result.Should().Be(6)
    End Sub

    Public Function Add(ByRef x As Integer) As Integer
        x = x + 1
        Return x
    End Function

Mamy prostą metodę Add, w której zwiększamy podaną wartość o 1. Przekazujemy tę zmienną przez referencję, więc również należy ją zmienić.

Zauważ, że w AddByValueTest przekazujemy (a) i w ten sposób możemy przekazać zmienną przez wartość. Zauważ, że działa to tylko dla typów wartościowych.

A wyniki dowodzą, że tak naprawdę działa:

Zamiana enum na string

Ta różnica mnie zaskoczyła, kiedy ją zobaczyłem i była to ostateczna motywacja do napisania tego postu. Robiłem refaktoryzację wokół Vb.Net z konwersją enum-ów, a mój kolega przeglądał zadanie i powiedział.

– Hej, fajna refaktoryzacja, ale obawiam się, że może nie zadziałać. – powiedział kolega.

– Naprawdę? Uruchomiłem ten kod i działał poprawnie. – Powiedziałem.

– Lepiej sprawdź to jeszcze raz, dla pewności napisałem nawet test jednostkowy. – powiedział kolega.

Rzućmy okiem na ten test. Konwertuję enum na ciąg znaków.

    public enum ProfileType
    {
        Person = 1,
        Company = 2
    }

    [Test]
    public void ConvertEnumTest()
    {
        Convert.ToString(ProfileType.Company).Should().Be("2");
    }

A kiedy go uruchomisz, zawodzi, mój kolega miał rację. 😲

Jednak byłem podejrzliwy i kod, który napisałem był w Vb.Net, a nie w C#, więc napisałem kolejny test. Ale tym razem w Vb.Net.

    <Test>
    Public Sub ConvertEnumTest()

        Convert.ToString(ProfileType.Company).Should().Be("2")

    End Sub

I wiesz co? Przechodzi!

Więc mój oryginalny kod był poprawny. Jednak Convert.ToString() na enum działa inaczej w Vb.Net i C#.

Summary

Praca z Vb.Net brzmi jak powrót do średniowiecza, ale w rzeczywistości dość szybko przyzwyczaiłem się do składni Visual Basic. Możliwości obu są bardzo podobne, ponieważ pod spodem nadal jest ten sam kod, ale C# wydaje mi się nieco bardziej zwięzły i intuicyjny. Może dlatego, że w szkole wolałem C++ od Pascala 😁

Mam nadzieję, że post Ci się podobał, miłego dnia! 😊

Refaktoryzacja przy pomocy refleksji

Czasami zdarza się, że muszę przeprowadzić refaktoryzację, w której Resharper nie może mi pomóc. W moim ostatnim poście opisałem, jak przydatne mogą być wyrażenia regularne przy takiej pracy: Refaktoryzacj przy pomocy wyrażeń regularnych w Visual Studio

Tym razem sprawa jest inna i prosta podmiana nie zadziała w tym przypadku.

Domyślna wartość dla właściwości

Natrafiłem na kod, w którym atrybut DefaultValue był często używany, ale w nie tak, jak powinien. Atrybut DefaultValue jest częścią platformy .Net Framework i jest używany przez generatory kodu w celu sprawdzenia, czy wartość domyślna właściwości jest taka sama jak wartość bieżąca i czy należy wygenerować dla tej właściwości kod. Nie chcę wchodzić w szczegóły, ale możesz zajrzeć do tego artykułu, aby uzyskać więcej informacji.

W moim przypadku musiałem zmienić wszystkie domyślne wartości na  przypisania właściwości podczas tworzenia klasy. Problem polegał na tym, że ta klasa miała ponad 1000 takich właściwości, a zrobienie tego ręcznie nie tylko zajęłoby dużo czasu, ale mogłoby potencjalnie wprowadzić błędy.

Spójrzmy na przykładowy kod:

    public class DefaultSettings
    {
        public DefaultSettings()
        {
            // assignments should be here
        }

        [DefaultValue(3)]
        public int LoginNumber { get; set; }

        [DefaultValue("PrimeHotel")]
        public string HotelName { get; set; }

        [DefaultValue("London")]
        public string Town { get; set; }

        [DefaultValue("Greenwod")]
        public string Street { get; set; }

        [DefaultValue("3")]
        public string HouseNumber { get; set; }

        [DefaultValue(RoomType.Standard)]
        public RoomType Type { get; set; }
    }

Tutaj przydaje się refleksja. Mógłbym łatwo zidentyfikować atrybuty właściwości dla danego typu, ale jak zmienić je w kod?

Napiszmy test jednostkowy! Test jednostkowy to mały fragment kodu, który może korzystać z niemal każdej klasy z testowanego projektu i co najważniejsze – można go łatwo uruchomić. 💪

    [TestFixture]
    public class PropertyTest
    {
        [Test]
        public void Test1()
        {
            var prop = GetProperties();

            Assert.True(true);
        }

        public string GetProperties()
        {
            var sb = new StringBuilder();

            PropertyDescriptorCollection sourceObjectProperties = TypeDescriptor.GetProperties(typeof(DefaultSettings));

            foreach (PropertyDescriptor sourceObjectProperty in sourceObjectProperties)
            {
                var attribute = (DefaultValueAttribute)sourceObjectProperty.Attributes[typeof(DefaultValueAttribute)];

                if (attribute != null)
                {
                    // produce a string
                }
            }

            return sb.ToString();
        }
    }

To prosty kod, który uruchamia metodę GetProperties. Metoda ta pobiera PropertyDescriptorCollection która reprezentuje wszystkie właściwości w klasie DefaultSettings. Następnie dla każdej z nich sprawdzamy, czy zawiera DefaultValueAttribute. Teraz pozostaje tylko wygenerowanie kodu dla każdej właściwości przy użyciu klasy StringBuilder. Domyślasz się jak? Jest to łatwiejsze niż może się wydawać.

Sprawdźmy jak kod będzie się różnił dla poszczególnych typów:

public string GetProperties()
{
    var sb = new StringBuilder();

    PropertyDescriptorCollection sourceObjectProperties = TypeDescriptor.GetProperties(typeof(DefaultSettings));

    foreach (PropertyDescriptor sourceObjectProperty in sourceObjectProperties)
    {
        var attribute = (DefaultValueAttribute)sourceObjectProperty.Attributes[typeof(DefaultValueAttribute)];

        if (attribute != null)
        {
            if (sourceObjectProperty.PropertyType.IsEnum)
            {
                sb.AppendLine($"{sourceObjectProperty.Name} = {sourceObjectProperty.PropertyType.FullName.Replace("+", ".")}.{attribute.Value};");
            }
            else if (sourceObjectProperty.PropertyType.Name.Equals("string", StringComparison.OrdinalIgnoreCase))
            {
                sb.AppendLine($"{sourceObjectProperty.Name} = \"{attribute.Value}\";");
            }
            else
            {
                var value = attribute.Value == null ? "null" : attribute.Value;
                sb.AppendLine($"{sourceObjectProperty.Name} = {value};");
            }
        }
    }

    return sb.ToString();
}

Dla typu enum, musimy wyświetlić przestrzeń nazw(namespace) oraz typ i jego wartość po kropce. Dla ciągów znaków,  musimy wstawić apostrofy, a dla typu nullowalnego, wartością powinno być null. Dla innych musimy tylko przypisać wartość.

Jesteś ciekawy co z tego wyszło? 🤔

Powiedziałbym, że całkiem, całkiem:) 

Kiedy wkleję go do mojego konstruktora, kod będzie wyglądał następująco:

    public DefaultSettings()
    {
        LoginNumber = 3;
        HotelName = "PrimeHotel";
        Town = "London";
        Street = "Greenwod";
        HouseNumber = "3";
        Type = PrimeHotel.Web.Models.RoomType.Standard;
    }

I wiesz co? Działa!

Podsumowanie

Używanie refleksji nie jest najlepszą praktyką, ale jest ona bardzo potężnym narzędziem. W tym przykładzie pokazałem, jak jej użyć i wywołać ten kod w bardzo prosty sposób – jako test jednostkowy. Nie jest to kod, który bym commitował, ale w przypadku podejścia prób i błędów jest świetny. Jeśli natrafiłeś na takie ręczne zadanie w swoim kodzie, spróbuj je zautomatyzować 😎

Mam nadzieję, że nauczyłeś się czegoś dzisiaj i jeżeli tak, to świetnie! 🍺

Nie zapomnij zapisać się na newsletter, aby nie przegapić kolejnych postów. 📣

 

 

Refaktoryzacj przy pomocy wyrażeń regularnych w Visual Studio

Wyrażenia regularne to uniwersalne narzędzie w skrzynce narzędziowej każdego programisty. Jednym z miejsc, w których mogą one być przydatne, są okna dialogowe Quick Find i Quick Replace w Visual Studio. W tym poście pokażę, jak wykorzystać możliwości wyrażeń regularnych w inteligentnej refaktoryzacji.

Zmiana enum na string

Załóżmy, że chciałbym zmienić enum na ciąg znaków w mojej aplikacji, ponieważ zdałem sobie sprawę, że ta właściwość może zawierać wartość, którą użytkownik dodał ręcznie i nie można jej wstępnie zdefiniować. Rzućmy okiem na ten enum:

public enum EventType
{
    Unknown = 0,
    Concert = 1,
    Movie = 2
}

Teraz spróbujmy znaleźć wszystkie zastosowania tego enuma. Do tego zadania używam Quick Find w programie Visual Studio, Ctrl + F.

Czy zauważyłeś niebieski prostokąt w oknie dialogowym Quick Find? Jest to opcja wyszukiwania za pomocą wyrażenia regularnego. Włączmy to i przejdźmy do okna dialogowego Quick Replace. Możesz to zrobić za pomocą przełącznika po lewej stronie lub skrótu Ctrl + H.

Wpisałem wyrażenie EventType\.(\w+), które oznacza że szukam czegoś, co zaczyna się od EventType, później następuje zwyczajna kropka, którą muszę poprzedzić \. Następnie nawiasy, ktore oznaczają, że rozliczynam podgrupę, a /w+ oznacza dowolną literę jeden lub więcej razy. Zamienię to na "$1", czyli standardowe cudzyslowy oraz $1, który jest oznaczeniem pierwszej grupy.

Rezultat jest całkiem niezły:

Możemy nieco dopracować to wyrażenie i dodać nazwę grupy.

Zamieniając (\w+) na  (?<entryType>\w+) nadaję nazwę rezultatom tej grupy, której możemy użyć w Quick Replace.

Podsumowanie

Tworzenie wyrażeń regularnych to proces prób i błędów, w którym trzeba dopracować wyrażenie kilka razy, aż będzie pasowało do tego co potrzebujemy. Rezultaty mogą jednk przerosnąć nasze oczekiwania i przy takiej ręcznej pracy można zaoszczędzić dużo czasu.

Mam nadzieję, że nauczyłeś się dziś czegoś nowego, a jeśli tak, to ten dzień na pewno będzie spoko! 💗 

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 😀

 

 

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! 😄

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.

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! 😉 

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 😊

 

Jak stworzyć relacje w Entity Framework Core 5

Relacje w kontekście bazy danych definiują, w jaki sposób dwie encje są ze sobą powiązane. Entity Framework Core naprawdę błyszczy w sposobie w jaki wspiera relacje. Oferuje konfigurację opartą na konwencji, która skonfiguruje relacje w oparciu o dostarczony model. W bardziej zaawansowanych przypadkach możemy skorzystać z solidnych możliwości Fluent API, które zapewnia większą elastyczność.

Muszę przyznać, że praca z relacjami w Entity Framework Core 5 jest dla programisty bardzo naturalna i być może to właśnie jest jego najważniejsza cecha.

Typy relacji

Relacje w bazie danych będą oznaczać, że dwie encje są ze sobą powiązane. Są logicznie połączone. Spójrzmy na przykład model hotelu:

Mamy rezerwację, która dotyczy jednego pokoju i zawiera listę gości. Do pokoju można przypisać wiele rezerwacji. Profil również można przypisać do wielu rezerwacji, ale jest powiązany tylko z jednym adresem. Mamy tu zdefiniowane 3 różne typy relacji:

  • jeden-do-wielu – pokój do rezerwacji
  • wiele-do-wielu – rezerwacja i profil
  • jeden-do-jednego – profil i adres

Te typy są dobrze obsługiwane przez Entity Framework Core, więc przyjrzyjmy się modelowi, który odpowiada temu schematowi. Oto Reservation:

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

        public int RoomId { get; set; }

        public Room Room { get; set; }

        public List<Profile> Profiles { get; set; }

        public DateTime Created { get; set; }

        public DateTime From { get; set; }

        public DateTime To { get; set; }
    }

Oraz Room:

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

        public int Number { get; set; }

        public string Description { get; set; }

        public DateTime LastBooked { get; set; }

        public int Level { get; set; }

        public RoomType RoomType { get; set; }

        public bool WithBathroom { get; set; }

        public int NumberOfPlacesToSleep { get; set; }
    }

    public enum RoomType
    {
        Standard,
        Suite
    }

Oraz Profile:

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

        public string Ref { get; set; }

        public string Salutation { get; set; }

        public string Forename { get; set; }

        public string Surname { get; set; }

        public string TelNo { get; set; }

        public string Email { get; set; }

        public string Country { get; set; }

        public DateTime? DateOfBirth { get; set; }

        public Address Address { get; set; }

        public List<Reservation> Reservations { get; set; }
    }

I w końcu Address:

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

        public string Street { get; set; }

        public string HouseNumber { get; set; }

        public string City { get; set; }

        public string PostCode { get; set; }

        public int ProfileId { get; set; }

        public Profile Profile { get; set; }
    }

I wisienka na torcie, czyli PrimeDbContext

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

        public virtual DbSet<Address> Address { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
        }
    }

Proszę zwróć uwagę na bardzo ważną rzecz, dzięki konfiguracji opartej na konwencji nie jest wymagana dodatkowa konfiguracja w klasach modelu i klasie PrimeDbContext.

Konfiguracja jest prosta

Czy zauważyłeś, jak łatwo jest skonfigurować relacje w Entity Framework Core 5? Jeśli masz właściwości poprawnie nazwane, EF Core samodzielnie rozpozna twoje relacje. Relacja jest definiowana przez właściwość nawigacyjne, czyli encję wewnątrz innej encji. Spójrz na rezerwację. Istnieje Room, czyli właściwość nawigacyjna, oraz RoomId, który będzie traktowany jako klucz obcy w celu zdefiniowania powiązania.

Istnieją 3 sposoby konfigurowania modelu i relacji:

  • oparta na konwencji – z odpowiednio nazwanymi właściwościami EF Core rozlizna jak jednostki są powiązane
  • data annotations – przydatne atrybuty, które można umieścić we nad właściwością encji
  • Fluent API – w pełni funkcjonalne API do konfigurowania relacji i encji według własnego uznania

Oto przykład data annotations, aby ustawić niestandardowo nazwany klucz obcy:

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogForeignKey { get; set; }

    [ForeignKey("BlogForeignKey")]
    public Blog Blog { get; set; }
}

Jeśli klucz obcy miałby nazwę BlogId, to zostałby skonfigurowany automatycznie, ale nazwa niestandardowa musi być obsłużona ręcznie.

Nawet w przypadku relacji wiele-do-wielu nie ma potrzeby definiowania tabeli łączącej i pisania dodatkowej konfiguracji. Jest to nowa funkcja dostępna od wersji RC1, więc dokumentacja dostępna na oficjalnej stronie Microsoft może wprowadzać w błąd. Miejmy nadzieję, że wkrótce zostanie zaktualizowana.

Na szczęście w większości przypadków nie musisz ręcznie pisać dużej części konfiguracji, ponieważ jest to konieczne głównie podczas radzenia sobie z zaawansowanymi scenariuszami i niestandardowymi mapowaniami.

Podejście Model First

Podejście Model First umożliwia definiowanie modelu i relacji oraz używanie Entity Framework Core do generowania kodu SQL. Wszystko, co musisz zrobić, to stworzyć modele, które chcesz, a kiedy skończysz, po prostu utwórz migrację bazy danych. Przy założeniu, że masz już migracje EF Core.

Wspomniane podejście działa również w przypadku aktualizowania modelu, gdy trzeba dodać powiązaną encję, migracje EF Core poradzą sobie z tym zaskakująco dobrze.

Powiedzmy, że mam encję Profile i chcemy dodać encję Address w relacji jeden-do-jednego. Możesz spojrzeć na kod obu tych klas powyżej. Po dodaniu nowej migracji za pomocą dotnet CLI otrzymuję nową migrację, już wygenerowaną, na podstawie zmian mojego modelu.

    public partial class AddAddress : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Address",
                columns: table => new
                {
                    Id = table.Column<int>(type: "int", nullable: false)
                        .Annotation("SqlServer:Identity", "1, 1"),
                    Street = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    HouseNumber = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    City = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    PostCode = table.Column<string>(type: "nvarchar(max)", nullable: true),
                    ProfileId = table.Column<int>(type: "int", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Address", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Address_Profiles_ProfileId",
                        column: x => x.ProfileId,
                        principalTable: "Profiles",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Address_ProfileId",
                table: "Address",
                column: "ProfileId",
                unique: true);
        }

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

Łatwo i przyjemnie, ale co najważniejsze – działa!

Podsumowanie

Konfigurowanie relacji w Entity Framework Core 5 jest tak proste, jak to tylko możliwe. Większość mapowania może być wykonana automatycznie przez framework, po prostu przez odpowiednie nazwanie właściwości. Jeśli zmagasz się z bardziej zaawansowanymi scenariuszami, możesz skorzystać z Fluent API, które oferuje dużo i a jego zapis jest dość zwięzły. W zaledwie kilku wierszach można zdefiniować na przykład mapowanie widoku na encję.

Moją ulubioną częścią jest jednak podejście Model First, w którym tworzysz model, z którym chcesz pracować, i generujesz kod SQL z migracjami EF Core.

Cały wymieniony tutaj kod jest dostępny na moim GitHubie, kodzie używającym również tych relacji! Zapraszam do odwiedzenia.