Search code examples
entity-frameworkentity-framework-corecomposite-primary-keyany

Custom EF Core AddOrUpdate with composite keys


I've built an extension for Microsoft.EntityFrameworkCore that implements the AddOrUpdateMethod. It's working fine, but with entities with a composite primary key the AnyAsync method return always false, even if there are objects with the same key.

This is the method:

public static async Task AddOrUpdateAsync<TEntity>(this DbSet<TEntity> table, Expression<Func<TEntity, object>> key, Expression<Func<TEntity, bool>> deleteExpression, params TEntity[] entities) where TEntity : class
{
    var getKeyFunction = key.Compile();
    var getShouldDeleteFunction = deleteExpression.Compile();
    var context = GetDbContext(table);
    foreach (var entity in entities)
    {
        var primaryKey = getKeyFunction(entity);
        var body = Expression.Equal(Expression.Convert(key.Body, primaryKey.GetType()), Expression.Constant(primaryKey));
        Expression<Func<TEntity, bool>> query = Expression.Lambda<Func<TEntity, bool>>(body, key.Parameters);
        var exist = await table.AnyAsync(query);
        context.Entry(entity).State = exist
            ? getShouldDeleteFunction(entity) ? EntityState.Deleted : EntityState.Modified
            : getShouldDeleteFunction(entity) ? EntityState.Detached : EntityState.Added;
    }
}

private static DbContext GetDbContext<T>(this DbSet<T> table) where T : class
{
    var infrastructure = table as IInfrastructure<IServiceProvider>;
    var serviceProvider = infrastructure.Instance;
    var currentDbContext = serviceProvider.GetService(typeof(ICurrentDbContext)) as ICurrentDbContext;
    return currentDbContext.Context;
}

and I'm using it like this:

await db.Reports.AddOrUpdateAsync(r => new { r.Number, r.Year }, r => r.Active == false, response.Reports.ToArray());

I think that's happening because I'm using an anonymous type as the key, but I've no idea how to fix this.


Solution

  • The problem seems to be the usage of the anonymous type constant expression, which currently is causing client evaluation, and C# operator == compares anonymous types by reference, hence always returns false.

    The trick to get the desired server translation is to "invoke" the key expression with entity by replacing the parameter with Expression.Constant(entity) (Expression.Invoke doesn't work in this case)

    So remove the line var getKeyFunction = key.Compile(); as no longer needed, and use the following:

    foreach (var entity in entities)
    {
        var parameter = key.Parameters[0];
        var body = Expression.Equal(
            key.Body,
            key.Body.ReplaceParameter(parameter, Expression.Constant(entity))
        );
        var query = Expression.Lambda<Func<TEntity, bool>>(body, parameter);
        var exist = await table.AnyAsync(query);
        // ...
    }
    

    where ReplaceParameter is the usual expression helper method:

    public static partial class ExpressionUtils
    {
        public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
            => new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    
        class ParameterReplacer : ExpressionVisitor
        {
            public ParameterExpression Source;
            public Expression Target;
            protected override Expression VisitParameter(ParameterExpression node)
                => node == Source ? Target : node;
        }
    }