Tag Archives: PrimeHotel

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

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.

How to configure relationships in Entity Framework Core 5

Relationships in a database context define how two entities relate to each other. Entity Framework Core really shines in supporting relationships. It offers a convention-based configuration, that will configure relationships based on the model provided. For more advanced cases we can use robust Fluent API capabilities, that offer greater flexibility.

I must admit that working with relationships in Entity Framework Core 5 feels very natural for a developer and that might be simply its most important feature.

Types of relationships

Relationships in the database will mean, that two entities relate to each other. They are logically connected. Let’s take a look at a hotel model example:

We have a reservation, that has a single room and a list of guests. The room can be assigned to many reservations. A profile can be assigned to many reservations, but it has only one address related. We have 3 different types of relationships defined here:

  • one-to-many – room to a reservation
  • many-to-many – reservation to profile
  • one-to-one – profile to address

Those types are nicely supported by Entity Framework Core, so let’s have a look at the model that corresponds to this schema. Here is a 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; }
    }

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

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

And finally, 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; }
    }

And a cherry on top, a 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)
        {
        }
    }

Please notice a very important thing, thanks to a convention-based configuration, there is no additional configuration needed in the model classes and PrimeDbContext class.

Configuration is easy

Have you noticed how easy it is to configure relationships in Entity Framework Core 5? If you have properties named correctly, then EF Core will deduct relationships on its own. A relationship is defined by a navigation property, that is an entity inside an entity. Look at the Reservation. There is a Room, that is a navigation property and there is a RoomId that will be treated as a foreign key to define a constraint. 

There are 3 ways to configure model and relationships:

  • convention-based – with properly named properties, EF Core will deduct how entities are related
  • data annotations – handy attributes that you can put on the entity property
  • Fluent API – a fully-featured API for configuring relations and entities as you wish

Here is an example of a data annotation to set a non-standard named foreign key:

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

If the foreign key would be named BlogId, then it would be configured automatically, but a custom name has to be handled manually.

Even with many-to-many relationships there is no need to define a joining table and write additional configuration. It is a new feature available from RC1 version, so documentation available on the official Microsoft page can be misleading. Hopefully, it will be updated soon.

Luckily, in most cases, you would not need to write much of configuration manually, cause it is mainly required when coping with advanced scenarios and custom mappings.

Model First approach

Model First approach lets you define your model and relationships and use Entity Framework Core to generate SQL for you. All you need to do is to create models that you want and when you’re done, just create a database migration. This is true of course when you already have EF Core migrations in place. 

It also works for updating the model, when you need to add a related entity, EF Core migrations will handle that surprisingly well.

Let’s say I had a profile entity and I wanted to add an Address entity in a one-to-one relationship. You can take a look at the code of those both classes above. When I add a new migration from dotnet CLI, I get a new migration like this, already generated for me, based on my model changes.

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

Nice and easy, but what’s most important – it works!

Summary

Configuring relationships in Entity Framework Core 5 is as simple as it can possibly be. Most of the mapping can be done automatically by the framework, just by naming properties right. If you’re struggling with more advanced scenarios you can go with Fluent API, which offers a lot and is rather compact. In just a few lines you can define how to map a view to an entity.

My favorite part, though, is a Model First approach, where you create a model that you would like to work with and generate SQL with EF Core migrations. 

All code mentioned here is available on my GitHub, code that uses those relationships as well! Feel free to drop by.

Wykonanie polecenia SQL w Entity Framework Core 5

Entity Framework Core 5 to lekki, rozszerzalny i wieloplatformowy ORM typu open source. Jest łatwy w zastosowaniu i sprawia, że dostęp do bazy danych jest bardzo prosty. Jednak czasami praca z tabelami i widokami po prostu nie wystarczy. Jak wykonać surowy skrypt SQL za pomocą Entity Framework Core 5? Dowiedzmy Się.

Wykonanie polecenia SQL

Uruchomianie SQL bez mapowania wyniku jest dość proste. Spójrz na ten przykład:

var sql = "UPDATE Profiles SET Country = 'Poland' WHERE LEFT(TelNo, 2) = '48' AND Id > @p0";
await primeDbContext.Database.ExecuteSqlRawAsync(sql, parameters: new[] { minimalProfileId.ToString() });

Ten kod SQL aktualizuje kolumnę Country na podstawie TelNo dla profili z identyfikatorem wyższym niż podany. To tylko kilka linijek kodu i działa idealnie! Pokazuje również, jak możemy przekazać parametr do polecenia SQL, ale możesz także sformatować go za pomocą nawiasów klamrowych.

Wykonanie procedury składowanej

Procedura składowana jest doskonałym przykładem kodu SQL, który możesz chcieć uruchomić bezpośrednio w bazie danych, ale zachować SQL po stronie bazy danych. Załóżmy, że masz już procedurę składowaną o nazwie UpdateProfilesCountry z jednym parametrem. Jeśli chcesz go po prostu wykonać, możesz napisać taki kod:

await primeDbContext.Database.ExecuteSqlRawAsync(
    "UpdateProfilesCountry @p0",
    parameters: new[] { minimalProfileId.ToString() });

Nie musimy mapować wyników, więc możemy użyć metodyDbContext.Database.ExecuteSqlRawAsync i przekazać potrzebne parametry.

Jeżeli interesuje Cię ten proces dokładnie, na przykładzie, to opisałem to wszystko tutaj: Wykonanie procedury składowanej w Entity Framework Core.

Wykonanie procedury składowanej z wynikiem

Podczas gdy procedura składowana lub zwykłe polecenie SQL można wykonać bezpośrednio na poziomie bazy danych, zwrócenie wyniku jest nieco bardziej skomplikowane. Przede wszystkim musisz dodać model nie posiadający klucza, aby zmapować wyniki. Powiedzmy, że mamy procedurę składowaną GetGuestsForDate i chcielibyśmy zwrócić model o nazwie GuestArrival.

[Keyless]
public class GuestArrival
{
    public string Forename { get; set; }

    public string Surname { get; set; }

    public string TelNo { get; set; }

    public DateTime From { get; set; }

    public DateTime To { get; set; }

    public int RoomNumber { get; set; }
}

Musimy także dodać kolekcję do naszej klasy PrimeDbContext.

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

    // from stored procedures
    public virtual DbSet<GuestArrival> GuestArrivals { get; set; }
}

Teraz możemy iść dalej i użyć go. Oto, jak możemy to osiągnąć:

var guests = primeDbContext.GuestArrivals.FromSqlRaw($"GetGuestsForDate '{date}'").ToList();

A jeśli umieszczę go w moim projekcie API, zmapuje wyniki na encje:

Zauważ, że aby odwzorować wyniki na obiekty, musimy użyć kolekcji DbSet.

Jeśli chcesz uzyskać pełny obraz, przeczytaj osobny artykuł: Pobranie danych za pomocę procedury składowanej w Entity Framework Core 5

Podsumowanie

Wykonanie polecenia SQL w Entity Framework Core 5 jest nie tylko możliwe, ale także zaskakująco łatwe. Możesz wykonywać SQL na poziomie bazy danych, ale jeśli zależy Ci na zwracanym rezultacie, musisz dodać DbSet reprezentujący Twoje wyniki.

Cały zamieszczony tutaj kod można znaleźć na moim GitHubie, czeka żeby z nim poeksperymentować 🙂

Pozdrawiam!

Executing raw SQL with Entity Framework Core 5

Entity Framework Core 5 is an open-source, lightweight, extensible, and a cross-platform ORM. It is easy to apply and it makes database access super simple. However, sometimes working with tables and views is just not enough. How to execute raw SQL script with Entity Framework Core 5? Let’s find out.

Running the raw SQL

Running a SQL without carrying about the result is quite easy. Have a look at this example:

