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?
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.
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.
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.
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>()
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With