Wprowadzenie

Do tej pory wszystkie operacje wykonywaliśmy w obrębie zdefiniowanego kontekstu. DbContext był w pełni świadomy wszelkich modyfikacji, dodawania czy kasowania danych dzięki czemu EntityState był modyfikowany automatycznie w celu wykonania odpowiedniej operacji CRUD na bazie danych.

W tym wpisie poznamy podejście w tzw. scenariuszu rozłączenia w którym kontekst nie jest świadomy zachodzących operacji – wymagane są dodatkowe kroki związane z ustawieniem odpowiedniego stanu EntityState.

Spójrzcie na poniższy diagram przedstawiający wykonanie operacji dodawania/aktualizacji/kasowania na bazie danych: EF Core: scenariusz rozłączenia

Jak doskonale widzicie zmiany dokonywane na poszczególnych encjach nie są śledzone przez DbContext - EntityState nie jest automatycznie modyfikowany. Przeprowadzając takie operacje musimy dokonać dołączenia encji do DbContext wraz z odpowiednim stanem nowej encji. Dopiero ten krok będzie skutkował wykonaniem polecenia INSERT, UPDATE lub DELETE po wywołaniu metody SaveChanges().

Powyższe kroki są jasne. Sprawdźmy jak takie operacje wyglądają w praktyce:

// Odłączona encja
var employee = new Employee() { FullName = "Paweł" };

using(var context = new ApplicationDbContext())
{
	// Dołączenie encji do kontekstu
	// EntityState: Add
	context.Add<Employee>(employee);

	// Podejście alternatywne
	context.Employee.Add(employee);
	context.Entry<Employee>(employee).State = EntityState.Added;
	context.Attach<Employee>(employee);

	// Zapisanie zmian
	context.SaveChanges();
}

W powyższym przykładzie tworzenie nowego użytkownika odbywa się poza kontekstem. Metoda Add pozwala na dołącznie encji do kontekstu oraz zmianę stanu na Added. Metoda SaveChanges() spowoduje wywołanie instrukcji INSERT na określonej bazie danych:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Employee] ([FullName])
VALUES (@p0);
SELECT [Id]
FROM [Employee]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(4000)',@p0=N'Paweł'

Dostępne metody

Zanim przejdziemy do omówienia różnego typu operacji zapoznamy się z metodami dostępnymi w obrębie DbContext (DbSet udostępnia ten sam zestaw metod), które pozwalają na dołączenie encji do kontekstu oraz wykonywanie operacji INSERT, UPDATE oraz DELETE. Spójrzmy na poniższą tabelę:

Typ operacji Metoda DbContext Opis
INSERT DbContext.Attach Dołączenie encji do DbContext. Stan Unchanged dla encji, której klucz posiada wartość lub stan Added dla encji o pustej lub domyślnej wartości klucza.
DbContext.Add Dołączenie encji do DbContext ze stanem Added.
DbContext.AddRange Dołącznie kolekcji encji do DbContext ze stanem Added.
DbContext.Entry Dostęp do informacji o śledzaniu zmian i operacji dla wskazanej encji. (Brak odpowiednika dla DbSet)
DbContext.AddAsync Asynchroniczna metoda dołączająca encję do DbContext ze stanem Added oraz rozpoczęcie śledzenia zmian (dla scenariusza rozłączenia). Dane zostaną wstawione do bazy danych po użyciu metody SaveChangesAsync().
DbContext.AddRangeAsync Asynchroniczna metoda dołączająca kolekcję encji do DbContext ze stanem Added oraz rozpoczęcie śledzenia zmian (dla scenariusza rozłączenia). Dane zostaną wstawione do bazy danych po użyciu metody SaveChangesAsync().
UPDATE DbContext.Update Dołączenie encji do DbContext ze stanem Modified.
DbContext.UpdateRange Dołączenie kolekcji encji do DbContext ze stanem Modified.
DELETE DbContext.Remove Dołączenie wskazanej encji do DbContext ze stanem Deleted oraz rozpoczęcie śledzenia zmian.
DbContext.RemoveRange Dołączenie kolekcji lub listy encji do DbContext ze stanem Deleted oraz rozpoczęcie śledzenia zmian.

Podsumowanie

Scenariusz rozłączenia jest niezwykle powszechny w aplikacjach webowych gdzie kontekst dla każdego żądania jest nowy a obiekty przekazywane są z zewnętrznego źródła. W takiej sytuacji sami musimy ustawić stan encji tak, aby kontekst wiedział czy ma dokonać aktualizacji, dodania czy usunięcia danych.

Prostym przykładem dla scenariusza połączonego (operacji wykonywanych w ramach kontekstu) może być pobranie danych wszystkich pracowników z bazy danych a następnie aktualizacjach tych danych (z jakiegokolwiek powodu):

// Pobranie listy wszytkich pracowników
public void ChangeEmployeeName()
{
	try
	{
		// Wszelkie zmiany dokonywane w obrębie kontekstu
		using (var context = new ApplicationDbContext())
		{
			var employeeList = context.Employee.ToList();

			foreach (var emp in employeeList)
			{
				emp.FullName = $"(Updated: {emp.FullName})";
			}

			context.SaveChanges();
		}
	}
	catch (Exception ex)
	{
		// Add Exception Logging
	}
}

Przeciwieństem będzie scenariusz rozłączony w którym dane dotyczące pracownika będą przekazane do metody z warstwy prezentacji naszej aplikacji:

// Dane pracownika do aktualizacji przekazane z warstwy prezentacji
public bool UpdateEmployeeName(Employee emp)
{
	try
	{
		using (var context = new ApplicationDbContext())
		{
			// Rozpoczęcie śledzenie zmian z domyślnym stanem 'Unchanged'
			context.Employee.Attach(emp);

			// Zmiana stanu na 'Modified'
			context.Entry(emp).State = EntityState.Modified;

			// Zapisanie zmian -> wykonanie operacji UPDATE na bazie danych
			context.SaveChanges();
		}

		return true;
	}
	catch (Exception ex)
	{
		// Add Exception Logging
	}

	return false;
}

W trzech kolejnych wpisach przejdziemy bardziej szczegółowo przez scenariusz rozłączenia skupiając się na operacjach INSERT, UPDATE oraz DELETE.