await primeDbContext.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE Profiles SET Country = 'Poland' WHERE LEFT(TelNo, 2) = '48' AND Id > {minimalProfileId}");

This SQL updates the Country based on TelNo column for profiles with Id higher then the one provided. It is just a few lines of code and it works perfectly! It also shows how we can pass a parameter to SQL, but you also can format it with curly braces.

Executing a stored procedure

A stored procedure is a perfect example of a SQL, that you might want to run directly on the database, but keep SQL on the database side. Let’s say you already have a stored procedure named UpdateProfilesCountry with one parameter. If you would just like to execute it, you could simply have a code like this:

await primeDbContext.Database.ExecuteSqlInterpolatedAsync(
    $"UpdateProfilesCountry {minimalProfileId}");

You don’t need a DbSet to map the results, so you can use DbContext.Database.ExecuteSqlRawAsync and pass parameters if you’d like to.

If you’d like to get the full picture, read the separate article: Execute a stored procedure with Entity Framework Core 5.

Running a stored procedure with a result

While running a stored procedure or a plain SQL command can be done directly on the database level, returning the result is slightly more complicated. First of all, you need to add a keyless model to map your results. Let’s say we have a stored procedure GetGuestsForDate and we would like to return a model named GuestArrival.

[Keyless]
public class GuestArrival
{
    public string Forename { get; set; }

    public string Surname { get; set; }

    public string TelNo { get; set; }

    public DateTime From { get; set; }

    public DateTime To { get; set; }

    public int RoomNumber { get; set; }
}

We also need to add a DbSet to our PrimeDbContext.

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

    // from stored procedures
    public virtual DbSet<GuestArrival> GuestArrivals { get; set; }
}

Now we can go ahead and use it. Here is how we can accomplish that:

var guests = primeDbContext.GuestArrivals.FromSqlInterpolated($"GetGuestsForDate '{date}'").ToList();

And if I place it in my API project, it will map results to entities:

Notice that to map results to objects, we need to use DbSet collection.

If you’d like to get the full picture, read the separate article: Select data with a stored procedure with Entity Framework Core 5

Summary

It is not only possible but also surprisingly easy to run any SQL on the database with Entity Framework Core 5. You can execute SQL on a database level, but when you care about the result, you need to add a DbSet representing your results.

All code mentioned here can be found on my GitHub, feel free to experiment with it.

Cheers!

 

Widoki w Entity Framework Core 5

Widok w kontekście baz danych jest wirtualną tabelą opartą na zestawie wyników uzyskanych przez wykonanie zapytaniem SQL. Są one zwykle używane jako obiekty tylko do odczytu, które są zoptymalizowane pod kątem dostarczania danych dla danego scenariusza. Entity Framework Core 5 obsługuje widoki, a w tym artykule pokażę, jak to działa.

Dodanie widoku

Pierwszą rzecz jaką musimy zrobić, to dodać widok do bazy danych. Najlepszym sposobem na to jest dodanie migracji bazy danych z odpowiednim kodem SQL. Zacznijmy od dodania migracji za pomocą polecenia narzędzia globalnego EF Core:

dotnet ef migrations add vwGuestArrivals

To polecenie wygeneruje migrację, w której możemy umieścić nasz SQL. Zobaczmy, jak to może wyglądać:

public partial class vwGuestArrivals : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        var sql = @"
            CREATE OR ALTER VIEW [dbo].[vwRoomsOccupied] AS
                SELECT r.[From], r.[To], ro.Number As RoomNumber, ro.Level, ro.WithBathroom
                FROM Reservations r
                JOIN Rooms ro ON r.RoomId = ro.Id";

        migrationBuilder.Sql(sql);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"DROP VIEW vwRoomsOccupied");
    }
}

Ten widok przedstawia zajęte pokoje, które możemy filtrować według daty. Tego rodzaju dane mogą być przydatne na przykład podczas planowania konserwacji i prac remontowych.

