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
InMemoryTable
s 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:
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.
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.
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
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;
}
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?
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.
Post a Comment