Czasem może nas spotkać sytuacja, że bardzo potrzebujemy stworzyć zapytanie do Entity Framework, które musi dynamicznie reagować na nasze wymagania i w jednym konkretnym przypadku będzie potrzebowało dodatkowego Where(). Oczywiście, możemy napisać oddzielne zapytania i warunek, ale istnieje odrobinę sprytniejszy sposób.
Zacznijmy od nakreślenia prostego kontekstu sytuacji. Załóżmy, że coś sprzedajemy i na przykład w poniedziałki nie chcemy, by była widoczna część naszej oferty. Nie ma to najmniejszego sensu i akurat ten przykład da się rozwiązać w nieco inny i lepszy sposób, ale do zobrazowania idei jak najbardziej wystarczy.
Standardowym zapytaniem zwracającym produkty w te piękniejsze dni tygodnia byłoby poniższe kilka linijek opakowane w jakąś metodę:
1 2 3 4 5 6 7 8 |
var result = new List<Product>(); using (var context = new DBContext()) { result = context.Products .Where(p => p.Available == true) .ToList(); } return result; |
Te samo zapytanie wykonywane w poniedziałek wyglądałoby następująco:
1 2 3 4 5 6 7 8 9 10 |
var result = new List<Product>(); using (var context = new DBContext()) { result = context .Products .Where(p => p.Available == true) .Where(p =>p.HiddenOnMondays == false) .ToList(); } return result; |
Co możemy z tym zrobić? Najprostsze rozwiązanie przychodzi mi do głowy niemalże od razu:
1 2 3 4 5 6 7 8 9 10 11 |
var isMonday = DateTime.Now.DayOfWeek == DayOfWeek.Monday; var result = new List<Product>(); using (var context = new DBContext()) { result = context .Products .Where(p => p.Available == true) .Where(p =>p.HiddenOnMondays == isMonday) .ToList(); } return result; |
Nie chodzi nam jednak o to, bo o ile w tym przypadku jest to proste, możemy spotkać się z inną sytuacją. Sęk w tym, że naszym zamiarem jest umieszczenie w zapytaniu lub usunięcie z niego tego jednego Where(), w zależności od tego jaki mamy dzień tygodnia. W tym celu możemy wykorzystać to jak zachowuje się LINQ, a w zasadzie to cokolwiek co korzysta z Fluent API, o którym pisałem kiedyś w tym poście. Chodzi o to, że przy łączeniu metod w łańcuchy, zwracany jest nam cały czas jeden i ten sam obiekt. Możemy więc pokusić się o taką konstrukcję, którą celowo rozdrobniłem na najdrobniejsze etapy, w których budowane jest całe zapytanie:
1 2 3 4 5 6 7 8 9 10 |
var result = new List<Product>(); using (var context = new DBContext()) { var query = context.Products; //query = typ IQueryable zawierający wszystkie produkty query = query.Where(p => p.Available == true); // query = typ IQueryable zawierający wszystkie produkty, które są dostępne if (DateTime.Now.DayOfWeek == DayOfWeek.Monday) query = query.Where(p => p.HiddenOnMonday == false); //Ten element dodawany jest w poniedziałki result = query.ToList(); //W zależności od dnia tygodnia, w tym miejscu możemy mieć dwa rózne zapytania } return result; |
Takich warunków możemy mieć bardzo dużo i dzięki temu możemy dosyć dynamicznie dodawać lub usuwać fragmenty zapytania, do Entity Framework. Najfajniejsze w tym wszystkim jest to, że do momentu wywołania metody .ToList() operujemy na IQueryable, więc nasz ORM skonstruuje nam jedno zapytanie do bazy.
A co jeżeli mamy różne dziwne kaprysy w każdym możliwym dniu tygodnia? I nie chcemy robić bajzlu w kodzie, a koniecznie chcemy mieć ustawione 7 warunków na każdy pojedynczy dzień? Oczywiście moglibyśmy rzucić jakimś switchem i zupełnie nie przejmować się tym jak to wygląda. Moglibyśmy też całość opakować w jakąś ładną metodę rozszerzającą. Na przykład taką:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public static IQueryable<Product> TodayIs(this IQueryable<Product> query, DayOfWeek day) { switch (day) { case DayOfWeek.Monday: return query.Where(p => p.HiddenOnMonday == false); case DayOfWeek.Tuesday: return query.Where(p => p.HiddenOnTuesday) == false); case DayOfWeek.Wednesday: return query.Where(p => p.HiddenOnWednesday == false); //etc. } } |
A wykorzystanie jej w kodzie mogłoby wyglądać następująco:
1 2 3 4 5 6 7 8 9 10 |
var result = new List<Product>(); using (var context = new DBContext()) { result = context .Products .Where(p => p.Available == true) .TodayIs(DateTime.Now.DayOfWeek) .ToList(); } return result; |
Mam nadzieję, że tym prostym przykładem udało mi się w miarę treściwie pokazać, jak można modyfikować zapytanie w trakcie jego budowania.
PS. Fajnie jest wrócić do technicznych treści 😉
EDIT: Na twitterze wywiązała się bardzo fajna dyskusja i faktycznie zapomniałem wspomnieć o tym, że typ IQueryable wymaga ostrożności i generalnie nie powinien być wyprowadzany poza repozytorium/inną warstwę dostępu do danych. Więcej w poście Jakuba Skoczenia tutaj.
EDIT2: Na blogu Pasja Programowania pojawiła się bardzo ciekawa odpowiedź na powyższego posta z nieco ładniejszym rozwiązaniem. Zachęcam do przeczytania tutaj.