Use AutoMapper for adding, updating and removing items in List

asp.net-core automapper c# entity-framework-core

Question

Does AutoMapper not have a native approach to updating of nested lists where the instances need to be removed, added or updated?

Hello, I am using AutoMapper in my ASP.Net Core application with EF Core to map my API resources to my models. This has been working fine for most of my application, but I am not pleased with my solution for updating mapped nested lists where the listed instances need to persist. I don't want to overwrite the existing list, I want to delete instances that are no longer in my incoming resource, add new instances, and update existing instances.

The models:

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

The resources:

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

The controller:

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(); 
    }
}

The mapping profile:

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

What I am doing here is a custom mapping, but I feel this is such a generic case that I expect AutoMapper to be able to handle this by some extension. I have seen some examples where the mapping profile uses a custom mapping (.AfterMap), but then the actual mapping is done by a static instance of AutoMapper. I am not sure if that is appropriate for my use of AutoMapper through dependency injection: I am not an experienced programmer but it doesn't seem sound to me.

1
3
1/16/2019 3:13:06 PM

Accepted Answer

Thanks to Ivan Stoev I started looking at AutoMapper.Collection, which is really the extension I had hoped to find. After implementing my lists get updated as I had wanted to. The configuration is straightforward in my usage as I only have to specify the Id of my objects.

My startup configuration is changed to:

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

And my mapping profile:

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

    CreateMap<SubItem, SubItemResource>().ReverseMap()
        .EqualityComparison((sir, si) => sir.Id == si.Id);
4
1/17/2019 6:09:16 AM

Popular Answer

This is a problem more difficult to solve than it would seem on the surface. It's easy enough for you to do custom mapping of your lists, because you know your application; AutoMapper does not. For example, what makes a source item equal to a destination item, such that AutoMapper would be able to discern that it should map over the existing rather than add? The PK? Which property is the PK? Is that property the same on both the source and destination? These are questions you can easily answer in your AfterMap, not so much for AutoMapper.

As a result, AutoMapper always maps collections over as new items. If that's not the behavior you want, that's where things like AfterMap come in. Also bear in mind that AutoMapper isn't designed specifically to work with EF, which is really the issue here, not AutoMapper. EF's change tracking is what causes AutoMapper's default collection mapping behavior to be problematic. In other situations and scenarios, it's not an issue.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow