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.
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;
}
}