Trying Out Persistence Ignorance with LINQ (Part II)

Wednesday, August 29, 2007

In Part I, we defined an IUnitOfWork interface to avoid coding directly to the System.Data.Linq.DataContext class. IUnitOfWork gives us the opportunity to manipulate IDataSource<T> objects.

IDataSource<T> is another abstraction. When the software is running in earnest, we need to back an IDataSource<T> with a table in a SQL Server database. However, during unit testing we might prefer to have an IDataSource<T> backed by an in-memory data structure. Essentially, we want the ability to switch between a System.Data.Linq.Table<T> implementation (SQL Server) and a System.Collections.Generic.List<T> implementation (in-memory) - without changing the code and LINQ expressions in the upper layers of software.

Fortunately, a handful of interfaces define every Table<T> object. If we implement the same interfaces, we can walk and talk just like a real Table<T>.

public interface IDataSource<T> : IQueryable<T>, IEnumerable<T>,
                                 
ITable, IQueryProvider
{

}

Implementing a persistent data source, one that talks to SQL Server, is as simple as forwarding the calls to an underlying Table<T> implementation.

class PersistentDataSource<T> : IDataSource<T> where T: class
{
    
Table<T> _table = null;
    
DataContext _context = null;
  
    
public PersistentDataSource(DataContext context)
    {
        
Check.ArgIsNotNull(context, "context");

        _context = context;
        _table = context.GetTable<T>();

        
Check.IsNotNull(_table, "Could not retrieve table for "
                                +
typeof(T).ToString());
    }

    
void ITable.Add(object o)
    {
        ((
ITable)_table).Add(o);
    }

    
public IQueryable CreateQuery(Expression expression)
    {
        
return ((IQueryProvider)_table).CreateQuery(expression);
    }
    

    
// ...

Implementing an in-memory data source is a little bit trickier, but appears possible thanks to extension methods like AsQueryable on the System.Linq.Queryable class.

class InMemoryDataStore<T> : IDataSource<T>
{
    
List<T> _list = new List<T>();

    
public void Add(object entity)
    {
        _list.Add((T)entity);
    }

    
public IQueryable CreateQuery(Expression expression)
    {
        
IQueryable queryable = _list.AsQueryable();
        
return queryable.Provider.CreateQuery(expression);
    }
    
    
// ...

Of course, this naïve implementation could never support the full application, but should be suitable for the majority of isolated unit tests.

In the next post, we'll tie everything together to see how all these abstractions work together.

In the meantime, read Rick Strahl's "Dynamic Expression in LINQ to SQL". It is scenarios like the ones that Rick is presenting that make me worry that this approach will fall apart when it hits real requirements.


Comments
Frans Bouma Wednesday, August 29, 2007
Isn't this just moving the reliance on a 3rd party assembly to another class but still keep the reliance on that 3rd party assembly? I mean, IDataSource<T>... isn't that linq-to-sql ?

Persistence Ignorance is actually a myth. It doesn't exist. People want so hard to believe that it does, but it doesn't. Sure, pure poco classes which are persisted look great. But then we get into the fine details.

Customer c = somecontext.GetInstance<Customer>(key);
Order o = new Order();
o.Customer = c;

now, question. Is this true after this code snippet:
c.Orders.Contains(o);

?

If you say: YES, then you don't use nhibernate. If you say: NO, then you are using nhibernate and not using another o/r mapper.

Question two: has anyone ever re-used a domain model in code in another unrelated app? No? Hmm.

Question three: has anyone ever swapped the o/r mapper X you were using with o/r mapper Y and both were PI o/r mappers, and you didn't have to do ANYTHING to make it work? Noone? Hmm...

Instead of chasing a ghost called PI, people should focus on real problems, like implementing the functionality they promissed their customers.
scott Wednesday, August 29, 2007
Frans:

Good points.

In this case I'm not trying to isolate a third party assembly or reuse the domain model in an unrelated app. I just want to test some of my business logic without getting a database involved.
If I can do it easily - great, I'll move ahead with the approach. If this causes pain and additional work, then it gets cut.
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!