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.