【问题标题】:EF & Automapper. Update nested collections英孚和自动映射器。更新嵌套集合
【发布时间】:2017-05-19 20:17:56
【问题描述】:

我正在尝试更新 Country 实体的嵌套集合(Cities)。

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

这段代码抛出异常:

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

即使上次映射后的实体看起来还不错,并且正确反映了所有更改。

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

【问题讨论】:

  • country.cities[0].Id 映射后是否有价值?如果不是,EF 会尝试将 null 设置为外键并导致问题
  • @esiprogrammer,是的。

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


【解决方案1】:

问题是您从数据库中检索的country 已经有一些城市。当您像这样使用 AutoMapper 时:

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

AutoMapper 正在做一些事情,例如正确创建 IColletion&lt;City&gt;(在您的示例中使用一个城市),并将这个全新的集合分配给您的 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));
            }
        }
    }
}

用于CitiesIgnore() 配置使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();
    }

【讨论】:

  • 我还不清楚,抱歉。如果在使用countryDTO.Cities 进行更改后,我创建newCities 就像你写的那样拥有所有城市,并将它们添加到国家/地区的foreach 循环中。城市进行复制。如果我在检索原始实体后清除城市并随后运行 foreach,我会得到相同的 The operation failed: 错误。我错过了什么?
  • @AkmalSalikhov 我编辑了我的代码,提供了另一种方式来配置自动映射器来做你想做的事。因此,您可以正常工作的代码,我封装在 automapper 中,我认为这就是您想要的。现在 EF 知道要添加什么以及要更新什么。
  • Alisson,在 automapper 配置中封装函数非常有用,谢谢!但有一件事。如果我使用 .ForMember(dest =&gt; dest.Cities, src =&gt; src.Ignore()) 而不是 UseDestinationValue,您的代码运行良好。使用 UseDestinationValue 导致相同的无法更改关系错误
  • 嗯,这有点奇怪,但有道理,因为Ignore() 使您的实体保留其原始 Cities 属性。正如你提到的,我正在编辑我的答案,谢谢。此外,您应该将一个答案设为已接受,以便未来的用户可以直接查看该答案。
  • @Alisson 缺乏 EF 核心支持的正确解决方案
【解决方案2】:

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

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

使用它需要进行一些重写,但它大大减少了您必须编写的代码量,尤其是在您使用 EF 并且可以使用 AutoMapper.Collection.EntityFramework 的情况下。

【讨论】:

【解决方案3】:

当保存更改时,所有城市都被认为是添加的,因为 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();

【讨论】:

    【解决方案4】:

    我好像找到了解决办法:

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

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

    【讨论】:

    • 为什么在相同的上下文中将 DTO 转换为实体,而不是将实体转换为 DTO?只需简单地编辑您的实体对象,然后将最终结果转换为 DTO
    • @esiprogrammer 我认为他只是简化了代码来说明他的问题。他可能会在某些 Get 方法中转换为 DTO,然后使用 post 方法将编辑/新城市作为 DTO 并转换为实体进行保存。
    • @Alisson 我不这么认为,正如他在“为了简单起见在控制台应用程序中测试”的问题中提到的那样。这是他使用的实际代码
    • @Alisson 是对的。实际上,我使用的是 MVC 5 应用程序。
    【解决方案5】:

    Alisson 的非常好的解决方案。这是我的解决方案... 正如我们所知,EF 不知道请求是更新还是插入,所以我要做的是首先使用 RemoveRange() 方法删除并发送集合以再次插入它。在后台,这就是数据库的工作方式,然后我们可以手动模拟这种行为。

    代码如下:

    //country object from request for example

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

    dbcontext.Cities.RemoveRange(cities);

    /* 现在进行映射并发送对象,这将批量插入相关的表中 */

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-09-26
      • 2020-12-12
      • 1970-01-01
      • 1970-01-01
      • 2021-08-16
      • 2015-02-07
      • 1970-01-01
      相关资源
      最近更新 更多