Implementing Missing Features in Entity Framework Core – Part 2: Explicit Loading
Explicit Loading
Update: this is now implemented in Entity Framework Core 1.1: https://github.com/aspnet/EntityFramework/releases/tag/rel%2F1.1.0.
This is the second in a series of posts about missing functionality in EF Core. You can find the first here.
Entity Framework used to support three ways to load related entities (one to one, one to many, many to one, many to many):
-
Explicit loading: introduced in the first version of EF, required an explicit method call to DbEntityEntry<TEntity>.Collection<TElement>.Reload() or DbEntityEntry<TEntity>.Reference<TElement>.Reload() (in Code First), and wasn’t that popular, IMO;
-
Eager loading: related entities and collections would be fetched at the same time as their containing entity, through a call to one of the Include methods;
-
Lazy loading: entities would be loaded when and if they declaring property was accessed.
Entity Framework Core doesn’t include (yet) the explicit loading and lazy loading mechanisms. Lazy loading is not included because EF Core does not generate proxies for the entities it loads, and I guess nobody bothered to implement explicit loading. Well, that’s exactly what we’ll do here!
The idea is, even if we didn’t require a collection to be loaded when we issued the query, we can still load it afterwards. We should keep a syntax similar to the previous version, when this feature was available:
//load a blog
var blog = ctx.Blogs.Single(b => b.BlogId == 1);
//Posts collection is empty
blog.Posts.Count(); //0
//eager load all Posts
ctx.Entry(blog).Load(b => b.Posts);
//Posts collection is no longer empty
blog.Posts.Count(); //!= 0
We will create an extension method over EntityEntry<T>:
public static void Load<TSource, TDestination>(this EntityEntry<TSource> entry, Expression<Func<TSource, IEnumerable<TDestination>>> path, Expression<Func<TDestination, TSource>> pathBack = null) where TSource : class where TDestination : class
{
var entity = entry.Entity;
var context = entry.Context;
var entityType = context.Model.FindEntityType(typeof(TSource));
var keys = entityType.GetKeys();
var keyValues = context.GetEntityKey(entity);
var query = context.Set<TDestination>() as IQueryable<TDestination>;
var parameter = Expression.Parameter(typeof(TDestination), "x");
PropertyInfo foreignKeyProperty = null;
if (pathBack == null)
{
foreignKeyProperty = typeof(TDestination).GetProperties().Single(p => p.PropertyType == typeof(TSource));
}
else
{
foreignKeyProperty = (pathBack.Body as MemberExpression).Member as PropertyInfo;
}
var i = 0;
foreach (var property in keys.SelectMany(x => x.Properties))
{
var keyValue = keyValues[i];
var expression = Expression.Lambda(
Expression.Equal(
Expression.Property(Expression.Property(parameter, foreignKeyProperty.Name), property.Name),
Expression.Constant(keyValue)),
parameter) as Expression<Func<TDestination, bool>>;
query = query.Where(expression);
i++;
}
var list = query.ToList();
var prop = (path.Body as MemberExpression).Member as PropertyInfo;
prop.SetValue(entity, list);
}
The GetEntityKey method was introduced in the previous post, but I will add it here, for your convenience:
public static object[] GetEntityKey<T>(this DbContext context, T entity) where T : class
{
var state = context.Entry(entity);
var metadata = state.Metadata;
var key = metadata.FindPrimaryKey();
var props = key.Properties.ToArray();
return props.Select(x => x.GetGetter().GetClrValue(entity)).ToArray();
}
You can see that the Load method takes two parameters: the first is required, it represents the collection that we with to load, the second is optional, and it represents the go-back property, in case there is more than one property of the root entity’s type. For example:
ctx.Entry(blog).Load(b => b.Posts, p => p.Blog);
In this case, the path p => p.Blog is needless, because a Post only belongs to one Blog.
A similar approach can be used to load explicitly other kinds of relations (one to one and many to one).
Hope you find this useful!