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; } | |
} |
public class Category : BaseModel | |
{ | |
public string Name { get; set; } | |
public virtual ICollection<Product> Products { get; set; } | |
} |
public class Product : BaseModel | |
{ | |
public string Name { get; set; } | |
public int CategoryId { get; set; } | |
public virtual Category Category { get; set; } | |
} |
Do tego DataContext:
public class DataContext : DbContext | |
{ | |
public DataContext() | |
: base("Name=DefaultConnection") | |
{ | |
} | |
public DbSet<Category> Categories { get; set; } | |
public DbSet<Product> Products { get; set; } | |
} |
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}"); | |
} | |
} | |
} |
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.
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}"); | |
} | |
} | |
} |
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] |
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}"); | |
} | |
} | |
} |
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 |
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ć.
1 thought on “Filtrowanie w Entity Framework Plus”