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

Saving AutoMapper mapped Collections of Entities using Entity Framework

我有以下 Entity Framework 个实体:

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 转换为实体,这样我就可以保存一个新区域。这是我的映射代码:

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

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

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

IMO 更安全、更简单的方法是从 DbContext 加载 Region 实体,然后 add/remove 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);
        }
    }
}

更新

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

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

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

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

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

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