Friday, March 23, 2012

Change Tracking with an Event

So I've continued to tinker with this Collection As A Repository thing today, off and on between other work. Something about it just keeps me interested, and it's a welcome break from the non-coding work I have to do on my current project.

I seem to be in a good spot right now in terms of tracking changes. What I've done is expand my domain model's properties to be more manual and to raise an event when they change (INotifyPropertyChanged, of course, since I imagine that will be useful in other contexts as well). Then the collection listens on that event for any model it returns and, in response to the event, updates the local data context.

So at this point here's the custom model:

public class Product : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;

  private int _productID;
  public int ProductID
  {
    get
    {
      return _productID;
    }
  }

  private string _name;
  public string Name
  {
    get { return _name; }
    set
    {
      _name = value;
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs("Name"));
    }
  }

  private string _productNumber;
  public string ProductNumber
  {
    get { return _productNumber; }
    set
    {
      _productNumber = value;
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs("ProductNumber"));
    }
  }

  private decimal _listPrice;
  public decimal ListPrice
  {
    get { return _listPrice; }
    set
    {
      _listPrice = value;
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs("ListPrice"));
    }
  }

  private Product() { }

  public Product(string name, string productNumber, decimal listPrice)
  {
    _name = name;
    _productNumber = productNumber;
    _listPrice = listPrice;
  }

  public Product(int productID, string name, string productNumber, decimal listPrice)
  {
    _productID = productID;
    _name = name;
    _productNumber = productNumber;
    _listPrice = listPrice;
  }
}

It took me a while to come to terms with the fact that I'm not using auto-implemented properties anymore. I just love the simple clean syntax of them. And the repetition of these properties... Not so much. But the convincing argument I keep giving myself is that these are not merely data structures. These are potentially rich domain models. It's fully acceptable and expected for their properties to be expanded out and to contain stuff under the hood.

And here's the current state of my "repository":

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

And the implementation that I'm injecting for it:

public class Products : IProducts
{
  private AdventureWorksDataContext _db = new AdventureWorksDataContext();
  private IList<Product> _products = new List<Product>();

  public Models.Product this[int id]
  {
    get
    {
      if (id < 1)
        throw new ArgumentException("ID must be a positive integer.");

      if (ObjectHasNotBeenFetchedFromTheDatabase(id: id))
        FetchObjectFromTheDatabase(id: id);
      return TransformEntityToModel(_products.Single(p => p.ProductID == id));
    }
  }

  public Models.Product this[string name]
  {
    get
    {
      if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException("Name must be a valid string.");

      if (ObjectHasNotBeenFetchedFromTheDatabase(name: name))
        FetchObjectFromTheDatabase(name: name);
      return TransformEntityToModel(_products.Single(p => p.Name == name));
    }
  }

  public void Add(Models.Product product)
  {
    var productEntity = TransformModelToEntity(product);
    _products.Add(productEntity);
    _db.Products.InsertOnSubmit(productEntity);
  }

  public void Remove(Models.Product product)
  {
    var productEntity = TransformModelToEntity(product);
    _products.Remove(TransformModelToEntity(product));
    _db.Products.DeleteOnSubmit(productEntity);
  }

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

  public System.Collections.IEnumerator GetEnumerator()
  {
    var localProducts = _products.Select(p => p.ProductID);
    foreach (var product in _products.Concat(_db.Products.Where(p => !localProducts.Contains(p.ProductID))).OrderBy(p => p.ProductID))
      yield return TransformEntityToModel(product);
  }

  IEnumerator<Models.Product> IEnumerable<Models.Product>.GetEnumerator()
  {
    var localProducts = _products.Select(p => p.ProductID);
    foreach (var product in _products.Concat(_db.Products.Where(p => !localProducts.Contains(p.ProductID))).OrderBy(p => p.ProductID))
      yield return TransformEntityToModel(product);
  }

  private bool ObjectHasNotBeenFetchedFromTheDatabase(int id = 0, string name = "")
  {
    if ((id == 0) && (string.IsNullOrWhiteSpace(name)))
      throw new ArgumentException("Must supply either an id or a name.");

    if (id > 0)
      return _products.SingleOrDefault(p => p.ProductID == id) == null;
    else
      return _products.SingleOrDefault(p => p.Name == name) == null;
  }

  private void FetchObjectFromTheDatabase(int id = 0, string name = "", Product product = null)
  {
    if ((id == 0) && (string.IsNullOrWhiteSpace(name)) && (product == null))
      throw new ArgumentException("Must supply either an id or a name or a product.");

    if (id > 0)
      _products.Add(_db.Products.Single(p => p.ProductID == id));
    else if (!string.IsNullOrWhiteSpace(name))
      _products.Add(_db.Products.Single(p => p.Name == name));
    else
      _products.Add(product);
  }

  private Models.Product TransformEntityToModel(Product product)
  {
    var productModel = new Models.Product(product.ProductID, product.Name, product.ProductNumber, product.ListPrice);
    productModel.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(Changed);
    return productModel;
  }

  private Product TransformModelToEntity(Models.Product product)
  {
    var existingProduct = _products.SingleOrDefault(p => p.ProductID == product.ProductID);
    if (existingProduct == null)
      existingProduct = _db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
    return existingProduct ?? new Product
    {
      ProductID = product.ProductID,
      Name = product.Name,
      ProductNumber = product.ProductNumber,
      ListPrice = product.ListPrice,
      SafetyStockLevel = 1000,
      ReorderPoint = 750,
      SellStartDate = DateTime.Now,
      ModifiedDate = DateTime.Now
    };
  }

  private void Changed(object product, PropertyChangedEventArgs args)
  {
    var eventProduct = product as Models.Product;
    var existingProduct = _products.SingleOrDefault(p => p.ProductID == eventProduct.ProductID);
    if (existingProduct == null)
    {
      FetchObjectFromTheDatabase(id: eventProduct.ProductID);
      existingProduct = _products.Single(p => p.ProductID == eventProduct.ProductID);
    }
    existingProduct.ProductID = eventProduct.ProductID;
    existingProduct.Name = eventProduct.Name;
    existingProduct.ProductNumber = eventProduct.ProductNumber;
    existingProduct.ListPrice = eventProduct.ListPrice;
  }
}

It's very rough at the moment, of course. Much of the code can be re-factored and cleaned up. There are probably edge cases I haven't discovered yet. (Some of the code in there now is to handle the edge case of adding an object to the collection and then removing it before committing it, for example.) The general idea of having that local copy of stuff can certainly use some polishing. On the one hand, it provides a potentially useful caching construct. On the other hand, it's kind of ugly and makes other code throughout the class kind of ugly.

At the moment I'm not making it production-worthy. I'm just making a proof of concept. And one of the concepts I think I'd like to try with this is getting rid of SaveChanges() entirely and just using an implied save any time anything changes. Of course, if I go that route then I'll definitely want to implement a Unit Of Work object to wrap it all up (unless I just want to assume the use of MSDTC).

No comments:

Post a Comment