Wprowadzenie

W Entity Framework Core pojawił się nowy typ właściwości o nazwie Shadow. W polskim tłumaczeniu będą to właściwości cienia.

Właściwości cienia to specjalny rodzaj właściwości, które nie są bezpośrednio definiowane w klasie encji – zamiast tego są one konfigurowane dla określonego typu encji w modelu danych encji. Ich konfiguracji dokonujemy w metodzie OnModelCreating() klasy kontekstowej wykorzystując Fluent API.

Spójrzcie na diagram ilustrujący powyższy opis: Schemat przedstawiający dostępność właściwości cienia

Doskonale na nim widać to co napisałem powyżej – właściwości cienia nie są częścią klasy reprezentującej encję. Mogą one zostać skonfigurowane tylko dla konkretnego typu jednostki podczas tworzenia modelu danych jednostki (Entity Data Model). Musimy jednocześnie pamiętać, że właściwości te są mapowane na kolumnę bazy danych. Wartość i stan Shadow Property jest przechowywana wyłącznie w module śledzenia zmian, tj. klasa ChangeTracker, którą omówiłem w poprzednim wpisie.

Po co to wszystko? Wyobraźmy sobie, że musimy zachować dane utworzenia i aktualizacji każdego rekordu tabeli w bazie danych. Co więcej, chcemy, żeby aktualizacja tych wartości odbywała się automatycznie za każdym razem kiedy zapisujemy zmiany w naszym kontekście (metoda SaveChanges()). Z pomocą przychodzą nam właściwości cienia.

Spójrzmy na klasę:

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

	public string FullName { get; set; }

	public DateTime BirthData { get; set; }

	public float Weight { get; set; }
}
Powyższa klasa Person nie zawiera właściwości CreatedDate i UpdatedDate, które pozwalałyby nam na przechowywanie informacji dotyczącej daty utworzenia i aktualizacji. Zamiast jawnych właściwości zdefiniujemy je jako właściwości cienia dla encji PersonV2.

Właściwości cienia

Definicja właściwości cienia dla danej encji jest możliwa przy wykorzystaniu Fluent API wewnątrz OnModelCreating() przy wykorzystaniu metody Property():

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	// Konfiguracja właściwości cienia
	modelBuilder.Entity<PersonV2>()
		.Property<DateTime>("CreatedDate");

	modelBuilder.Entity<PersonV2>()
		.Property<DateTime>("UpdatedDate");
}

Metoda Property() jest wykorzystywana do konfiugracji właściwości cienia. Określamy typ danych oraz nazwę. Jeżeli ta ostatnia będzie zgodna z nazwą istniejącej właściwości, EF Core dokona rekonfiguracji istniejącej właściwości jako właściwość cienia – nie dojdzie do utworzenia nowej właściwości.

Właściwość cienia w bazie danych

Wraz z definicją właściwości cienia musimy dokonać aktualizacji schematu bazy danych ponieważ musi dojść do zmapowania do odpowiedniej kolumny bazy danych. W tym celu wykonamy znane (z poprzednich wpisów) polecenia: add-migration oraz update-database.

Po wykonaniu powyższych kroków możemy podejrzeć wygenerowaną/zaktualizowaną tabelę: Tabela bazy danych z wygenerowanymi właściwościami cienia

Użycie właściwości cienia

Wartość właściwości cienia może zostać pobrana lub ustawiona za pomocą metody Property() obiektu EntityEntry:

using(var context = new ApplicationDbContext())
{
	var person = new Person() { FullName = "Shadow Property Sample" };

	// ustawienie wartości właściwości cienia
	context.Entry(person).Property("CreatedDate").CurrentValue = DateTime.Now;

	// odczytanie wartości
	var createdDate = context.Entry(person).Property("CreatedDate").CurrentValue;
}

We wstępie wspomniałem o automatycznym ustawianiu wartości a nie podejściu manualnym. Zanim przejdziemy dalej dokonamy odpowiednich zmian w metodzie SaveChanges() tak, żebyśmy nie musieli ustawiać wartości ręcznie dla każdego obiektu danej encji. W tym celu przechodzimy do klasy kontekstowej i nadpisujemy domyślną implementację metody SaveChanges():

public override int SaveChanges()
{
	// pamiętajcie o paczce: System.Linq
	// pobieramy zmodyfikowane encje o odpowiednich stanach: Added | Modified 
	var entries = ChangeTracker.Entries()
		.Where(a => a.State == EntityState.Added || a.State == EntityState.Modified);

	foreach (var entry in entries)
	{
		// Sprawdzamy czy encja jest zgodna z modelem do którego dodaliśmy właściwości cienia
		// Jeżeli nie zobaczymy błąd: System.InvalidOperationException
        // 'The property '(nazwa właściwości)' could not be found. 
        // Ensure that the property exists and has been included in the model.'
		if (entry.Metadata.Name == "EFCoreCarDatabase.Model.PersonV2")
		{
			entry.Property("UpdateDate").CurrentValue = DateTime.Now;

			if (entry.State == EntityState.Added)
			{
				entry.Property("CreatedDate").CurrentValue = DateTime.Now;
			}
		}
	}

	// bazowe wywołanie metody SaveChanges po wprowadzeniu naszych zmian
	return base.SaveChanges();
}
Powyższy kod pozwala na automatyczne ustawienie wartości CreatedDate i/lub UpdatedDate w zależności od operacji, które wykonaliśmy na danej encji.

Przejdźmy do testów i wykonajmy poniższy kod:

using(var context = new ApplicationDbContext())
{
	var person = new PersonV2() { FullName = "Test scenario" };
	context.Add(person);

	context.SaveChanges();
}
Efektem wykonania kodu będzie dodanie rekordu do bazy danych oraz automatyczne ustawienie wartości kolumn CreatedDate oraz UpdatedDate: Automatyczna aktualizacja właściwości cienia Takie podejście pozwala na nieuwzględnianie (dodatkowych) właściwości w klasie encji.

Właściwości cienia dla wszystkich encji

W powyższym przypadku dokonaliśmy konfiguracji właściwości cienia jedynie dla jednej encji. W metodzie SaveChanges() musieliśmy dodać zabezpieczenie, aby uniknąć wyjątku System.InvalidOperationException: 'The property '(nazwa_właściwości)' could not be found. Ensure that the property exists and has been included in the model.'.

Nic jednak nie stoi na przeszkodzie w konfiguracji właściwości cienia dla wszystkich encji. Możemy tego dokonać w poniższy sposób:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var allEntities = modelBuilder.Model.GetEntityTypes();

    foreach (var entity in allEntities)
    {
	    entity.AddProperty("CreatedDate", typeof(DateTime));
	    entity.AddProperty("UpdatedDate", typeof(DateTime));
    }
}

Kiedy używać właściwości cienia?

Pierwszym przykładem jest scenariusz omówiony powyżej, tj. nie chcemy ujawniać kolumn bazy danych na mapowanych encjach (ograniczamy widoczność pewnych właściwości dostępnych w ramach naszego kontekstu).

Właściwości cienia mogą być również przydatne gdy pracujemy na klasach encji dostępnych w ramach zewnętrznych komponentów (paczek) – nie mamy możliwości wprowadzenia zmian w klasach (modelach). Możemy wówczas dodać dodatkowe pole(a) do modelu definiując je jako Shadow Property.