2011-02-05 5 views
10

Muszę mieć możliwość przeszukiwania kont klientów za pomocą wielu pól wyszukiwania. W tej chwili mam swoją logikę wyszukiwania w moim repozytorium. Logika wyszukiwania obejmuje pewne filtrowanie, które wydaje się bardziej podobne do warstwy domeny, ale oznaczałoby to użycie czegoś takiego jak IQueryable i nie jestem pewien, czy mi się to podoba.Co jest lepsze? Masz skomplikowaną logikę wyszukiwania w repozytorium lub w usłudze na poziomie domeny (przez IQueryable lub inne)?

Na przykład, teraz mam klasy wyszukiwania, który ma wszystkie pola za pomocą którego użytkownik może wyszukać:

public class AccountSearch 
{ 
    public decimal Amount { get; set; } 
    public string CustomerId { get; set; } 
    public string Address { get; set; } 
    public string CustomerName { get; set; } 
    public string City { get; set; } 
    public string PostalCode { get; set; } 
    public string Email { get; set; } 
    public string PhoneNumber { get; set; } 
    public string State { get; set; } 
} 

Następnie mam usługę poziomie domeny, które po prostu przechodzi klasę szukaj od do magazyn. Nie lubię go:

public class AccountsService : IAccountsService 
{ 
    private readonly IAccountRepository _accountRepository; 

    public AccountsService(IAccountRepository accountRepository) 
    { 
     _accountRepository = accountRepository;    
    } 

    public IEnumerable<Account> Search(AccountSearch accountSearch) 
    { 
     return _accountRepository.Search(accountSearch); 
    } 
} 

A potem, mam wszystkie logiki filtrowania w moim repozytorium realizacji:

public class AccountRepository : IAccountRepository 
{ 
    private AccountDataContext _dataContext; 

    public AccountRepository(AccountDataContext entityFrameworkDataContext) 
    { 
     _dataContext = entityFrameworkDataContext; 
    } 

    public IEnumerable<Account> Search(AccountSearch accountSearch) 
    { 
     // My datacontext contains database entities, not domain entities. 
     // This method must query the data context, then map the database 
     // entities to domain entities. 

     return _dataContext.Accounts 
      .Where(TheyMeetSearchCriteria) 
      .Select(MappedAccounts); 
    } 

    // implement expressions here: 
    // 1. TheyMeetSearchCriteria filters the accounts by the given criteria 
    // 2. MappedAccounts maps from database to domain entities 
} 

Nie jestem pewien, czy powinienem czuć się dobrze o tym czy powinienem znaleźć inny sposób wdrożenia takiego wyszukiwania. Co zrobiłbyś w tej sytuacji?

+0

Mogę zapytać o co chodzi z 'AccountsService' tutaj Wszystko wydaje do zrobienia jest zawijanie 'IAccountRepository' dla pozornie bezcelowego celu: –

+0

Poprawnie Jak zwykle, usunąłem inne metody ze względu na to pytanie, a twoje pytanie jest takie samo jak moje, z wyjątkiem tego, że wolałbym, żeby logika wyszukiwania znajdowała się w nie wiem, jak najlepiej go wdrożyć –

Odpowiedz

3

Dlaczego nie ujawnisz samego siebie z repozytorium? Umożliwiłoby to uruchomienie dowolnej kwerendy LINQ z kodu żądania.

public class AccountRepository : IAccountRepository 
{ 
    AccountContext context = new AccountContext(); 

    public IQueryable<Account> GetItems() 
    { 
     return context.Accounts; 
    } 
} 

Można dokonać AccountSearch odpowiedzialny za budowanie kwerendy według własnej logiki:

public class AccountSearch 
{ 
    public decimal Amount { get; set; } 
    public string CustomerId { get; set; } 
    public string Address { get; set; } 
    public string CustomerName { get; set; } 
    public string City { get; set; } 
    public string PostalCode { get; set; } 
    public string Email { get; set; } 
    public string PhoneNumber { get; set; } 
    public string State { get; set; } 

    public IQueryable<Account> BuildQuery (IQueryable<Account> source) 
    { 
     var query = source.Where (a => 
      a.Amount == Amount); 

     // you can use more twisted logic here, like applying where clauses conditionally 
     if (!string.IsNullOrEmpty (Address)) 
      query = query.Where (a => 
       a.Address == Address); 

     // ... 

     return query;  
    } 
} 

następnie używać go z kodem klienckim:

var filter = GetSearchFields(); // e.g. read from UI 
var allItems = repository.GetItems(); 

var results = filter.BuildQuery (allItems).ToList(); 

Jest to tylko jedna z możliwości podejść, ale podoba mi się, ponieważ pozwala na złożoną logikę w klasie filtru wyszukiwania. Na przykład możesz mieć przycisk w interfejsie użytkownika z różnymi typami wyszukiwania, które z kolei wyszukują według różnych pól. To wszystko można wyrazić w AccountSearch, gdy używasz tego wzoru. Możesz również ustawić niektóre pola wyszukiwania jako opcjonalne, tak jak zrobiłem to w tym przykładzie z Address. W końcu bierzesz odpowiedzialność za zbudowanie zapytania z kodu klienta do AccountSearch, które najlepiej do niego pasuje, ponieważ najlepiej zna warunki wyszukiwania i ich znaczenie.

+0

Yessir, bardzo to lubię, zamierzam to wypróbować, a dam ci znać, jak to działa –

+0

OK, jesteśmy prawie Kiedy go wdrażałem, zdałem sobie sprawę, że nie wspomniałem o tym repozytorium musi mapować z jednostek baz danych do jednostek domeny. –

