EF和Automapper.更新嵌套集合

Akm*_*hov 13 c# entity-framework-6 automapper-5

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

只是简单的enitities和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; }
}
Run Code Online (Sandbox Code Playgroud)

代码本身(为了简单起见,在控制台应用程序中测试):

        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();
        }
Run Code Online (Sandbox Code Playgroud)

此代码抛出异常:

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

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

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

Ali*_*son 27

问题是country你从数据库检索已经有一些城市.当您使用AutoMapper时,如下所示:

// mapping 
        AutoMapper.Mapper.Map(countryDTO, country);
Run Code Online (Sandbox Code Playgroud)

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));
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Ignore()用于的配置Cities使AutoMapper只保留原始代理引用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();
    }
Run Code Online (Sandbox Code Playgroud)


pba*_*nis 9

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

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

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

  • 如果您使用 .NET Core,请使用:https://github.com/AutoMapper/AutoMapper.Collection.EFCore (4认同)