Saturday, October 25, 2008

TDD and LINQ to SQL

Microsoft has made the unfortunate mistake of not making System.Data.Linq.Table<T> implement an interface. If they had only done this it would have been easy to mock out the Persistence Layer when using LINQ to SQL.

Since it is a class and a sealed one at that, this is not an available path for TDD. What to do? If we can’t implement it we can adapt it. What is required for this to work is two interfaces: IUnitOfWork and ITable<T>, two classes LinqToSqlUnitOfWork and LinqToSqTable<T> and for testing purposes two additional classes InMemoryUnitOfWork and InMemoryTable<T>.

ITable<T> looks like this. I usually only include the methods that I need and add more by need.


   public interface ITable<T> : IQueryable<T> where T : class
    {
        void Attach(object entity);
        IQueryable<TElement> CreateQuery<TElement>(Expression expression);
        IQueryable CreateQuery(Expression expression);
        void DeleteAllOnSubmit(IEnumerable entities);
        void DeleteOnSubmit(T entity);
        object Execute(Expression expression);
        TResult Execute<TResult>(Expression expression);
        void InsertAllOnSubmit(IEnumerable entities);
        void InsertOnSubmit(T entity);
    }

IUnitOfWork looks like this with one accessor method for every aggregate in my domain model. I have only included SubmitChanges in the interface but it is entirely possible to include all the methods of DataContext here if you have the need for it.


   public interface IUnitOfWork
   {
       ITable<Player> Players { get; }
       ITable<Round> Rounds { get; }
       void SubmitChanges();
   }

LinqToSqlUnitOfWork looks like this. Every method wraps the sealed Table<T> in a LinqToSqlTable<T>.


public class LinqToSqlUnitOfWork : IUnitOfWork
{
     private readonly LinqToSqlDataContext dataContext;

     public LinqToSqlUnitOfWork(LinqToSqlDataContext dataContext)
     {
         this.dataContext = dataContext;
     }

     public ITable<Player> Players
     {
         get { return new LinqToSqlTable<Player>(dataContext.GetTable<Player>()); }
     }

     public ITable<Round> Rounds
     {
         get { return new LinqToSqlTable<Round>(dataContext.GetTable<Round>()); }
     }

     public void SubmitChanges()
     {
         dataContext.SubmitChanges();
     }

 }

The next class need is LinqToSqlTable<T>. As shown above the constructor takes a Table<T> as a parameter. All actual work is delegated to the Table<T>.


public class LinqToSqlTable<T> : ITable<T> where T : class
{
    private readonly Table<T> table;

    public LinqToSqlTable(Table<T> table)
    {
        this.table = table;
    }


    public void Attach(T entity)
    {
        table.Attach(entity);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        return ((IQueryProvider) table).CreateQuery<TElement>(expression);
    }

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

    public void DeleteOnSubmit(T entity)
    {
        table.DeleteOnSubmit(entity);
    }
    ...

Finally we have the InMemory classes. InMemoryUnitOfWork works similarly to SqlToLinqUnitOfWork creating InMemoryTables that implement the ITable<T> Interface. The SubmitChanges method is left as an interesting exercise for the reader :)


public class InMemoryUnitOfWork : IUnitOfWork
{
    private readonly ITable<Player> players;
    private readonly ITable<Round> rounds;

    public InMemoryUnitOfWork()
    {
        players = new InMemoryTable<Player>();
        rounds = new InMemoryTable<Round>();
    }

    public ITable<Player> Players
    {
        get { return players; }
    }

    public ITable<Round> Rounds
    {
        get { return rounds; }
    }

    public void SubmitChanges()
    {
        //TODO
    }
}

In its simplest form the InMemoryTable<T> just uses a List<T> as storage, delegating all possible functionality to the list. A lot of methods are left out for brevity.


    public class InMemoryTable<T> : ITable<T> where T : class 
    {
        private readonly IList<T> list;
 
        public InMemoryTable() : this(new List<T>())
        {
        }

        public InMemoryTable(IList<T> aList)
        {
            list = aList;
        }

        public IEnumerator<T> GetEnumerator()
        {
            return list.GetEnumerator();
        }

        public Expression Expression
        {
            get { return list.AsQueryable().Expression; }
        }
   ...


This simple wrapper gives me the ability to create repositories for my classes that work for both LingToSql and with in memory collections. Here is an example:


public class RoundRepository
 {
     private readonly IUnitOfWork unitOfWork;

     public RoundRepository(IUnitOfWork unitOfWork)
     {
         this.unitOfWork = unitOfWork;
     }

     public IQueryable<Round> Rounds
     {
         get { return unitOfWork.Rounds; }
     }

     public void AddRound(Round round)
     {
         unitOfWork.Rounds.InsertOnSubmit(round);
     }

     public void ClearRounds()
     {
         unitOfWork.Rounds.DeleteAllOnSubmit(unitOfWork.Rounds);
     }

     public IEnumerable<Round> ByYear(int year)
     {
         var rounds = from round in Rounds
                      where round.PlayDate.Year == year
                      select round;
         return rounds;
     }
 }


This gives me the ability to test my domain classes at the relatively small cost of six new classes and interfaces. It also allows me to use the same test code for both unit testing and integration testing. Not bad for a days work :)

6 comments:

Wolfbyte said...

Nice work Anders.

It's a little thing but the IUnitOfWork interface that you have there kinda violates the Open/Closed Principle. Every time you add a domain object you're going to have to crack that open and provide yet another member on it. As you're using it as an abstraction, this means you'll have two other places (at least) to mess with it as well :(

Instead, why not duplicate what the DataContext was doing originally and expose a GetTable<T> which returns an ITable<T>. That way your InMemoryUnitOfWork and LinqToSqlUnitOfWork don't have to change no matter how many domain classes you create.

The only issue with this is that it hides the intent a little bit but given that (I assume) you won't be using the IUnitOfWork directly maybe that is ok.

At least that is what I've been doing. Just a thought.

Anders Janmyr said...

wolfbyte: Good point! I'll update the code. I will also have to make InMemoryUnitOfWork hold a Dictionary with a mapping between class and table instead of instance variables for every table.

Daniel Root said...

Does this really work with complex LINQ-to-SQL queries? If so, where are the relationships between tables handled? ie:

from r in Rounds
where r.Score < 100
select r.Player

Anders Janmyr said...

Daniel: It works fine.

The relationships are set up in the entity classes with attributes or with an external mapping file just as you would if you were using the tables directly.

The only thing you gain is testability.

Here is an example of one of my queries:


public IEnumerable<TotalResult> CalculateTotalResults(IEnumerable<Round> playedRounds)

{

var totalResults = from round in playedRounds

from result in round.GetCalculatedResults()

group result by result.Player

into playerResults

select new TotalResult(playerResults.Key, playerResults);



var totalResultsSorted = from r in totalResults

orderby r.GetTotal() descending

select r;

return totalResultsSorted;

}

Daniel Root said...

Awesome. I've thought about this before, but assumed some magic was happening in DataContext to wire up the relationships. Especially since EntitySet lives in System.Data.Linq. I just tried and indeed it's handling those for queries just fine. I'm still a little mystified, though - what's doing that work? In other words, how does the EntitySet of Round know where to find the related List of Round in InMemoryUnitOfWork?

Probably related - one test that's failing for me is inserting entity with related data:

var player = new Player() { ... }
var round = new Round() {... }
player.Rounds.Add(round);
context.InsertOnSubmit(player);
context.SubmitChanges();

Assert.IsTrue(context.Rounds.Count() == 1);

Fails for MemoryUnitOfWork, but succeeds for LinqToSql. Which I guess means my InsertOnSubmit/SubmitChanges isn't right. Thoughts?

Anders Janmyr said...

Daniel: I finally got the time to check out your example.

The InMemoryTables and the InMemoryUnitOfWork do not work like a full-fledged LinqToSql-replacement.
They are just meant to support my unit tests. The test that you have shown I would consider an integration test and it is not part of the case I'm trying to solve.

I am considering scaling the InMemory project to support what you are suggesting if I can only find the time.