Wprowadzenie

Fluent API jest używany do przygotowania konfiuguracji klas domenowych. Konfiguracje te nadpisują domyślne konwencje zaimplementowane we frameworku. Warto również wiedzieć, że Fluent API ma wyższy priorytet niż atrybuty adnotacji danych.

W Entity Framework Core miejscem działającym jako Fluent API jest klasa ModelBuilder. Pozwala ona na dużo bardziej złożoną konfigurację (z uwagi na liczbę dostarczonych opcji) niż data annotations, które wspomniałem w poprzednim wpisie.

Spójrzmy na możliwości konfiguracyjne z wykorzystaniem Fluent API:

  • Konfiguracja modelu do mapowania bazodanowego obejmuje domyślny schemat, funkcje, dodatkowe atrybuty adnotacji danych oraz encje, które mają być wyłączone z mapowania.
  • Konfiguracja encji do postaci tabeli oraz mapowania relacji takich jak jeden do jednego, jeden do wielu, wiele do wielu oraz właściwości takich tak PrimaryKey, AlternateKey czy nazwy tabeli oraz nałożonych indeksów.
  • Konfiguracja właściwości do określenia nazw kolumn, wartości domyślnych, typoów pustych, klucza obcego, typu danych czy współbieżnych kolumn (w bardzo dużym uproszczeniu: jest to tzw. ‘kolumna śledząca’, której możemy użyć do określenia kiedy dany wiersz został zmieniony).

Zanim przejdziemy dalej (do praktycznych przykładów) zapoznamy się z metodami Fluent API udostępnionymi dla poszczególnych obszarów konfiguracji:

Rodzaj konfiguracji Metody Fluent API Użycie
Konfiguracja modelu HasDbFunction() Konfiguracja funkcji bazodanowej (w modelu relacyjnym), która może być wykorzystana przez EF Core do zdefiniowana metod, które zostaną zmapowane do postaci funkcji bazodanowej pozwalając na wykorzystanie jej w zapytaniach LINQ.
HasDefaultSchema() Definicja schematu bazy danych.
HasAnnotation() Dodanie lub aktualizacja atrybutów adnotacji danych w danej encji.
HasSequence() Konfiguracja sekwencji bazy danych przy pracy nad relacyjnym modelem danych - jest to zbiór liczb całkowitych, które są generowane i wspierane przez niektóre systemy bazodanowe w celu tworzenia unikalnych wartości na żądanie.
Konfiguracja encji HasAlternateKey() Konfiguracja klucza zastępczego dla danej encji.
HasIndex() Konfiguracja indeksu dla określonej właściwości.
HasKey() Konfiguracja właściwości lub listy właściwości jako klucz główny.
HasMany() Konfiguracja części relacji (wiele) gdzie encja zawiera referencyjną właściwość kolekcji innego typu dla relacji jeden do wielu lub wiele do wielu.
HasOne() Konfiguracja części relacji (jeden) gdzie encja zawiera referencyjną właściwość innego typu dla relacji jeden do jednego lub jeden do wielu.
Ignore() Konfiguracja dla danej klasy mówiąca iż dana właściwość nie powinna być mapowana do tabeli lub kolumny.
OwnsOne() Konfiguracja relacji w której jednostka docelowa jest własnością tej jednostki.
ToTable() Konfiguracja tabeli do której mapowana jest dana encja.
Konfiguracja właściwości HasColumnName() Konfiguracja nazwy kolumny tabeli bazy danych dla danej właściwości.
HasColumnType() Konfiguracja typu danych danej kolumny w bazie danych dla danej właściwości.
HasComputedColumnSql() Konfiguracja właściwości do mapowania kolumny obliczeniowej w bazie danych (tylko dla relacyjnego modelu bazy danych). Kolumna obliczeniowa to wirtualna kolumna, która nie jest fizycznie przechowywana w tabeli o ile nie została oznaczona jako PERSISTED. Kolumna taka pozwala np. na łącznie wartości dwóch innych kolumn (np. imię i nazwisko) i sprawniejsze wyszukiwanie połączonych danych.
HasDefaultValue() Konfiguracja domyślnej wartości dla kolumny do której mapowana jest właściwość (tylko dla relacyjnego modelu bazy danych).
HasDefaultValueSql() Konfiguracja domyślnej wartości wyrażenia dla danej kolumny do której mapowana jest właściwość (tylko dla relacyjnego modelu bazy danych).
HasField() Określenie pola zapasowego używanego z daną właściwością. Wartość danej właściwości zostanie zapisana/odczytana z innego pola.
HasMaxLength() Konfiguracja maksymalnej długości danych, które mogą być przechowywane w danej właściwości.
IsConcurrencyToken() Konfiguracja właściwości, która ma być używana jako token współbieżności - metoda określa, że dana właściwość powinna być zawarta w klauzuli WHERE w instrukcjach UPDATE lub DELETE w ramach zarządzania współbieżnością (konflikty współbieżności występują, gdy jeden użytkownik pobiera dane w celu ich modyfikacji a następnie innych użytkownik aktualizuje te dane zanim zmiany pierwszego użytkownika zostaną zapisane w bazie danych).
IsRequired() Konfiguracja pozwalająca na określenie czy wartość jest wymagana lub czy wartość null jest również akceptowana.
IsRowVersion() Konfiguracja właściwości, która ma być używana w ramach wykrywania współbieżności. W przypadku wykorzystania metody IsRowVersion dla właściwości tablicy bajtowej (public byte[] RowVersion { get; set; }) oznacza, że dana właściwość powinna być mapowana na typ danych, który zapewnia automatyczne przechowywanie wersji wierszy.
IsUnicode() Konfiguracja właściwości string, która może lub nie zawierać znaki z alfabetu Unicode.
ValueGeneratedNever() Konfiguracja właściwości, która nie może mieć wygenerowanej wartości, gdy encja jest zapisywana.
ValueGeneratedOnAdd() Konfiguracja właściwości, która ma wygenerowaną wartość podczas zapisywania nowej encji.
ValueGeneratedOnAddOrUpdate() Konfiguracja właściwości, która ma wygenerowaną wartość podczas zapisywania nowej lub istniejącej encji.
ValueGeneratedOnUpdate() Konfiguracja właściwości, która ma wygenerowaną wartość podczas zapisywania istniejącej encji.

