Friday, October 19, 2012

Enhancing Your Entity Repositories

Using a repository pattern for data access gives you clear separation and abstraction in your data layer.  The basic idea of adding repositories for data access is illustrated nicely on MSDN:



In the case of an MVC application, your controllers fit into the "Client Business Logic" area, and your Database resides in the "Data Source" area.  What's left is the repository itself, here is a typical example generated using the MVC Scaffolding project.


namespace DataModel.Models
{
    public class PartyRepository : IPartyRepository
    {
        public IQueryable<Party> All
        {
            get { return _context.Parties; }
        }
        public IQueryable<Party> AllIncluding(params Expression<Func<Party, object>>[] includeProperties)
        {
            IQueryable<Party> query = _context.Parties;
            foreach (var includeProperty in includeProperties) {
                query = query.Include(includeProperty);
            }
            return query;
        }
        public Party Find(int id){ return _context.Parties.Find(id);}
       
  //Methods removed for brevity
    }
    public interface IPartyRepository : IDisposable
    {
        IQueryable<Party> All { get; }
        IQueryable<Party> AllIncluding(params Expression<Func<Party, object>>[] includeProperties);
        Party Find(int id);
        //Methods removed for brevity
    }
}



The repository itself abstracts the dirty work of dealing with the context, and provides a great element of re-usability in your application (you can inject a repository anywhere you like and it will use the same context code).  Here are some sample use cases for some of the above methods:


_repository.Find(id); //simple lookup
_repository.All; //get everything
_repository.AllIncluding(model => model.Property1, model => model.Property2); //Get all and include some navigation properties
_repository.AllIncluding(<insert ALL navigation properties>); //Full eager fetch



Simple enough, but this structure raises some concerns:

  1. Specifying the navigation properties in your controller actions when using the repositories feels like a violation of your abstractions.  Dealing with relationships between data driven objects should stick to the data layer as much as possible.
  2. There really isn't a good method to do a full eager fetch in this scenario.  You could provide all of the include properties but that creates a maintainability issue when you add a new navigation property and have to change every location you used the including options.

Our solution to this issue was a small and simple re-working of these methods, here is an example from another object:

namespace DataModel.Models
{
    public class DocumentRepository : IDocumentRepository
    {
        private readonly Expression<Func<Document, object>>[] _allIncludes =
            {
                d => d.Department,
                d => d.Organization,
                d => d.DocumentStatus,
                d => d.DocumentType,
                d => d.FavoriteUsers
            };
        public IQueryable<Document> All(params Expression<Func<Document, object>>[] includeProperties)
        {
            IQueryable<Document> query = _context.Documents;
            foreach (var includeProperty in includeProperties)
            {
                query = query.Include(includeProperty);
            }
            return query.Where(doc => doc.Organization.AccountId == accountId);
        }
        public IQueryable<Document> All(bool eager, params Expression<Func<Document, object>>[] includeProperties)
        {
            var includes = eager ? _allIncludes : includeProperties;
            return All(accountId, includes);
        }
        public Document Find(int id, params Expression<Func<Document, object>>[] includeProperties)
        {
            IQueryable<Document> query = _context.Documents;
            foreach (var includeProperty in includeProperties)
            {
                query = query.Include(includeProperty);
            }
            return query.SingleOrDefault(doc => doc.Id == id);
        }
        public Document Find(int id, bool eager, params Expression<Func<Document, object>>[] includeProperties)
        {
            var includes = eager ? _allIncludes : includeProperties;
            return Find(id, includes);
        }
  //Methods removed for brevity
    }
    public interface IDocumentRepository
    {
        IQueryable<Document> All(params Expression<Func<Document, object>>[] includeProperties);
        IQueryable<Document> All(bool eager = false, params Expression<Func<Document, object>>[] includeProperties);
        Document Find(int id, params Expression<Func<Document, object>>[] includeProperties);
        Document Find(int id, bool eager = false, params Expression<Func<Document, object>>[] includeProperties);
        //Methods removed for brevity
    }
}

We simply added an eager option and an overload for the Find and the All(converting it from property to method).  Here are the new use cases:

_repository.Find(id); //simple lookup
_repository.All(); //get everything - lazy loaded
_repository.All(model => model.Property1, model => model.Property2); //Get all and include some navigation properties
_repository.All(true); or the more readable: _repository.All(eager:true); //Full eager fetch

With this modification, controlling the type of fetch you want to do is much more clear, and if we add navigation properties to the model, we only need to update the allIncludes property in the repository, not everywhere the repository is used for eager fetching.  We also still preserved the ability to lazy load, as well as specify exactly the properties you want during a fetch.

A side effect though, is we have some strange edge cases that result, for example:

_repository.All(eager:true, model => model.Property1);

In this case, the provided property is ignored and all properties are fetched.  We chose to lay the blame for this sort of issue on the developer as there are easier ways to use the methods to achieve the desired result, whatever that may be.


All code examples taken from our next version of Contract Guardian.

For more information, check out our Web Site.


No comments:

Post a Comment