J'écris une méthode de départ en utilisant EntityFramework.Core 7.0.0-rc1-final.
Qu'est-il arrivé à la méthode AddOrUpdate de DbSet?
Je pense que c'est ce que tu veux.
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);
}
}
Je pense que cette solution est une solution plus simple à ce problème, si la prise en charge d’une classe d’entité de base est une option légitime. La simplicité provient des entités de votre domaine qui implémentent DomainEntityBase, ce qui allège la complexité des autres solutions suggérées.
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; }
}
Il existe une méthode d'extension Upsert .
context.Upsert(new Role { Name = "Employee", NormalizedName = "employee" })
.On(r => new { r.Name })
.Run();
Vous pouvez utiliser cette méthode d'extension que j'ai créée pour corriger notre base de code pour la migration vers 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);
}
J'ai commencé avec la réponse de Tjaart et modifié deux choses:
Le suivi des modifications est activé et je reçois l’erreur mentionnée par d’autres à propos du fait que EF la surveille déjà. Cela fait une recherche sur l'entité déjà suivie et copie les valeurs de l'entité entrante vers elle, puis met à jour l'entité d'origine
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;
}
J'ai réussi à obtenir un kilométrage décent jusqu'à présent sans aucun problème.
J'utilise ceci sur EFCore 2.1
J'ai trouvé une solution intéressante qui vous permet de spécifier la propriété qui doit correspondre. Cependant, il ne faut pas une seule entité, mais une liste dans chaque appel. Cela vous indiquera peut-être comment implémenter une meilleure version qui fonctionne comme la version ancienne.
(Le code n'est pas le mien)
Aucune des réponses n’ayant fonctionné avec Entity Framework Core (2.0), voici la solution qui a fonctionné pour moi:
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);
}
}
}
Vous devrez peut-être mettre à jour la méthode ReplaceParameter () si vous avez un identifierExpressExpress plus complexe. Les accesseurs de propriété simples fonctionneront bien avec cette implémentation. par exemple.:
context.Projects.AddOrUpdate(x => x.Name, new Project { ... })
context.Projects.AddOrUpdate(x => new { x.Name, x.Description }, new Project { ... })
Ensuite, context.SaveChanges () va valider les données dans la base de données