Repository vs. IQueryable vs. sorting a paging
Stránkovanie, sortovanie etc. výsledkov query metód by malo byť záležitosťou prezentačnej vrstvy, nie otázkou domain(business) modelu. Vzhľadom na vznik interface IQueryable sa začali hromadiť články, ktoré obhajujú exposenutie Linqovského IQueryable prezentačnej vrstve s týmto zámerom. V reakcii na tieto blog posty prichádzajú zase ďalšie, ktoré správne argumentujú tým, že sa stráca jasnosť a sila kontraktu a že používateľ modelu okrem sortovania može vyrobiť akékoľvek query resp. že má takto prístup priamo ku databáze.(s tým sa samozrejme spája aj security riziko). Jedno z načastejšie sa vyskytujúcich riešení s ktorými som sa stretol je zamaskovanie IQueryable cez castnutie za IEnumerable. Spomínané podstatné problémy s IQueryable to však nerieši.
Rieši ich takýto prístup, ktorý ma napadol pri diskusii s Vlkom. Včera mi to prišlo vhod, pretože presne ku IQueryable dielme sa dopracovali kolegovia vrámci svojho projektu. Myšlienku som podsunul a dotiahol v conzolovke do prototypu.
public class QueryResult<T>
{
private readonly IQueryable<T> _queryable;
//private IList<T> _result;
public QueryResult(IQueryable<T> queryable)
{
_queryable = queryable;
}
public QueryResult<T> Slice(int from, int count)
{
return new QueryResult<T>(_queryable.Skip(from).Take(count).AsQueryable());
}
public QueryResult<T> OrderBy<TKey>(Func<T, TKey> query)
{
return OrderBy(query, SortOrder.Ascending);
}
public QueryResult<T> OrderBy<TKey>(Func<T, TKey> query, SortOrder order)
{
IOrderedEnumerable<T> result = order == SortOrder.Ascending
? _queryable.OrderBy(query)
: _queryable.OrderByDescending(query);
return new QueryResult<T>(result.AsQueryable());
}
public int Count()
{
return _queryable.Count();
}
public List<T> ToList()
{
return _queryable.ToList();
}
public static implicit operator List<T>(QueryResult<T> result)
{
return result.ToList();
}
}
public enum SortOrder
{
Ascending,
Descending
}
Príklad používania by potom mohol vyzerať takto:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public static QueryResult<Person> BusinessMethod()
{
List<Person> persons = new List<Person>(100);
for (int i = 0; i < 100; i++)
persons.Add(new Person
{
FirstName = "Fero" + i,
LastName = "Mrkvička" + i
});
return new QueryResult<Person>(persons.AsQueryable());
}List<Person> allPersons = BusinessMethod();
Console.WriteLine("Count: {0}", allPersons.Count);
Console.WriteLine("------");
List<Person> persons = BusinessMethod()
.OrderBy(x => x.LastName, SortOrder.Descending).Slice(2, 5);
foreach (Person person in persons)
Console.WriteLine(person.FirstName + " "+ person.LastName);
Console.WriteLine("------");
QueryResult<Person> result = BusinessMethod();
int total = result.Count();
persons = result.OrderBy(x => x.LastName, SortOrder.Descending).Slice(2, 5);
foreach (Person person in persons)
Console.WriteLine("{0} {1}", person.FirstName, person.LastName);
Console.WriteLine("{0} records of total {1}", persons.Count, total);
Jeden problém však predsalen zostáva.Repository je v pozícii boundary medzi domain modelom(business vrstvou) a infraštruktúrou. Ak našu QueryResult triedu vystavíme v tejto podobe, limitujeme infraštrukúru pod repository na takú, ktorá má implementovaného Linq providera.(ano, nezdá sa mi to OK, napriek tomu, že interface je súčasťou .NET framework libiek)
Mimochodom, to je ďalšia výhrada voči prístupom spomínaným na začiatku článku. Naše repository by preto mali vystaviť von len interface a postarať sa o implementáciu.(hoci s využítím, tak ako v mojom príklade, práve IQueryable<T> ak to infraštruktúra umožňuje/podporuje). OK a odtiaľto to začína trošku krívať. IQueryResult, ISliceable, ISortable, ICountable ... sú cool interfaces, z ktorých by sme mohli vyskladať naše result triedy. Výsledkom by boli nakoniec ale len nejaké babylon triedy, ktoré by kombinovali vždy niekoľko features. Ak postavíme GUI komponenty tak, že budú vedieť priamo obsluhovať takéto interfaces, má význam ich implementovať z tohoto hľadiska. V praxi sa dopracujeme väčšinou ku jednej result triede so všetkými features a ak teda je dôvod vyskladať ho z interfaces v duchu tých, ktoré som načtol, nech sa tak stane, neskúšajme ísť cestou ... SlicableSortableQueryResult + SlicableSortableCountableQueryResult ... ;-)