TDD, Pipelining, Queries, Oh My!
Over the past week or so we've been keeping an eye on Rob Conery's blog, watching his series of "MVC Storefront" screencasts. Rob's putting together an e-commerce site using the new ASP.NET MCV framework, and he's doing it from the ground up as an exercise in test-driven development (TDD).
Rob's approach involves a "repository" layer, which is essentially a class that's very close to the underlying data store. If I'm right, it'll eventually give him basic CRUD operations that he can call from the next layer up, his "service" layer. Right now it has a "GetProducts" method which returns an IQueryable<Product> - so he has a way to get a query that can give him a list of all products. To that he has added some extension methods so that he can dynamically add "where" clauses to that query. He ended up with code like this:
var prods = _repository.GetProducts().WithCategory(1).ToList();
This is a really interesting approach, in that he's able to "pipeline" his "where" clauses to chain together a query, which he then "executes" by calling ToList() at the end.
Here's where I'm not following him though:
Right now, all of Rob's tests are hitting a faked-out version of his repository. He's got his GetProducts fake method just returning a dummy list of products which he's instantiating on the fly. His goal, as I see it, is to eventually plug in a LINQ to SQL implementation so the queries are passed directly to the database. However, LINQ to SQL is going to want to define its own entity classes for Product, Category etc. I know that LINQ to SQL has some sort of rudimentary mapping technology so you can map your custom classes to it, but is that the direction it's going to go? What if his Product class had an actual Category property of type Category, rather than a CategoryID property of type int? Surely then it would be much harder to map that directly to a LINQ to SQL entity?
In a small project we've just started here at work, we're trying to emulate Rob's approach - TDD, repository and service layers etc. However, I don't think I want to go down this "chaining queries" path using IQueryable<T> just yet - I'm just not convinced that it'll work when we come to implement the repository as a LINQ to SQL class. Instead we're going to give the repository methods that wrap entire queries (eg "GetProductsByCategoryID").
I'm really looking forward to part four of Rob's screencast, though - it's has been very enlightening.
Comments
# Rob Conery
15/04/2008 3:02 PM
Hey Matt - thanks for the writeup. I'd like to recommend you ditch the twitter box on here because your blog doesn't load when Twitter goes down - it just hangs :).
RE your thoughts here...
>>> His goal, as I see it, is to eventually plug in a LINQ to SQL implementation so the queries are passed directly to the database<<<
I'm trying to be very TDD here and see where it leads me. I'll admit that Linq To Sql is my primary target and you're correct, there are some interesting things that using it presents. In terms of mapping - no, there is no mapping there, only partial classes that you can extend. Linq To Sql isn't really an ORM tool - it just allows you to query your DB using LINQ - which is an important distinction.
I'll offer that having methods like "GetXFilteredByY" is a standard route to go, however it's not very flexible and is indeed confining when you think about imlementing IRepository.
For instance - if I wanted to our commerce site to hit a SQL Database (which would be the logical first step) then there is no problem. But when the client says "I want to pull products as a reseller from Amazon as well" (for instance) - you can do this with the Repository pattern nicely - just implement an AmazonRepository that uses IRepository.
The difference here is that in your method above, you will end up implementing 20 methods of "GetProductByX" whereas if you return IQueryable, you implement 5 methods (perhaps): GetProducts(), GetCategories(), GetReviews(), etc.
This puts the responsibility of filtering the bits in the logic layer, where it belongs - done once and only once (think DRY).
At this point, to be honest, I'm thinking I might go Linq To Entities - I just don't know yet.
# mabster
15/04/2008 5:37 PM
Hi Rob,
Thanks for the reply!
I really like the pipelining approach. Your code to get products with a specific category is really readable. I just worry that you're gonna hit a brick wall when you come to the actual implementation.
We've used a simple set of CRUD operations as our repository methods for now, with the "R" part of that being a single "GetXByY" style method. That'll do us for now, but I'm keeping a keen eye on your progress!
And yeah, LINQ to Entities might be the only approach if you want to maintain your existing classes and pass the query directly to the db.
# Rob Conery
16/04/2008 3:53 AM
...And as you predicted, the wall has presented itself. But the neat thing is that I have a very specific set of needs in front of me that i can present to others in terms of needing help :).
In a nutshell (and this is forthcoming on a screencast) - I don't want my relational model to dictate my App model. This is seen directly when thinking about localization of the Product. I want to support 10 languages in terms of the Product Description. All my model cares about is one field - Product.Description. The UI element (culture) is part of the App's domain and my Product should remain ignorant of that.
My DB, however, wants at least 3 tables to support this (Product, ProductDescriptions, and Cultures). I don't want these things in my model!
Impedance Mismatch, here I come head-first!
# mabster
16/04/2008 7:08 AM
Can't wait to see what you come up with Rob!