23

我试图更新国家实体的嵌套集合(城市)。

只是简单的实体和 dto:

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

此代码引发异常:

操作失败:无法更改关系,因为一个或多个外键属性不可为空。当对关系进行更改时,相关的外键属性将设置为空值。如果外键不支持空值,则必须定义新的关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。

即使上次映射后的实体看起来很好并且正确地反映了所有更改。

我花了很多时间寻找解决方案,但没有得到任何结果。请帮忙。

4

6 回答 6

43

问题是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()用于使 AutoMapper的配置Cities只保留由EntityFramework.

然后我们只是AfterMap()用来调用一个动作,完全按照你的想法去做:

  • 对于新城市,我们从DTO映射到实体(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();
    }
于 2017-01-05T10:41:08.423 回答
10

这本身不是对 OP 的答案,但是今天看到类似问题的任何人都应该考虑使用AutoMapper.Collection。它为这些过去需要大量代码来处理的父子集合问题提供了支持。

对于没有提供好的解决方案或更多细节,我深表歉意,但我现在只是加快速度。上面链接中显示的 README.md 中有一个很好的简单示例。

使用它需要进行一些重写,但它大大减少了您必须编写的代码量,特别是如果您使用的是 EF 并且可以使用AutoMapper.Collection.EntityFramework.

于 2019-06-27T16:53:16.977 回答
1

当保存更改时,所有城市都被认为是添加的,因为 EF 现在直到节省时间才知道它们。因此 EF 尝试将 null 设置为旧城市的外键并插入它而不是更新。

使用ChangeTracker.Entries()您将了解 EF 将对 CRUD 进行哪些更改。

如果您只想手动更新现有城市,您可以简单地执行以下操作:

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

context.SaveChanges();
于 2017-01-05T11:25:42.153 回答
0

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

于 2017-07-20T05:06:52.313 回答
0

好像我找到了解决方案:

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

此代码更新已编辑的项目并添加新项目。但也许有一些我现在无法发现的陷阱?

于 2017-01-05T11:51:58.943 回答
0

我花了一些时间为 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!");
    }
}
于 2022-02-11T17:01:59.423 回答