This will be the sixth post in my series of posts about bringing the features that were present in Entity Framework pre-Core into EF Core. The others are:
Part 1: Introduction, Find, Getting an Entity’s Id Programmatically, Reload, Local, Evict
Part 2: Explicit Loading
Part 3: Validations
Part 4: Conventions
Part 5: Getting the SQL for a Query
As you may know, the second major version of Entity Framework Core, 1.1, was released recently, however, some of the features that used to be in the non-Core versions still didn’t make it. One of these features is lazy loading of collections, and I set out to implement it… or, any way, something that I could use instead of it!
Here’s what I came up with. First, let’s define a class that will act as a proxy to the collection to be loaded. I called it CollectionProxy<T>, and it goes like this:
internal sealed class CollectionProxy<T> : IList<T> where T : class
{
private bool _loaded;
private bool _loading;
private readonly DbContext _ctx;
private readonly string _collectionName;
private readonly object _parent;
private readonly List<T> _entries = new List<T>();
public CollectionProxy(DbContext ctx, object parent, string collectionName)
{
this._ctx = ctx;
this._parent = parent;
this._collectionName = collectionName;
}
private void EnsureLoaded()
{
if (this._loaded == false)
{
if (this._loading == true)
{
return;
}
this._loading = true;
var entries = this
._ctx
.Entry(this._parent)
.Collection(this._collectionName)
.Query()
.OfType<T>()
.ToList();
this._entries.Clear();
foreach (var entry in entries)
{
this._entries.Add(entry);
}
this._loaded = true;
this._loading = false;
}
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
this.EnsureLoaded();
return this._entries.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return (this as ICollection<T>).GetEnumerator();
}
int ICollection<T>.Count
{
get
{
this.EnsureLoaded();
return this._entries.Count;
}
}
bool ICollection<T>.IsReadOnly
{
get
{
return false;
}
}
void ICollection<T>.Add(T item)
{
this.EnsureLoaded();
this._entries.Add(item);
}
void ICollection<T>.Clear()
{
this.EnsureLoaded();
this._entries.Clear();
}
bool ICollection<T>.Contains(T item)
{
this.EnsureLoaded();
return this._entries.Contains(item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
this.EnsureLoaded();
this._entries.CopyTo(array, arrayIndex);
}
bool ICollection<T>.Remove(T item)
{
this.EnsureLoaded();
return this._entries.Remove(item);
}
T IList<T>.this[int index]
{
get
{
this.EnsureLoaded();
return this._entries[index];
}
set
{
this.EnsureLoaded();
this._entries[index] = value;
}
}
int IList<T>.IndexOf(T item)
{
this.EnsureLoaded();
return this._entries.IndexOf(item);
}
void IList<T>.Insert(int index, T item)
{
this.EnsureLoaded();
this._entries.Insert(index, item);
}
void IList<T>.RemoveAt(int index)
{
this.EnsureLoaded();
this._entries.RemoveAt(index);
}
public override string ToString()
{
this.EnsureLoaded();
return this._entries.ToString();
}
public override int GetHashCode()
{
this.EnsureLoaded();
return this._entries.GetHashCode();
}
}
You can see that, in order to be as compliant as possible, I made it implement IList<T>; this way, it can be easily compared and switched with, for example, ICollection<T> and, of course, the mother of all collections, IEnumerable<T>. How it works is simple:
I created as well an extension method to make it’s usage more simple:
public static class CollectionExtensions
{
public static void Wrap<TParent, TChild>(this DbContext ctx, TParent parent, Expression<Func<TParent, IEnumerable<TChild>>> collection) where TParent : class where TChild : class
{
var prop = ((collection.Body as MemberExpression).Member as PropertyInfo);
var propertyName = prop.Name;
prop.SetValue(parent, new CollectionProxy<TChild>(ctx, parent, propertyName));
}
}
As you can see, I kept it very simple – no null/type checking or whatever, that is left to you, dear reader, as an exercise!
Finally, here’s how to use it:
using (var ctx = new MyContext())
{
var parentEntity = ctx.MyParentEntities.First();
ctx.Wrap(parentEntity, x => x.MyChildren); //sets up the proxy collection
var childEntitiesCount = parentEntity.MyChildren.Count(); //forces loading
foreach (var child in parentEntity.MyChildren) //already loaded, so iterate in memory
{
child.ToString();
}
}
Hope you like it! Let me know your thoughts!