Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use AutoMapper for adding, updating and removing items in List

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

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.

like image 736
Superman.Lopez Avatar asked Jan 16 '19 15:01

Superman.Lopez


People also ask

How do I use AutoMapper to list a map?

How do I use AutoMapper? First, you need both a source and destination type to work with. The destination type's design can be influenced by the layer in which it lives, but AutoMapper works best as long as the names of the members match up to the source type's members.

Where is AutoMapper used?

AutoMapper is used whenever there are many data properties for objects, and we need to map them between the object of source class to the object of destination class, Along with the knowledge of data structure and algorithms, a developer is required to have excellent development skills as well.

How does AutoMapper work internally?

How AutoMapper works? AutoMapper internally uses a great concept of programming called Reflection. Reflection in C# is used to retrieve metadata on types at runtime. With the help of Reflection, we can dynamically get a type of existing objects and invoke its methods or access its fields and properties.

Does AutoMapper map both ways?

Yes, or you can call CreateMap<ModelClass, ViewModelClass>(). ReverseMap() .


3 Answers

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<SubItem, SubItemResource>().ReverseMap()
        .EqualityComparison((sir, si) => sir.Id == si.Id);
like image 162
Superman.Lopez Avatar answered Nov 12 '22 02:11

Superman.Lopez


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.

like image 20
Chris Pratt Avatar answered Nov 12 '22 02:11

Chris Pratt


I believe I have achieved a configuration where my InputModels and EntityClasses can contain an Id, and I simply have to create a mapping profile and call: Mapper.Map(inputModelClassInstance, entityClassInstance);

Hence AutoMapper and EF CORE will use Id's of my instances to see if they are equal and Add, Delete or Update as intended.

Here's what I did:

I used: https://github.com/AutoMapper/Automapper.Collection.EFCore

(Modified code from the link):

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

Then my CreateMap<>()'s looks like this:

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

And I simply perform an update by:

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

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

I can add and remove from the parent's child collection, and EF CORE + AutoMapper will add, delete and update as intended.

I believe .UseEntityFrameworkCoreModel<MyContext>(serviceProvider) adds the configuration that AutoMapper will use Ids to compare what are to be added, deleted and updated.

Important: It's very important that you include child entities, else they wont be updated by AutoMapper.

like image 35
PeterOeClausen Avatar answered Nov 12 '22 03:11

PeterOeClausen