Writing (safe) LINQ queries

Naked Objects allows you to switch from working with objects in memory, to working with persistent objects on a database, just by changing the Persistor property on the Run class. When you do this any LINQ query methods (e.g on your repositories) will transparently be switching from being executed by LINQ to Objects, to LINQ to Entities, say, or vice versa. The capabilities of these two implementations are different - the LINQ queries may still compile, but errors may show up at run-time. Since LINQ to Entities has rather more constraints, recommended best practice is to write all queries to run against Entities - in which case they should still run when working with objects in memory. (The reverse is not true). Follow these four rules of thumb:

1. Don't use the equality operator on objects; test for equality on the value properties

Don't write:
public IQueryable<Product> FindProducts(ProductCategory category) {
  return Container.Instances<Product>().Where(x => x.Category == category);
}  

Write:
public IQueryable<Product> FindProducts(ProductCategory category) {
  return Container.Instances<Product>().Where(x => x.Category.Id == category.Id);
}

2. Don't call any method on a domain object within a query; refer only to properties

Don't write:
public IQueryable<Product> ListDiscontinuedProducts() {
  return Container.Instances<Product>().Where(x => x.IsDiscontinued());
}  

Write:
public IQueryable<Product> ListDiscontinuedProducts() {
  return Container.Instances<Product>().Where(x => x.Status == "Discontinued");
}  


Note, though that you can call methods on System classes e.g. Trim().ToUpper() on string.

3. Don't navigate references on any objects passed into the query; pass in any such required indirect references as variables in their own right

Don't write:
public IQueryable<Order> FindOrdersNotSentToBillingAddress(Customer cust) {
  return Instances<Order>().Where(x => x.SentTo.Id != cust.BillingAddress.Id);
}

Write:
public IQueryable<Order> FindOrdersNotSentToBillingAddress(Customer cust) {
  Address billing = cust.BillingAddress;
  return Instances<Order>().Where(x => x.SentTo.Id != billing.Id);
}

4. When doing a join, don't try to use Container.Instances<T>() more than once inside a query; define a separate IQueryable<T> outside the query:

Don't write:
var q = from p in Container.Instances<Product>()
             from c in Container.Instances<Customer>()
             where ...

Write:
var customers = Container.Instances<Customer>();
var q = from p in Container.Instances<Product>()
             from c in customers
             where …

Or, for greater clarity:
var customers = Container.Instances<Customer>();
var products = Container.Instances<Product>();

var q = from p in products
             from c in customers
             where …

Last edited Jul 11, 2012 at 9:00 AM by RichardPawson, version 5