+0

Nigdy wcześniej nie brałem pod uwagę tej techniki, ale myślę, że podoba mi się to podejście lepiej niż klasyczny wzór specyfikacji.Jest to rodzaj odwróconego wzorca specyfikacji, w którym "kryteria" przeprowadzają wyszukiwanie. Zainspirowany twoim przykładem, zamierzam dodać zmodyfikowaną technikę do mojej odpowiedzi tylko po to, żeby mi to wyszło z głowy. +1 dla ciebie. –

6

Istnieje wiele technik, z których można korzystać, a najlepsze z nich będą zależeć od konkretnego scenariusza.

Zamiast jedynie omawiać logikę wyszukiwania pod względem lokalizacji (np. W usłudze lub w domenie), pomocne może być rozróżnienie między lokalizacją specyfikacji a lokalizacją wykonania. Według lokalizacji specyfikacji mam na myśli to, w jakich warstwach określasz pola, które chcesz przeszukiwać. Przez lokalizację wykonania rozumiem natychmiastowe lub odroczone wykonanie.

Jeśli masz kilka wykluczających się rodzajów wyszukiwań (np. W scenariuszu A, który chcesz przeszukiwać według CustomerId, oraz w scenariuszu B, który chcesz przeszukiwać według CustomerName), można to osiągnąć, tworząc repozytorium specyficzne dla domeny z dedykowanym metody dla każdego typu wyszukiwania lub .Net możesz użyć wyrażenia LINQ.Np

domeny specyficzne metody wyszukiwania:

_customers.WithName("Willie Nelson") 

LINQ zapytania w repozytorium wykonania IQueryable:

_customers.Where(c => c.Name.Equals("Willie Nelson") 

Były pozwala na bardziej wyraziste domeny natomiast drugi zapewnia większą elastyczność używać z nieco krótszym czasem programowania (być może kosztem czytelności).

Aby uzyskać bardziej złożone kryteria wyszukiwania, można użyć opisanej techniki przekazywania w zbiorze kryteriów wyszukiwania (silnie wpisane lub nie), lub można skorzystać z Specification Pattern. Zaletą Wzorca specyfikacji jest to, że zapewnia on bardziej ekspresyjny język zapytań o bogatych domenach. Przykładem użycia może być:

_customers.MeetingCriteria(
     Criteria.LivingOutsideUnitedStates.And(Criteria.OlderThan(55))) 

Skład warunkiem poprzez doprecyzowanie wzór może być dostarczone przez użytkownika LINQ .NET API, a także, choć z mniejszą kontrolę nad określeniem intencji-odkrywcze kod.

W odniesieniu do czasu wykonania, repozytoriów można zapisać, aby zapewnić odroczone wykonanie, zwracając IQueryable lub dopuszczając przekazywanie wyrażeń LINQ do oceny metodą repozytorium. Np

odroczony zapytań:

var customer = (from c in _customers.Query() 
        where c.Name == "Willie Nelson" 
        select c).FirstOrDefault(); 

Wykonywane przez Query() Sposób:

var customer = 
    _customers.Query(q => from c in q 
          where c.Name == "Willie Nelson" 
          select c).FirstOrDefault(); 

Sposób pierwszego zapytania(), która zwraca IQueryable ma tę zaletę, że jest nieco łatwiejsze do testowania ponieważ kwerenda() może być łatwo zgaszona w celu zapewnienia kolekcji operowanej przez kod wywołujący, podczas gdy druga ma tę przewagę, że jest bardziej deterministyczna.

===== EDIT ====

Zainspirowany podejścia gaearon, postanowiłem zmienić moją odpowiedź z podobną techniką. Jego podejście jest nieco odwróconym Wzorem Specyfikacji, w którym specyfikacja wykonuje faktyczne zapytanie. Wynika to przede wszystkim sprawia, że ​​kwerenda w sobie, więc niech po prostu nazwać to, że:

public class SomeClass 
{ 
    // Get the ICustomerQuery through DI 
    public SomeClass(ICustomerQuery customerQuery) 
    { 
     _customerQuery = customerQuery; 
    } 

    public void SomeServiceMethod() 
    { 
     _customerQuery() 
      .WhereLivingOutSideUnitedStates() 
      .WhereAgeGreaterThan(55) 
      .Select(); 
    } 
} 

Więc, gdzie jest magazyn, który może Cię zapytać? Nie potrzebujemy go tutaj. Nasz ICustomerQuery może po prostu wstrzykuje z IQueryable, które można zrealizować w dowolny sposób (być może Rejestracja IoC, że po prostu zwraca następujące dla NHibernate:

_container.Resolve<ISession>().Linq<Customer>() 
+0

Doskonała odpowiedź. Poprawiłem nieco moje pytanie, które może mieć wpływ na twoją odpowiedź. Czy możesz spojrzeć jeszcze raz? –

+0

Wygląda na to, że musisz sam wykonać encję danych -> encje domen. W takim przypadku ostatni przykład będzie nadal działał. Wystarczy użyć Mapowania danych w dowolnym komponencie, którego używasz do obsługi IQueryable , które wstrzykniesz do ICustomerQuery. –

+0

Niesamowita odpowiedź, dzięki. Cieszę się, że podoba ci się moje podejście. Używałem go w kilku aplikacjach LINQ do SQL i NHibernate i bardzo podoba mi się sposób, w jaki sprawia, że ​​zapytania są elastyczne. Zastosowanie wzoru specyfikacji w sposób, który opisałeś jest również fajne, szczególnie z punktu widzenia składni. Spróbuję tego kiedyś. –

Powiązane problemy