In one of the first few episodes of the MVC Storefront, Ayende decided to be brilliant and came up with a really cool way to handle lazy loading with Linq To Sql: The LazyList. If you're not familiar with the term, "Lazy Loading" applies to the concept of an object that has a property which is a collection of child objects - like a Category would have Products.
When using an ORM (object-relational mapper) tool, the question of whether/when to load these child objects comes up a lot, as it has some serous performance implications.
IQueryable As Game-changer
IQueryable is the cornerstone of Linq To Sql, and has (what I think) is a killer feature: delayed execution. IQueryable essentially creates an Expression for you that you can treat as an enumerable list - and only when you iterate or ask for a value will the query be executed.
This has a lot of implications - especially for how you think of your Data Access Layer. It led me to make some tweaks to the Repository Pattern (which made a lot of people cranky) and also enabled a really nice way to do Lazy Loading for the objects in my model.
The Idea
... Was Ayende's. We were going over Data Access strategy and he got really excited and said "GIMME CONTROL!" (we were using SharedView at the time) and then started banging out some code. What he created was an implementation of IList<> that he called LazyList (this pattern is used in Java, Ruby, and other languages too) that took an IQueryable definition in the constructor. When you iterated the LazyList (or asked for a value), the query would get fired, populating the list.
This is a great pattern - it's almost a step between Lazy and Eager Loading - if you need it, it's there. Otherwise it's not.
The original thought here was that I could define a child collection (let's use Products on a Category) as an IList, and in the mapping code I could set that property to be a LazyList. Seems like it should work, but it didn't.
In summary there were some bugs in the Linq To Sql butter that cause the list to iterate, and I had a show to do so I just moved on, hoping some smart guy would figure it out. And lo and behold he did! I love community development...
Fooling Linq To Sql
K. Scott Allen came up with the answer (I never trust people who use an initial as their first name - I never know what to call them! K? K. Scott? What's the "K" for anyway? Was is that bad that you had to shorten it and go with an initial? If so, now I'm really curious... or perhaps that's what he wants... see this kind of thing is social quicksand... I bet his name is Karl and he hated telling people "no, with a K!").
The answer is two-fold:
- Linq To Sql will do it's best to create a query for you, and it will use magic if necessary.
- You have to have type parity when assigning a list - otherwise the list contents will get copied - and this will break the delayed execution.
If you want to read more - see Scott's entry.
Implementing Lazy List - Again
The good news is I didn't need to change any code in the LazyList. All I needed to do was to "fool" Linq to Sql when mapping my object so that it couldn't try to write a query for the relationship (I did this with a "let" statement), and I reset my Products child list from an IList<Product> to a LazyList<Product>. And it worked.
To illustrate, here's my changed Product with it's child collections (Images, Reviews, etc) set to LazyList:
public class Product { //... public LazyList<ProductReview> Reviews { get; set; } public LazyList<ProductImage> Images { get; set; } public LazyList<Product> RelatedProducts { get; set; } public LazyList<Product> CrossSells { get; set; } //... }
Here's the changed mapping code, which sets those lists:
var result = from p in _db.Products join detail in cultureDetail on p.ProductID equals detail.ProductID let images = GetImages(p.ProductID) let crosses = GetCrossSells(p.ProductID) let related = GetRelated(p.ProductID) let reviews = GetReviews(p.ProductID) select new Product { ID = p.ProductID, Name = p.ProductName, Description = detail.Description, ShortDescription = detail.ShortDescription, Price = detail.UnitPrice ?? p.BaseUnitPrice, Manufacturer = p.Manufacturer, ProductCode = p.ProductCode, Images = new LazyList<ProductImage>(images), CrossSells = new LazyList<Product>(crosses), RelatedProducts = new LazyList<Product>(related), Reviews = new LazyList<ProductReview>(reviews), Delivery = (DeliveryMethod) p.DeliveryMethodID };
You'll notice here that the "let" statements point to some methods that take a Product ID. This is part of fooling Linq To Sql so it doesn't try to introspect this relationship and build some weird SQL statement. I had to create these methods and they are simply filter statements that return IQueryable<Product> (you can see these in the checked in Storefront code).
The code looks a lot cleaner too!
And finally, for reference, here's the LazyList code:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Collections; namespace Commerce.MVC.Data { /// <summary> /// An IList implementation that flexes IQueryable's delayed loading /// </summary> /// <typeparam name="T">IList of T</typeparam> public class LazyList<T> : IList<T> { public LazyList() { } public LazyList(IQueryable<T> query) { this.query = query; } private IQueryable<T> query; private IList<T> inner; public int IndexOf(T item) { return Inner.IndexOf(item); } public void Insert(int index, T item) { Inner.Insert(index, item); } public void RemoveAt(int index) { Inner.RemoveAt(index); } public T this[int index] { get { return Inner[index]; } set { Inner[index] = value; } } public void Add(T item) { inner = inner ?? new List<T>(); Inner.Add(item); } public void Clear() { if(inner!=null) Inner.Clear(); } public bool Contains(T item) { return Inner.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { Inner.CopyTo(array, arrayIndex); } public bool Remove(T item) { return Inner.Remove(item); } public int Count { get { return Inner.Count; } } public bool IsReadOnly { get { return Inner.IsReadOnly; } } IEnumerator<T> IEnumerable<T>.GetEnumerator() { return Inner.GetEnumerator(); } public IEnumerator GetEnumerator() { return ((IEnumerable)Inner).GetEnumerator(); } public IList<T> Inner { get { if (inner == null) inner = query.ToList(); return inner; } } } }
