Pages

Monday, January 27, 2014

Generic Repositories Including Includes!

The real credit for this solution goes to Steve Moss. He posted this article last year:

http://www.appetere.com/Blogs/SteveM/May-2012/Passing-Include-statements-into-a-Repository

All that I've done is modified his work to fit in with the generic repository pattern that I often use.

http://www.viamacchina.com/2013/07/a-generic-repository-for-entity.html

One of the difficulties in using a repository to manage your data is that the repository itself can end up obscuring functionality from the underlying data provider. The pattern that I previously wrote about did just this. If you needed to Include relationships that were not originally established in your entity model, you had to create additional logic to retrieve that missing data.

As the Entity Framework has matured, the support for Including data from related entities has improved. Specifically, since EF 4.1, you can use strongly typed lambda's to specify relationships to eagerly load data. This, in combination with the existing string "Include", make working with entity relationships much more convenient.

To extend the original repository pattern to support dynamic includes, all that has to be done is to add an additional Select overload to the repository interface, and then make a quick change to the base class and the child classes that implement it.

1. Update the interface definition. The overloaded Select method that accepts an array of Expressions is the key.
public interface IRepository : IDisposable
{
    void Insert<E>(E entity) where E : class;

    void Update<E>(E entity) where E : class;

    void Delete<E>(E entity) where E : class;

    IQueryable<E> Select<E>() where E : class;

    IQueryable<E> Select<E>(params Expression<Func<E, object>>[] includeExpressions) where E : class;

    E Select<E>(object key) where E : class;
}


2. Update the BaseRepository class.
public abstract class BaseRepository : IRepository, ISaveChanges
{
    #region Methods

    public abstract void Insert<E>(E entity) where E : class;

    public abstract void Update<E>(E entity) where E : class;

    public abstract void Delete<E>(E entity) where E : class;

    public int SaveChanges()
    {
        return SaveChanges(true);
    }

    public abstract int SaveChanges(bool validateEntities);

    public abstract IQueryable<E> Select<E>() where E : class;

    public abstract IQueryable<E> Select<E>(params Expression<Func<E, object>>[] includeExpressions) where E : class;

    public abstract E Select<E>(object key) where E : class;

    public abstract void Dispose();

    #endregion
}

3. Update any class that implements the BaseRepository. In this case I am working with a repository for a DbContext.
public class DbContextRepository<T> : BaseRepository where T : DbContext
{
    #region Fields

    private T _context;

    #endregion

    #region Properties

    public T Context
    {
        get
        {
            if (_context == null)
                _context = Activator.CreateInstance<T>();

            return _context;
        }
    }

    #endregion

    #region Methods

    public override void Insert<E>(E entity)
    {
        Context.Set<E>().Add(entity);
    }

    public override void Update<E>(E entity)
    {
        Context.Set<E>().Attach(entity);
        Context.Entry<E>(entity).State = EntityState.Modified;
    }

    public override void Delete<E>(E entity)
    {
        Context.Set<E>().Attach(entity);
        Context.Entry<E>(entity).State = EntityState.Deleted;
    }

    public override int SaveChanges(bool validateEntities)
    {
        if (!Context.Database.Connection.IsDatabaseOnline())
            throw new Exception(NotificationManager.DatabaseOfflineMessage);

        Context.Configuration.ValidateOnSaveEnabled = validateEntities;
        return Context.SaveChanges();
    }

    public override IQueryable<E> Select<E>()
    {
        if (!Context.Database.Connection.IsDatabaseOnline())
            throw new Exception(NotificationManager.DatabaseOfflineMessage);

        return Context.Set<E>();
    }

    public override IQueryable<E> Select<E>(params Expression<Func<E, object>>[] includeExpressions)
    {
        IQueryable<E> result = null;

        if (includeExpressions.Any())
        {
            result = includeExpressions.Aggregate<Expression<Func<E, object>>, IQueryable<E>>
                        (Context.Set<E>(), (current, expression) => current.Include(expression));
        }

        return result;
    }

    public override E Select<E>(object key)
    {
        if (!Context.Database.Connection.IsDatabaseOnline())
            throw new Exception(NotificationManager.DatabaseOfflineMessage);

        return Context.Set<E>().Find(key);
    }

    public override void Dispose()
    {
        if (Context != null)
            Context.Dispose();
    }

    #endregion
}

As you can see, Steve's use of Aggregating the parameterized Include Expressions are the body of our new Select overload.

Now, when working with a repository object, eagerly loading data from related entities is as easy as specify the Include when you call Select:

return Select(i => i.FileTypeHeader.FileTypeGroup, i => i.FileTemplateInputs);

This obviously makes working with the data much easier. More importantly, this allows the repository to get out of the way and focus on what it is designed to do, which is to abstract away the underlying data provider.

So, thanks to Steve, and I hope this helps someone out!

No comments:

Post a Comment