Contrasting Two MVC / LINQ to SQL Applications for the Web

Tuesday, May 6, 2008

There are two applications on CodePlex that are interesting to compare and contrast. The MVC Storefront and Background Motion.

MVC Storefront

MVC Storefront is Rob Conery's work. You can watch Rob lift up the grass skirt as he builds this application in a series of webcasts (currently up to Part 8). Rob is using the ASP.NET MVC framework and LINQ to SQL. The Storefront is a work in progress and Rob is actively soliciting feedback on what you want to see.

At the it's lowest level, the Storefront uses a repository type pattern.

public interface ICatalogRepository {
    
IQueryable<Category> GetCategories();
    
IQueryable<Product> GetProducts();
    
IQueryable<ProductReview> GetReviews();
    
IQueryable<ProductImage> GetProductImages();
}

The repository interface is implemented by a TestCatalogRepository (for testing), and a SqlCatalogRepository (when the application needs to get real work done). Rob uses the repositories to map the LINQ to SQL generated classes into his own model, like the following code that maps a ProductImage (the LINQ generated class with a [Table] attribute) into a ProductImage (the Storefront domain class).

public IQueryable<ProductImage> GetProductImages() {

    
return from i in ReadOnlyContext.ProductImages
          
select new ProductImage
           {
               ID = i.ProductImageID,
               ProductID = i.ProductID,
               ThumbnailPhoto = i.ThumbUrl,
               FullSizePhoto = i.FullImageUrl
           };
}

Notice the repository also allows IQueryable to "float up", which defers the query execution. The repositories are consumed by a service layer that the application uses to pull data. Here is an excerpt of the CatalogService.

public Category GetCategory(int id) {
    
    
Category result = _repository.GetCategories()
        .WithCategoryID(id)
        .SingleOrDefault();

    
return result;
}

Controllers in the web application then consume the CatalogService.

public ActionResult Show(string productcode) {

    
CatalogData data = new CatalogData();
    
CatalogService svc = new CatalogService(_repository);

    data.Categories = svc.GetCategories();
    data.Product = svc.GetProduct(productcode);

    
return RenderView("Show",data);
}

Another interesting abstraction in Rob's project is LazyList<T> - an implementation of IList<T> that wraps an IQueryable<T> to provide lazy loading of a collection. LINQ to SQL provides this behavior with the EntitySet<T>, but Rob is isolating his upper layers from LINQ to SQL needs a different strategy. I'm not a fan of the GetCategories method in CatalogService – that looks like join that the repository should put together for the service, and the service layer itself doesn't appear to add a tremendous amount of value, but overall the code is easy to follow and tests are provided. Keep it up, Rob!

Background Motion

The Background Motion (BM) project carries significantly more architectural weight. Not saying this is better or worse, but you know any project using the Web Client Software Factory is not going to be short on abstractions and indirections.

Unlike the Storefront app, the BM app uses a model that is decorated with LINQ to SQL attributes like [Table] and [Column]. BM has a more traditional repository pattern and leverages both generics and expression trees to give the repository more functionality and flexibility.

public interface IRepository<T> where T : IIdentifiable
{
  
int Count();
  
int Count(Expression<Func<T, bool>> expression);
  
void Add(T entity);
  
void Remove(T entity);
  
void Save(T entity);
  T FindOne(
int id);
  T FindOne(
Expression<Func<T, bool>> expression);
  
bool TryFindOne(Expression<Func<T, bool>> expression, out T entity);
  
IList<T> FindAll();
  
IList<T> FindAll(Expression<Func<T, bool>> expression);
  
IQueryable<T> Find();
  
IQueryable<T> Find(int id);
  
IQueryable<T> Find(Expression<Func<T, bool>> expression);
}

Notice the BM repositories will "float up" a deferred query into higher layers by returning an IQueryable, and allow higher layers to "push down" a specification in the form of an expression tree. Combining this technique with generics means you get a single repository implementation for all entities and minimal code. Here is the DLinqRepository implementation of IRepository<T>'s Find method.

public override IQueryable<T> Find(Expression<Func<T, bool>> expression)
{
  
return DataContext.GetTable<T>().Where(expression);
}

Where FindOne can be used like so:

Member member = Repository<Member>.FindOne(m => m.HashCookie == cookieValue);

BM combines the repositories with a unit of work pattern and consumthines both directly in the website controllers.

public IList<Contribution> GetMostRecentByContentType(int contentTypeId, int skip)
{
  
using (UnitOfWork.Begin())
  {
    
return ModeratedContributions
      .Where(c => c.ContentTypeId == contentTypeId)
      .OrderByDescending(c => c.AddedOn)
      .Skip(skip)
      .Take(
Constants.PageSize).ToList();
  }
}

The Background Motion project provides stubbed implementation of all the required repositories and an in-memory unit of work class for unit testing, although the test names leave something to be desired. One of the interesting classes in the BM project is LinqContainsPredicateBuilder – a class whose Build method takes a collection of objects and a target property name. The Build method returns an expression tree that checks to see if the target property equals any of the values in the collection (think of the IN clause in SQL). 

If you want to see Background Motion in action, check out backgroundmotion.com!

Comments
kasperp Tuesday, May 6, 2008
Here's an even simpler repository-like implementation using Linq to SQL: www.iridescence.no/...
stephen patten Thursday, May 8, 2008
Any idea why this might be happening Scott? I followed the direction to install and set up the db.... thank you

stephen

Server Error in '/' Application.
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.

Requested URL: /Contribute.aspx

scott Thursday, May 8, 2008
Stephen - offhand I'm not sure..
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!