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.
[more]
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.