Pobieranie danych z widoku

W Entity Framework Core 5 widoki mogą być reprezentowane jako zwykła kolekcja DbSet. W moim przypadku, aby zmapować wszystkie kolumny widoku, musimy stworzyć model RoomOcupied, który wygląda tak:

[Keyless]
public class RoomOccupied
{
    public DateTime From { get; set; }

    public DateTime To { get; set; }

    public int RoomNumber { get; set; }

    public int Level { get; set; }

    public bool WithBathroom { get; set; }
}

Teraz musimy dodać DbSet do mojego PrimeDbContext i musimy skonfigurować nasz model, aby RoomsOccupied był wykonywany na konkretnym widoku. Zobaczmy, jak można to osiągnąć:

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

    public virtual DbSet<RoomOccupied> RoomsOccupied { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<RoomOccupied>(eb =>
            {
                eb.HasNoKey();
                eb.ToView("vwRoomsOccupied");
            });
    }
}

Jak widać, jest to normalna kolekcja DbSet, na który można filtrować jak jak chcemy. Jest jednak drobiazg, który wyróżnia tę kolekcję. Zwróć uwagę, że konfigurujemy encję RoomOccupied tak, aby nie miała klucza. W ten sposób nie musimy mieć klucza w wyniku, ale oznacza to również, że byłby to model tylko do odczytu.

Obecnie Entity Framework Core 5 nie obsługuje aktualizowania widoku, chociaż jest to możliwe w bazie danych SQL Server. Możesz jednak zdefiniować widok, który posiada klucz. Pamiętaj tylko, aby usunąć HasNoKey w konfiguracji i atrybut [Keyless] w encji.

Użyjmy kodu, który właśnie napisaliśmy. Aby to zrobić w najprostszy możliwy sposób, dodałem metodę do mojego interfejsu API ASP.NET Core. Oto jak to wygląda:

[HttpGet("GetRoomsOccupied")]
public IActionResult GetGuestArrivalsFromView([FromQuery] string date)
{
    var parsedDate = DateTime.ParseExact(date, "dd-MM-yyyy", CultureInfo.InvariantCulture);
    var rooms = primeDbContext.RoomsOccupied.Where(r => r.From <= parsedDate && r.To >= parsedDate);

    return Ok(rooms);
}

Tutaj podaję datę w formacie dd-MM-rrrr i pytam o wszystkie pokoje zajęte w tym dniu. Oto wynik.

Używam vwRoomsOccupied i wykonuje zapytanie SQL z zastosowanymi wszystkimi filtrami. W SQL Server Profiler możemy spojrzeć na SQL, który został wykonany.

Zauważ, że w tym przykładzie używamy tylko dat bez czasu i wszystko działa dobrze. Jeśli jednak chcesz porównać również daty z czasem, musisz zastosować nieco inne podejście.

Podsumowanie

Entity Framework Core 5 bezbłędnie obsługuje widoki. Musisz go skonfigurować w swojej klasie DbContext i określić, że określona jednostka zostanie zamapowana na widok. Gdy to zrobisz, możesz użyć DbSet, jak chcesz, a wszystkie filtry zostaną zastosowane bezpośrednio do wygenerowanego kodu SQL.

Co więcej, możesz obsłużyć dodawanie lub aktualizowanie widoku za pomocą migracji EF Core, co oznacza, że wszystkie wymagane prace można wykonać za pomocą EF Core.

Cały pokazany tutaj kod można znaleźć na moim GitHubie, możesz spokojnie z nim eksperymentować, zachętam!

 

Working with views in Entity Framework Core 5

In SQL, a view is a virtual table based on the result-set of an SQL statement. They are typically used as read-only objects that are optimized to provide data for a given scenario. Entity Framework Core 5 can handle views and in this article, I’m going to show you how.

Adding a view

First of all, we need to add a view to the database. The best way to do so is to add a database migration with an appropriate SQL. Let’s start by adding a migration with EF Core global tool command:

