Что лучше? У вас сложная логика поиска в репозитории или в службе уровня домена (через IQueryable или другую)?

Мне нужно иметь возможность искать учетные записи клиентов по нескольким полям поиска. Прямо сейчас, у меня есть логика поиска в моем репозитории. Логика поиска включает в себя некоторую фильтрацию, которая больше похожа на принадлежащий ей уровень домена, но это будет означать использование чего-то вроде IQueryable, и я тоже не уверен, что мне это тоже нравится.

Например, прямо сейчас у меня есть класс поиска, который имеет все поля, по которым пользователь может искать:

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 class AccountsService : IAccountsService
{
    private readonly IAccountRepository _accountRepository;

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

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

И тогда у меня есть вся логика фильтрации в моей реализации репозитория:

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
}

Не уверен, если я буду чувствовать себя хорошо об этом или если я найду другой способ реализовать такой поиск. Что бы вы сделали в этой ситуации?

Ответ 1

Существует несколько методов, которые наилучшим образом зависят от вашего конкретного сценария.

Вместо того, чтобы просто обсуждать логику поиска с точки зрения местоположения (например, в службе или в домене), может быть более полезно провести различие между местоположением спецификации и местом выполнения. По спецификации местоположения я имею в виду, в каких слоях вы указываете, какие поля вы хотите искать. По месту выполнения я подразумеваю немедленное или отсроченное выполнение.

Если у вас есть несколько взаимоисключающих типов поиска (т.е. в сценарии А, который вы хотите выполнить поиск по CustomerId, а в сценарии B, который вы хотите найти по CustomerName), это может быть выполнено путем создания репозитория, специфичного для домена, с выделенными методами для каждого типа поиска или в .Net вы можете использовать выражение LINQ. Например:

Метод поиска по домену:

_customers.WithName("Willie Nelson")

Запрос LINQ в репозитории, реализующем IQueryable:

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

Первый позволяет более экспрессивную область, в то время как последняя обеспечивает большую гибкость использования со слегка уменьшенным временем разработки (возможно, за счет удобочитаемости).

Для более сложных критериев поиска вы можете использовать описанный вами метод прохождения в наборе критериев поиска (строго типизированный или иначе), или вы можете использовать Спецификация.. Преимущество шаблона спецификаций заключается в том, что он обеспечивает более выразительный, богатый доменом язык запросов. Одним из примеров может быть:

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

Композиция, предоставленная через шаблон спецификации, может быть предоставлена ​​также через API.NET LINQ, хотя и с меньшим контролем над указанием кода выявления намерений.

В отношении времени выполнения репозитории могут быть записаны для обеспечения отложенного выполнения путем возврата IQueryable или путем разрешения выражений LINQ для оценки методом репозитория. Например:

Отложенный запрос:

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

Выполняется методом Query():

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

Предыдущий метод Query(), который возвращает IQueryable, имеет преимущество в том, что его немного легче тестировать, потому что Query() можно легко заглушить, чтобы обеспечить коллекцию, управляемую вызовом кода, в то время как последнее имеет то преимущество, что больше детерминированным.

===== ==== EDIT

Вдохновленный подходом gaearon, я решил изменить свой ответ подобным методом. Его подход представляет собой несколько инвертированный шаблон спецификации, где спецификация выполняет фактический запрос. Это по сути делает запрос самостоятельно, поэтому позвольте просто называть его, что:

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

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

Итак, где вы можете спросить репозиторий? Нам здесь не нужен. На наш ICustomerQuery можно просто ввести IQueryable, который может быть реализован, как вам нравится (возможно, регистрация IoC, которая возвращает только NHibernate для следующего:

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

Ответ 2

Почему бы вам не выставить IQueryable из самого хранилища? Это позволит запускать любой запрос LINQ из запроса кода.

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

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

Вы можете сделать AccountSearch ответственным за построение запроса в соответствии с его собственной логикой:

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;     
    }
}

Затем используйте его из кода клиента:

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

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

Это всего лишь один из возможных подходов, но мне это нравится, потому что он позволяет сложную логику в классе фильтра поиска. Например, у вас может быть радиокнопка в пользовательском интерфейсе с разными типами поиска, которые, в свою очередь, ищут разные поля. Это все выражается в AccountSearch при использовании этого шаблона. Вы также можете сделать некоторые поля поиска дополнительными, как это было сделано в Address в этом примере. В конце концов, вы берете на себя ответственность за фактическое построение запроса от клиентского кода до AccountSearch, который лучше всего подходит для него, потому что он лучше знает условия поиска и их смысл.