try-catch-FAIL

Failure is inevitable

NAVIGATION - SEARCH

A Generic Entity Framework 5 Repository With Eager-Loading

I’ve been doing some work with Entity Framework 5 lately.  Here’s a simple generic repository I created that allows you to “Include” related entities by applying an attribute.

Consider these sample entities:

public class User
{
    [Key]
    public int UserId { get; set; }

    [Required, MaxLength(50)]
    public string Username { get; set; }

    [Required]
    public UserDetails Details { get; set; }

    [Required]
    public SecurityQuestions SecurityQuestion { get; set; }

    [Required, MaxLength(50)]
    public string SecurityAnswer { get; set; }

    [Required]
    public RegisterStatus RegisterStatus { get; set; }
}

public class UserDetails
{
    [Key]
    public int UserId { get; set; }
    [ForeignKey("UserId")]
    public virtual User User { get; set; }

    [Required]
    public int AddressId { get; set; }
    [ForeignKey("AddressId")]
    public Address Address { get; set; }

    [Required, MaxLength(50)]
    public string FirstName { get; set; }

    [Required, MaxLength(80)]
    public string LastName { get; set; }

    [Required, MaxLength(20)]
    public string Phone { get; set; }

    [Required, MaxLength(100)]
    public string Email { get; set; }

    public bool IsDeleted { get; set; }
}

public class Address
{
    [Key]
    public int AddressId { get; set; }

    [Required, MaxLength(120)]
    public string Address1 { get; set; }

    [Required, MaxLength(120)]
    public string City { get; set; }

    [Required, MaxLength(3)]
    public string State { get; set; }

    [Required, MaxLength(20)]
    public string Zip { get; set; }

    public double Longitude { set; get; }

    public double Latitude { set; get; }
}

There’s a simple one-to-one relationship between a User and UserDetails, and a one-to-one relationship from a UserDetails to Address.  With Entity Framework Code-First’s out-of-the-box configuration, the User.Details property will not be loaded with it’s parent User object.  Neither will the UserDetails.Address property.  EF provides the Include method to specify which related entities to pull in when you make a query, but the whole point of a generic repository is that it doesn’t truly know about the entity it contains.  We could expose the Include method through the repository, but that’s pretty ugly and is just one more thing we have to think about when utilizing the repository.  We could also take advantage of lazy-loading by making the properties virtual, but for simple one-to-one relationships, lazy-loading could result in unnecessary roundtrips to the database. 

Instead, I created a simple marker attribute called “Include.”  When applied to a property, it instructs the generic repository to include that property when it retrieves the parent object.  Here’s the repository:

public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
    private static readonly PropertyIncluder<TEntity> Includer = new PropertyIncluder<TEntity>();

    private readonly DbSet<TEntity> _dbSet;
    private readonly RecRentContext _context;

    public Repository(RecRentContext context)
    {
        _dbSet = context.Set<TEntity>();
        _context = context;
    }

    public void Add(TEntity entity)
    {
        _dbSet.Add(entity);
    }

    public void Update(TEntity entity)
    {
        _context.Entry(entity).State = EntityState.Modified;
    }

    public void Delete(object id)
    {
        Delete(_dbSet.Find(id));
    }

    public void Delete(TEntity entity)
    {
        if (_context.Entry(entity).State == EntityState.Detached)
        {
            _dbSet.Attach(entity);
        }

        _dbSet.Remove(entity);
    }

    public IQueryable<TEntity> Query()
    {
        return Includer.BuildQuery(_dbSet);
    }
}

And here’s the PropertyIncluder, which creates and caches a method to apply the appropriate Include calls at runtime:

public class PropertyIncluder<TEntity> where TEntity : class
{
    private readonly Func<DbQuery<TEntity>, DbQuery<TEntity>>  _includeMethod;
    private readonly HashSet<Type> _visitedTypes;

    public PropertyIncluder()
    {
        //Recursively get properties to include
        _visitedTypes = new HashSet<Type>();
        var propsToLoad = GetPropsToLoad(typeof (TEntity)).ToArray();

        _includeMethod = d =>
        {
            var dbSet = d;
            foreach (var prop in propsToLoad)
            {
                dbSet = dbSet.Include(prop);
            }

            return dbSet;
        };
    }

    private IEnumerable<string> GetPropsToLoad(Type type)
    {
        _visitedTypes.Add(type);
        var propsToLoad = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                          .Where(p => p.GetCustomAttributes(typeof (IncludeAttribute), true).Any());

        foreach (var prop in propsToLoad)
        {
            yield return prop.Name;

            if (_visitedTypes.Contains(prop.PropertyType))
                continue;

            foreach (var subProp in GetPropsToLoad(prop.PropertyType))
            {
                yield return prop.Name + "." + subProp;
            }
        }
    }

    public DbQuery<TEntity> BuildQuery(DbSet<TEntity> dbSet)
    {
        return _includeMethod(dbSet);
    }
}

This approach does utilize reflection, but since it caches the method it creates, you only pay a small penalty during application startup.

About Matt Honeycutt...

Matt Honeycutt is a software architect specializing in ASP.NET web applications, particularly ASP.NET MVC. He has over a decade of experience in building (and testing!) web applications. He’s an avid practitioner of Test-Driven Development, creating both the SpecsFor and SpecsFor.Mvc frameworks.

He's also an author for Pluralsight, where he publishes courses on everything from web applications to testing!

blog comments powered by Disqus