LINQ - jak na dynamickou podmínku
LINQ asi není potřeba nijak detailně představovat. Asi každý .NET vývojář se již setkal s LINQem, nejčastěji ve formě dotazu, kde byla jeho podoba již programově dána a měnit se mohl snad jen nějaký parameter dotazu:
public void Linq1()
{ int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var lowNums = from n in numbers
where n < 5
select n;
foreach (var x in lowNums)
{ Console.WriteLine(x);
}
}
Poznámka: tento příklad byl převzat ze stránky 101 LINQ Samples - výborný zdroj pro studium.
Jak to ale řešit, pokud chceme podmínku sestavit dynamicky, například provozujeme realitní server a chceme jej rozšířit o službu, která na základě zaslaných podmínek v textové podobě vrátí seznam vyhovujících nemovitosti?
Databáze nemovitostí
Řešení se pokusím vysvětli v tomto článku. Příklad bude maximálně zjednodušen, proto nebudeme dělat žádné rozhraní webové služby, ale spokojíme se s výstupem na konzolu. Též náše realitní databáze bude maximálně zjednodušena a u každého objektu si budeme držet jen tři údaje:
- název místa, kde nemovitost leží
- typ nemovitosti (výběr z číselníku)
- počet ložnic
Class diagram pak vypádá takto:
Naším vstupem pak bude řetězec ve tvaru: Název vlastnosti:přípustné hodnoty|Název vlastnosti: přípustné hodnoty,
Při předání řetězce "Location:Humpolec, Polna|PropertyType:Castle,Flat,Chateu,TerraceHouse|Bedrooms:1-20" budeme očekávat seznam všech bytů, zámků a hradů s dvěmi až dvaceti ložnicemi nacházejícími se v lokalitě Polná a Humpolec. Při předání řetězce "Bedrooms:5-10|PropertyType:Castle" pak zase seznam všech hradů s pěti až deseti ložnicemi.
Potřebovali bychom tedy nějak dynamicky sestavit omezující podmínku výběru, neboli filtr, neboli predikát - tedy pokud bych použil příklad ze začátku článku, potřebujeme dynamicky sestavit část "where n < 5".
To vyřešíme tak, že si definujeme metody pro sestavení každého filtru (predikátu), který bychom mohli potřebovat. Náš případ je zjednodušený - máme tři vlastnosti podle kterých chceme vyhledávat, budeme tedy definovat tři metody pro:
- získání predikátu omezujícího na základě lokality
- získání predikátu omezujícího na základě typu nemovitosti
- získání predikátu omezujícího na základě rozsahu počtu ložnic
Příprava - převedení řetězce na hodnotu z číselníku
Požadovaný typ nemovitosti je předáván ve formě řetězce, kde jsou jednotlivé typy oddělené čárkou. Tento řetězec musíme převést na kolekci hodnot našeho enumerátoru. K tomu nám poslouží statická funkce:
public static IEnumerable<PropertyType> GetPropertyTypesFromString(string types)
{ List<PropertyType> propertyTypes = new List<PropertyType>();
PropertyType propertyType;
var rawPropertyTypes = types.Split(",".ToCharArray());
foreach (string rawPropertyType in rawPropertyTypes)
{ propertyType = (PropertyType) Enum.Parse(typeof(PropertyType), rawPropertyType);
propertyTypes.Add(propertyType);
}
return propertyTypes;
}
Metody pro získání predikátů
Bude se jednat o statické metody. Pro získání predikátu omezujího na základě lokality nemovitosti použijeme tuto statickou metodu:
public static Expression<Func<IRealProperty, bool>> IsInLocation(string locations)
{ var predicate = PredicateBuilder.False<IRealProperty>();
var rawLocations = locations.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
foreach (string location in rawLocations)
{ string tempLocation = location.Trim();
predicate = predicate.Or(p => p.Location.Contains(tempLocation));
}
return predicate;
}
Pokud by snad někoho zarážel příkaz string tempLocation = location.Trim(); - je tam jednoduše proto, že LINQ věci se vykonávají v okamžiku, kdy jsou použity, ne kdy jsou definovány. Pokud bychom tento řádek vynechaly a další řádek použili ve tvaru predicate = predicate.Or(p => p.Location.Contains(location)); byla by použita hodnota proměnné location v okamžiku volání, tedy v tomto případě poslední název lokace. Díky příkazu string tempLocation = location.Trim(); je tedy pro každou podmínku použita vlastní proměnná držící jedinečnou hodnotu lokace v okamžiku použití.
V příkladu jsou použiti metody třídy PredicateBuilder - to je pomocná třída pro sestavování predikátů. Pokud bych to popsal polopaticky, tak předchozí funkce sestaví podmínku (v případě, že vyhledávácí řetězec obsahoval "Location:Humpolec, Polna|PropertyType:Castle,Flat,Chateu,TerraceHouse|Bedrooms:1-20" :
podmínka = False Or Location='Humpolec' Or Location='Polna'
Třídu PredicateBuilder jsem si vypůjčil ze stránky Dynamically Composing Expression Predicates - lze ji v různých obdobách nalézt i jinde, ale myslím, že tohle byl původní zdroj-
Podobně by vypadaly i metody pro omezení dle typu nemovitosti:
public static Expression<Func<IRealProperty, bool>> IsOfType(string types)
{ var predicate = PredicateBuilder.False<IRealProperty>();
var propertyTypes = GetPropertyTypesFromString(types);
foreach (PropertyType propertyType in propertyTypes)
{ PropertyType tempType = propertyType;
predicate = predicate.Or(p => p.Type ==tempType);
}
return predicate;
}
a i metoda omezující dle poču ložnic:
public static Expression<Func<IRealProperty, bool>> HasBedroomCount(int min, int max)
{ var predicate = PredicateBuilder.False<IRealProperty>();
predicate = predicate.Or(p => p.BedroomCount >=min && p.BedroomCount<=max);
return predicate;
}
Zpracování vyhledávací podmínky
Po definici jednotlivých metod již stačí jednoduše zpracovat předávaný řetězec a jednotlivé metody zkombinovat. Pro jednoduchost nejsou přidány žádné kontroly formální správnosti předávaného řetězce - spoléháme se, že tvar bude dodržen:
string searchConditions = "Location:Humpolec, Polna|PropertyType:Castle,Flat,Chateu,TerraceHouse|Bedrooms:1-20";
var conditions = searchConditions.Split("|".ToCharArray());
var predicate = PredicateBuilder.True<IRealProperty>();
foreach (string condition in conditions)
{ var keyValuePair = condition.Split(":".ToCharArray());
switch (keyValuePair[0])
{ case "PropertyType":
predicate = predicate.And(RealProperty.IsOfType(keyValuePair[1]));
break;
case "Bedrooms":
var bedroomRange = keyValuePair[1].Split("-".ToCharArray()); int min= int.Parse(bedroomRange[0]);
int max = int.Parse(bedroomRange[1]);
predicate = predicate.And(RealProperty.HasBedroomCount(min, max));
break;
case "Location":
predicate = predicate.And(RealProperty.IsInLocation(keyValuePair[1]));
break;
}
}
Provedení dotazu
Nyní stačí získaný předpis jen zkompilovat, použít v LINQ dotazu a výsledek vypsat:
Func<IRealProperty, bool> SatisfiesSearchConditions = predicate.Compile();
var query = from property in propertis
where SatisfiesSearchConditions(property)
select property;
foreach(RealProperty property in query)
{ Console.WriteLine(property.ToString());
}
A je to.
Závěr
Snažil jsem ukázat, jak lze dynamicky konstruovat omezovací podmínky LINQ dotazu. V některém volném pokračování ukáži jak lze podobně provést i seřazení výsledků a i reálné použítí. Ačkoliv byl tento článek založen na zjednodušeném příkladě, myslím že není problém přenést principy zde ukázané jinam.
Zdrojový kód příkladu je jako obvykle přiložen v zip balíčku.
Na závěr tedy ještě přehledně dva odkazy zmíněné v článku, kde lze nalézt další informace a studijní materiál: