7
votes

Mettre à jour les collections parent et enfant sur le référentiel générique avec EF Core

Disons que j'ai une classe Sale :

    public async Task<T> GetByIdAsync<T>(int id, params Expression<Func<T, object>>[] includes) where T : BaseEntity
    {
        var query = _dbContext.Set<T>().AsQueryable();

        if (includes != null)
        {
            query = includes.Aggregate(query,
              (current, include) => current.Include(include));
        }

        return await query.SingleOrDefaultAsync(e => e.Id == id);
    }

Et une classe Item :

    public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
    {
        var dbEntity = _dbContext.Set<T>().Find(entity.Id);

        var dbEntry = _dbContext.Entry(dbEntity);

        dbEntry.CurrentValues.SetValues(entity);            

        foreach (var property in navigations)
        {
            var propertyName = property.GetPropertyAccess().Name;

            await dbEntry.Collection(propertyName).LoadAsync();

            List<BaseEntity> dbChilds = dbEntry.Collection(propertyName).CurrentValue.Cast<BaseEntity>().ToList();

            foreach (BaseEntity child in dbChilds)
            {
                if (child.Id == 0)
                {
                    _dbContext.Entry(child).State = EntityState.Added;
                }
                else
                {
                    _dbContext.Entry(child).State = EntityState.Modified;
                }
            }
        }

        return await _dbContext.SaveChangesAsync();
    }


0 commentaires

3 Réponses :


2
votes

Le plus simple serait de récupérer toutes les entités Deleted , de les convertir en BaseEntity et de vérifier leurs identifiants avec les identifiants actuels dans la collection de relations de l'entité.

Quelque chose dans le sens de:

foreach (var property in navigations)
{
    var propertyName = property.GetPropertyAccess().Name;

    await dbEntry.Collection(propertyName).LoadAsync();

    // this line specifically might need some changes
    // as it may give you ICollection<SomeType>
    var currentCollectionType = property.GetPropertyAccess().PropertyType;

    var deletedEntities = _dbContext.ChangeTracker
        .Entries
        .Where(x => x.EntityState == EntityState.Deleted && x.GetType() == currentCollectionType)
        .Select(x => (BaseEntity)x.Id)
        .ToArray();

    List<BaseEntity> dbChilds = dbEntry.Collection(propertyName).CurrentValue.Cast<BaseEntity>().ToList();

    foreach (BaseEntity child in dbChilds)
    {
        if (child.Id == 0)
        {
            _dbContext.Entry(child).State = EntityState.Added;
        }

        if (deletedEntities.Contains(child.Id))
        {
            _dbContext.Entry(child).State = EntityState.Deleted;
        }
        else
        {
            _dbContext.Entry(child).State = EntityState.Modified;
        }
    }
}


3 commentaires

Le tableau suppriméEntities ne contient aucun élément, même si je supprime le conditionnel Type


en regardant dans le ChangeTracker je remarque que les objets Item sont à l'état Inchangé


@GonzaloLorieto Cela suppose que vous avez appelé _dbContext.Remove (item) qui remplit le ChangeTracker avec ces entités supprimées



13
votes

