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

发布于 2024-10-15 18:46:49 字数 1954 浏览 4 评论 0原文

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

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

I need to be able to search customer accounts by multiple search fields. Right now, I have my search logic in my repository. The search logic includes some filtering that feels more like it belongs in the domain layer, but that would mean using something like IQueryable and I'm not sure I like that either.

For example, right now I have a search class that has all the fields by which the user can search:

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

Then, I have a domain level service that simply passes the search class off to the repository. I don't like it:

public class AccountsService : IAccountsService
{
    private readonly IAccountRepository _accountRepository;

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

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

And then, I have all the filtering logic in my repository implementation:

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
}

Not sure if I should feel okay about this or if I should find another way to implement a search like this. What would you do in this situation?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

旧故 2024-10-22 18:46:49

您可以使用多种技术,其中最好的技术取决于您的特定场景。

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

如果您有多种相互排斥的搜索类型(即,在场景 A 中您希望通过 CustomerId 进行搜索,而在场景 B 中您希望通过 CustomerName 进行搜索),则可以通过为每个搜索创建一个具有专用方法的特定于域的存储库来实现类型,或者在 .Net 中您可以使用 LINQ 表达式。例如:

特定于域的搜索方法:

_customers.WithName("Willie Nelson")

在实现 IQueryable 的存储库上进行 LINQ 查询:

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

前者允许更具表现力的域,而后者则提供更大的使用灵活性,并稍微减少开发时间(可能以牺牲可读性为代价)。

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

_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() 进行存根以提供通过调用代码操作的集合,而后者的优点是更具确定性。

=====编辑====

受到 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>()

There are a number of techniques you can use, the best of which will depend upon your particular scenario.

Rather than merely discussing search logic in terms of location (e.g. in a service or in a domain), it may be more helpful to draw a distinction between specification location and execution location. By specification location, I mean in what layers you specify which fields you are wanting to search on. By execution location, I mean immediate or deferred execution.

If you have several mutually exclusive types of searches (i.e. in scenario A you want to search by CustomerId, and in scenario B you want to search by CustomerName), this can be accomplished by creating a domain-specific repository with dedicated methods for each search type, or in .Net you might use a LINQ expression. For example:

Domain-specific search method:

_customers.WithName("Willie Nelson")

LINQ query on a repository implementing IQueryable:

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

The former allows for a more expressive domain while the latter provides more flexibility of use with a slightly decreased development time (perhaps at the expense of readability).

For more complex search criteria needs, you can use the technique you have described of passing in a collection of search criteria (strongly typed or otherwise), or you can use the Specification Pattern. The advantage of the Specification Pattern is that it provides a more expressive, domain-rich query language. One example usage might be:

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

The composition provided through the Specification Pattern can be provided through .Net's LINQ API as well, though with less control over specifying intention-revealing code.

With respect to execution time, repositories can be written to provide deferred execution by returning IQueryable, or by allowing LINQ expressions to be passed in to be evaluated by the repository method. For example:

Deferred query:

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

Executed by Query() method:

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

The former Query() method which returns an IQueryable has the advantage of being slightly easier to test because the Query() can be easily stubbed to provide the collection operated upon by calling code, while the latter has the advantage of being more deterministic.

=====EDIT====

Inspired by gaearon's approach, I decided to amend my answer with a similar technique. His approach is somewhat of an inverted Specification Pattern, where the specification performs the actual query. This essentially makes it a query in its own right, so let's just call it that:

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

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

So, where's the repository you might ask? We don't need one here. Our ICustomerQuery can just get injected with an IQueryable which can be implemented however you like (perhaps an IoC registration that just returns the following for NHibernate:

 _container.Resolve<ISession>().Linq<Customer>()
萝莉病 2024-10-22 18:46:49

为什么不从存储库本身公开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 的查询,因为它最了解搜索条件及其含义。

Why wouldn't you expose IQueryable from repository itself? This would allow any LINQ query to be run from requesting code.

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

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

You can make AccountSearch responsible for building up the query according to its own logic:

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

Then use it from client code:

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

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

This is just one of possible approaches but I like it because it allows complex logic in search filter class. For example, you might have a radio button in UI with different search types which in turn search by different fields. This is all expressible in AccountSearch when using this pattern. You can make some search fields optional as well, as I've done with Address in this example. After all, you take the responsibility to actually build the query from client code to AccountSearch which is best fit for it because it knows best about search conditions and their meaning.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文