Thursday, March 22, 2012

Collection as a Repository

Yesterday I posted about how I'm finding myself conceptually moving away from generic repositories and into more custom ones. And I started to think about the idea of using a custom collection as a repository.

Today I threw together a simple little proof of concept just to get a picture of what it is I'm actually thinking. Currently it's sort of just a pass-through to Linq2Sql tables, using the Linq2Sql objects (for their change tracking capabilities). This is what I threw together:

public interface IProducts : IEnumerable
{
  Product this[int id] { get; set; }
  Product this[string name] { get; set; }
  void Add(Product product);
  void Remove(Product product);
  void SaveChanges();
}

public class Products : IProducts
{
  private AdventureWorksDataContext _db = new AdventureWorksDataContext();

  public Product this[int id]
  {
    get
    {
      return _db.Products.Single(p => p.ProductID == id);
    }
    set
    {
      var existingProduct = _db.Products.Single(p => p.ProductID == id);
      existingProduct.Name = value.Name;
      existingProduct.ProductNumber = value.ProductNumber;
      existingProduct.ListPrice = value.ListPrice;
    }
  }

  public Product this[string name]
  {
    get
    {
      return _db.Products.Single(p => p.Name == name);
    }
    set
    {
      var existingProduct = _db.Products.Single(p => p.Name == name);
      existingProduct.Name = value.Name;
      existingProduct.ProductNumber = value.ProductNumber;
      existingProduct.ListPrice = value.ListPrice;
    }
  }

  public void Add(Product product)
  {
    _db.Products.InsertOnSubmit(product);
  }

  public void Remove(Product product)
  {
    _db.Products.DeleteOnSubmit(product);
  }

  public void SaveChanges()
  {
    _db.SubmitChanges();
  }

  public System.Collections.IEnumerator GetEnumerator()
  {
    foreach (var product in _db.Products)
      yield return product;
  }
}

I really like using indexers like that. Think of them like .GetBySomeKey() methods, but a little cleaner in their usage. After all, I always find .GetBySomeKey() methods to feel clunky. We're just trying to fetch an object from a collection based on a key, right? Well the language already has a standard for doing just that. Think of a Dictionary object, for example.

I'd also like to experiment a little with the different interfaces to implement. IList will be more involved, but may have a more intuitive and cleaner result.

Naturally, there's some re-factoring to do in the code above. The setters on the indexers can certainly be re-factored and cleaned up. (And, indeed, I kept them at a minimum just to demonstrate the concept... There are considerably more fields than the three being set.)

My biggest problem with this so far is that I'm using the Linq2Sql objects as my models, which means my DAL implementation is bleeding out into my domain. I find that distasteful and don't want such things polluting my model. However, without it I run into a problem of change tracking. For example, if I use a simple object as my domain model:

public class Product
{
  public int ProductID { get; set; }
  public string Name { get; set; }
  public string ProductNumber { get; set; }
  public decimal ListPrice { get; set; }
}

(Imagine this with more business logic baked into it, of course... But I'm keeping it simple for this post.)

Now I need to translate between my database entities and my business models. So adding one would look more like this:

public void Add(Models.Product product)
{
  _db.Products.InsertOnSubmit(new Product
  {
    ListPrice = product.ListPrice,
    Name = product.Name,
    ProductID = product.ProductID,
    ProductNumber = product.ProductNumber
  });
}

That's all well and good, but what about when I get an item:

public Product this[int id]
{
  get
  {
    return _db.Products
              .Select(p => new Models.Product
              {
                ListPrice = p.ListPrice,
                Name = p.Name,
                ProductID = p.ProductID,
                ProductNumber = p.ProductNumber
              })
              .Single(p => p.ProductID == id);
  }
  set
  {
    // ...
  }
}

Oops, now I've lost my change tracking. When using this, I would intuitively expect .SaveChanges() to save the changes I make to the models I fetch from the collection. But in this example it won't. So, of course, I need to bake more intelligence into this. The question becomes whether I can keep that change tracking intelligence from overly polluting my model. (Normally when I use repositories it's all pretty well abstracted into the DAL and the models don't know or care about it, but that's because the repositories have all kinds of explicit GetBySomething() and Save() methods where I can intuitively expect that logic to reside. This is a more abstracted interface, so this will require more thought.)

1 comment:

  1. The more I think about it, the more I think that this is a job for AOP. Change tracking is very clearly a cross-cutting concern.

    PostSharp looks nice, but licensing is a problem. I can't use it at work with a personal license because nobody else on the team would be able to use it afterward.

    If only C# had native AOP constructs...

    ReplyDelete