dotnet ef migrations add vwGuestArrivals

This will generate a migration, that we can put our SQL into. Let’s see how it may look:

public partial class vwGuestArrivals : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        var sql = @"
            CREATE OR ALTER VIEW [dbo].[vwRoomsOccupied] AS
                SELECT r.[From], r.[To], ro.Number As RoomNumber, ro.Level, ro.WithBathroom
                FROM Reservations r
                JOIN Rooms ro ON r.RoomId = ro.Id";

        migrationBuilder.Sql(sql);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.Sql(@"DROP VIEW vwRoomsOccupied");
    }
}

This view presents rooms that are occupied, where we can filter by date. This kind of data can be useful for example when planning maintenance.

Getting view data

In Entity Framework Core 5 views can be represented as a regular DbSet. In my case, to map all view’s columns, we need to create a RoomOcupied model, that would look like this:

[Keyless]
public class RoomOccupied
{
    public DateTime From { get; set; }

    public DateTime To { get; set; }

    public int RoomNumber { get; set; }

    public int Level { get; set; }

    public bool WithBathroom { get; set; }
}

Now we need to add a DbSet to my PrimeDbContext and we need to configure our model, so that RoomsOccupied will be executed against view. Let’s see how that can be accomplished:

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

    public virtual DbSet<RoomOccupied> RoomsOccupied { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<RoomOccupied>(eb =>
            {
                eb.HasNoKey();
                eb.ToView("vwRoomsOccupied");
            });
    }
}

As you can see it is a normal DbSet, that can be queried as we wish. However, there is a tiny detail that makes this collection different. Notice that we configure RoomOccupied entity to have no key. This way we do not need to have a key in the result, but it also means that it would only be read-only model.

Currently, Entity Framework Core 5 does not support updating the view, while it is possible in SQL Server database. You can specify a view with a key, though. Just remember to remove HasNoKey in configuration and [Keyless] attribute in the entity.

Let’s use the code that we just wrote. To do so the easiest possible way, I just added a method to my ASP.NET Core API. Here is how it looks like:

[HttpGet("GetRoomsOccupied")]
public IActionResult GetGuestArrivalsFromView([FromQuery] string date)
{
    var parsedDate = DateTime.ParseExact(date, "dd-MM-yyyy", CultureInfo.InvariantCulture);
    var rooms = primeDbContext.RoomsOccupied.Where(r => r.From <= parsedDate && r.To >= parsedDate);

    return Ok(rooms);
}

Here I’m passing date in dd-MM-yyyy format and list all occupied rooms at the given date. Here is the result.

It uses a vwRoomsOccupied and executes a SQL query with all filters applied. We can take a look in SQL Server Profiler at the SQL that was executed.

Note that in this example we are using only dates with no time and it works fine. However, if you would like to compare dates with time as well, you would need to use a slightly different approach.

Summary

Entity Framework Core 5 can handle views flawlessly. You need to configure it in your DbContext class and specify that a specific entity will be mapped to the view. When that’s done, you can use a DbSet as you wish and all filters will be applied directly into generated SQL.

What’s more, you can handle adding or updating the view with EF Core migrations, which means that all work required can be done with EF Core.

All code mentioned here can be found on my GitHub, feel free to experiment with it.

Cheers!

 

Scalanie migracji w Entity Framework Core 5

Podczas pracy z szybko rozwijającym się projektem zmiany zachodzą szybko nie tylko w kodzie projektu, ale także w schemacie bazy danych. Dzieje się tak zwłaszcza podczas pracy nad mikroserwisem od samego początku, kiedy często zmienia się jego przeznaczenie.

Jak działają migracje w EF Core 5

