4
votes

Utilisez AutoMapper pour ajouter, mettre à jour et supprimer des éléments dans la liste

AutoMapper n'a-t-il pas une approche native de la mise à jour des listes imbriquées dans lesquelles les instances doivent être supprimées, ajoutées ou mises à jour ?

Bonjour, j'utilise AutoMapper dans mon application ASP.Net Core avec EF Core pour mapper mes ressources API à mes modèles. Cela fonctionne bien pour la plupart de mes applications, mais je ne suis pas satisfait de ma solution de mise à jour des listes imbriquées mappées où les instances répertoriées doivent persister. Je ne veux pas écraser la liste existante, je veux supprimer les instances qui ne sont plus dans ma ressource entrante, ajouter de nouvelles instances et mettre à jour les instances existantes.

Les modèles:

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<MainObject, MainObjectResource>();
        CreateMap<SubItem, SubItemResource>();

        CreateMap<MainObject, MainObjectResource>().ReverseMap()
        .ForMember(m => m.Id, opt => opt.Ignore())
        .ForMember(m => m.SubItems, opt => opt.Ignore())
        .AfterMap((mr, m) => {
                //To remove
                List<MainObject> removedSubItems = m.SubItems.Where(si => !mr.SubItems.Any(sir => si.Id == sir.Id)).ToList();
                foreach (SubItem si in removedSubItems)
                    m.SubItems.Remove(si);
                //To add
                List<SubItemResource> addedSubItems = mr.SubItems.Where(sir => !m.SubItems.Any(si => si.Id == sir.Id)).ToList();
                foreach (SubItemResource sir in addedSubItems)
                    m.SubItems.Add( new SubItem {
                        Value = sir.Value,
                    });
                // To update
                List<SubItemResource> updatedSubItems = mr.SubItems.Where(sir => m.SubItems.Any(si => si.Id == sir.Id)).ToList();
                SubItem subItem = new SubItem();
                foreach (SubItemResource sir in updatedSubItems)
                {
                    subItem = m.SubItems.SingleOrDefault(si => si.Id == sir.Id);
                    subItem.Value = sir.Value;
                }
            });
    }
}

Les ressources:

public class MainController : Controller
{
    private readonly IMapper mapper;
    private readonly IMainRepository mainRepository;
    private readonly IUnitOfWork unitOfWork;

    public MainController(IMapper mapper, IMainRepository mainRepository, IUnitOfWork unitOfWork)
    {
        this.mapper = mapper;
        this.mainRepository = mainRepository;
        this.unitOfWork = unitOfWork;
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateMain(int id, [FromBody] MainObjectResource mainObjectResource)
    {
        MainObject mainObject = await mainRepository.GetMain(id);
        mapper.Map<MainObjectResource, MainObject>(mainObjectResource, mainObjectResource);
        await unitOfWork.CompleteAsync();

        return Ok(); 
    }
}

Le contrôleur:

public class MainObjectResource
{
    public int Id { get; set; }
    public ICollection<SubItemResource> SubItems { get; set; }

}

public class SubItemResource
{
    public int Id { get; set; }
    public double Value { get; set; }
}

Le profil de mappage:

public class MainObject
{
    public int Id { get; set; }
    public List<SubItem> SubItems { get; set; }

}

public class SubItem
{
    public int Id { get; set; }
    public int MainObjectId { get; set; }
    public MainObject MainObject { get; set; }
    public double Value { get; set; }
}

Ce que je fais ici est un mappage personnalisé, mais je pense que c'est un cas tellement générique que je m'attends à ce qu'AutoMapper soit capable de gérer cela avec une extension. J'ai vu quelques exemples où le profil de mappage utilise un mappage personnalisé (.AfterMap), mais le mappage réel est effectué par une instance statique d'AutoMapper. Je ne suis pas sûr que cela soit approprié pour mon utilisation d'AutoMapper via l'injection de dépendances: je ne suis pas un programmeur expérimenté mais cela ne me semble pas correct.


2 commentaires

Jetez un œil à AutoMapper.Collection.EFCore


Merci Ivan, pour moi, il était assez facile d'utiliser AutoMapper.Collection. J'ai essayé d'utiliser AutoMapper.Collection.EFCore (et je vérifierai si mes exigences changent) mais j'avais du mal à le configurer avec l'injection de dépendances. Principalement en raison de mon inexpérience et du manque de documentation et de messages incohérents dans le suivi des problèmes.


3 Réponses :


3
votes

C'est un problème plus difficile à résoudre qu'il n'y paraît à première vue. Il est assez facile pour vous de faire un mappage personnalisé de vos listes, car vous connaissez votre application; AutoMapper ne le fait pas. Par exemple, qu'est-ce qui rend un élément source égal à un élément de destination, de sorte que AutoMapper puisse discerner qu'il doit mapper sur l'existant plutôt que d'ajouter? Le PK? Quelle propriété est le PK? Cette propriété est-elle la même sur la source et la destination? Ce sont des questions auxquelles vous pouvez facilement répondre dans votre AfterMap , pas tellement pour AutoMapper.

En conséquence, AutoMapper mappe toujours les collections en tant que nouveaux éléments. Si ce n'est pas le comportement que vous souhaitez, c'est là que des éléments comme AfterMap entrent en jeu. Gardez également à l'esprit qu'AutoMapper n'est pas spécialement conçu pour fonctionner avec EF, ce qui est vraiment le problème ici, pas avec AutoMapper. Le suivi des modifications d'EF est ce qui cause le comportement de mappage de collection par défaut d'AutoMapper. Dans d'autres situations et scénarios, ce n'est pas un problème.


3 commentaires

Merci Chris d'avoir expliqué pourquoi ce n'est pas simple. Avez-vous des conseils sur la façon dont cela est normalement implémenté dans .AfterMap? De sorte que le mappage ne soit pas fait manuellement propriété par propriété comme dans mon exemple? Ou devrais-je poser une nouvelle question à ce sujet?


Non, c'est à peu près tout. C'est moche mais tout est au même endroit et vous n'avez à le configurer qu'une seule fois. Je pense qu'il existe une pépite tierce qui tente d'ajouter des fonctionnalités pour cela à AutoMapper, mais je ne suis pas sûr de son statut - en particulier si cela fonctionne ou non avec EF Core.


@ChrisPratt à la recherche d'AutoMapper.Collection.EntityFrameworkCore? J'ai ajouté une réponse et expliqué son utilisation aussi.



5
votes

Grâce à Ivan Stoev, j'ai commencé à regarder AutoMapper.Collection , qui est en fait l'extension I avait espéré trouver. Après la mise en œuvre, mes listes sont mises à jour comme je le souhaitais. La configuration est simple dans mon utilisation car je n'ai qu'à spécifier l'ID de mes objets.

Ma configuration de démarrage est changée en:

