EF & Automapper. Update nested collections

£可爱£侵袭症+ 提交于 2019-12-03 02:30:21
Alisson

The problem is the country you are retrieving from database already has some cities. When you use AutoMapper like this:

// mapping 
        AutoMapper.Mapper.Map(countryDTO, country);

AutoMapper is doing something like creating an IColletion<City> correctly (with one city in your example), and assigning this brand new collection to your country.Cities property.

The problem is EntityFramework doesn't know what to do with the old collection of cities.

  • Should it remove your old cities and assume only the new collection?
  • Should it just merge the two lists and keep both in database?

In fact, EF cannot decide for you. If you want to keep using AutoMapper, you can customize your mapping like this:

// AutoMapper Profile
public class MyProfile : Profile
{

    protected override void Configure()
    {

        Mapper.CreateMap<CountryData, Country>()
            .ForMember(d => d.Cities, opt => opt.Ignore())
            .AfterMap(AddOrUpdateCities);
    }

    private void AddOrUpdateCities(CountryData dto, Country country)
    {
        foreach (var cityDTO in dto.Cities)
        {
            if (cityDTO.Id == 0)
            {
                country.Cities.Add(Mapper.Map<City>(cityDTO));
            }
            else
            {
                Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id));
            }
        }
    }
}

The Ignore() configuration used for Cities makes AutoMapper just keep the original proxy reference built by EntityFramework.

Then we just use AfterMap() to invoke an action doing exactly what you thougth:

  • For new cities, we map from DTO to Entity (AutoMapper creates a new instance) and add it to country's collection.
  • For existing cities, we use an overload of Map where we pass the existing entity as the second parameter, and the city proxy as first parameter, so automapper just updates the existing entity's properties.

Then you can keep your original code:

using (var context = new Context())
    {
        // getting entity from db, reflect it to dto
        var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();

        // add new city to dto 
        countryDTO.Cities.Add(new CityData 
                                  { 
                                      CountryId = countryDTO.Id, 
                                      Name = "new city", 
                                      Population = 100000 
                                  });

        // change existing city name
        countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name";

        // retrieving original entity from db
        var country = context.Countries.FirstOrDefault(x => x.Id == 1);

        // mapping 
        AutoMapper.Mapper.Map(countryDTO, country);

        // save and expecting ef to recognize changes
        context.SaveChanges();
    }

when save changes all cities are considered as added becasue EF didn't now about them till saving time. So EF tries to set null to foreign key of old city and insert it instead of update.

using ChangeTracker.Entries() you will find out what changes CRUD is going to be made by EF.

If you want just update existing city manually, you can simply do :

foreach (var city in country.cities)
{
    context.Cities.Attach(city); 
    context.Entry(city).State = EntityState.Modified;
}

context.SaveChanges();

It seems like I found solution:

var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
countryDTO.Cities.Add(new CityData { CountryId = countryDTO.Id, Name = "new city 2", Population = 100000 });
countryDTO.Cities.FirstOrDefault(x => x.Id == 11).Name = "another name";

var country = context.Countries.FirstOrDefault(x => x.Id == 1);

foreach (var cityDTO in countryDTO.Cities)
{
    if (cityDTO.Id == 0)
    {
        country.Cities.Add(cityDTO.ToEntity<City>());
    }
    else
    {
        AutoMapper.Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id)); 
    }
}

AutoMapper.Mapper.Map(countryDTO, country);

context.SaveChanges();

this code updates edited items and add new ones. But maybe there are some pitfalls I cant detect for now?

Very Good solution of Alisson. Here is my solution... As We know EF does not know whether the request is for update or insert so what I would do is delete first with RemoveRange() method and send the collection to insert it again. In background this is how database works then we can emulate this behavior manually.

Here is the code:

//country object from request for example

var cities = dbcontext.Cities.Where(x=>x.countryId == country.Id);

dbcontext.Cities.RemoveRange(cities);

/* Now make the mappings and send the object this will make bulk insert into the table related */

This is not an answer per se to the OP, but anyone looking at a similar problem today should consider using AutoMapper.Collection. It provides support for these parent-child collection issues that used to require a lot of code to handle.

I apologize for not including a good solution or more detail, but I am only coming up to speed on it now. There is an excellent simple example right in the README.md displayed on the link above.

Using this requires a bit of a rewrite, but it drastically cuts down on the amount of code you have to write, especially if you're using EF and can make use of AutoMapper.Collection.EntityFramework.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!