在EF 7/Core中AddOrUpdate发生了什么?

Pau*_*hal 30 entity-framework-core

我正在使用EntityFramework.Core 7.0.0-rc1-final编写种子方法.

DbSet的AddOrUpdate方法发生了什么变化?

bri*_*lam 22

它正在等待实施.请参阅问题#629#4526.

更新:根据下面的评论(未经验证) - 此功能最终将在.NET Core 2.1中发布!

  • @ user1477388请参阅文档中的[断开实体:保存单个实体](https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities#saving-single-entities)。 (3认同)
  • 您的答案是一年半以前的。它还在等待实施吗?似乎所有的东西都被丢弃了... (2认同)
  • [数据播种](https://docs.microsoft.com/en-us/ef/core/modeling/data-seeding)即将推出EF Core 2.1. (2认同)
  • Data Seeding 提供了模型创建的 upsert,但是应用程序中的其他地方呢?例如,如果您想在操作或服务端点内执行更新插入? (2认同)

par*_*gls 10

我想这就是你想要的.

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 我知道这是一个旧答案,但它真的从表中提取所有记录,在内存中过滤它,然后在上下文中更新更改状态吗?那太可怕了。 (7认同)
  • 遗憾的是这个答案居然得到了赞成票。它有多个缺陷,首先是 a) 检索所有行,或 2) 访问私有 _context 字段而不是使用“dbSet.GetService.GetService&lt;ICurrentDbContext&gt;()”,或 3) 在关键成员上使用 for 循环而不是 dbSet .查找等等... (2认同)

Sim*_*wsi 9

以下 MS Docs 文章Disconnected entity说,只要数据库中的主键列具有自动生成的(例如身份)值,从 EF Core 2.0 开始,仅使用 Update 将充当 AddOrUpdate。

引用文章:

如果知道是否需要插入或更新,则可以适当地使用添加或更新。

但是,如果实体使用自动生成的键值,则 Update 方法可用于两种情况。

Update 方法通常将实体标记为更新,而不是插入。但是,如果实体具有自动生成的键,并且未设置键值,则实体会自动标记为插入。

此行为是在 EF Core 2.0 中引入的。对于早期版本,始终需要明确选择添加或更新。

如果实体未使用自动生成的密钥,则应用程序必须决定是否应插入或更新实体。

我已经在一个测试项目中尝试了这个,并且可以确认 Update 适用于在 EF Core 2.2 中添加和更新实体,并使用自动生成的密钥。

上面链接的断开连接的实体文章还包括自制 InsertOrUpdate 方法的示例代码、EF Core 的早期版本或实体没有自动生成的密钥。示例代码特定于特定实体类,需要修改以使其通用。

  • 我认为这现在应该是公认的答案。我不认为其他答案与 EF Core 2.0 以上的任何内容相关 (4认同)

sdr*_*evk 6

我从Tjaart的回答开始,修改了两件事:

  1. 我使用 fluent 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;
    }
    
    Run Code Online (Sandbox Code Playgroud)

到目前为止,我已经能够从中获得不错的里程数,没有任何问题。

我在 EFCore 2.1 上使用它


MSC*_*MSC 5

我认为,如果假定基本实体类是合法的选择,则此解决方案是解决此问题的更简单解决方案。简单性来自实现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; }
}
Run Code Online (Sandbox Code Playgroud)

  • 这迫使您让每个模型都继承自该基础,我认为这对域来说不是一个好主意。 (2认同)

Fin*_*Now 5

有一个扩展方法Upsert

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

在 Github 上

  • 不幸的是,这会直接在数据库中保存更改,而添加/更新会更新上下文。当某处使用时间戳类型字段时,它不起作用。您应该提及此方法的限制,如此处所述:https://www.flexlabs.org/2018/02/adding-upsert-support-for-entity-framework-core (2认同)

Bam*_*dad 5

我不明白为什么人们试图在其他答案中找到主键。只需在调用方法时传递它,就像在EF 6 AddOrUpdate 方法中完成的那样。

public static TEntity AddOrUpdate<TEntity>(this DbSet<TEntity> dbSet, DbContext context, Func<TEntity, object> identifier, TEntity entity) where TEntity : class
{
    TEntity result = dbSet.Find(identifier.Invoke(entity));
    if (result != null)
    {
        context.Entry(result).CurrentValues.SetValues(entity);
        dbSet.Update(result);
        return result;
    }
    else
    {
        dbSet.Add(entity);
        return entity;
    }
}
Run Code Online (Sandbox Code Playgroud)

稍后像这样使用它:

dbContext.MyModels.AddOrUpdate(dbContext, model => model.Id, new MyModel() { Id = 3 });
Run Code Online (Sandbox Code Playgroud)

干净且高性能。