In return to my previous posts about DDD & TDD with EF. I discovered few catches in my design that I wished to high light and discuss in this post. To summarize my take here are the head lines:
- What is the Responsibility of ObjectContext in EF?
- What is the Responsibilities of Repositories and whether they should be coupled with EF or not?
I come up with this post after few discussions through e-mail with Davy Landman.
Responsibility of ObjectContext:
After reviewing my code and in previous post, I found that I am giving too much responsibilities to ObjectContext. Which will simply make my Repositories abstract wrappers for ObjectContext. On the other hand the benefit is very low. My target was to decouple repository and EF! And the benefit cannot be mentioned. Because if I want to achieve decoupling that means I can use same IDataContext interface with another Data Access Helper class such as LINQ to SQL context or any other Data Access produced by any ORM. And this might not be possible.
So I decided to put a limitation to my IDataContext and make it coupled with EF. Then the main usage of IDataContext would be for TDD support.
I would like to refer to a great post made by Kazi about how to build 100% Testable LINQ to SQL Repositories. I am actually applying his idea on EF.
I will not talk about Repositories responsibilities, because they are clearly defined in DDD. But there is not harm if you made your Data Access Layer generated from any ORM coupled with Repositories. It will ease the development and keep everything be in the right direction.
Code Updated:
Because I changed my design, I've updated my code to remove few methods and simplify IDataContext.
public interface IDataContext
{
IQueryable<Category> CategoryDataSource { get; }
IQueryable<Product> ProductDataSource { get; }
IQueryable<Supplier> SupplierDataSource { get; }
IQueryable<T> GetQueryable<T>(string queryString)
where T: EntityObject;
void AddObject<T>(string entitySetName, T entity)
where T : EntityObject;
void InsertOnSubmit<T>(string entitySetName, T entity)
where T : EntityObject;
void DeleteOnSubmit<T>(T entity)
where T : EntityObject;
void SubmitChanges();
}
public partial class NorthwindDataContext : IDataContext
{
private IQueryable<Category> _categorySet;
private IQueryable<Product> _productSet;
private IQueryable<Supplier> _supplierSet;
public IQueryable<Category> CategoryDataSource
{
get
{
if (_categorySet == null)
{
_categorySet = GetQueryable<Category>("[CategorySet]");
}
return _categorySet;
}
}
public IQueryable<Product> ProductDataSource
{
get
{
if (_productSet == null)
{
_productSet = GetQueryable<Product>("[ProductSet]");
}
return _productSet;
}
}
public IQueryable<Supplier> SupplierDataSource
{
get
{
if (_supplierSet == null)
{
_supplierSet = GetQueryable<Supplier>("[SupplierSet]");
}
return _supplierSet;
}
}
public virtual IQueryable<T> GetQueryable<T>(string queryString) where T : EntityObject
{
return base.CreateQuery<T>(queryString);
}
public void InsertOnSubmit<T>(string entitySetName, T entity)
where T : EntityObject
{
AddObject<T>(entitySetName, entity);
}
public void DeleteOnSubmit<T>(T instance)
where T : EntityObject
{
DeleteObject(instance);
}
public virtual void AddObject<T>(string entitySetName, T entity)
where T : EntityObject
{
base.AddObject(entitySetName, entity);
}
public new virtual void DeleteObject(object instance)
{
base.DeleteObject(instance);
}
public void SubmitChanges()
{
base.SaveChanges(true);
}
}
Unit Test Code:
[TestClass]
public class NorthwindDataContextTest
{
private readonly Mock<NorthwindDataContext> _context;
private readonly List<Category> _categories;
private readonly List<Product> _products;
private readonly List<Supplier> _suppliers;
public NorthwindDataContextTest()
{
_context = new Mock<NorthwindDataContext>(ConfigurationManager
.ConnectionStrings["NorthwindDataContext"]
.ConnectionString);
_categories = new List<Category>();
_products = new List<Product>();
_suppliers = new List<Supplier>();
}
[TestMethod]
public void NorthwindDataContext_CategoryDataSource_Should_Call_GetQueryable()
{
_context.Expect(c => c.GetQueryable<Category>("[CategorySet]"))
.Returns(_categories.AsQueryable())
.Verifiable();
Assert.IsNotNull(_context.Object.CategoryDataSource);
_context.Verify();
}
[TestMethod]
public void NorthwindDataContext_ProductDataSource_Should_Call_GetQueryable()
{
_context.Expect(c => c.GetQueryable<Product>("[ProductSet]"))
.Returns(_products.AsQueryable())
.Verifiable();
Assert.IsNotNull(_context.Object.ProductDataSource);
_context.Verify();
}
[TestMethod]
public void NorthwindDataContext_SupplierDataSource_Should_Call_GetQueryable()
{
_context.Expect(c => c.GetQueryable<Supplier>("[SupplierSet]"))
.Returns(_suppliers.AsQueryable())
.Verifiable();
Assert.IsNotNull(_context.Object.SupplierDataSource);
_context.Verify();
}
[TestMethod]
public void NorthwindDataContext_Insert_Should_Call_AddObject()
{
var category = Category.CreateCategory(1, String.Empty);
_context.Expect(c => c.AddObject("[CategorySet]",category)).Verifiable();
_context.Object.InsertOnSubmit("[CategorySet]", category);
_context.Verify();
}
[TestMethod]
public void NorthwindDataContext_Delete_Should_Call_DeleteObject()
{
var category = Category.CreateCategory(1, String.Empty);
_context.Expect(c => c.DeleteObject(category)).Verifiable();
_context.Object.DeleteOnSubmit(category);
_context.Verify();
}
}
Conclusion:
EF is not testable by nature, you'll have to find a way to do unit testing independent of EF. That is why I am keeping updating my code till I reach to a an ideal way. Maybe not the best way but at least a method that will ease this job for the time being. I think it might also result in writing your own EF code generator using EdmGem.
I've update the code on this post, you can download it here.