Dobrze zaprojektowane polecenia i / lub specyfikacje zapytań

92

Od dłuższego czasu szukałem dobrego rozwiązania problemów przedstawionych przez typowy wzorzec Repozytorium (rosnąca lista metod zapytań specjalistycznych itp. Patrz: http://ayende.com/blog/3955/repository- is-the-new-singleton ).

Bardzo podoba mi się pomysł używania zapytań poleceń, szczególnie przy użyciu wzorca Specification. Jednak mój problem ze specyfikacją polega na tym, że odnosi się ona tylko do kryteriów prostych selekcji (w zasadzie klauzula where) i nie zajmuje się innymi kwestiami zapytań, takimi jak łączenie, grupowanie, selekcja podzbiorów lub projekcja itp. w zasadzie wszystkie dodatkowe obręcze, przez które musi przejść wiele zapytań, aby uzyskać prawidłowy zestaw danych.

(uwaga: używam terminu „polecenie” we wzorcu polecenia, znanym również jako obiekty zapytań. Nie mówię o poleceniu, jak w przypadku separacji polecenie / zapytanie, w którym istnieje rozróżnienie między zapytaniami a poleceniami (aktualizacja, usuwanie, wstawić))

Dlatego szukam alternatyw, które obejmują całe zapytanie, ale są na tyle elastyczne, że nie wystarczy zamienić repozytoria spaghetti na eksplozję klas poleceń.

Użyłem na przykład Linqspecs i chociaż znajduję wartość w możliwości przypisywania znaczących nazw kryteriom wyboru, to po prostu nie wystarczy. Być może szukam mieszanego rozwiązania, które łączy wiele podejść.

Szukam rozwiązań, które mogli opracować inni, aby rozwiązać ten problem lub rozwiązać inny problem, ale nadal spełnia te wymagania. W powiązanym artykule Ayende sugeruje bezpośrednie użycie kontekstu nHibernate, ale wydaje mi się, że znacznie komplikuje to twoją warstwę biznesową, ponieważ teraz musi ona również zawierać informacje o zapytaniach.

Oferuję za to nagrodę, gdy tylko upłynie okres oczekiwania. Więc proszę, spraw, aby Twoje rozwiązania były godne uznania, z dobrymi wyjaśnieniami, a ja wybiorę najlepsze rozwiązanie i zagłosuję w górę.

UWAGA: Szukam czegoś opartego na ORM. Nie musi być jawnie EF lub nHibernate, ale te są najczęstsze i pasowałyby najlepiej. Jeśli można go łatwo dostosować do innych ORMów, byłby to bonus. Kompatybilny z Linq również byłby miły.

AKTUALIZACJA: Jestem naprawdę zaskoczony, że nie ma tu wielu dobrych sugestii. Wygląda na to, że ludzie są albo całkowicie CQRS, albo całkowicie w obozie Repozytorium. Większość moich aplikacji nie jest na tyle skomplikowana, aby gwarantować CQRS (coś, co większość zwolenników CQRS chętnie twierdzi, że nie należy go używać).

AKTUALIZACJA: Wydaje się, że jest tu trochę zamieszania. Nie szukam nowej technologii dostępu do danych, ale raczej dobrze zaprojektowanego interfejsu między biznesem a danymi.

Idealnie, to, czego szukam, to coś w rodzaju skrzyżowania obiektów Query, wzorca specyfikacji i repozytorium. Jak powiedziałem powyżej, wzorzec specyfikacji zajmuje się tylko aspektem klauzuli gdzie, a nie innymi aspektami zapytania, takimi jak łączenia, sub-selekcje itp. Repozytoria zajmują się całym zapytaniem, ale po chwili wymykają się spod kontroli . Obiekty zapytań również zajmują się całym zapytaniem, ale nie chcę po prostu zastępować repozytoriów eksplozjami obiektów zapytań.

Erik Funkenbusch
źródło
5
Fantastyczne pytanie. Ja też chciałbym zobaczyć, co ludzie z większym doświadczeniem niż sugeruję. W tej chwili pracuję nad bazą kodu, w której repozytorium generyczne zawiera również przeciążenia dla obiektów Command lub obiektów Query, których struktura jest podobna do tego, co opisuje Ayende na swoim blogu. PS: Może to również zwrócić uwagę programistów.
Simon Whitehead,
Dlaczego nie po prostu użyć repozytorium, które uwidacznia IQueryable, jeśli nie masz nic przeciwko zależności od LINQ? Typowym podejściem jest repozytorium ogólne, a następnie, gdy potrzebujesz logiki wielokrotnego użytku powyżej, tworzysz pochodny typ repozytorium za pomocą dodatkowych metod.
devdigital
@devdigital - Zależność od Linq to nie to samo, co zależność od implementacji danych. Chciałbym używać Linq do obiektów, aby móc sortować lub wykonywać inne funkcje warstwy biznesowej. Ale to nie znaczy, że chcę zależności od implementacji modelu danych. To, o czym naprawdę mówię, to interfejs warstwy / warstwy. Na przykład chcę mieć możliwość zmiany zapytania i nie musieć go zmieniać w 200 miejscach, co się dzieje, gdy wepchniesz IQueryable bezpośrednio do modelu biznesowego.
Erik Funkenbusch,
1
@devdigital - który po prostu przenosi problemy z repozytorium do warstwy biznesowej. Po prostu tasujesz problem.
Erik Funkenbusch

Odpowiedzi:

95

Zastrzeżenie: Ponieważ nie ma jeszcze żadnych świetnych odpowiedzi, zdecydowałem się opublikować fragment świetnego posta na blogu, który przeczytałem jakiś czas temu, skopiowany prawie dosłownie. Możesz znaleźć pełny post na blogu tutaj . Więc oto jest:


Możemy zdefiniować dwa następujące interfejsy:

public interface IQuery<TResult>
{
}

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult>
{
    TResult Handle(TQuery query);
}

W IQuery<TResult>Określa komunikat, który definiuje konkretne zapytanie z danymi to powraca użyciu TResulttypu rodzajowego. Za pomocą wcześniej zdefiniowanego interfejsu możemy zdefiniować komunikat zapytania w następujący sposób:

public class FindUsersBySearchTextQuery : IQuery<User[]>
{
    public string SearchText { get; set; }
    public bool IncludeInactiveUsers { get; set; }
}

Ta klasa definiuje operację zapytania z dwoma parametrami, której wynikiem będzie tablica Userobiektów. Klasę obsługującą tę wiadomość można zdefiniować w następujący sposób:

public class FindUsersBySearchTextQueryHandler
    : IQueryHandler<FindUsersBySearchTextQuery, User[]>
{
    private readonly NorthwindUnitOfWork db;

    public FindUsersBySearchTextQueryHandler(NorthwindUnitOfWork db)
    {
        this.db = db;
    }

    public User[] Handle(FindUsersBySearchTextQuery query)
    {
        return db.Users.Where(x => x.Name.Contains(query.SearchText)).ToArray();
    }
}

Teraz możemy pozwolić konsumentom polegać na ogólnym IQueryHandlerinterfejsie:

public class UserController : Controller
{
    IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler;

    public UserController(
        IQueryHandler<FindUsersBySearchTextQuery, User[]> findUsersBySearchTextHandler)
    {
        this.findUsersBySearchTextHandler = findUsersBySearchTextHandler;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        User[] users = this.findUsersBySearchTextHandler.Handle(query);    
        return View(users);
    }
}

Od razu ten model daje nam dużą elastyczność, ponieważ możemy teraz zdecydować, co wstrzyknąć do UserController. Możemy wstrzyknąć zupełnie inną implementację lub taką, która otacza rzeczywistą implementację, bez konieczności wprowadzania zmian w UserController(i wszystkich innych użytkownikach tego interfejsu).

IQuery<TResult>Interfejs daje nam czas kompilacji wsparcia przy określaniu lub wstrzyknięcie IQueryHandlersw naszym kodzie. Kiedy zamiast tego zmienimy FindUsersBySearchTextQueryzwracaną wartość UserInfo[](przez implementację IQuery<UserInfo[]>), UserControllerkompilacja nie powiedzie się, ponieważ ograniczenie typu ogólnego na IQueryHandler<TQuery, TResult>nie będzie mogło zostać zmapowane FindUsersBySearchTextQuerydo User[].

Wstrzyknięcie IQueryHandlerinterfejsu do konsumenta wiąże się jednak z pewnymi mniej oczywistymi problemami, które nadal wymagają rozwiązania. Liczba zależności naszych konsumentów może być zbyt duża i może prowadzić do przeciążenia konstruktora - gdy konstruktor przyjmuje zbyt wiele argumentów. Liczba zapytań wykonywanych przez klasę może się często zmieniać, co wymagałoby ciągłych zmian w liczbie argumentów konstruktora.

Możemy rozwiązać problem polegający na tym, że zbyt wiele IQueryHandlersz nich musimy wprowadzić dodatkową warstwę abstrakcji. Tworzymy mediatora, który znajduje się między konsumentami a obsługą zapytań:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

Jest IQueryProcessorto nieogólny interfejs z jedną ogólną metodą. Jak widać w definicji interfejsu, IQueryProcessorzależy to od IQuery<TResult>interfejsu. Dzięki temu możemy mieć obsługę czasu kompilacji u naszych konsumentów, która jest zależna od IQueryProcessor. Przepiszmy, UserControlleraby używał nowego IQueryProcessor:

public class UserController : Controller
{
    private IQueryProcessor queryProcessor;

    public UserController(IQueryProcessor queryProcessor)
    {
        this.queryProcessor = queryProcessor;
    }

    public View SearchUsers(string searchString)
    {
        var query = new FindUsersBySearchTextQuery
        {
            SearchText = searchString,
            IncludeInactiveUsers = false
        };

        // Note how we omit the generic type argument,
        // but still have type safety.
        User[] users = this.queryProcessor.Process(query);

        return this.View(users);
    }
}

UserControllerTeraz zależy na zasadzie IQueryProcessor, że może obsługiwać wszystkie nasze pytania. W UserController„S SearchUserssposób wywołuje IQueryProcessor.Processsposób przechodzi w zainicjowany przedmiotem zapytania. Ponieważ FindUsersBySearchTextQueryimplementuje IQuery<User[]>interfejs, możemy przekazać go do Execute<TResult>(IQuery<TResult> query)metody generycznej . Dzięki wnioskowaniu o typie C # kompilator jest w stanie określić typ ogólny, co oszczędza nam konieczności jawnego określania typu. ProcessZnany jest również zwracany typ metody.

Obecnie obowiązkiem wdrożenia prawa IQueryProcessorjest znalezienie odpowiedniego IQueryHandler. Wymaga to pewnego dynamicznego pisania i opcjonalnie użycia struktury Dependency Injection, a wszystko to można zrobić za pomocą zaledwie kilku wierszy kodu:

sealed class QueryProcessor : IQueryProcessor
{
    private readonly Container container;

    public QueryProcessor(Container container)
    {
        this.container = container;
    }

    [DebuggerStepThrough]
    public TResult Process<TResult>(IQuery<TResult> query)
    {
        var handlerType = typeof(IQueryHandler<,>)
            .MakeGenericType(query.GetType(), typeof(TResult));

        dynamic handler = container.GetInstance(handlerType);

        return handler.Handle((dynamic)query);
    }
}

QueryProcessorKlasa tworzy specyficzny IQueryHandler<TQuery, TResult>rodzaj na podstawie typu dostarczonego przykład zapytania. Ten typ służy do żądania od podanej klasy kontenera pobrania wystąpienia tego typu. Niestety musimy wywołać Handlemetodę z użyciem refleksji (w tym przypadku używając słowa kluczowego dymamic C # 4.0), ponieważ w tym momencie nie jest możliwe rzutowanie instancji handlera, ponieważ TQueryargument generyczny nie jest dostępny w czasie kompilacji. Jednak jeśli Handlenazwa metody nie zostanie zmieniona lub nie otrzyma innych argumentów, to wywołanie nigdy się nie powiedzie, a jeśli chcesz, bardzo łatwo jest napisać test jednostkowy dla tej klasy. Korzystanie z odbicia może nieco spaść, ale nie ma się czym martwić.


Aby odpowiedzieć na jedno z twoich obaw:

Dlatego szukam alternatyw, które obejmują całe zapytanie, ale są na tyle elastyczne, że nie wystarczy zamienić repozytoria spaghetti na eksplozję klas poleceń.

Konsekwencją korzystania z tego projektu jest to, że w systemie będzie wiele małych klas, ale posiadanie wielu małych / skupionych klas (z wyraźnymi nazwami) jest dobrą rzeczą. Takie podejście jest zdecydowanie lepsze niż posiadanie wielu przeciążeń z różnymi parametrami dla tej samej metody w repozytorium, ponieważ można je pogrupować w jednej klasie zapytania. Więc nadal masz dużo mniej klas zapytań niż metod w repozytorium.

david.s
źródło
2
Wygląda na to, że dostałeś nagrodę. Lubię koncepcje, po prostu liczyłem, że ktoś zaprezentuje coś zupełnie innego. Gratulacje.
Erik Funkenbusch
1
@FuriCuri, czy pojedyncza klasa naprawdę potrzebuje 5 zapytań? Być może mógłbyś spojrzeć na to jako na klasę ze zbyt wieloma obowiązkami. Alternatywnie, jeśli zapytania są agregowane, być może powinny być pojedynczym zapytaniem. To oczywiście tylko sugestie.
Sam
1
@stakx Masz całkowitą rację, że w moim początkowym przykładzie ogólny TResultparametr IQueryinterfejsu nie jest użyteczny. Jednak w mojej zaktualizowanej odpowiedzi TResultparametr jest używany przez Processmetodę w IQueryProcessorcelu rozwiązania IQueryHandleratrybutu at.
david.s
1
Mam też bloga z bardzo podobną realizacją co sprawia, że ​​wydaje mi się, że jestem na dobrej drodze, to jest link jupaol.blogspot.mx/2012/11/ ... i używam go od jakiegoś czasu w aplikacjach PROD, ale miałem problem z takim podejściem. Tworzenie łańcuchów i ponowne wykorzystywanie zapytań Załóżmy, że mam kilka małych zapytań, które trzeba połączyć, aby utworzyć bardziej złożone zapytania. Skończyło się na tym, że po prostu skopiowałem kod, ale szukam lepszego i czystszego podejścia. Jakieś pomysły?
Jupaol
4
@Cemre Skończyło się na hermetyzowaniu moich zapytań w metodach rozszerzenia, zwracając IQueryablei upewniając się, że nie wyliczam kolekcji, a następnie z QueryHandlerwłaśnie wywołanego / łańcuchowego zapytań. Dało mi to elastyczność testowania jednostkowego moich zapytań i łączenia ich w łańcuch. Mam usługę aplikacji na górze QueryHandler, a mój kontroler jest odpowiedzialny za bezpośrednią rozmowę z usługą zamiast z
programem
4

Mój sposób radzenia sobie z tym jest w rzeczywistości uproszczony i agnostyczny dla ORM. Mój pogląd na repozytorium jest następujący: Zadaniem repozytorium jest dostarczenie aplikacji modelu wymaganego dla kontekstu, więc aplikacja po prostu pyta repozytorium o to , czego chce, ale nie mówi mu, jak to zdobyć.

Dostarczam metodzie repozytorium Kryteria (tak, w stylu DDD), które zostaną użyte przez repozytorium do utworzenia zapytania (lub cokolwiek jest wymagane - może to być żądanie usługi sieciowej). Łączenia i grupy imho to szczegóły tego, jak, a nie co i kryteria powinny być tylko podstawą do zbudowania klauzuli where.

Model = ostateczny obiekt lub struktura danych potrzebna aplikacji.

public class MyCriteria
{
   public Guid Id {get;set;}
   public string Name {get;set;}
    //etc
 }

 public interface Repository
  {
       MyModel GetModel(Expression<Func<MyCriteria,bool>> criteria);
   }

Prawdopodobnie możesz użyć kryteriów ORM (Nhibernate) bezpośrednio, jeśli chcesz. Implementacja repozytorium powinna wiedzieć, jak używać kryteriów z bazowym magazynem lub DAO.

Nie znam Twojej domeny i wymagań modelu, ale byłoby dziwne, gdyby najlepszym sposobem było, aby aplikacja sama zbudowała zapytanie. Model zmienia się tak bardzo, że nie można zdefiniować czegoś stabilnego?

To rozwiązanie wyraźnie wymaga dodatkowego kodu, ale nie łączy reszty z ORM lub czymkolwiek, z czego korzystasz, aby uzyskać dostęp do magazynu. Repozytorium spełnia swoje zadanie, działając jako fasada, a IMO jest czyste, a kod „tłumaczenia kryteriów” jest wielokrotnego użytku

MikeSW
źródło
Nie rozwiązuje to problemów związanych z rozwojem repozytoriów i stale rozszerzającą się listą metod zwracania różnych rodzajów danych. Rozumiem, że możesz nie widzieć z tym problemu (wiele osób nie), ale inni widzą to inaczej (sugeruję przeczytanie artykułu, do którego się połączyłem, jest wiele innych osób o podobnych opiniach).
Erik Funkenbusch
1
Zajmuję się tym, ponieważ kryteria sprawiają, że wiele metod jest niepotrzebnych. Oczywiście nie ze wszystkich nie mogę wiele powiedzieć, nie wiedząc nic o tym, czego potrzebujesz. Jestem pod wrażeniem, że chcesz bezpośrednio zapytać o bazę danych, więc prawdopodobnie repozytorium jest po prostu przeszkodą. Jeśli chcesz pracować bezpośrednio z relacyjnym sotrage, idź do tego bezpośrednio, bez potrzeby posiadania repozytorium. Uwaga: denerwujące jest to, jak wiele osób cytuje Ayende w tym poście. Nie zgadzam się z tym i myślę, że wielu deweloperów używa tego wzorca w niewłaściwy sposób.
MikeSW
1
Może to nieco zmniejszyć problem, ale biorąc pod uwagę wystarczająco dużą aplikację, nadal będzie tworzyć repozytoria potworów. Nie zgadzam się z rozwiązaniem Ayende polegającym na użyciu nHibernate bezpośrednio w głównej logice, ale zgadzam się z nim co do absurdu niekontrolowanego wzrostu repozytoriów. Nie chcę bezpośrednio przesyłać zapytań do bazy danych, ale nie chcę też tylko przenosić problemu z repozytorium do eksplozji obiektów zapytań.
Erik Funkenbusch
2

Zrobiłem to, poparłem to i cofnąłem.

Główny problem jest następujący: nie ważne jak to zrobisz, dodatkowa abstrakcja nie zapewni Ci niezależności. Z definicji wycieknie. Zasadniczo wymyślasz całą warstwę tylko po to, aby twój kod wyglądał uroczo ... ale nie zmniejsza to konserwacji, nie poprawia czytelności ani nie daje ci żadnego rodzaju agnostycyzmu modelowego.

Zabawne jest to, że odpowiedziałeś na swoje własne pytanie w odpowiedzi na odpowiedź Oliviera: „jest to zasadniczo dublowanie funkcjonalności Linq bez wszystkich korzyści, jakie daje Linq”.

Zadaj sobie pytanie: jak to możliwe?

Stu
źródło
Cóż, zdecydowanie napotkałem problemy związane z integracją Linq z twoją warstwą biznesową. Jest bardzo potężny, ale kiedy wprowadzamy zmiany w modelu danych, jest to koszmar. Rzeczy są lepsze dzięki repozytoriom, ponieważ mogę wprowadzać zmiany w zlokalizowanym miejscu bez znacznego wpływu na warstwę biznesową (z wyjątkiem sytuacji, gdy trzeba również zmienić warstwę biznesową, aby wspierać zmiany). Ale repozytoria stają się tymi rozdętymi warstwami, które masowo naruszają SRP. Rozumiem twój punkt widzenia, ale tak naprawdę nie rozwiązuje to również żadnych problemów.
Erik Funkenbusch
Jeśli Twoja warstwa danych używa LINQ, a zmiany modelu danych wymagają zmian w warstwie biznesowej ... nie tworzysz poprawnie warstw.
Stu
Myślałem, że mówisz, że nie dodajesz już tej warstwy. Kiedy mówisz, że dodana abstrakcja nic Ci nie daje, oznacza to, że zgadzasz się z Ayende co do przekazywania sesji nHibernate (lub kontekstu EF) bezpośrednio do warstwy biznesowej.
Erik Funkenbusch
1

Możesz użyć płynnego interfejsu. Podstawową ideą jest to, że metody klasy zwracają bieżącą instancję tej samej klasy po wykonaniu jakiejś akcji. To pozwala ci łączyć wywołania metod.

Tworząc odpowiednią hierarchię klas, można stworzyć logiczny przepływ dostępnych metod.

public class FinalQuery
{
    protected string _table;
    protected string[] _selectFields;
    protected string _where;
    protected string[] _groupBy;
    protected string _having;
    protected string[] _orderByDescending;
    protected string[] _orderBy;

    protected FinalQuery()
    {
    }

    public override string ToString()
    {
        var sb = new StringBuilder("SELECT ");
        AppendFields(sb, _selectFields);
        sb.AppendLine();

        sb.Append("FROM ");
        sb.Append("[").Append(_table).AppendLine("]");

        if (_where != null) {
            sb.Append("WHERE").AppendLine(_where);
        }

        if (_groupBy != null) {
            sb.Append("GROUP BY ");
            AppendFields(sb, _groupBy);
            sb.AppendLine();
        }

        if (_having != null) {
            sb.Append("HAVING").AppendLine(_having);
        }

        if (_orderBy != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderBy);
            sb.AppendLine();
        } else if (_orderByDescending != null) {
            sb.Append("ORDER BY ");
            AppendFields(sb, _orderByDescending);
            sb.Append(" DESC").AppendLine();
        }

        return sb.ToString();
    }

    private static void AppendFields(StringBuilder sb, string[] fields)
    {
        foreach (string field in fields) {
            sb.Append(field).Append(", ");
        }
        sb.Length -= 2;
    }
}

public class GroupedQuery : FinalQuery
{
    protected GroupedQuery()
    {
    }

    public GroupedQuery Having(string condition)
    {
        if (_groupBy == null) {
            throw new InvalidOperationException("HAVING clause without GROUP BY clause");
        }
        if (_having == null) {
            _having = " (" + condition + ")";
        } else {
            _having += " AND (" + condition + ")";
        }
        return this;
    }

    public FinalQuery OrderBy(params string[] fields)
    {
        _orderBy = fields;
        return this;
    }

    public FinalQuery OrderByDescending(params string[] fields)
    {
        _orderByDescending = fields;
        return this;
    }
}

public class Query : GroupedQuery
{
    public Query(string table, params string[] selectFields)
    {
        _table = table;
        _selectFields = selectFields;
    }

    public Query Where(string condition)
    {
        if (_where == null) {
            _where = " (" + condition + ")";
        } else {
            _where += " AND (" + condition + ")";
        }
        return this;
    }

    public GroupedQuery GroupBy(params string[] fields)
    {
        _groupBy = fields;
        return this;
    }
}

Możesz to tak nazwać

string query = new Query("myTable", "name", "SUM(amount) AS total")
    .Where("name LIKE 'A%'")
    .GroupBy("name")
    .Having("COUNT(*) > 2")
    .OrderBy("name")
    .ToString();

Możesz tylko utworzyć nowe wystąpienie Query. Pozostałe klasy mają chronionego konstruktora. Celem hierarchii jest „wyłączenie” metod. Na przykład GroupBymetoda zwraca GroupedQueryklasę bazową Queryi nie ma Wheremetody (metoda where jest zadeklarowana w Query). Dlatego nie można dzwonić Wherepo GroupBy.

Nie jest jednak doskonały. Dzięki tej hierarchii klas możesz sukcesywnie ukrywać członków, ale nie pokazywać nowych. Dlatego Havingzgłasza wyjątek, gdy jest wywoływany wcześniej GroupBy.

Pamiętaj, że można dzwonić Wherekilka razy. Powoduje to dodanie nowych warunków ANDdo istniejących warunków. Ułatwia to programowe tworzenie filtrów na podstawie pojedynczych warunków. To samo jest możliwe z Having.

Metody akceptujące listy pól mają parametr params string[] fields. Pozwala na przekazywanie nazw pojedynczych pól lub tablicy ciągów.


Fluent interfejsy są bardzo elastyczne i nie wymagają tworzenia wielu przeciążeń metod z różnymi kombinacjami parametrów. Mój przykład działa ze stringami, jednak podejście to można rozszerzyć na inne typy. Możesz również zadeklarować predefiniowane metody dla przypadków specjalnych lub metody akceptujące typy niestandardowe. Możesz także dodać metody takie jak ExecuteReaderlub ExceuteScalar<T>. Umożliwiłoby to zdefiniowanie takich zapytań

var reader = new Query<Employee>(new MonthlyReportFields{ IncludeSalary = true })
    .Where(new CurrentMonthCondition())
    .Where(new DivisionCondition{ DivisionType = DivisionType.Production})
    .OrderBy(new StandardMonthlyReportSorting())
    .ExecuteReader();

Nawet polecenia SQL skonstruowane w ten sposób mogą mieć parametry poleceń, a tym samym uniknąć problemów z wstrzykiwaniem SQL i jednocześnie umożliwiać buforowanie poleceń przez serwer bazy danych. Nie jest to zamiennik dla programu odwzorowującego O / R, ale może pomóc w sytuacjach, w których w przeciwnym razie utworzyłbyś polecenia przy użyciu prostej konkatenacji ciągów.

Olivier Jacot-Descombes
źródło
3
Hmm ... Interesujące, ale twoje rozwiązanie wydaje się mieć problemy z możliwościami SQL Injection i tak naprawdę nie tworzy przygotowanych instrukcji dla wstępnie skompilowanego wykonania (przez co działa wolniej). Prawdopodobnie można go dostosować do rozwiązania tych problemów, ale utknęliśmy w wynikach bezpiecznego zestawu danych bez typu, a co nie. Wolałbym rozwiązanie oparte na ORM i być może powinienem to wyraźnie określić. Zasadniczo powiela to funkcjonalność Linq bez wszystkich korzyści, jakie daje Linq.
Erik Funkenbusch,
Jestem świadomy tych problemów. To tylko szybkie i brudne rozwiązanie, pokazujące, jak można zbudować płynny interfejs. W prawdziwym rozwiązaniu prawdopodobnie „upieczesz” swoje dotychczasowe podejście w płynnym interfejsie dostosowanym do Twoich potrzeb.
Olivier Jacot-Descombes