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

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

    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.

Published by

Ricardo Peres

Technical Evangelist at Simplifydigital. Microsoft MVP.

Leave a Reply

Your email address will not be published. Required fields are marked *