I\'m writing a seed method using EntityFramework.Core 7.0.0-rc1-final.
What happened to the AddOrUpdate method of DbSet?
Here is my solution based on other solutions from this thread.
public object PrimaryKeyValues(TEntity entity)
{
var properties = _appDb.Model.FindEntityType(typeof(TEntity)).FindPrimaryKey().Properties;
var entry = _appDb.Entry(entity);
var values = properties?.Select(p => entry.Property(p.Name).CurrentValue);
if (values?.Count() == 1)
return values.Single();
return values?.ToArray();
}
public async Task AddOrUpdateAsync(TEntity entity) where TEntity : class
{
var pkValue = PrimaryKeyValues(entity);
if (pkValue == null)
{
throw new Exception($"{typeof(TEntity).FullName} does not have a primary key specified. Unable to exec AddOrUpdateAsync call.");
}
if ((await _appDb.FindAsync(typeof(TEntity), pkValue)) is TEntity dbEntry)
{
_appDb.Entry(dbEntry).CurrentValues.SetValues(entity);
_appDb.Update(dbEntry);
entity = dbEntry;
}
else
{
_appDb.Add(entity);
}
return entity;
}
Complete solution. Doesn't support keys that are shadow properties
DbContextExtensions.cs
// FIND ALL
// ===============================================================
///
/// Tries to get all entities by their primary keys. Return all/partial/empty array of database entities.
///
///
///
///
///
public static async Task FindAllAsync(this DbContext dbContext, IEnumerable args) where TEntity : class
{
return await Task.Run(() => {
var dbParameter = Expression.Parameter(typeof(TEntity), typeof(TEntity).Name);
var properties = dbContext.Model.FindEntityType(typeof(TEntity)).FindPrimaryKey()?.Properties;
if (properties == null)
throw new ArgumentException($"{typeof(TEntity).FullName} does not have a primary key specified.");
if (args == null)
throw new ArgumentNullException($"Entities to find argument cannot be null");
if (!args.Any())
return Enumerable.Empty().ToArray();
var aggregatedExpression = args.Select(entity =>
{
var entry = dbContext.Entry(entity);
return properties.Select(p =>
{
var dbProp = dbParameter.Type.GetProperty(p.Name);
var left = Expression.Property(dbParameter, dbProp);
var argValue = entry.Property(p.Name).CurrentValue;
var right = Expression.Constant(argValue);
return Expression.Equal(left, right);
})
.Aggregate((acc, next) => Expression.And(acc, next));
})
.Aggregate((acc, next) => Expression.OrElse(acc, next));
var whereMethod = typeof(Enumerable).GetMethods().First(m => m.Name == "Where" && m.GetParameters().Length == 2);
MethodInfo genericWhereMethod = whereMethod.MakeGenericMethod(typeof(TEntity));
var whereLambda = Expression.Lambda(aggregatedExpression, dbParameter);
var set = dbContext.Set();
var func = whereLambda.Compile();
var result = genericWhereMethod.Invoke(null, new object[] { set, func}) as IEnumerable;
return result.ToArray();
});
}
// ADD OR UPDATE - RANGE - ASYNC
// ===============================================================
///
/// Foreach entity in a range, adds it when it doesn't exist otherwise updates it. Bases decision on Pk.
///
///
///
///
///
public static async Task<(int AddedCount, int UpdatedCount)> AddOrUpdateRangeAsync(this DbContext dbContext, IEnumerable entities) where TEntity : class
{
var existingEntitiesHashes = (await dbContext.FindAllAsync(entities)).Select(x =>
{
dbContext.Entry(x).State = EntityState.Detached;
return dbContext.PrimaryKeyHash(x);
});
var (True, False) = entities.DivideOn(x => existingEntitiesHashes.Contains(dbContext.PrimaryKeyHash(x)));
dbContext.UpdateRange(True);
dbContext.AddRange(False);
return (AddedCount: False.Count(), UpdatedCount: True.Count());
}
// ADD OR UPDATE - ASYNC
// ===============================================================
///
/// Adds when not existing otherwise updates an entity. Bases decision on Pk.
///
///
///
///
///
public static async Task AddOrUpdateAsync(this DbContext dbContext, TEntity entity) where TEntity : class
=> await dbContext.AddOrUpdateRangeAsync(new TEntity[] { entity });
// PK HASH
// ===============================================================
///
/// Returns the compounded hash string of all primary keys of the entity
///
///
///
///
///
public static string PrimaryKeyHash(this DbContext dbContext, TTarget entity)
{
var properties = dbContext.Model.FindEntityType(typeof(TTarget)).FindPrimaryKey().Properties;
var entry = dbContext.Entry(entity);
return properties.Select(p => Crypto.HashGUID(entry.Property(p.Name).CurrentValue))
.Aggregate(string.Empty, (acc, next) => acc += next);
}
Crypto.cs
public class Crypto
{
///
/// RETURNS A HASH AS A GUID BASED ON OBJECT.TOSTRING()
///
///
///
public static string HashGUID(object obj)
{
string text = string.Empty;
MD5CryptoServiceProvider md5CryptoServiceProvider = new MD5CryptoServiceProvider();
byte[] bytes = new UTF8Encoding().GetBytes(obj.ToString());
byte[] array = md5CryptoServiceProvider.ComputeHash(bytes);
for (int i = 0; i < array.Length; i++)
{
text += Convert.ToString(array[i], 16).PadLeft(2, '0');
}
md5CryptoServiceProvider.Clear();
return text.PadLeft(32, '0');
}
}
IEnumerableExtensions.cs
///
/// Divides into two based on predicate
///
///
///
///
///
public static (IEnumerable True, IEnumerable False) DivideOn(this IEnumerable source, Func predicate)
=> (source.Where(x => predicate(x)), source.Where(x => !predicate(x)));
Comment if use it (ノ◕ヮ◕)ノ✲゚。⋆