Что случилось с AddOrUpdate в EF 7/Core?

Я пишу семенной метод, используя EntityFramework.Core 7.0.0-rc1-final.

Что случилось с методом AddOrUpdate для DbSet?

Ответ 1

Это ждет, чтобы быть реализованным. Смотрите выпуски # 629 и # 4526.

Обновление: согласно комментариям ниже (не проверено) - эта функция, наконец, намечена к выпуску в .NET Core 2.1!

Ответ 2

Я думаю, что это то, что вы хотите.

public static class DbSetExtension
{
    public static void AddOrUpdate<T>(this DbSet<T> dbSet, T data) where T : class
    {
        var context = dbSet.GetContext();
        var ids = context.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.Select(x => x.Name);

        var t = typeof(T);
        List<PropertyInfo> keyFields = new List<PropertyInfo>();

        foreach (var propt in t.GetProperties())
        {
            var keyAttr = ids.Contains(propt.Name);
            if (keyAttr)
            {
                keyFields.Add(propt);
            }
        }
        if (keyFields.Count <= 0)
        {
            throw new Exception($"{t.FullName} does not have a KeyAttribute field. Unable to exec AddOrUpdate call.");
        }
        var entities = dbSet.AsNoTracking().ToList();
        foreach (var keyField in keyFields)
        {
            var keyVal = keyField.GetValue(data);
            entities = entities.Where(p => p.GetType().GetProperty(keyField.Name).GetValue(p).Equals(keyVal)).ToList();
        }
        var dbVal = entities.FirstOrDefault();
        if (dbVal != null)
        {
            context.Entry(dbVal).CurrentValues.SetValues(data);
            context.Entry(dbVal).State = EntityState.Modified;
            return;
        }
        dbSet.Add(data);
    }

    public static void AddOrUpdate<T>(this DbSet<T> dbSet, Expression<Func<T, object>> key, T data) where T : class
    {
        var context = dbSet.GetContext();
        var ids = context.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.Select(x => x.Name);
        var t = typeof(T);
        var keyObject = key.Compile()(data);
        PropertyInfo[] keyFields = keyObject.GetType().GetProperties().Select(p=>t.GetProperty(p.Name)).ToArray();
        if (keyFields == null)
        {
            throw new Exception($"{t.FullName} does not have a KeyAttribute field. Unable to exec AddOrUpdate call.");
        }
        var keyVals = keyFields.Select(p => p.GetValue(data));
        var entities = dbSet.AsNoTracking().ToList();
        int i = 0;
        foreach (var keyVal in keyVals)
        {
            entities = entities.Where(p => p.GetType().GetProperty(keyFields[i].Name).GetValue(p).Equals(keyVal)).ToList();
            i++;
        }
        if (entities.Any())
        {
            var dbVal = entities.FirstOrDefault();
            var keyAttrs =
                data.GetType().GetProperties().Where(p => ids.Contains(p.Name)).ToList();
            if (keyAttrs.Any())
            {
                foreach (var keyAttr in keyAttrs)
                {
                    keyAttr.SetValue(data,
                        dbVal.GetType()
                            .GetProperties()
                            .FirstOrDefault(p => p.Name == keyAttr.Name)
                            .GetValue(dbVal));
                }
                context.Entry(dbVal).CurrentValues.SetValues(data);
                context.Entry(dbVal).State = EntityState.Modified;
                return;
            }                
        }
        dbSet.Add(data);
    }
}

public static class HackyDbSetGetContextTrick
{
    public static DbContext GetContext<TEntity>(this DbSet<TEntity> dbSet)
        where TEntity : class
    {
        return (DbContext)dbSet
            .GetType().GetTypeInfo()
            .GetField("_context", BindingFlags.NonPublic | BindingFlags.Instance)
            .GetValue(dbSet);
    }
}

Ответ 3

Это решение, я думаю, более простое решение этой проблемы, если предположить, что базовый класс сущности является законным вариантом. Простота происходит от ваших доменных объектов, реализующих DomainEntityBase, что облегчает многие сложности в других предлагаемых решениях.

