使用Entity Framework保存AutoMapper映射的实体集合

时间:2015-03-18 13:01:53

标签: c# .net entity-framework entity-framework-6 automapper

我有以下实体框架实体:

public class Region
{
    public int RegionId { get; set; } // Primary Key
    public string Name { get; set; }
    public virtual ICollection<Country> Countries { get; set; } // Link Table
}
public class Country
{
    public int CountryId { get; set; } // Primary Key
    public string Name { get; set; }
    public int RegionId { get; set; } // Foreign Key
}

我使用AutoMapper将这些映射到以下ViewModel:

public class RegionViewModel
{
    public int RegionId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<int> Countries { get; set; }
}
public class CountryViewModel
{
    public int CountryId { get; set; }
    public string Name { get; set; }
}

我想使用AutoMapper将我的ViewModel转换为实体,这样我就可以保存一个新的Region。这是我的映射代码:

Mapper.CreateMap<RegionViewModel, Region>()
    .ForMember(x => x.Countries, x => x.MapFrom(y => y.Countries.Select(z => new Country() { CountryId = z }).ToArray()));

这会在存储库中添加区域时导致异常,因为它还尝试创建具有空名称的Country的新实例。一种解决方案是更改存储库中的Add方法,将国家/地区对象的状态设置为“未更改”。

public async Task Add(Region region)
{
    foreach (Country country in region.Countries)
    {
        this.Context.Entry(country).State = EntityState.Unchanged;
    }
    await base.Add(region);
}

另一种替代解决方案是使用更复杂的翻译逻辑,该逻辑使用另一个存储库来获取真实的国家/地区对象。这种方法的性能较慢,因为它必须对数据库进行额外的调用,但是你也可以获得更完整的Region对象。

Mapper.CreateMap<RegionViewModel, Region>();
Mapper.CreateMap<int[], Country[]>().ConvertUsing(x => countryRepository.GetAll().Result.Where(y => x.Contains(y.CountryId)).ToArray());

我倾向于第一个,但正确的方法是什么?

2 个答案:

答案 0 :(得分:4)

第一种方法与将状态设置为UnChanged的循环一起绝对是最好的方法。它是轻量级的,因为您不必从数据库中不必要地获取Country。相反,由映射器部分......

y.Countries.Select(z => new Country() { CountryId = z })

...您创建存根实体,即用作真实事物占位符的不完整实体。这通常是recommended approach to reduce network traffic

将状态设置为UnChanged是将存根Country附加到上下文的几种方法之一。您必须在调用base.Add(region)之前附加它们(我假设将该区域添加到上下文的Regions),因为Add将添加的实体中的对象图中的所有实体标记为新的(Added)当他们尚未附加到上下文时。

答案 1 :(得分:1)

好吧,我认为将实体图附加到DbContext不是正确的方法,因为它会强制您编写大量代码来修复实体状态,以防止EF复制您的实体。

更安全,更简单的方法IMO是从DbContext加载Region实体,然后从Countries集合中添加/删除Country实体,然后调用SaveChanges。

你可以写一个通用的集合映射方法,比如(未经测试):

static class EfUtils
{
    public static void SyncCollections<TEntity>(
        ICollection<TEntity> collectionFromDb,
        IEnumerable<TEntity> collectionFromVm,
        IEqualityComparer<TEntity> equalityComparer,
        Action<TEntity, TEntity> syncAction)
        where TEntity : class, new()
    {
        var dbToVmEntitiesMap = new Dictionary<TEntity, TEntity>();
        var newEntities = new List<TEntity>();

        foreach (var vmEntity in collectionFromVm)
        {
            var dbEntity = collectionFromDb.FirstOrDefault(x => equalityComparer.Equals(x, vmEntity));
            if (dbEntity == null)
            {
                dbEntity = new TEntity();
                newEntities.Add(dbEntity);
            }

            dbToVmEntitiesMap.Add(dbEntity, vmEntity);
        }

        var removedEntities = collectionFromDb.Where(x => !dbToVmEntitiesMap.ContainsKey(x)).ToList();

        foreach (var addedOrUpdatedEntityPair in dbToVmEntitiesMap)
        {
            syncAction(addedOrUpdatedEntityPair.Key, addedOrUpdatedEntityPair.Value);
        }

        foreach (var removedEntity in removedEntities)
        {
            collectionFromDb.Remove(removedEntity);
        }

        foreach (var newEntity in newEntities)
        {
            collectionFromDb.Add(newEntity);
        }
    }
}

<强>更新

我假设Countries集合包含可编辑的Country视图模型。 但实际上它包含国家的ID。 在这种情况下,您需要应用相同的添加/删除模式:

var regionFromDb = dbContext.Set<Region>().Find(regionVm.RegionId);
var countriesToRemove = regionFromDb.Countries.Where(x => !regionVm.Countries.Contains(x.CountryId)).ToList();
foreach (var country in countriesToRemove)
{
    regionFromDb.Countries.Remove(country);
}

var countryIdsToAdd = regionVm.Countries.Where(x => !regionFromDb.Countries.Any(c => c.CountryId == x)).ToList();

// Load countries where CountryId in countryIdsToAdd collection
var countriesToAdd = dbContext.Set<Country>().Where(x => countryIdsToAdd.Contains(x.CountryId));
foreach (var country in countriesToAdd)
{
    regionFromDb.Countries.Add(country);
}

dbContext.SaveChanges();