Implementing Missing Features in Entity Framework Core

Introduction

By now, we all know that Entity Framework Core 1.0 will not include several features that we were used to. In this post, I will try to explain how we can get over this by implementing them ourselves, or, at least, working out some workaround.

This time, I am going to talk about mimicking the Reload and Find methods, plus, give a set of other useful methods for doing dynamic programming.

Find

Update: this is now implemented in Entity Framework Core 1.1: https://github.com/aspnet/EntityFramework/releases/tag/rel%2F1.1.0.

Find lets us query an entity by its identifier. We will first define a strongly typed version:

public static TEntity Find<TEntity>(this DbSet<TEntity> set, params object[] keyValues) where TEntity : class
{
    var context = set.GetInfrastructure<IServiceProvider>().GetService<IDbContextServices>().CurrentContext.Context;
    var entityType = context.Model.FindEntityType(typeof(TEntity));
    var keys = entityType.GetKeys();
    var entries = context.ChangeTracker.Entries<TEntity>();
    var parameter = Expression.Parameter(typeof(TEntity), "x");
    IQueryable<TEntity> query = context.Set<TEntity>();
 
    //first, check if the entity exists in the cache
    var i = 0;
 
    //iterate through the key properties
    foreach (var property in keys.SelectMany(x => x.Properties))
    {
        var keyValue = keyValues[i];
 
        //try to get the entity from the local cache
        entries = entries.Where(e => keyValue.Equals(e.Property(property.Name).CurrentValue));
 
        //build a LINQ expression for loading the entity from the store
        var expression = Expression.Lambda(
                Expression.Equal(
                    Expression.Property(parameter, property.Name),
                    Expression.Constant(keyValue)),
                parameter) as Expression<Func<TEntity, bool>>;
 
        query = query.Where(expression);
 
        i++;
    }
 
    var entity = entries.Select(x => x.Entity).FirstOrDefault();
 
    if (entity != null)
    {
        return entity;
    }
 
    //second, try to load the entity from the data store
    entity = query.FirstOrDefault();
 
    return entity;
}

And then a loosely-typed one:

private static readonly MethodInfo SetMethod = typeof(DbContext).GetTypeInfo().GetDeclaredMethod("Set");
 
public static object Find(this DbContext context, Type entityType, params object[] keyValues)
{
    dynamic set = SetMethod.MakeGenericMethod(entityType).Invoke(context, null);
    var entity = Find(set, keyValues);
    return entity;
}

Not sure if you’ve had to do queries through an entity’s Type, but I certainly have!

The Find method will first look in the DbContext local cache for an entity with the same keys, and will return it if it finds one. Otherwise, it will fallback to going to the data store, for that, it needs to build a LINQ expression dynamically.

Sample usage:

//strongly typed version
var blog = ctx.Blogs.Find(1);
 
//loosely typed version
var blog = (Blog) ctx.Find(typeof(Blog), 1);

Getting an Entity’s Id Programmatically

This is also important: getting an entity’s id values dynamically, that is, without knowing beforehand what are the properties (normally just one) that keeps them. Pretty simple:

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();
}

Here’s how to use:

Blog blog = ...;
var id = ctx.GetEntityKey(blog);

Reload

The Reload method tells Entity Framework to re-hydrate an already loaded entity from the database, to account for any changes that might have occurred after the entity was loaded by EF. In order to properly implement this, we will first need to define the two methods shown above (no need for the loosely-coupled version of Find, though):

public static TEntity Reload<TEntity>(this DbContext context, TEntity entity) where TEntity : class
{
    return context.Entry(entity).Reload();
}
 
public static TEntity Reload<TEntity>(this EntityEntry<TEntity> entry) where TEntity : class
{
    if (entry.State == EntityState.Detached)
    {
        return entry.Entity;
    }
 
    var context = entry.Context;
    var entity = entry.Entity;
    var keyValues = context.GetEntityKey(entity);
 
    entry.State = EntityState.Detached;
 
    var newEntity = context.Set<TEntity>().Find(keyValues);
    var newEntry = context.Entry(newEntity);
 
    foreach (var prop in newEntry.Metadata.GetProperties())
    {
        prop.GetSetter().SetClrValue(entity, prop.GetGetter().GetClrValue(newEntity));
    }
 
    newEntry.State = EntityState.Detached;
    entry.State = EntityState.Unchanged;
 
    return entry.Entity;
}

Here’s two versions of Reload: one that operates on an existing EntityEntry<T>, and another for DbContext; one can use them as:

Blog blog = ...;
 
//first usage
ctx.Entry(blog).Reload();
 
//second usage
ctx.Reload(blog);

You will notice that the code is updating the existing instance that was already loaded by EF, if any, and setting its state to Unchanged, so any changes made to it will be lost.

Local

EF Core 1.0 also lost the Local property, which allows us to retrieve cached entities that were previously loaded. Here’s one implementation of it:

public static IEnumerable<EntityEntry<TEntity>> Local<TEntity>(this DbSet<TEntity> set, params object [] keyValues) where TEntity : class
{
    var context = set.GetInfrastructure<IServiceProvider>().GetService<DbContext>();
    var entries = context.ChangeTracker.Entries<TEntity>();
 
    if (keyValues.Any() == true)
    {
        var entityType = context.Model.FindEntityType(typeof(TEntity));
        var keys = entityType.GetKeys();
        var i = 0;
 
        foreach (var property in keys.SelectMany(x => x.Properties))
        {
            var keyValue = keyValues[i];
            entries = entries.Where(e => keyValue.Equals(e.Property(property.Name).CurrentValue));
            i++;
        }
    }
 
    return entries;
}

