EF& Automapper。更新嵌套集合

时间:2017-01-05 10:22:44

标签: c# entity-framework-6 automapper-5

我尝试更新国家/地区实体的嵌套集合(城市)。

只是简单的恩赐和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();
        }

此代码抛出异常:

  

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

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

我花了很多时间寻找解决方案却没有结果。请帮忙。

5 个答案:

答案 0 :(得分:23)

问题是您从数据库中检索的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()的{​​{1}}配置使AutoMapper保留Cities构建的原始代理参考。

然后我们只使用EntityFramework来调用一个完全符合您要求的动作:

  • 对于新城市,我们从 DTO 映射到实体(AutoMapper创建一个新的 实例)并将其添加到国家/地区的集合中。
  • 对于现有城市,我们使用AfterMap()的重载,我们将现有实体作为第二个参数传递,将城市代理作为第一个参数传递,因此automapper只更新现有实体的属性。 / LI>

然后你可以保留原始代码:

Map

答案 1 :(得分: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();

答案 2 :(得分: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();

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

答案 3 :(得分: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 */

答案 4 :(得分:0)

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

很抱歉,我没有提供好的解决方案或更多细节,但我现在只是加快步伐。上面链接上显示的README.md中有一个非常简单的示例。

使用此方法需要进行一些重写,但是彻底减少了您必须编写的代码量,尤其是如果您使用的是EF并且可以使用{{1} }。