Filtrowanie w Entity Framework Plus

Wprowadzenie

Entity Framework jest dość przyjemnym narzędziem do pracy z danymi w aplikacji. Prawie każdy, kto spróbował w nim pracy, nie ma za dużej ochoty wracać do pisania zapytań w czystym SQLu. Ale niestety ma też swoje problemy, o których już nieraz pisałem. Na szczęście mamy takie firmy jak np. ZZZ Project, które tworzą biblioteki rozwiązujące te problemy.

W tym wpisie chciałbym przedstawić Ci kolejną funkcjonalność darmowej biblioteki Entity Framework Plus. Wcześniej pokazałem, w jaki sposób efektywniej aktualizować dane bez ich pobierania oraz jak zapisywać historię zmian danych. Natomiast teraz zobaczymy, jak efektywnie filtrować dane.

Przykład do testów

Zanim przejdę do omawiania kolejnych funkcji, zrobię małe wprowadzenie do projektu, na którym będziemy je testowali.

Jest to mniej więcej ten sam przykład, którego używam w większości wpisów. W testowej aplikacji mamy trzy klasy modelu: BaseModel, Category oraz Product, przy czym kategoria oraz produkt są połączone relacją jeden do wielu:

public class BaseModel
{
public BaseModel()
{
IsActive = true;
}
public int Id { get; set; }
public bool IsActive { get; set; }
}
view raw BaseModel.cs hosted with ❤ by GitHub
public class Category : BaseModel
{
public string Name { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
view raw Category.cs hosted with ❤ by GitHub
public class Product : BaseModel
{
public string Name { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
view raw Product.cs hosted with ❤ by GitHub

Do tego DataContext:

public class DataContext : DbContext
{
public DataContext()
: base("Name=DefaultConnection")
{
}
public DbSet<Category> Categories { get; set; }
public DbSet<Product> Products { get; set; }
}
view raw DataContext.cs hosted with ❤ by GitHub

W przykładzie dodałem również generowanie danych testowych z wykorzystaniem biblioteki Bogus w metodzie Seed, która dodaje 10 kategorii oraz 100 produktów.

Filtrowanie w Entity Framework Plus

Pierwszą funkcjonalnością, którą opiszę, jest filtrowanie. Dzięki niemu możemy łatwiej i efektywniej zarządzać dodatkowymi warunkami, która muszą zostać uwzględnione w zapytaniach. W moich aplikacjach każda klasa modelu ma właściwość IsActive, która określa, czy dane są dostępne, czy może oznaczone jako usunięte. Następnie w zapytaniach dodajemy warunek, w którym sprawdzamy, czy IsActive jest ustawione na true.

Niestety takie podejście powoduje, że wcześniej czy później w jakimś miejscu zapomnimy dodać ten warunek i wtedy aplikacja zwróci błędne dane. Właśnie w tym miejscu przydaje się filtrowanie z Entity Framework Plus.

Filtry możemy skonfigurować zaraz po utworzeniu instancji DataContext. Określamy, dla jakiego typu definiujemy filtr oraz jaki on jest. Później tego warunku nie musimy powtarzać w samym zapytaniu:

public class FilterDemo
{
public void Run()
{
LocalFilter();
}
private void LocalFilter()
{
using (var db = new DataContext())
{
db.Filter<Product>(q => q.Where(p => p.IsActive));
var count = db.Products.Count();
Console.WriteLine($"Products count: {count}");
var category = db.Categories.FirstOrDefault();
count = category.Products.Count;
Console.WriteLine($"Products count in category {category.Name}: {count}");
}
}
}
view raw LocalFilter.cs hosted with ❤ by GitHub

Powyższa metoda dodaje filtr dla klasy Product, który ustawia warunek, aby IsActive miało wartość true. Pobieramy i wyświetlamy liczbę produktów (tych aktywnych). W dalszej części metody pobieramy kategorie z bazy i wyświetlamy liczbę produktów w tej kategorii (korzystamy z właściwości Products oraz lazy loading).

W wyniku działania powyższego kodu na bazie na tabeli Products zostaną wykonane zapytania:

SELECT [GroupBy1].[A1] AS [C1]
FROM (SELECT COUNT(1) AS [A1]
FROM [dbo].[Products] AS [Extent1]
WHERE [Extent1].[IsActive] = 1) AS [GroupBy1]
SELECT [Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[CategoryId] AS [CategoryId],
[Extent1].[IsActive] AS [IsActive]
FROM [dbo].[Products] AS [Extent1]
WHERE ([Extent1].[IsActive] = 1)
AND ([Extent1].[CategoryId] = 1 /* @EntityKeyValue1 - [CategoryId] */)

Widać, że w obu zapytaniach jest dodany warunek dla IsActive, mimo że w samym zapytaniu tego nie ma.

Taki sposób filtrowanie jest bardzo fajny. Szczególnie w przypadku korzystania z dodatkowych właściwości do pobierania danych zamiast korzystania bezpośrednio z DataContext, jak to jest w przypadku drugiego zapytania. O ile w przypadku pierwszego pobrania liczby produktów możemy sobie to opakować w wywołanie repozytorium, tam dodać warunek i zapomnieć o nim, o tyle w drugim przypadku już musimy pamiętać o dodatkowym warunku.

Darmowy kurs Visual Studio

Pracując z setkami programistów, zauważyłem, że większość osób nie pracuje efektywnie w Visual Studio. W skrajnych przypadkach korzystali z kopiowania z wykorzystaniem menu Edit. Wiem, że to dziwne, ale naprawdę niektórzy tak pracują. Dlatego postanowiłem stworzyć kurs Visual Studio – aby pomóc koleżankom i kolegom w efektywniejszej pracy.

Przygotowałem 30 lekcji e-mail, w których pokażę Ci, w jaki sposób pracować efektywniej i szybciej w Visual Studio. Poznasz dodatki, bez których nie wyobrażam sobie pracy w tym IDE.

Po więcej informacji zapraszam na dedykowaną stronę kursu: Darmowy Kurs Visual Studio.

Quiz C#

Ostatnio przygotowałem również quiz C#, w którym możesz sprawdzić swoją wiedzę. Podejmiesz wyzwanie?

Globalne filtry

Powyższy kod dodawał filtrowanie na poziomie instancji pojedynczego obiektu DataContext. Możemy również zrobić to globalnie dla wszystkich instancji. Robi się to bardzo podobnie, różnica polega na tym, czym wywołujemy metodę Filter:

public class FilterDemo
{
public void Run()
{
GlobalFilter();
}
private void GlobalFilter()
{
QueryFilterManager.Filter<Product>(q => q.Where(p => p.IsActive));
using (var db = new DataContext())
{
var count = db.Products.Count();
Console.WriteLine($"Products count: {count}");
}
using (var db = new DataContext())
{
var category = db.Categories.FirstOrDefault();
var count = category.Products.Count;
Console.WriteLine($"Products count in category {category.Name}: {count}");
}
}
}

Efekt działania jest taki sam, dlatego nie będę wrzucał jeszcze raz tego samego SQLa.

Dzięki filtrom globalnym możemy w jednym miejscu zdefiniować filtry dla całej aplikacji bez konieczności określania tego w poszczególnych instancjach.

Filter IsActive jest przykładem takiego warunku, który warto zdefiniować sobie globalnie.

Niektóre filtry są natomiast filtrami typowo lokalnymi – w przypadku tworzenia aplikacji typu multitenant, w której mamy jedną bazę dla wszystkich klientów/aplikacji i w której każdy rekord ma przypisany identyfikator aplikacji. W takiej sytuacji możemy wykorzystać lokalny filtr, który ustawi warunek dla id aplikacji na podstawie zalogowanego użytkownika, gdzie poszczególne warunki między użytkownikami różnią się wartością tego identyfikatora. W takiej sytuacji globalny filtr za bardzo nie zadziała.

Wyłączenie filtrowania

W przypadku gdy korzystamy z filtrów globalnych (ale w lokalnych też to działa), możemy wyłączyć filtrowanie. W przypadku warunku IsActive czasami mamy potrzebę wyłączenia go. Gdy wyświetlamy historyczne zamówienie dla produktu, który został „usunięty”, musimy wyłączyć warunek, aby móc takie dane historyczne pokazać.

Filtr wyłącza się na poziomie zapytania z wykorzystaniem metody AsNoFilter:

public class FilterDemo
{
public void Run()
{
NoFilter();
}
private void NoFilter()
{
QueryFilterManager.Filter<Product>(q => q.Where(p => p.IsActive));
using (var db = new DataContext())
{
var count = db.Products.AsNoFilter().Count();
Console.WriteLine($"Products count: {count}");
}
}
}
view raw NoFilter.cs hosted with ❤ by GitHub

W efekcie otrzymamy zapytanie bez warunku dla IsActive:

SELECT [GroupBy1].[A1] AS [C1]
FROM (SELECT COUNT(1) AS [A1]
FROM [dbo].[Products] AS [Extent1]) AS [GroupBy1]
view raw NoFilter.sql hosted with ❤ by GitHub

Entity Framework Plus umożliwia też bardziej rozbudowane konfigurowanie włączania oraz wyłączania filtrowania. Opcji jest sporo, ale osobiście z nich nie korzystałem, ponieważ nie wiem, czy czasem na dłuższą metę takie podejście za bardzo nie komplikuje utrzymania aplikacji. Polecam przejrzenie dokumentacji, może znajdziesz coś ciekawego dla siebie w tym temacie – https://entityframework-plus.net/query-filter.

Filtrowanie i early loading

Filtrowanie z Entity Framework Plus działa również w przypadku korzystania z metody Include:

public class FilterDemo
{
public void Run()
{
IncludeWithoutFilter();
IncludeWithFilter();
}
private void IncludeWithoutFilter()
{
using (var db = new DataContext())
{
var categories = db.Categories.Include(c => c.Products).ToList();
Console.WriteLine($"Products count: {categories.Count}");
}
}
private void IncludeWithFilter()
{
using (var db = new DataContext())
{
db.Filter<Product>(q => q.Where(p => p.IsActive));
var categories = db.Categories.Include(c => c.Products).ToList();
Console.WriteLine($"Products count: {categories.Count}");
}
}
}
view raw Include.cs hosted with ❤ by GitHub

Pierwsza metoda wykorzystuje Include bez filtra, druga występuje z filtrem dla IsActive. Wykonanie powyższego kodu daje takie zapytania na bazie:

SELECT [Project1].[Id] AS [Id],
[Project1].[Name] AS [Name],
[Project1].[IsActive] AS [IsActive],
[Project1].[C1] AS [C1],
[Project1].[Id1] AS [Id1],
[Project1].[Name1] AS [Name1],
[Project1].[CategoryId] AS [CategoryId],
[Project1].[IsActive1] AS [IsActive1]
FROM (SELECT [Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[IsActive] AS [IsActive],
[Extent2].[Id] AS [Id1],
[Extent2].[Name] AS [Name1],
[Extent2].[CategoryId] AS [CategoryId],
[Extent2].[IsActive] AS [IsActive1],
CASE
WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int)
ELSE 1
END AS [C1]
FROM [dbo].[Categories] AS [Extent1]
LEFT OUTER JOIN [dbo].[Products] AS [Extent2]
ON [Extent1].[Id] = [Extent2].[CategoryId]) AS [Project1]
ORDER BY [Project1].[Id] ASC,
[Project1].[C1] ASC
SELECT [Project1].[Id] AS [Id],
[Project1].[Name] AS [Name],
[Project1].[IsActive] AS [IsActive],
[Project1].[C1] AS [C1],
[Project1].[Id1] AS [Id1],
[Project1].[Name1] AS [Name1],
[Project1].[CategoryId] AS [CategoryId],
[Project1].[IsActive1] AS [IsActive1]
FROM (SELECT [Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name],
[Extent1].[IsActive] AS [IsActive],
[Extent2].[Id] AS [Id1],
[Extent2].[Name] AS [Name1],
[Extent2].[CategoryId] AS [CategoryId],
[Extent2].[IsActive] AS [IsActive1],
CASE
WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int)
ELSE 1
END AS [C1]
FROM [dbo].[Categories] AS [Extent1]
LEFT OUTER JOIN [dbo].[Products] AS [Extent2]
ON ([Extent1].[Id] = [Extent2].[CategoryId])
AND ([Extent2].[IsActive] = 1)) AS [Project1]
ORDER BY [Project1].[Id] ASC,
[Project1].[C1] ASC
view raw Include.sql hosted with ❤ by GitHub

Oba zapytania są bardzo podobne, ale w linijce 49 listingu widać, że pojawia się dodatkowy warunek dla IsActive, którego nie było wcześniej, co jest tutaj bardzo fajne i przydatne. Nie musimy później pisać dodatkowego kodu po stronie aplikacji, który dodatkowo pobiera mniejszą liczbę danych.

Przykład

Na githubie (https://github.com/danielplawgo/EFPlus) znajduje się już klasycznie przykład dla tego wpisu. Po jego pobraniu należy ustawić poprawny connection string w app.confing.

Podsumowanie

Entity Framework Plus jest biblioteką, której nie wypada nie znać (a tym bardziej nie wypada z niej niekorzystać) w momencie, gdy korzystamy z Entity Framework w aplikacji. Mechanizm filtrowania, który pokazałem Ci w tym wpisie, jest bardzo fajny i wygodny. Za jego pomocą możemy znacząco ułatwić sobie pisanie kodu (w szczególności podczas korzystania z lazy/early loading). A dodatkowo zabezpieczymy się przed brakującymi warunkami w aplikacji (np. dla IsActive).

Za tydzień pozostaniemy w temacie Entity Framework Plus. Pokażę Ci pozostałe funkcjonalności, które również warto znać.

Szkolenie Entity Framework Core

Szkolenie Entity Framework Core

Zainteresował Ciebie ten temat? A może chcesz więcej? Jak tak to zapraszam na moje autorskie szkolenie z Entity Framework Core.

1 thought on “Filtrowanie w Entity Framework Plus

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.