W migracjach Entity Framework Core 5 dodajemy migrację jako zmiany pomiędzy naszą klasą DbContext a istniejącą [nazwa DbContext] ModelSnapshot. Podczas generowania nowej migracji narzędzie CLI wygeneruje tylko różnice między tymi dwoma bytami i umieści je w dwóch metodach: Up i Down. W pierwszej nastąpi zmiana dotycząca zastosowania migracji, aw drugiej usunięcia migracji.

Po zastosowaniu migracji jej nazwa jest zapisywana w tabeli __EFMigrationsHistory.

Scalanie wielu migracji

Powiedzmy, że po wielokrotnej zmianie schematu na wczesnych etapach nasz projekt jest teraz stabilny. Mamy kilka migracji, które można by połączyć w jedną, która raz stworzyłaby model, bez wielu małych aktualizacji.

Scalanie kiedy możemy wszystko usunąć

Najłatwiejszym sposobem scalenia wszystkich migracji byłoby usunięcie wszystkiego! A dokładnie mam na myśli taki proces:

  • usuń katalog Migrations ze wszystkimi migracjami
  • wyczyść tabelę __EFMigrationHistory
  • usuń wszystkie tabele i inne obiekty bazy danych, które zostały dodane podczas migracji
  • utwórz nową migrację ze wszystkimi zmianami

Jest to drastyczny sposób scalania migracji, ponieważ utracimy wszystkie dane. Jest to jednak bardzo proste i może w niektórych przypadkach spełniać swoją rolę.

Scalanie, kiedy chcemy zachować dane

Kiedy chcemy zachować dane, nie możemy usunąć wszystkich już utworzonych obiektów bazy danych, ale możemy scalić pliki migracji w naszym kodzie. Zobaczmy, jak można to zrobić.

  • Usuń wszystkie skrypty migracji z folderu Migrations
  • Dodaj nową migrację za pomocą polecenia dotnet ef migrations add MergedMigration
  • Skopiuj cały plik i wyczyść obie metody Up i Down
  • Zaktualizuj bazę danych i zastosuj migrację MergedMigration za pomocą polecenia dotnet ef database update
  • Następnie zamień zawartość pliku MergedMigration na wcześniej wygenerowany kod

W rezultacie będziesz mieć tylko jeden plik migracji. W moim przykładzie tabela __EFMigrationHistory wygląda następująco.

A teraz w Visual Studio widzę tylko jedną migrację.

Zawiera ona zmiany ze wszystkich moich poprzednich migracji, które zostały scalone.

Zadziałało!

Wskazówka! Możesz również nazwać scaloną migrację tak jak pierwszą, która została już zastosowana, aby nie trzeba było aktualizować bazy danych i dodawać jej do tabeli __EFMigrationHistory.

Nie zadziała za każdym razem

Migracje możemy łatwo łączyć, gdy są stosowane tylko do kontrolowanej przez nas bazy danych. Ponadto ten proces nie będzie działać w środowiskach, w których nie zastosowano wszystkich scalonych migracji. Proces jest łatwy, ale są pewne rzeczy, które należy zwrócić uwagę.

Z drugiej strony, czy musimy zachować wszystkie migracje, nawet jeśli wiemy, że nigdy więcej ich nie uruchomimy? Nie sądzę. W tym miejscu byłoby wspaniale połączyć stare migracje, ale zostawić te najnowsze. Można to osiągnąć w bardzo podobny sposób.

  • Przywróć ostatnie N migracji lokalnie, jedna po drugiej i przenieś je gdzieś
  • Przywróć stan projektu w miejscu pasującym do migracji (git checkout w odpowiednim miejscu w historii)
  • Scal wszystkie istniejące migracje
  • Przywróć stan projekt do najnowszej wersji
  • Dodawaj po kolei zapisane najnowsze migracje

W takim przypadku zachowamy najnowsze migracje i utworzymy dużą migrację początkową, która byłaby zgodna z Twoim projektem. Jeśli wystąpi przypadek, w którym wszystkie migracje będą musiały zostać zastosowane do bazy danych, schemat bazy danych nie zostanie uszkodzony.

Podsumowanie