    CreateMap<MainObject, MainObjectResource>().ReverseMap()
        .ForMember(m => m.Id, opt => opt.Ignore());

    CreateMap<SubItem, SubItemResource>().ReverseMap()
        .EqualityComparison((sir, si) => sir.Id == si.Id);

Et mon profil de mappage:

using AutoMapper;
using AutoMapper.EquivalencyExpression;
[....]
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAutoMapper(cfg => {
                cfg.AddCollectionMappers();
                });
        }
[....]


0 commentaires

1
votes

Je crois avoir réalisé une configuration où mes InputModels et EntityClasses peuvent contenir un identifiant, et je dois simplement créer un profil de mappage et appeler: Mapper.Map (inputModelClassInstance, entityClassInstance);

Par conséquent, AutoMapper et EF CORE utiliseront les identifiants de mes instances pour voir si elles sont égales et ajouter, supprimer ou mettre à jour comme prévu.

Voici ce que j'ai fait:

J'ai utilisé: https://github.com/AutoMapper/Automapper.Collection.EFCore

(Code modifié à partir du lien):

//Fetch entityClassInstance from db:
var entityClassInstance = Context.EntityClasses
.Where(ec => ex.id == id)
.Include(ec => ec.children)
.FirstOrDefault();

//Perform update:
Mapper.Map(inputModelClassInstance, entityClassInstance);

Puis mon apparence de CreateMap () comme ceci:

CreateMap<InputModelClass, EntityClass>(MemberList.Source);
CreateMap<ChildInputModelClass, ChildClass>(MemberList.Source);

Et j'effectue simplement une mise à jour par:

var services = new ServiceCollection();
services
    .AddEntityFrameworkSqlServer() //Use your EF CORE provider here
    .AddDbContext<MyContext>(); //Use your context here

services.AddAutoMapper((serviceProvider, automapper) =>
{
    automapper.AddCollectionMappers();
    automapper.UseEntityFrameworkCoreModel<MyContext>(serviceProvider);
}, typeof(DB).Assembly);

var serviceProvider = services.BuildServiceProvider();

Je peux ajouter et supprimer de l'enfant du parent collection, et EF CORE + AutoMapper ajoutera, supprimera et mettra à jour comme prévu.

Je crois que .UseEntityFrameworkCoreModel (serviceProvider) ajoute la configuration qu'AutoMapp er utilisera les identifiants pour comparer ce qui doit être ajouté, supprimé et mis à jour.

Important: Il est très important que vous incluiez les entités enfants, sinon elles ne sera pas mis à jour par AutoMapper.


3 commentaires

À quoi ressemblent vos entités et modèles? Je suis actuellement à la recherche de cette solution mais je ne parviens pas à faire supprimer ou détacher des entités lors du mappage de la collection de modèles sur des entités. Je me demande si c'est parce que je n'ai pas de clés étrangères nullables.


Un peu gênant, mais pour aider quelqu'un d'autre, je publierai ma découverte ici - j'avais simplement oublié d'utiliser ThenInclude pour inclure et commencer à suivre les entités enfants imbriquées.


Oui, vous avez raison, les entités enfants doivent être incluses pour permettre le suivi. (et donc suppression lors de la mise à jour) des entités enfants. Merci d'avoir signalé que vous avez trouvé une solution.