哪个更好?在存储库或域级服务中(通过IQueryable或其他)具有复杂的搜索逻辑?

时间:2011-02-05 15:33:06

标签: c# search domain-driven-design repository

我需要能够通过多个搜索字段搜索客户帐户。现在,我的搜索逻辑在我的存储库中。搜索逻辑包括一些感觉更像它属于域层的过滤,但这意味着使用像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
}

不确定我是否应该对此感到满意,或者我是否应该找到另一种方法来实现这样的搜索。在这种情况下你会做什么?

2 个答案:

答案 0 :(得分:6)

您可以使用多种技术,其中最好的技术取决于您的具体情况。

不是仅仅根据位置(例如,在服务中或在域中)讨论搜索逻辑,而是在规范位置和执行位置之间进行区分可能更有帮助。根据规范位置,我的意思是在您指定要在哪些字段中搜索哪些字段。通过执行位置,我的意思是立即执行或延期执行。

如果您有多种互斥类型的搜索(即在方案A中您希望按CustomerId进行搜索,而在方案B中您希望按CustomerName进行搜索),则可以通过使用专用方法创建特定于域的存储库来完成此操作对于每种搜索类型,或在.Net中,您可以使用LINQ表达式。例如:

特定于域的搜索方法:

_customers.WithName("Willie Nelson")

实现IQueryable的存储库的LINQ查询:

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

前者允许更具表现力的领域,而后者提供更大的使用灵活性,同时略微缩短开发时间(可能以牺牲可读性为代价)。

对于更复杂的搜索条件需求,您可以使用您描述的传递搜索条件集合(强类型或其他方式)的技术,或者您可以使用Specification Pattern。规范模式的优点是它提供了一种更具表现力,领域丰富的查询语言。一个示例用法可能是:

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

通过规范模式提供的组合也可以通过.Net的LINQ API提供,但是对指定意图揭示代码的控制较少。

关于执行时间,可以编写存储库以通过返回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();

返回IQueryable的前一个Query()方法具有稍微容易测试的优点,因为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>()

答案 1 :(得分:3)

为什么不从存储库本身公开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 ();

这只是一种可能的方法,但我喜欢它,因为它允许搜索过滤器类中的复杂逻辑。例如,您可能在UI中有一个带有不同搜索类型的单选按钮,而这些按钮又按不同的字段进行搜索。使用此模式时,这在AccountSearch中都可以表达。您也可以将某些搜索字段设为可选字段,就像我在此示例中使用Address一样。毕竟,您负责将查询从客户端代码构建到最适合它的AccountSearch,因为它最了解搜索条件及其含义。