Scalanie migracji w Entity Framework Core 5 jest możliwe i mogę powiedzieć, że jest to zaskakująco łatwe. Obejmuje jednak proces automatycznego generowania i musisz sprawdzić, czy migracja scalona działa dokładnie tak samo, jak wszystkie migracje stosowane pojedynczo. Co więcej, istnieje więcej niż jeden sposób łączenia migracji i musisz wybrać ten, który jest dla Ciebie najbardziej odpowiedni.

Cały kod zamieszczony tutaj jest dostępny na moim GitHubie, więc możesz go pobrać i bawić się nim. Spójrz również na ten post, jak go uruchomić: PrimeHotel – jak uruchomić projekt.

Dzięki za czytanie i do zobaczenia ponownie 🙂

Merging migrations in Entity Framework Core 5

When working with a fast-evolving project changes happen rapidly not only to the project’s code but also to the database schema. It happens especially when working on a micro-service from the start when its purpose shifts.

How EF Core 5 migrations work

In Entity Framework Core 5 migrations we are adding a migration as a delta changes between our DbContext class and existing [DbContext name]ModelSnapshot. When generating new migration a CLI Tool will generate only the differences between those two and put them in two methods: Up and Down. In the first one, there will be a change to apply the migration and in the second one, to remove the migration.

After the migration is applied, its name is noted in the __EFMigrationsHistory table.

Merging multiple migrations 

Let’s say that after changing a schema multiple times in the early stages our project is now stable. We have a bunch of migrations that could be merged into one, that would create a model once, without many small updates. 

When we can remove everything

The easiest way to merge all migrations would be removing everything! What I mean is:

  • remove Migrations directory with all migrations
  • clear __EFMigrationHistory table
  • remove all tables and other database objects that were added via migrations
  • create new migration with all changes

This is a drastic way of merging migrations because we will lose all the data. However, it’s super simple and it might work in some cases.

When we need to preserve data

This means that we cannot remove all already created database objects, but we can merge migration files in our code. Let’s see how that can be done.

  1. Delete all migration scripts from Migrations folder
  2. Add a new migration with command dotnet ef migrations add MergedMigration
  3. Copy the entire file and clear both Up and Down methods
  4. Update the database and apply MergedMigration migration with command dotnet ef database update
  5. After that, replace the content of the MergedMigration file with earlier generated code

As a result, you will have only one migration file. In my example the __EFMigrationHistory table looks like this.

And now in the Visual Studio, I can see only on migration.

That contains changes from all my previous migrations merged together.

It worked!

Tip! You can also name you merged migration as the first one, that was already applied so that you wouldn’t have to update the database and add it to the __EFMigrationHistory table. 

It will not work in every case

We can easily merge migrations when they are applied only to the database that we control. I addition, it will not work for environments that don’t have all merged migrations applied. The process is easy, but there are certain things that need to be taken into account.

On the other hand, do we need to keep all migrations, even when we know that we won’t run them ever again? I don’t think so. At this point, it would be great to merge old migrations but leave the newest ones. It could be accomplished with a very similar process.

  1. Revert last N migrations locally, one by one, and move them somewhere
  2. Check out the project in the place matching the migrations
  3. Merge all existing migrations
  4. Check out the project in the newest state
  5. Add saved newest migrations, one by one

In this case, we will keep the newest migrations and create a big initial migration, that would be consistent with your project. If there will be a case, where all migrations would need to be applied to a database, the database schema wouldn’t be broken.

Summary

Merging migrations in Entity Framework Core 5 is possible and I can say it’s surprisingly easy. However, it involves an automatic generation process and you need to check if merged migration does exactly the same thing as all migrations applied one by one. Moreover, there is more than one way to merge migrations and you need to choose the one that is best for you.

All code posted here is available at my GitHub, so you can download it and play with it. Take a look also at this post on how to run it: PrimeHotel – how to run this project.

Thanks for reading and hopefully see you again 🙂