web-dev-qa-db-fra.com

Qu'est-il arrivé à AddOrUpdate dans EF 7/Core?

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?

22
Paul Sohal

Il attend d'être mis en œuvre. Voir les numéros # 629 & # 4526 .

Mise à jour: selon les commentaires ci-dessous (non vérifiés) - cette fonctionnalité est enfin prévue pour publication dans .NET Core 2.1!

19
bricelam

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);
    }
}
9
parallelbgls

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; }
}
4
MSC

Il existe une méthode d'extension Upsert .

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

Sur Github

3
FindOutIslamNow

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);
        }
3
Tjaart

J'ai commencé avec la réponse de Tjaart et modifié deux choses:

  1. J'utilise l'api couramment pour la désignation de clé, je recherche donc la clé primaire de l'entité au lieu d'un attribut sur l'entité
  2. 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

2
sdrevk

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.

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

(Le code n'est pas le mien)

1
SuperJMN

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

0
Dan Chenier