Apparemment, la question est d'appliquer les modifications de l ' entité déconnectée (sinon vous n'aurez rien d'autre à faire que d'appeler SaveChanges ) contenant les propriétés de navigation de collection qui doivent refléter les éléments ajoutés / supprimés / mis à jour de l'objet passé.

EF Core ne fournit pas une telle capacité prête à l'emploi. Il prend en charge les simples upsert (insérer ou mettre à jour) via la méthode Update pour les entités avec des clés générées automatiquement, mais il ne détecte ni ne supprime les éléments supprimés.

Vous devez faire cette détection vous-même. Le chargement des éléments existants est un pas dans la bonne direction. Le problème avec votre code est qu'il ne tient pas compte des nouveaux éléments, mais qu'il effectue à la place une manipulation d'état inutile des éléments existants extraits de la base de données.

Voici l'implémentation correcte de la même idée. Il utilise certains éléments internes d'EF Core ( IClrCollectionAccessor renvoyés par la méthode GetCollectionAccessor () - les deux nécessitent utilisant Microsoft.EntityFrameworkCore.Metadata.Internal; ) manipulez la collection, mais votre code utilise déjà la méthode interne GetPropertyAccess () , donc je suppose que cela ne devrait pas être un problème - au cas où quelque chose serait changé dans une future version d'EF Core, le code devrait être mis à jour en conséquence. L'accesseur de collection est nécessaire car si IEnumerable peut être utilisé pour accéder de manière générique aux collections en raison de la covariance, on ne peut pas en dire autant de ICollection car il est invariant , et nous avons besoin d'un moyen d'accéder aux méthodes Add / Remove . L'accesseur interne fournit cette capacité ainsi qu'un moyen de récupérer de manière générique la valeur de propriété de l'entité transmise.

Mise à jour: À partir d'EF Core 3.0, GetCollectionAccessor et IClrCollectionAccessor font partie de l'API publique.

Voici le code:

_dbContext.Remove(oldItem);

L'algorithme est assez standard. Après avoir chargé la collection depuis la base de données, nous créons un dictionnaire contenant les éléments existants saisis par Id (pour une recherche rapide). Ensuite, nous faisons un seul passage sur les nouveaux éléments. Nous utilisons le dictionnaire pour trouver l'élément existant correspondant. Si aucune correspondance n'est trouvée, l'élément est considéré comme nouveau et est simplement ajouté à la collection cible (suivie). Sinon, l'élément trouvé est mis à jour à partir de la source et supprimé du dictionnaire. De cette façon, après avoir terminé la boucle, le dictionnaire contient les éléments qui doivent être supprimés, donc tout ce dont nous avons besoin est de les supprimer de la collection cible (suivie).

Et c'est tout. Le reste du travail sera effectué par l'outil de suivi des modifications EF Core - les éléments ajoutés à la collection cible seront marqués comme Ajouté , la mise à jour - soit Inchangé ou Modifié , et les éléments supprimés, en fonction du comportement de la cascade de suppression, seront marqués pour suppression ou mise à jour (dissociation du parent). Si vous souhaitez forcer la suppression, remplacez simplement

accessor.Remove(dbEntity, oldItem);

par

public async Task<int> UpdateAsync<T>(T entity, params Expression<Func<T, object>>[] navigations) where T : BaseEntity
{
    var dbEntity = await _dbContext.FindAsync<T>(entity.Id);

    var dbEntry = _dbContext.Entry(dbEntity);
    dbEntry.CurrentValues.SetValues(entity);

    foreach (var property in navigations)
    {
        var propertyName = property.GetPropertyAccess().Name;
        var dbItemsEntry = dbEntry.Collection(propertyName);
        var accessor = dbItemsEntry.Metadata.GetCollectionAccessor();

        await dbItemsEntry.LoadAsync();
        var dbItemsMap = ((IEnumerable<BaseEntity>)dbItemsEntry.CurrentValue)
            .ToDictionary(e => e.Id);

        var items = (IEnumerable<BaseEntity>)accessor.GetOrCreate(entity);

        foreach (var item in items)
        {
            if (!dbItemsMap.TryGetValue(item.Id, out var oldItem))
                accessor.Add(dbEntity, item);
            else
            {
                _dbContext.Entry(oldItem).CurrentValues.SetValues(item);
                dbItemsMap.Remove(item.Id);
            }
        }

        foreach (var oldItem in dbItemsMap.Values)
            accessor.Remove(dbEntity, oldItem);
    }

    return await _dbContext.SaveChangesAsync();
}


13 commentaires

A travaillé comme un charme !!! Remove n'a pas cette surcharge, mais le accessor.Remove (...) a fait le travail.


J'ai commencé une autre prime parce que j'ai raté le clic sur le bouton de la prime et l'ai donné à une autre personne.


Les deux méthodes fonctionnent, seul le contexte a un seul argument (faute de frappe stupide - merci de l'avoir signalé, corrigé). Mais retirer de la collection semble techniquement plus correct de toute façon. Content que tu aimes ça, bravo :)


Génial! Je vous remercie!


Une question supplémentaire: si les propriétés de navigation ont différents types d'ID (int ou Guid), comment cela peut-il être pris en compte?


@Julida Ce serait plus difficile, principalement parce que vous ne pouvez pas utiliser l'accesseur de propriété Dictionary et Id tapé. La classe BaseEntity ici aide beaucoup.


Merci, Ivan. J'ai trouvé une solution pour cela.


Salut @Julida, quelle est la solution que tu as trouvée? Je suis très intéressé. Merci.


@craigmoliver J'ai répondu à votre question dans une nouvelle réponse (trop de caractères pour un commentaire), voir ci-dessous


@IvanStoev Depuis EF Core 3.0, GetCollectionAccessor () fait partie de l'API publique: docs.microsoft.com/en-us/dotnet/api/...


@IvanStoev Merci , au lieu de l'entité de base, puis-je utiliser TEntity.? mon référentiel baseRepo et TId est une interface publique IEntity {TId Id {get; ensemble; }}


@Ajt Non, parce que nous manipulons des sous-collections de TEntity (par exemple Person.Blogs ), dont le type d'élément est différent. L'entité de base ici est nécessaire pour accéder à la propriété Id . L'interface peut être utilisée en général, mais pas générique comme la vôtre, c'est-à-dire ici ((IEnumerable ) dbItemsEntry.CurrentValue) .ToDicti‌ onary (e => e.Id) nous avons besoin d'un cast pour quelque chose de non générique ayant une propriété de type Id connue. Votre scénario nécessite une approche légèrement différente de celle présentée ici - probablement en déplaçant une partie du code dans une méthode générique et en l'appelant par réflexion.


Suis nouveau .net core..puis-je fournir une méthode non générique .. j'ai ajouté des questions, merci de vérifier. stackoverflow.com/questions/63352197/...



4
votes

@craigmoliver Voici ma solution. Ce n'est pas le meilleur, je sais - si vous trouvez un moyen plus élégant, partagez-le.

Dépôt:

 public IReadOnlyList<IProperty> FindPrimaryKeyProperties<T>(T entity)
        {
            return Model.FindEntityType(entity.GetType()).FindPrimaryKey().Properties;
        }

        public IEnumerable<object> FindPrimaryKeyValues<TEntity>(TEntity entity) where TEntity : class
        {
            return from p in FindPrimaryKeyProperties(entity)
                   select entity.GetPropertyValue(p.Name);
        }

Contexte:

public async Task<TEntity> UpdateAsync<TEntity, TId>(TEntity entity, bool save = true, params Expression<Func<TEntity, object>>[] navigations)
            where TEntity : class, IIdEntity<TId>
        {
            TEntity dbEntity = await _context.FindAsync<TEntity>(entity.Id);

        EntityEntry<TEntity> dbEntry = _context.Entry(dbEntity);
        dbEntry.CurrentValues.SetValues(entity);

        foreach (Expression<Func<TEntity, object>> property in navigations)
        {
            var propertyName = property.GetPropertyAccess().Name;
            CollectionEntry dbItemsEntry = dbEntry.Collection(propertyName);
            IClrCollectionAccessor accessor = dbItemsEntry.Metadata.GetCollectionAccessor();

            await dbItemsEntry.LoadAsync();
            var dbItemsMap = ((IEnumerable<object>)dbItemsEntry.CurrentValue)
                .ToDictionary(e => string.Join('|', _context.FindPrimaryKeyValues(e)));

            foreach (var item in (IEnumerable)accessor.GetOrCreate(entity))
            {
                if (!dbItemsMap.TryGetValue(string.Join('|', _context.FindPrimaryKeyValues(item)), out object oldItem))
                {
                    accessor.Add(dbEntity, item);
                }
                else
                {
                    _context.Entry(oldItem).CurrentValues.SetValues(item);
                    dbItemsMap.Remove(string.Join('|', _context.FindPrimaryKeyValues(item)));
                }
            }

            foreach (var oldItem in dbItemsMap.Values)
            {
                accessor.Remove(dbEntity, oldItem);
                await DeleteAsync(oldItem as IEntity, false);

            }
        }

        if (save)
        {
            await SaveChangesAsync();
        }

        return entity;
    }


1 commentaires

Si ma classe a 2 ensembles de collections, comment appeler cette fonction? .. merci