Fluent API w praktyce

Tak jak wspomniałem we wprowadzeniu, nadpiszemy metodę OnModelCreating oraz wykorzystamy parametr modelBuilder typu ModelBuilder do przygotowania własnej konfiguracji klasy domenowej:

public class ApplicationDbContext : DbContext
{
	public virtual DbSet<CarDefintion> CarDefintion { get; set; }

	public ApplicationDbContext()
	{

	}

	protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
	{
		if (!optionBuilder.IsConfigured)
		{
			// Wymagana paczka: Microsoft.EntityFrameworkCore.Proxies
			optionBuilder
				.UseLazyLoadingProxies()
				.UseSqlServer(@"Server=PAWEL;database=EFCoreCarDefintionDB;Integrated Security=true;MultipleActiveResultSets=true");
		}
	}

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		// W tym miejscu możemy przygotować konfigurację
		// wykorzystując Fluent API

		// W przykładzie wykorzystuje model z poprzednich wpisów
		modelBuilder.Entity<CarDefintion>()
			.Property(c => c.Id)
			.HasColumnName("Id")
			.HasDefaultValue(0)
			.IsRequired();
	}
}

Zwróćcie uwagę, że w powyższym przykładzie konfiguracja właściwości następuje przez wywołanie łańcucha różnych metod. Dokonujemy konfiguracji pola Id, nadajemy mu nazwę przy użyciu HasColumnName, wskazujemy wartość domyślną przy wykorzystaniu HasDefaultValue oraz wskazujemy na konieczność określenia wartości (pole niepuste) przy użyciu metody IsRequired - całej konfiguracji dokonujemy w jednej instrukcji składającej się z wielu metod.

Powyższy zapis (łańcuch metod) jest znacznie czytelniejszy niż osobne instrukcje:

modelBuilder.Entity<CarDefintion>().Property(c => c.Id).HasColumnName("Id");
modelBuilder.Entity<CarDefintion>().Property(c => c.Id).HasDefaultValue(0);
modelBuilder.Entity<CarDefintion>().Property(c => c.Id).IsRequired();
Dodatkowo, w przypadku bardziej złożonych konfiguracji, pojawia się oszczędność czasu wynikająca z użycia metod a nie pisania całych (osobnych) instrukcji.

W kolejnych wpisach wykorzystamy Fluent API do konfiguracji różnych typów relacji.