public static class DbContextExtensions
{
    public static void AddOrUpdate<T>(this DbSet<T> dbSet, IEnumerable<T> records) 
        where T : DomainEntityBase
    {
        foreach (var data in records)
        {
            var exists = dbSet.AsNoTracking().Any(x => x.Id == data.Id);
            if (exists)
            {
                dbSet.Update(data);
                continue;
            }
            dbSet.Add(data);
        }
    }
}

public class DomainEntityBase
{
    [Key]
    public Guid Id { get; set; }
}

Ответ 4

Вы можете использовать этот метод расширения, который я создал для исправления нашей базы кода для перехода к EF Core:

   public static void AddOrUpdate<T>(this DbSet<T> dbSet, T data) where T : class
        {
            var t = typeof(T);
            PropertyInfo keyField = null;
            foreach (var propt in t.GetProperties())
            {
                var keyAttr = propt.GetCustomAttribute<KeyAttribute>();
                if (keyAttr != null)
                {
                    keyField = propt;
                    break; // assume no composite keys
                }
            }
            if (keyField == null)
            {
                throw new Exception($"{t.FullName} does not have a KeyAttribute field. Unable to exec AddOrUpdate call.");
            }
            var keyVal = keyField.GetValue(data);
            var dbVal = dbSet.Find(keyVal);
            if (dbVal != null)
            {
                dbSet.Update(data);
                return;
            }
            dbSet.Add(data);
        }

Ответ 5

Есть метод расширения Upsert.

context.Upsert(new Role { Name = "Employee", NormalizedName = "employee" })
       .On(r => new { r.Name })
       .Run();

На Github

Ответ 6

Я начал с ответа Тьяарта и изменил две вещи:

  1. Я использую свободный API для обозначения ключа, поэтому я ищу первичный ключ объекта вместо атрибута объекта
  2. Я включил отслеживание изменений и получал сообщение об ошибке, о котором упоминали другие, что EF уже отслеживает его. Это выполняет поиск уже отслеженного объекта и копирует в него значения из входящего объекта, а затем обновляет исходный объект.

    public TEntity AddOrUpdate(TEntity entity)
    {
        var entityEntry = Context.Entry(entity);
    
        var primaryKeyName = entityEntry.Context.Model.FindEntityType(typeof(TEntity)).FindPrimaryKey().Properties
            .Select(x => x.Name).Single();
    
        var primaryKeyField = entity.GetType().GetProperty(primaryKeyName);
    
        var t = typeof(TEntity);
        if (primaryKeyField == null)
        {
            throw new Exception($"{t.FullName} does not have a primary key specified. Unable to exec AddOrUpdate call.");
        }
        var keyVal = primaryKeyField.GetValue(entity);
        var dbVal = DbSet.Find(keyVal);
    
        if (dbVal != null)
        {
            Context.Entry(dbVal).CurrentValues.SetValues(entity);
            DbSet.Update(dbVal);
    
            entity = dbVal;
        }
        else
        {
            DbSet.Add(entity);
        }
    
        return entity;
    }
    

До сих пор мне удавалось получить приличный пробег без проблем.

Я использую это на EFCore 2.1

Ответ 7

Я нашел хорошее решение, которое позволяет указать свойство, которое должно соответствовать. Однако он не принимает ни одного объекта, а список в каждом вызове. Это может дать вам несколько советов о том, как реализовать лучшую версию, которая работает как хорошо-старая.

https://github.com/aspnet/MusicStore/blob/7787e963dd0b7293ff95b28dcae92407231e0300/samples/MusicStore/Models/SampleData.cs#L48

(Код не мой)

Ответ 8

Ни один из ответов не работал для меня, используя Entity Framework Core (2.0), поэтому вот решение, которое работало для меня:

public static class DbSetExtensions
{

    public static void AddOrUpdate<T>(this DbSet<T> dbSet, Expression<Func<T, object>> identifierExpression, params T[] entities) where T : class
    {
        foreach (var entity in entities)
            AddOrUpdate(dbSet, identifierExpression, entity);
    }


