Domain Driven Design & Test Driven Development\Design with Entity Framework Part 1.b, Review

by mosessaur| 21 February 2009| 3 Comments

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 , 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 . 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.

Comments (3) -

De
De Canada on 2/20/2009 3:58 AM Hi Moses,
Please do not hesitate to publish articles like this one - when you need to correct something, it helps a lot to see what was wrong and why you decided to change it.

Much appreciated.
mosessaur
mosessaur Egypt on 2/21/2009 2:04 AM @De thank you so much. I totally agree with you. I made that on purpose to share my learning progress. And you can't imagine the benefit I gained from my conversation with Davy Landman he provided me with valuable feedback and support. It is a good chance to thank him
Davy Landman
Davy Landman Netherlands on 2/21/2009 9:05 AM Nice refactoring Wink

You should include IDisposable on the IDataContext , because the ObjectContext is IDisposable, and really you just want to have an early release of all the memory used by any ORM...

I really liked how you removed the ugly switch-alike statement to determine which set..
public virtual IQueryable GetQueryable(string queryString) where T : EntityObject  
{  
    return base.CreateQuery(queryString);  
}  

The only downside is the usage of strings..

I think the EdmGen route is a very heavy one, and if you're willing to go that way, you might want to think about just another ORM?

Chees,
Davy

Pingbacks and trackbacks (1)+

Comments are closed