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 ... ;-)

Bookmark and Share
Zaradené do: , ,

Komentáre

# vlko said:

add "repository na takú, ktorá má implementovaného Linq providera": nad akoukolvek IEnumerable je mozno zavolat extension metodu .AsQueryable(), ale popravde, co uz dnes nema linq provider?:)

este ma zaujalo toto: public QueryResult<T> OrderBy<TKey>(Func<T, TKey> query, SortOrder order)

nebolo by lepsie z pohladu prehladnosti kodu pouzit tak ako v .net (teda minimalne ako dalsiu funkciu):

public QueryResult<T> OrderByDescending<TKey>(Func<T, TKey> query)

Thursday, May 07, 2009 10:21 AM
# T said:

@vlko:

Toto je IMHO hranicna diskusia...

Napr. web services, ktore mozem volat z domain layeru(casom prerobim cast domain modelu tak, aby ziskavali informacie z externeho zdroja)...ak sa bude domain layer a gui spoliehat na queryable tak implementaciu na web services nezmenim...(vtedy odpadaju vsetky vrstvy nizsie, pocnuc repository) ...nemusim chciet pouzit linq pod repositories napr. koly performance, alebo koly usenutiu starsieho modelu a dovodov(marginalnych) vygenerujem x...  

--------

Nemam na to vyhraneny nazor, vsetky tri sposoby sa mi zdaju OK. Zda sa mi v poriadku zgroupnut relevatne veci do jedneho volania v chaine, ale ked uz, tak potom skor cestou Reverse, co vytvaraja najflexi interface.

QueryResult<T>.OrderBy().Reverse().Slice()

QueryResult<T>.Slice().Reverse()

a etc.

...aj ked to uz je vacsi narok na inteligenciu linq providera ako sa vysporiada s chainutymi volaniami...

Thursday, May 07, 2009 3:58 PM
# vlko said:

add webservices: no mozes pouzit moj ExpressionEval Two [blog.vyvojar.cz/.../expressioneval-two-the-lambda-case.aspx] a linq si jednoducho zoserializujes:)

Thursday, May 07, 2009 4:34 PM
# T said:

nemozem a nechcem ;-) Pointa preco je v diskusii ku blog postu o Data Service ;-)

Thursday, May 07, 2009 4:47 PM
# vlko said:

Ak myslis REST, tak to je len implementacia, nie je problem to prerobit na SOAP, ak ti ide o nejaku separaciu, tak neviem preco, ked uz to bude raz v nejakom repository raz zaobalene.

Thursday, May 07, 2009 9:09 PM
# T said:

Nejde o protokol(to som sa v tej diskusii snazil povedat tiez), ide o filozofiu interface. Nechcem exposeovat univerzalne metody, ale len facade a jasne/konkretne/silne interfaces, presne v zmysle SOA.

Friday, May 08, 2009 1:49 PM
# vlko said:

Ok, to beriem, nemozes ovplyvnovat query na serveri. Ale to stale nebrani pouzivaniu iQueryable v repository, ved ho mas aj tak skryty cez QueryResult, staci uz iba vytvorit QueryObjekt, ktory bude navonok vystupovat ako samostatna entita bez iQueryable a v pripade prechodu z local pristupu na server pristup ho staci cez ioc replacnut nejakym inym objektom, ktory nebude volat iQueryable, ale priamo potrebny webservice.

Friday, May 08, 2009 2:30 PM
# vlko said:

No este popisem ako by vyzeral taky moj idealny genericky repository, ktory bude ale resolvovany cez IoC container

QueryResult - ako popisujes ty

QueryObjekt - obsahuje iba Execute metodu, interne pouziva Repository nad ktorom vola IQueryable a vracia QueryResult

Repository<T> - ma CRUD operacie + resolvuje QueryObjekt cez jeho typ a do konstruktora lezie dictionary povolenych QueryObjektov s typom ako identom

Ako by sa to potom pouzivalo?

Takze get repository

// uz tu cez ioc urcujes ktore objekty umoznuju pouzivat repository

var repository = IoC.GetRepository<Person>();

repository.Update(..)/Delete(..)/Get(..)/Insert(..)

// a teraz select

QueryResult result = repository.Query<IPersonValidQuery>.Execute(from, to);

Ze PersonValidQuery ma vola interny IQueryable z Repository<Person> nas nemusi zaujimat. Dolezite je ze:

- vieme obmedzit cez ioc zoznam objektov pre ktore je repository povolene

- cez ioc vieme ovplyvnovat zoznam QueryObjektov a tak az prejdeme z local implementacie IPersonValidQuery na tu ktora vola remote webservice, mozeme to bez rekompilacie projektu len zmenou configu ioc kontajnera

Friday, May 08, 2009 2:43 PM
# T said:

@vlko:

skryty queryResult - ano, to je jedna implmentacia queryResultu. Samozrejme, mozes pouzit IoC, to vobec nie je zly napad. Implementacia s IQueryable je pomerne univerzalna cez roznu infrastrukturu pod repository(tu su na mieste tie argumenty o linqProvideroch) a aj s web services je mozne riesenenie, ktore spominas, staci ak web service metody maju v ramci interface paging(bohuzial, tu nevidim take pekne riesenie, ako v pripade priameho volania modelu). Problem bude ale so zmysluplnou implementaciou countu.

---"Repository<T> - ma CRUD operacie + resolvuje QueryObjekt cez jeho typ a do konstruktora lezie dictionary povolenych QueryObjektov"

Keby si to generic repository pouzil tak, ze ho v pripade implementacie kazdeho konkretneho repistory pouzijes len interne...vystavis von len tie metody, ktore maju zmysel(CRUD) pripadne im das nazov specificky podla konktextu(nie user.Create ale user.Register, nie user.Update ale user.ChangePassowrd, user.ChangeProfile a pod.)...co sa tyka query objektu...

Repository acts like in memory collections...to znamena, ze mas kontext vzdy jedneho konkretneho domain objektu...cize zmysel v posielani query objektov nevidim, vidim zmysel v posielani criterii(to je ale diskusia, ktoru ozivil Ayende a tuto temu dalej rozvadza Greg Young...konci tym, ze ak mam naozaj vela specifickych queries(aj z hladiska lazy/eager politiky) tak nema zmysel zanasat tuto komplexnost do modelu, cez ktory robim write ale urobit specificky model pre queries a tam je miesto pre tento Tvoj pristup uplne dokonaly..query command separacia)

(trochu mimo...zaujimavy dosledok pre MVC by bol napr. taky, ze Controller by videl len Write model a View by pracovalo s Query modelom...a kedze pri query modely netreba riesit behavior, liezli by z neho DTO objekty)

Inak mohol by si napisat blog clanocek s nejakym console samplom, nech to vidim cele, mozno mi nieco uslo. V kazdom pripade nevidim v tom nejake "zlo"...preto ma to zaujima...len mam predstavu, aspon zo svojho pohladu, ako to z hladiska svojich potrieb, narokov riesit lepsie.  A minimalne to je inspiracia, ako riesit dobre pripadny query model. Len aby sme si rozumeli ;-)

Saturday, May 09, 2009 12:56 PM
# vlko said:

no jo planujem, ale kym spravicky napisem za chvilu s clankom sa clovek morduje aj 4 krat viac:)

Sunday, May 10, 2009 9:41 PM
# methodman said:

paradny clanok!!!

Tuesday, September 22, 2009 9:45 PM
Prihlásiť | Registrovať | Pomoc