the keyValues parameter is optional, it is the entity’s identifier values. If not supplied, Local will return all entries of the given type:

//all cached blogs
var cachedBlogs = ctx.Set<Blog>().Local();
 
//a single cached blog
var cachedBlog = ctx.Set<Blog>().Local(1).SingleOrDefault();

Evict

Entity Framework has no Evict method, unlike NHibernate, but it is very easy to achieve the same purpose through DbEntityEntry.State (now EntityEntry.State, in EF Core). I wrote an implementation that can evict several entities or one identified by an identifier:

public static void Evict<TEntity>(this DbContext context, TEntity entity) where TEntity : class
{
    context.Entry(entity).State = EntityState.Detached;
}
 
public static void Evict<TEntity>(this DbContext context, params object [] keyValues) where TEntity : class
{
    var tracker = context.ChangeTracker;
    var entries = tracker.Entries<TEntity>();
 
    if (keyValues.Any() == true)
    {
        var entityType = context.Model.FindEntityType(typeof (TEntity));
        var keys = entityType.GetKeys();
 
        var i = 0;
 
        foreach (var property in keys.SelectMany(x => x.Properties))
        {
            var keyValue = keyValues[i];
 
            entries = entries.Where(e => keyValue.Equals(e.Property(property.Name).CurrentValue));
 
            i++;
        }
    }
 
    foreach (var entry in entries.ToList())
    {
        entry.State = EntityState.Detached;
    }
}

As usual, an example is in order:

var blog = ...;
var id = ctx.GetEntityKey(blog);
 
//evict the single blog
ctx.Evict(blog);
 
//evict all blogs
ctx.Evict<Blog>();
 
//evict a single blog from its identifier
ctx.Evict<Blog>(id);

Conclusion

Granted, some of the missing functionality will give developers a major headache, but, even with what we have, it’s not impossible to go around them. Of course, this time, it was simple stuff, but in future posts I will try to address some more complex features.

                             

14 Comments

  • Hello,

    Is there some key differences between your Local() implementation and between this one found in stack overflow? http://stackoverflow.com/questions/33819159/is-there-a-dbsettentity-local-equivalent-in-entity-framework-7

  • Antti:
    Well, see for yourself! The difference is that mine can retrieve a local entity from its id, if it is there; the other exposes an observable collection, but I don't see much value in it, TBH.

  • Your Reload implementation wouldn't work for entities with shadow properties. For a way of generating a query that gets all the properties see the SelectDatabaseValues method in https://github.com/aspnet/EntityFramework/blob/dd262720f58894d60ddadde8a69acf4eec80f841/src/Microsoft.EntityFrameworkCore.Specification.Tests/OptimisticConcurrencyTestBase.cs

  • I tried your Find method, it raises NullReferenceException at

    var entityType = context.Model.FindEntityType(typeof(TEntity));

  • Hi, Ken!
    Yes, I know... things changed! Use this as the first two lines in Find:

    var svcs = set.GetInfrastructure().GetService<IDbContextServices>();
    var context = svcs.CurrentContext.Context;

  • Great stuff. Thank you kindly!

  • Struggling with .Net Core 1.0 & EF Core 1.0 and this Find method. I can't seem to get the first two lines to work. Has this changed from the RC to 1.0? Any help would be appreciated.

  • PaulB: you don't give much information, do you?
    Try adding a using statement for Microsoft.EntityFrameworkCore.Infrastructure.

  • I also get an error on the first line of find (or the two lines which just breaks that up):

    > Error CS1929 'IServiceProvider' does not contain a definition for 'GetService' and the best extension method overload 'AccessorExtensions.GetService<IDbContextServices>(IInfrastructure<IServiceProvider>)' requires a receiver of type 'IInfrastructure<IServiceProvider>'

  • Hi, Rick!
    Please add a using directive for Microsoft.EntityFrameworkCore.Infrastructure and let me know if it helps!

  • public abstract class DbSet<TEntity> : IQueryable<TEntity>, IEnumerable<TEntity>, IEnumerable, IQueryable, IAsyncEnumerableAccessor<TEntity>, IInfrastructure<IServiceProvider> where TEntity : class

  • Find
    replace var context = set.GetInfrastructure<IServiceProvider>().GetService<IDbContextServices>().CurrentContext.Context;
    to var context = set.GetService<IDbContextServices>().CurrentContext.Context;

    GetEntityKey

    replace return props.Select(x => x.GetGetter().GetClrValue(entity)).ToArray();
    to
    var model = context.Entry(entity);
    return key.Properties.Select(i => model.Property(i.Name).CurrentValue).ToArray();

  • Thanks for your great post.

    Please take into account that your `Evict()` method does not remove the entries from loaded collections. See https://github.com/aspnet/EntityFramework/issues/7253

    A possible workaround, not so efficient I afraid, is the follow one:

    ```csharp
    var collectionsOfTEntity = ChangeTracker.Entries()
    .SelectMany(x => x.Collections)
    .Where(x => x.IsLoaded)
    .Select(x => x.CurrentValue as ICollection<TEntity>)
    .Where(x => x != null)
    .ToArray();

    foreach (var entry in entriesToEvict)
    foreach (var collection in collectionsOfTEntity)
    {
    collection.Remove(entry.Entity);
    }
    ```

  • Ricardo, do you think that the previous solution is acceptable? What do you think that could it be an elegant solution to keep context graph updated after an evict?

Add a Comment

As it will appear on the website

Not displayed

Your website