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

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

Просто простые единицы и 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. Когда в отношение вносится изменение, для соответствующего свойства внешнего ключа устанавливается нулевое значение. Если внешний ключ не поддерживает нулевые значения, необходимо определить новое отношение, свойству внешнего ключа должно быть назначено другое ненулевое значение или несвязанный объект должен быть удален.

хотя сущность после последнего сопоставления выглядит просто отлично и правильно отражает все изменения.

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

6 ответов

Решение

Проблема заключается в 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((d,e) => AddOrUpdateCities(d, e)
            );


    }

    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 где мы передаем существующую сущность в качестве второго параметра, а прокси-сервер города - в качестве первого параметра, так что AutoMpper просто обновляет свойства существующей сущности.

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

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

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

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

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

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

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

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

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

context.SaveChanges();

Очень хорошее решение Алиссона. Вот мое решение... Как мы знаем, 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 */

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

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

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

Я потратил некоторое время на поиск лучшего решения для AutoMapper 11+, поскольку в настоящее время нет решения для EF Core и сопоставления коллекций отношений без использования AfterMap(). Это не так эффективно, как могло бы быть (требуется множественное перечисление), но избавляет от большого количества шаблонов при отображении множества дочерних отношений и поддерживает условия, если исходная и конечная коллекции имеют разный порядок:

      // AutoMapper Profile
public class MyProfile : Profile
{
  protected override void Configure()
  {
    Mapper.CreateMap<CountryData, Country>()
      .ForMember(d => d.Id, opt => opt.MapFrom(x => x.Id))
      // relationship collections must be ignored, CountryDataMappingAction will take care of it
      .ForMember(d => d.Cities, opt => opt.Ignore())
      .AfterMap<CountryDataMappingAction>();
  }

  public class CountryDataMappingAction : BaseCollectionMapperAction<CountryData, Country>
  {
    public override void Process(CountryData source, Country destination, ResolutionContext context)
    {
      MapCollection(source.Cities, destination.Cities, (x, y) => x.Id == y.Id, context);
    }
  }
}
      public class BaseCollectionMapperAction<TSource, TDestination> : IMappingAction<TSource, TDestination>
{
    public void MapCollection<TCollectionSource, TCollectionDestination>(IEnumerable<TCollectionSource> sourceCollection, IEnumerable<TCollectionDestination> destCollection, Func<TCollectionSource, TCollectionDestination, bool> predicate, ResolutionContext context)
    {
        MapCollection(sourceCollection.ToList(), destCollection.ToList(), predicate, context);
    }

    public void MapCollection<TCollectionSource, TCollectionDestination>(IList<TCollectionSource> sourceList, IList<TCollectionDestination> destList, Func<TCollectionSource, TCollectionDestination, bool> predicate, ResolutionContext context)
    {
        for (var sourceIndex = 0; sourceIndex < sourceList.Count; sourceIndex++)
        {
            for (var destIndex = 0; sourceIndex < destList.Count; destIndex++)
            {
                var result = predicate(sourceList[sourceIndex], destList[destIndex]);
                if (result)
                {
                    destList[destIndex] = context.Mapper.Map(sourceList[sourceIndex], destList[destIndex]);
                    break;
                }
            }
        }
    }

    public virtual void Process(TSource source, TDestination destination, ResolutionContext context)
    {
        throw new NotImplementedException("You must provide a mapping implementation!");
    }
}
Другие вопросы по тегам