'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:

  1. There is a feature request on github for this: https://github.com/dotnet/efcore/issues/7391. You can vote for it.

  2. 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