I'm sure this isn't the last time I'll have to wrestle with Linq/Linq To Sql; I'm just happy it's over. The good thing is I feel good about what I came up with. The client doesn't like delays, but it's important to think these things through and do it correctly, or your project will explode!
It's Not Linq's Fault
Steveo left this comment on my last post, in which I explained a roadblock due to some platform weirdness:
This is a great chance to refactor with NHibernate 1.2 and take advantage of a more mature ORM with a great community of support and fellow developers.
It's not too late - show us those refactoring skills now of how it is possible to swap out your data layer.
I recommend Spring.NET and NHibernate - mixed in with some NHibernate Query Generator. If your really stuck on LINQ you can get Ayende's Linq over NHibernate.
:)
NHibernate's pretty cool, to be sure, but I'm not entirely certain how it will make my LazyList work. The problem is a platform one, and what I found out today is that if you set any Enumerable anything as a property, it's Count property will be accessed when you load the parent object. This negates using any deferred loading for any POCOs, period.
The thing about this kind of thinking is that it's all too common - and I do it way too much. "Just toss what you've spent weeks on and go with Solution X". Now I'm always willing to admit I'm wrong (I do it often), but in this case I identified the problem pretty clearly, and changing directions was pretty simple. A wholesale swap out doesn't solve any problems - it usually creates new ones!
Yes, I know NHibernate has eager/lazy loading built in, but I don't think rebuilding my engine right now is going to improve my gas mileage :p so I've changed directions a bit. And I'm sure I'll get some comments on this...
Getting Dumber
The problem, as it turned out, was that I have to respect that what I'm doing involves seriously DUMB repositories with no logic in them at all. Yes, I do know that in the classic Repository pattern it hands back objects and does the querying. But in Rob's Repository, I want the Repositories to be a pipeline - i'll fill the cups on the other end.
To get around my LazyList problem, I simply stripped the list definitions from the Repository, and let the Service class do the "knitting". This added two lines of code to my service class for Catalog:
/// <summary> /// Returns a single product by ID /// </summary> /// <param name="id">The Product ID</param> /// <returns></returns> public Product GetProduct(int id) { //Get the product from the repository Product p = _repository.GetProducts() .WithProductID(id).SingleOrDefault(); //Images p.Images = _repository.GetProductImages().ImagesForProduct(p).ToList(); //Reviews p.Reviews = _repository.GetReviews().ReviewsForProduct(p).ToList(); return p; }
Unfortunately, going with this pattern means I have to add a class to the project that describes the relationship between Product and Category:
public class ProductCategoryMap { public int CategoryID { get; set; } public int ProductID { get; set; } }
And then provide a method in the Repository to get this information (as well as add an interface for it):
/// <summary> /// Gets the relationships from the DB for Products/Categories /// </summary> /// <returns></returns> public IQueryable<ProductCategoryMap> GetProductCategoryMap() { return from pc in db.Categories_Products select new ProductCategoryMap { ProductID = pc.ProductID, CategoryID = pc.CategoryID }; }
At first this really bumbed me out - I don't want DB structure bits to bleed into my application. At the same time, doing this allows me some greater power in terms of querying, and I'm not sure I've really violated anything here - I've just made life a whole lot simpler.
Finally, in order to load the Products for a selected Category, I had to adjust the GetCategory() method on my service class:
/// <summary> /// Returns a single category by ID /// </summary> /// <param name="id">The Category ID</param> /// <returns>Category</returns> public Category GetCategory(int id) { Category result = _repository.GetCategories() .WithCategoryID(id) .SingleOrDefault(); result.Products = (from p in _repository.GetProducts() join cp in _repository.GetProductCategoryMap() on p.ID equals cp.ProductID where cp.CategoryID ==id select p).ToList(); return result; }
I tweaked my tests, and everything is back in order! I have to say - it feels much better at this point; leaner and meaner, and my goal of keeping the Repository simple is working nicely.
Shopping Cart Redo
I think I got far too little sleep the other day. I looked over the cart refactor I did and slapped myself. Thank you for your patience in this matter...
You'd think that a Shopping Cart would be simple after how many times I've done this. But, mix it with Linq and TDD and, well, it sort of highlights the pain we as developers go through when new stuff comes out: how do we do the simple stuff with these new tools?
Anyway - I've greatly simplified the whole cart thing, and it's back to being it's dumb self (sort of). I've kept the list operations (Add, Remove, Find) on the cart, and refactored the Cart Repository to have 4 total method:
public interface IShoppingCartRepository { IQueryable<ShoppingCart> GetCarts(); IQueryable<ShoppingCartItem> GetCartItems(); void SaveCart(ShoppingCart cart); bool DeleteCart(ShoppingCart cart); }
The CartService class is back in action, and does what you'd think it should do - Gets and Saves a cart. This will change as we get into migrating unknown users - and yes I'll get there.
The next webcast will go farther down the ShoppingCart path, as we swap Repositories for authenticated users :).
