简体   繁体   English

EF 和自动映射器。 更新嵌套集合

[英]EF & Automapper. Update nested collections

I trying to update nested collection (Cities) of Country entity.我试图更新国家实体的嵌套集合(城市)。

Just simple enitities and dto's:只是简单的实体和 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; }
}

And code itself (tested in console app for the sake of simplicity):和代码本身(为了简单起见在控制台应用程序中测试):

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

This code throws exception:此代码引发异常:

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable.操作失败:无法更改关系,因为一个或多个外键属性不可为空。 When a change is made to a relationship, the related foreign-key property is set to a null value.当对关系进行更改时,相关的外键属性将设置为空值。 If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。

even though entity after last mapping seems just fine and reflects all changes properly.即使上次映射后的实体看起来很好并且正确反映了所有更改。

I've spent much time finding solution but got no result.我花了很多时间寻找解决方案,但没有结果。 Please help.请帮忙。

The problem is the country you are retrieving from database already has some cities.问题是您从数据库中检索的country已经有一些城市。 When you use AutoMapper like this:当您像这样使用 AutoMapper 时:

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

AutoMapper is doing something like creating an IColletion<City> correctly (with one city in your example), and assigning this brand new collection to your country.Cities property. AutoMapper 正在执行类似正确创建IColletion<City> (在您的示例中使用一个城市),并将这个全新的集合分配给您的country.Cities属性。

The problem is EntityFramework doesn't know what to do with the old collection of cities.问题是 EntityFramework 不知道如何处理旧的城市集合。

  • Should it remove your old cities and assume only the new collection?它是否应该删除您的旧城市并仅假设新系列?
  • Should it just merge the two lists and keep both in database?它应该合并两个列表并将它们保存在数据库中吗?

In fact, EF cannot decide for you.事实上,英孚无法为您做决定。 If you want to keep using AutoMapper, you can customize your mapping like this:如果你想继续使用 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));
            }
        }
    }
}

The Ignore() configuration used for Cities makes AutoMapper just keep the original proxy reference built by EntityFramework .用于CitiesIgnore()配置使 AutoMapper 只保留由EntityFramework构建的原始代理引用。

Then we just use AfterMap() to invoke an action doing exactly what you thought:然后我们只需使用AfterMap()来调用一个完全按照您的想法执行的操作:

  • For new cities, we map from DTO to Entity (AutoMapper creates a new instance) and add it to country's collection.对于新城市,我们从DTO映射到实体(AutoMapper 创建一个新实例)并将其添加到国家/地区的集合中。
  • For existing cities, we use an overload of Map where we pass the existing entity as the second parameter, and the city proxy as first parameter, so AutoMapper just updates the existing entity's properties.对于现有城市,我们使用Map的重载,其中我们将现有实体作为第二个参数传递,城市代理作为第一个参数,因此 AutoMapper 只更新现有实体的属性。

Then you can keep your original code:然后你可以保留你的原始代码:

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

This is not an answer per se to the OP, but anyone looking at a similar problem today should consider using AutoMapper.Collection .这本身并不是对 OP 的回答,但是今天任何看到类似问题的人都应该考虑使用AutoMapper.Collection It provides support for these parent-child collection issues that used to require a lot of code to handle.它为这些过去需要大量代码来处理的父子集合问题提供支持。

I apologize for not including a good solution or more detail, but I am only coming up to speed on it now.我很抱歉没有提供一个好的解决方案或更多的细节,但我现在只是加快速度。 There is an excellent simple example right in the README.md displayed on the link above.上面链接中显示的 README.md 中有一个很好的简单示例。

Using this requires a bit of a rewrite, but it drastically cuts down on the amount of code you have to write, especially if you're using EF and can make use of AutoMapper.Collection.EntityFramework .使用它需要一些重写,但它大大减少了您必须编写的代码量,特别是如果您使用 EF 并且可以使用AutoMapper.Collection.EntityFramework

when save changes all cities are considered as added becasue EF didn't now about them till saving time.当保存更改时,所有城市都被视为已添加,因为 EF 现在直到保存时间才了解它们。 So EF tries to set null to foreign key of old city and insert it instead of update.所以EF尝试将老城区的外键设置为null并插入它而不是更新。

using ChangeTracker.Entries() you will find out what changes CRUD is going to be made by EF.使用ChangeTracker.Entries()您将了解 EF 将对 CRUD 进行哪些更改。

If you want just update existing city manually, you can simply do :如果您只想手动更新现有城市,您只需执行以下操作:

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

context.SaveChanges();

It seems like I found solution:似乎我找到了解决方案:

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

this code updates edited items and add new ones.此代码更新已编辑的项目并添加新项目。 But maybe there are some pitfalls I cant detect for now?但也许有一些我现在无法检测到的陷阱?

Very Good solution of Alisson. Alisson 非常好的解决方案。 Here is my solution... As We know EF does not know whether the request is for update or insert so what I would do is delete first with RemoveRange() method and send the collection to insert it again.这是我的解决方案......正如我们所知,EF 不知道请求是更新还是插入,所以我要做的是先使用 RemoveRange() 方法删除,然后发送集合以再次插入。 In background this is how database works then we can emulate this behavior manually.在后台这就是数据库的工作方式,然后我们可以手动模拟这种行为。

Here is the code:这是代码:

//country object from request for example

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM