While I know there are a lot of people in the ALT.NET community who like to mock Enitity Framework, that is not what I am talking about.
The objective of this exercise is to develop an Interface that can be used by Business Logic to query and persist data as well as provide and in-memory “mock” for testing or prototyping without a database.
The paradigm used is that of object model aggregates typically found in Domain-driven designs (DDD). This is fortunate because it is the model used by EF, where the aggregates are called ObjectSets.
We want to take advantage of LINQ, because this will allow the consumer of the Repository to add query operators prior to executing the query. The Repository will provide a Query method, a New method, an Add method, a Remove method and a Commit method.
I used the term “a repository-like facade” in the title of this post because traditionally a Repository provides access to a single Aggregate. I have decided to allow an EF Context, which might have several aggregates, to be wrapped by my Repository-implementation. The root aggregate type is determined by the Type parameter affixed to the method. In addition, I will provide a number of convenience methods to chain some of these basic methods is a single method call.
Here is the interface:
using System;
using System.Linq;
namespace Interfaces
{
public interface IRepository
{
// returns a query object for an aggregate root
IQueryable<T> Query<T>();
// returns a new object of the type requested
T New<T>() where T : class;
// returns a new object of the type requested (with the next id set)
T Next<T>() where T : class;
// adds the object to the aggregate root
void Add<T>(T obj) where T : class;
// gets a new object and Adds it to the collection
T AddNew<T>() where T : class;
// gets a new object (with next id set) and Adds it to the collection
T AddNext<T>() where T : class;
// adds a new object with the next id, runs initializer and persists it
T CreateNext<T>(Action<T> initializer) where T : class;
// remove an aggregate root
void Remove<T>(T obj) where T : class;
// persists any changes
void Commit();
}
}
The following code is an abstract implementation of the convenience methods, which are independent of the actual method of creating the new objects and managing them in the context. The methods that are declared abstract are the ones that will need to be implemented by a concrete Repository.
Note: The exception handling in these code listings has been omitted for brevity.
using System;
using System.Linq;
namespace Interfaces
{
abstract public class BaseRepository : IRepository
{
abstract public IQueryable<T> Query<T>();
abstract public T New<T>() where T : class;
abstract public void SetNext<T>(T obj) where T : class;
abstract public void Add<T>(T obj) where T : class;
abstract public void Remove<T>(T obj) where T : class;
abstract public void Commit();
public T Next<T>() where T : class
{
var obj = New<T>();
SetNext<T>(obj);
return obj;
}
public T AddNew<T>() where T : class
{
var obj = New<T>();
Add<T>(obj);
return obj;
}
public T AddNext<T>() where T : class
{
var obj = Next<T>();
Add<T>(obj);
return obj;
}
public T CreateNext<T>(Action<T> initializer) where T : class
{
var obj = AddNext<T>();
if (initializer != null) initializer(obj);
Commit();
return obj;
}
}
}
We can implement an in-memory repository that maintains each type of aggregate root in an ArrayList and uses the Type to find the correct List and operate on it accordingly. To simulate auto-numbered primary keys, we assume that auto-numbered entities will have a long Id property and maintain a single counter for all entities. Of course, you are free to implement this any way that works for you. The Commit() method is a no-op since we don’t plan on persisting the data to disk, but you can implement a Repository that does this if you like.
This implementation works with concrete entity classes (POCOs). I originally had a version that worked with interfaces and the repository mapped the interfaces to an implementation. This worked well with Entity Framework and its generated EntityObject classes, but fell down when I tried to use it with WCF RIA Services.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Interfaces
{
public class InMemRepository : BaseRepository
{
public InMemRepository()
{
// could data load from disk here
}
Dictionary<Type, ArrayList> roots = new Dictionary<Type, ArrayList>();
long counter = DateTime.Now.Ticks;
private ArrayList GetRootList<T>()
{
ArrayList list;
if (!roots.TryGetValue(typeof(T), out list))
{
list = new ArrayList();
roots[typeof(T)] = list;
}
return list;
}
public override IQueryable<T> Query<T>()
{
var list = GetRootList<T>();
return list.Cast<T>().AsQueryable<T>();
}
public override T New<T>()
{
return Activator.CreateInstance<T>();
}
// assumes that there is an Id property (long)
public override void SetNext<T>(T obj)
{
var idProperty = typeof(T).GetProperty("Id");
idProperty.SetValue(obj, counter++, null);
}
public override void Add<T>(T obj)
{
var list = GetRootList<T>();
list.Add(obj);
}
public override void Remove<T>(T obj)
{
var list = GetRootList<T>();
list.Remove(obj);
}
public override void Commit()
{
// code here to write to disk
}
}
}
Here is a unit test that demonstrates how to work with the repository:
[TestClass()]
public class InMemRepositoryTest
{
public class Department
{
public virtual long Id { get; set; }
public virtual string Name { get; set; }
private IList<Course> _courses = new List<Course>();
public virtual IList<Course> Courses { get { return _courses; } }
}
public class Course
{
public virtual long Id { get; set; }
public virtual string Name { get; set; }
private Department _department;
public virtual Department Department { get { return _department; }
set
{
if (_department != null) _department.Courses.Remove(this);
_department = value;
if (_department != null) _department.Courses.Add(this);
}
}
}
[TestMethod]
public void Test_InMemRepository()
{
IRepository repo = new InMemRepository();
// create some departments
var math = repo.AddNext<Department>();
Assert.IsNotNull(math);
Assert.IsInstanceOfType(math, typeof(Department));
Assert.IsTrue(math.Id > 0);
math.Name = "Mathematics";
repo.Commit();
// use the convenience method
var compsci = repo.CreateNext<Department>(d =>
{
d.Name = "Computer Science";
});
Assert.IsNotNull(compsci);
Assert.IsInstanceOfType(compsci, typeof(Department));
Assert.IsTrue(compsci.Id > 0);
Assert.IsTrue(compsci.Name == "Computer Science");
var count = repo.Query<Department>().Count();
Assert.AreEqual<int>(count, 2);
// create some courses
var algebra = repo.CreateNext<Course>(c =>
{
c.Name = "Algebra";
c.Department = math;
});
var trig = repo.CreateNext<Course>(c =>
{
c.Name = "Trigonometry";
c.Department = math;
});
var lisp = repo.CreateNext<Course>(c =>
{
c.Name = "Lisp";
c.Department = math;
});
Assert.AreEqual<int>(3, repo.Query<Course>().Count());
Assert.AreEqual<int>(3, math.Courses.Count());
Assert.AreEqual<int>(0, compsci.Courses.Count());
lisp.Department = compsci;
Assert.AreEqual<int>(2, math.Courses.Count());
Assert.AreEqual<int>(1, compsci.Courses.Count());
}
}
My next post will show how to write a Repository implementation for Entity Framework.
No comments:
Post a Comment