Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which is better? Have complicated search logic in repository or in a domain level service (via IQueryable or other)?

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?

like image 972
Byron Sommardahl Avatar asked Feb 05 '11 15:02

Byron Sommardahl


People also ask

What is the difference between the Repository pattern and a service layer?

Repository layer is implemented to access the database and helps to extend the CRUD operations on the database. Whereas a service layer consists of the business logic of the application and may use the repository layer to implement certain logic involving the database.

Why do we need repository pattern?

The Repository pattern makes it easier to test your application logic. The Repository pattern allows you to easily test your application with unit tests. Remember that unit tests only test your code, not infrastructure, so the repository abstractions make it easier to achieve that goal.

What is Repository Service pattern?

The Repository-Service pattern breaks up the business layer of the app into two distinct layers. The lower layer is the Repositories. These classes handle getting data into and out of our data store, with the important caveat that each Repository only works against a single Model class.


2 Answers

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>()
like image 54
Derek Greer Avatar answered Oct 11 '22 14:10

Derek Greer


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.

like image 42
Dan Abramov Avatar answered Oct 11 '22 12:10

Dan Abramov