您可以创建通用的Upsert 扩展,它将通过实体键值在数据库中查找实体,然后添加实体或更新它:
public static class DbSetExtensions
{
private static Dictionary<Type, PropertyInfo> keys = new Dictionary<Type, PropertyInfo>();
public static T Upsert<T>(this DbSet<T> set, T entity)
where T : class
{
DbContext db = set.GetContext();
Type entityType = typeof(T);
PropertyInfo keyProperty;
if (!keys.TryGetValue(entityType, out keyProperty))
{
keyProperty = entityType.GetProperty(GetKeyName<T>(db));
keys.Add(entityType, keyProperty);
}
T entityFromDb = set.Find(keyProperty.GetValue(entity));
if (entityFromDb == null)
return set.Add(entity);
db.Entry(entityFromDb).State = EntityState.Detached;
db.Entry(entity).State = EntityState.Modified;
return entity;
}
// other methods explained below
}
此方法使用实体集元数据来获取关键属性名称。您可以在此处使用任何类型的配置 - xml、属性或 fluent API。将 set 加载到内存后,Entity Framework 知道哪个属性是键。当然可能有复合键,但当前的实现不支持这种情况。你可以扩展它:
private static string GetKeyName<T>(DbContext db)
where T : class
{
ObjectContext objectContext = ((IObjectContextAdapter)db).ObjectContext;
ObjectSet<T> objectSet = objectContext.CreateObjectSet<T>();
var keyNames = objectSet.EntitySet.ElementType.KeyProperties
.Select(p => p.Name).ToArray();
if (keyNames.Length > 1)
throw new NotSupportedException("Composite keys not supported");
return keyNames[0];
}
为了避免这种元数据搜索,您可以在keys Dictionary 中使用缓存。因此每个实体类型只会被检查一次。
不幸的是,EF 6 没有通过 DbSet 公开上下文。这不是很方便。但是你可以使用反射来获取上下文实例:
public static DbContext GetContext<TEntity>(this DbSet<TEntity> set)
where TEntity : class
{
object internalSet = set.GetType()
.GetField("_internalSet", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(set);
object internalContext = internalSet.GetType().BaseType
.GetField("_internalContext", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(internalSet);
return (DbContext)internalContext.GetType()
.GetProperty("Owner", BindingFlags.Instance | BindingFlags.Public)
.GetValue(internalContext, null);
}
用法很简单:
var db = new AmazonContext();
var john = new Customer {
SSN = "123121234", // configured as modelBuilder.Entity<Customer>().HasKey(c => c.SSN)
FirstName = "John",
LastName = "Snow"
};
db.Customers.Upsert(john);
db.SaveChanges();
进一步优化:如果您将创建Upsert 方法作为上下文类的成员,则可以避免反射 DbContext。用法看起来像
db.Upsert(john)