    public static void AddOrUpdate<T>(this DbSet<T> dbSet, Expression<Func<T, object>> identifierExpression, T entity) where T : class
    {
        if (identifierExpression == null)
            throw new ArgumentNullException(nameof(identifierExpression));
        if (entity == null)
            throw new ArgumentNullException(nameof(entity));

        var keyObject = identifierExpression.Compile()(entity);
        var parameter = Expression.Parameter(typeof(T), "p");

        var lambda = Expression.Lambda<Func<T, bool>>(
            Expression.Equal(
                ReplaceParameter(identifierExpression.Body, parameter),
                Expression.Constant(keyObject)),
            parameter);

        var item = dbSet.FirstOrDefault(lambda.Compile());
        if (item == null)
        {
            // easy case
            dbSet.Add(entity);
        }
        else
        {
            // get Key fields, using KeyAttribute if possible otherwise convention
            var dataType = typeof(T);
            var keyFields = dataType.GetProperties().Where(p => p.GetCustomAttribute<KeyAttribute>() != null).ToList();
            if (!keyFields.Any())
            {
                string idName = dataType.Name + "Id";
                keyFields = dataType.GetProperties().Where(p => 
                    string.Equals(p.Name, "Id", StringComparison.OrdinalIgnoreCase) || 
                    string.Equals(p.Name, idName, StringComparison.OrdinalIgnoreCase)).ToList();
            }

            // update all non key and non collection properties
            foreach (var p in typeof(T).GetProperties().Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
            {
                // ignore collections
                if (p.PropertyType != typeof(string) && p.PropertyType.GetInterface(nameof(System.Collections.IEnumerable)) != null)
                    continue;

                // ignore ID fields
                if (keyFields.Any(x => x.Name == p.Name))
                    continue;

                var existingValue = p.GetValue(entity);
                if (!Equals(p.GetValue(item), existingValue))
                {
                    p.SetValue(item, existingValue);
                }
            }

            // also update key values on incoming data item where appropriate
            foreach (var idField in keyFields.Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null))
            {
                var existingValue = idField.GetValue(item);
                if (!Equals(idField.GetValue(entity), existingValue))
                {
                    idField.SetValue(entity, existingValue);
                }
            }
        }
    }


    private static Expression ReplaceParameter(Expression oldExpression, ParameterExpression newParameter)
    {
        switch (oldExpression.NodeType)
        {
            case ExpressionType.MemberAccess:
                var m = (MemberExpression)oldExpression;
                return Expression.MakeMemberAccess(newParameter, m.Member);
            case ExpressionType.New:
                var newExpression = (NewExpression)oldExpression;
                var arguments = new List<Expression>();
                foreach (var a in newExpression.Arguments)
                    arguments.Add(ReplaceParameter(a, newParameter));
                var returnValue = Expression.New(newExpression.Constructor, arguments.ToArray());
                return returnValue;
            default:
                throw new NotSupportedException("Unknown expression type for AddOrUpdate: " + oldExpression.NodeType);
        }
    }
}

Вам может потребоваться обновить метод ReplaceParameter(), если у вас есть более сложный identifierExpression. Простые методы доступа к свойствам будут хорошо работать с этой реализацией. например:

context.Projects.AddOrUpdate(x => x.Name, new Project { ... })
context.Projects.AddOrUpdate(x => new { x.Name, x.Description }, new Project { ... })

Затем context.SaveChanges() передаст данные в базу данных.

Ответ 9

Основываясь на ответе Tjaart, мне нужно было работать с составным первичным ключом. Вам нужно будет добавить атрибут Key в свойства составного ключа вашей сущности.

    public static void AddOrUpdate<T>(this DbSet<T> dbSet, T data) where T : class
    {
        var t = typeof(T);

        var keyProperties = t.GetProperties().Where(o => o.GetCustomAttribute<KeyAttribute>() != null).ToList();

        if (!keyProperties.Any())
        {
            throw new Exception($"{t.FullName} does not any a KeyAttribute fields. Unable to exec AddOrUpdate call.");
        }

        var keyVals = keyProperties.Select(o => o.GetValue(data)).ToArray();

        var dbVal = dbSet.Find(keyVals);
        if (dbVal != null)
        {
            dbSet.Update(data);
            return;
        }
        dbSet.Add(data);
    }