Implementing Missing Features in Entity Framework Core – Part 6: Lazy Loading

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! Smile

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:

  1. It receives in its constructor a pointer to a DbContext, the collection’s parent, and the collection-to-be-made-lazy’s name;
  2. There is an EnsureLoaded method that essentially checks if the collection has already been loaded, and, if not the case, does so, through the new (in EF Core 1.1) explicit loading API; it populates an internal list with the loaded collection’s items;
  3. Inner fields _loading and _loaded act as defenses to prevent the collection to be loaded twice, or to enter an infinite loop (stack overflow);
  4. I implemented all inherited methods and properties as explicit implementations, but there was no need for that, just a personal preference; all of them ensure that the collection is loaded (EnsureLoaded) before delegating to its internal field list;
  5. ToString and GetHashCode delegate to the internal list as well.

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! Winking smile

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!

What’s New in Entity Framework Core 1.1

Introduction

Entity Framework Core 1.1 was released last November. With it, besides some bug fxes and semi-transparent improvements, came along a few goodies. If you read my previous post on features missing in Entity Framework Core 1.0, you’ll be please to know that a few have been addressed.

New API Methods

The Find method, for returning an entity from its primary key, is back (I had provided a workaround here):

var e1 = ctx.DbSet<MyEntity>().Find(1);

New is GetDatabaseValues, which goes to the database and fetches the current values for the current entity and primary key:

var dbProperties = ctx.Entry<MyEntity>(e).GetDatabaseValues();

Reload and Explicit Load

It is now again possible to reload an entity, causing it to be re-hydrated with the current values from the database, through the Reload method (also available as a workaround here):

ctx.Entry<MyEntity>(e).Reload();

And it is also possible to force load a not-loaded collection ():

ctx.Entry<MyEntity>(e).Collection(x => x.MyColl).Load();

As well as entity references (one-to-one, many-to-one):

ctx.Entry<MyEntity>(e).Reference(x => x.MyRef).Load();

Connection Resiliency

Connection resiliency made it way to version 1.1 as well:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer("my connection string", opt => opt.EnableRetryOnFailure());

base.OnConfiguring(optionsBuilder);
}

The EnableRetryOnFailure method is just a wrapper around ExecutionStrategy passing it SqlServerRetryingExecutionStrategy:

optionsBuilder
.ExecutionStrategy(x => new MyExecutionStrategy(x));

This one allows you to provide your own strategy for retries, by implementing IExecutionStrategy.

Configurable Change Tracking

Now, this is something that could have been really cool, but, as it is now, I find it somewhat crippled… you can now tell Entity Framework Core how should it find out if an entity has changed – the common change tracker functionality. But, the only supported techniques are the built-in (default, based on snapshots) or the use of INotifyPropertyChanged/INotifyCollectionChanged. This is not really that extensible, as you only have these two options. Here is how you configure it:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyEntity>()
.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangedNotifications);

base.OnModelCreating(modelBuilder);
}

If you want to use this approach, your entity must implement INotifyPropertyChanged and all of its collections must implement INotifyCollectionChanged. If any of the properties or collections in it changes, you must raise the PropertyChanged or CollectionChanged events, otherwise EF will not know that it is modified.

This can be set as the default for all entities, by the way:

modelBuilder
.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangedNotifications);

Using Fields

A most welcome addition, that was never previously available, is mapping to fields! This better supports a pure Domain Driven Design approach. It needs to be configured using code mapping:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<MyEntity>()
.Property(b => b.MyProp)
.HasField("_myField");

base.OnModelCreating(modelBuilder);
}

IEnumerable Collections

Another handy improvement is the ability to map collections declared as IEnumerable<T>, whereas in the past this was only possible for ICollection<T> (and derived classes, of course). The configuration is the same:

modelBuilder
.Entity<MyEntity>()
.HasMany(x => x.Children);

Of course, the concrete collection class must itself implement ICollection<T>, otherwise Entity Framework would have no way to populate it:

public class MyEntity
{
public IEnumerable<MyChild> Children { get; } = new HashSet<MyChild>();
}

Support for SQL Server In Memory Tables

In case you are using Hekaton, you can now tell Entity Framework that your entity is persisted as a memory-optimized table:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyEntity>()
.ForSqlServerIsMemoryOptimized();

base.OnModelCreating(modelBuilder);
}

Switching Services

Last, but not least, EF Core 1.1 makes it much easier to replace one of the services that EF uses internally:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.ReplaceService<IEntityStateListener, CustomEntityStateListener>();

base.OnConfiguring(optionsBuilder);
}

Conclusion

It’s nice to see things progressing, but there’s still a long way to go. In particular, as I said in my post about the missing features, there are quite a few features that still didn’t make it. In particular, I still miss:

  • Group By translation
  • Lazy loading
  • Date/Time operations
  • Support for custom SQL functions
  • Many-to-many relations
  • Command and query interception

We just have to wait for the next priorities of the team.

MVP Award Renewed

It was with great pleasure that I read Microsoft’s email today announcing me that I had renewed as MVP for the third year!

image

Being an MVP is both an honor and a privilege, and I feel both proud and humble to again be a part of this fantastic group of people! Thanks, Cristina, for this!

I also want to congratulate all of the other Portuguese MVPs that were renewed this year, and leave a word of comfort to those who haven’t. All the best, guys (and lady)! Winking smile