EF & Automapper. Обновление вложенных коллекций

Я пытаюсь обновить вложенную коллекцию (Города) объекта Country.

Просто простые enitities и dto's:

// EF Models
public class Country
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<City> Cities { get; set; }
}

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int CountryId { get; set; }
    public int? Population { get; set; }

    public virtual Country Country { get; set; }
}

// DTo's
public class CountryData : IDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection<CityData> Cities { get; set; }
}

public class CityData : IDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int CountryId { get; set; }
    public int? Population { get; set; }
}

И сам код (протестирован в консольном приложении для простоты):

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

Этот код генерирует исключение:

Операция завершилась неудачно: отношение не может быть изменено, поскольку одно или несколько свойств внешнего ключа не имеют значения NULL. Когда происходит изменение отношения, соответствующее свойство внешнего ключа устанавливается равным нулевому значению. Если внешний ключ не поддерживает нулевые значения, необходимо определить новое отношение, для свойства внешнего ключа должно быть назначено другое ненулевое значение, или не связанный с ним объект должен быть удален.

хотя объект после последнего сопоставления кажется просто прекрасным и отражает все изменения должным образом.

Я потратил много времени на поиск решения, но не получил никакого результата. Пожалуйста, помогите.

Ответ 1

Проблема в том, что country вы извлекаете из базы данных, уже имеет несколько городов. Когда вы используете AutoMapper, как это:

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

AutoMapper делает что-то вроде правильного создания IColletion<City> (с одним городом в вашем примере) и присваивает эту совершенно новую коллекцию свойству country.Cities.

Проблема в том, что EntityFramework не знает, что делать со старой коллекцией городов.

  • Должен ли он удалить ваши старые города и принять только новую коллекцию?
  • Стоит ли просто объединить два списка и сохранить оба в базе данных?

На самом деле, EF не может решить за вас. Если вы хотите продолжать использовать AutoMapper, вы можете настроить отображение следующим образом:

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

Конфигурация Ignore() используемая для Cities заставляет AutoMapper просто сохранять исходную ссылку на прокси, EntityFramework.

Затем мы просто используем AfterMap() чтобы вызвать действие, выполняющее то, что вы думаете:

  • Для новых городов мы сопоставляем DTO и Entity (AutoMapper создает новый экземпляр) и добавляем его в коллекцию стран.
  • Для существующих городов мы используем перегрузку Map которой мы передаем существующую сущность в качестве второго параметра, а прокси города - в качестве первого параметра, поэтому automapper просто обновляет свойства существующей сущности.

Тогда вы можете сохранить свой оригинальный код:

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

Ответ 2

при сохранении изменений все города считаются добавленными, потому что EF теперь не о них до экономии времени. Таким образом, EF пытается установить нуль в внешний ключ старого города и вставить его вместо обновления.

используя ChangeTracker.Entries(), вы узнаете, какие изменения CRUD будут сделаны EF.

Если вы хотите просто обновить существующий город вручную, вы можете просто сделать:

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

context.SaveChanges();

Ответ 3

Кажется, я нашел решение:

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

этот код обновляет отредактированные элементы и добавляет новые. Но, возможно, есть некоторые подводные камни, которые я пока не могу обнаружить?

Ответ 4

Очень хорошее решение Alisson. Вот мое решение... Поскольку мы знаем, что EF не знает, будет ли запрос обновляться или вставляться, то что бы я сделал, сначала удалите метод RemoveRange() и отправьте сборку, чтобы вставить ее снова. В фоновом режиме это то, как работает база данных, мы можем эмулировать это поведение вручную.

Вот код:

//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 */

Ответ 5

Это не само по себе ответ на OP, но любой, кто смотрит на подобную проблему сегодня, должен рассмотреть возможность использования AutoMapper.Collection. Он обеспечивает поддержку этих проблем родительско-дочерних коллекций, которые раньше требовали много кода для обработки.

Я прошу прощения за то, что не включил хорошее решение или более подробную информацию, но сейчас я только ускоряюсь. В README.md есть отличный простой пример, показанный по ссылке выше.

Использование этого требует некоторой переписки, но это существенно сокращает объем кода, который вы должны написать, особенно если вы используете EF и можете использовать AutoMapper.Collection.EntityFramework.