Trying Out Persistence Ignorance with LINQ (Part I)

Monday, August 27, 2007

In June, Ian Cooper wrote an article (Being Ignorant With LINQ to SQL) that provided marvelous details about using POCOs with persistence ignorant repositories. Ian's conclusion: "LINQ to SQL is usable with a TDD/DDD approach to development. Indeed the ability to swap between LINQ to Objects and LINQ to SQL promises to make much more of the code easily testable via unit tests than before."

I've been tinkering along the same lines as Ian, and although I think I started working at the other end of the problem first, I'm reaching the same conclusions (and ended up with much of the same code).

I started working with a trivial case, thinking if the trivial case becomes difficult, the topic isn't worth pursuing. Let's suppose we have the following class.

public class FoodFact
{
    
public int ID
    {
        
get { return _id; }
        
set { _id = value; }
    }
    
    
public string Name
    {
        
get { return _name; }
        
set { _name = value; }

    }

    
public int Calories
    {
        
get { return _calories; }
        
set { _calories = value; }
    }

    
int _id;
    
int _calories;
    
string _name;
}

The idea is that each FoodFact object will ultimately be persisted as a row in a SQL Server database, but I don't want to clutter up the FoodFact class with unrelated attribute decorations or other database nonsense. To get into the database, I do need some sort of gateway, or class that can track POCO objects and determine if they need persisted. In LINQ, this class is the System.Data.Linq.DataContext class. The DataContext class itself doesn't implement any interesting interfaces, so my plan was to abstract the DataContext in a a stubable, fakeable, mockable fashion.

public interface IUnitOfWork : IDisposable
{
    
IDataSource<T> GetDataSource<T>() where T : class;
    
void SubmitChanges();
}

We have not looked at IDataSource yet, but there will be at least two implementations of IUnitOfWork. One implementation will work against a real database using a persistent data source (where FoodDB is a class derived from DataContext):

public class UnitOfWork : IUnitOfWork
{
    
public UnitOfWork()
    {
        _context =
FoodDB.Create();
    }

    
public IDataSource<T> GetDataSource<T>() where T : class
    {
        
return new PersistentDataSource<T>(_context);
    }
  
    
public void SubmitChanges()
    {
        _context.SubmitChanges();
    }

    
public void Dispose()
    {
        _context.Dispose();
    }

   
FoodDB _context = null;
}

Another implementation will work with an "in memory data source". This class would be defined in a unit test project.

class InMemoryUnitOfWork : IUnitOfWork
{
    
public IDataSource<T> GetDataSource<T>() where T : class
    {
        
return new InMemoryDataStore<T>();
    }
    
    
public void SubmitChanges()
    {
        
    }

    
public void Dispose()
    {
        
    }
}

The goal is to consume IUnitOfWork along these lines:

public class FoodFactRepository
    :
IRepository<FoodFact>
{
    
IDataSource<FoodFact> _dataSource;
    
IUnitOfWork _context;
    
    
public FoodFactRepository(IUnitOfWork context)
    {
        
Check.ArgIsNotNull(context, "context");
        
        _context = context;
        _dataSource = _context.GetDataSource<
FoodFact>();

        
Check.IsNotNull(_dataSource, "Could not retrieve a data source");
    }

    
public FoodFact FindByID(int ID)
    {
        
return
            (
                
from fact in FoodFacts
                
where fact.ID == ID
                
select fact
            ).FirstOrDefault();
    }

    
public FoodFact Add(FoodFact fact)
    {
        
Check.ArgIsNotNull(fact, "fact");

        _dataSource.Add(fact);
        
return fact;
    }

    
public IDataSource<FoodFact> FoodFacts
    {
        
get { return _dataSource; }
    }        

    
// ...

What are these data sources? Stay tuned for part II ...


Comments
Thomas Eyde Monday, August 27, 2007
This is an interesting twist on TDD and LINQ. The LINQ expressions themselves are impossible to mock, I have heard, but this approach seems very nice.

However, I have trouble to follow the code when the interface is 'IUnitOfWork', while the parameter is named 'context'.
scott Monday, August 27, 2007
Thomas:

I agree, and that is something I need to go back and change. The iterface itself went through several iterations of renaming and I never cleaned up the parameter names.
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!