'Find entity entry using entity type and key
How to find tracked entity based on it's key value and entity type?
I mean something simmilar to DbSet<TEntity>.Find(params object keys):
e.g.:
object[] keys = ....;
dbContext.Set<TEntity>().Local.Find(keys)
//or
dbContext.ChangeTracker.Entries<TEntity>().Find(keys);
I could create my own Func<TEntity, bool> predicate using System.Linq.Expressions from entity metadata, but it is untrivial and maybe there is already something built in.
EDIT:
There is a feature request on github for this: https://github.com/dotnet/efcore/issues/7391. You can vote for it.
I came with this simple solution (thanks @SvyatoslavDanyliv's answer):
public static EntityEntry<TEntity> LocalEntryByPk<TEntity>(this DbContext dbContext, TEntity entity) where TEntity : class { EntityEntry<TEntity> entry = dbContext.Entry(entity); if (entry.State != EntityState.Detached) { return entry; } IKey key = entry.Metadata.FindPrimaryKey() ?? entry.Metadata.GetKeys().FirstOrDefault() ?? throw new InvalidOperationException("Key not found"); object[] keyValues = key.Properties.Select(p => entry.CurrentValues[p]).ToArray(); //with Internal API (IStateManager) var internalEntityEntry = dbContext.GetService<IStateManager>().TryGetEntry(key, keyValues); if (internalEntityEntry == null) return null; return new EntityEntry<TEntity>(internalEntry); //without internal API //return dbContext.ChangeTracker.Entries<TEntity>().FirstOrDefault(e => pk.Properties.All(p => Equals(entry.CurrentValues[p], e.CurrentValues[p]))); }
Solution 1:[1]
Check the following implementation, it has functionality for retrieving local entity Entry and sample how to update entity without worrying about exception
System.InvalidOperationException: The instance of entity type 'Some' cannot be tracked because another instance with the key value '{SomeId: 1}' is already being tracked.
Sample usage:
var entry = context.FindLocalEntry(someObj);
if (entry == null)
// someObj is not tracked
// updating
var entry = context.UpdateSafe(someObj);
And implementation:
#pragma warning disable EF1001 // Internal EF Core API usage.
public static class ChangeTrackerHelpers
{
private static readonly ConcurrentDictionary<(IModel model, IEntityType entityType), Func<IStateManager, object, InternalEntityEntry?>?> EntityKeyGetterCache = new();
private static readonly MethodInfo TryGetEntryMethodInfo =
typeof(IStateManager).GetMethods().First(mi =>
mi.Name == nameof(IStateManager.TryGetEntry) && mi.GetParameters().Length == 2 &&
mi.GetParameters()[0].ParameterType == typeof(IKey));
private static Func<IStateManager, object, InternalEntityEntry?>? CreateEntityRetrievalFunc(IEntityType entityType)
{
var stateManagerParam = Expression.Parameter(typeof(IStateManager), "sm");
var objParam = Expression.Parameter(typeof(object), "o");
var variable = Expression.Variable(entityType.ClrType, "e");
var assignExpr = Expression.Assign(variable, Expression.Convert(objParam, entityType.ClrType));
var key = entityType.GetKeys().FirstOrDefault();
if (key == null)
return null;
var arrayExpr = key.Properties.Where(p => p.PropertyInfo != null || p.FieldInfo != null).Select(p =>
Expression.Convert(Expression.MakeMemberAccess(variable, p.PropertyInfo ?? (MemberInfo)p.FieldInfo),
typeof(object)))
.ToArray();
if (arrayExpr.Length == 0)
return null;
if (arrayExpr.Length != key.Properties.Count)
return null;
var newArrayExpression = Expression.NewArrayInit(typeof(object), arrayExpr);
var body =
Expression.Block(new[] { variable },
assignExpr,
Expression.Call(stateManagerParam, TryGetEntryMethodInfo, Expression.Constant(key),
newArrayExpression));
var lambda =
Expression.Lambda<Func<IStateManager, object, InternalEntityEntry?>>(body, stateManagerParam, objParam);
return lambda.Compile();
}
private static Func<IStateManager, object, InternalEntityEntry?> GetEntityRetrievalFunc(IModel model,
IEntityType entityType)
{
var func = EntityKeyGetterCache.GetOrAdd((model, entityType),
key => CreateEntityRetrievalFunc(key.entityType));
if (func == null)
throw new InvalidOperationException($"Could not retrieve key information from '{entityType.Name}'.");
return func;
}
public static EntityEntry<TEntity>? FindLocalEntry<TEntity>(this DbContext context, TEntity entity)
where TEntity : class
{
var entityType = context.Model.FindEntityType(typeof(TEntity));
if (entityType == null)
throw new InvalidOperationException($"Entity type '{typeof(TEntity).Name}' is not registered in model.");
var stateManager = context.GetService<IStateManager>();
var func = GetEntityRetrievalFunc(context.Model, entityType);
var internalEntry = func(stateManager, entity);
if (internalEntry == null)
return null;
return new EntityEntry<TEntity>(internalEntry);
}
public static EntityEntry<TEntity> UpdateSafe<TEntity>(this DbContext context, TEntity entity)
where TEntity : class
{
var currentEntity = context.FindLocalEntry(entity);
if (currentEntity != null)
{
// Entity already in ChangeTracker, just copy properties if is not the same object
if (!ReferenceEquals(currentEntity.Entity, entity))
{
currentEntity.CurrentValues.SetValues(entity);
}
}
else
{
// Use standard function to attach entity
currentEntity = context.Update(entity);
}
return currentEntity;
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Svyatoslav Danyliv |
