使用 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
的循环,绝对是最好的方法。它是轻量级的,因为您不需要从数据库中获取 Country
s。相反,由映射器部分...
y.Countries.Select(z => new Country() { CountryId = z })
...您创建 存根实体,即作为真实事物占位符的不完整实体。这是一个常见的 recommended approach to reduce network traffic.
将状态设置为 UnChanged
是将存根 Country
附加到上下文的几种方法之一。您必须在调用 base.Add(region)
之前附加它们(我假设将区域添加到上下文的 Regions
),因为 Add
将对象图中的所有实体标记为新添加的实体(Added
) 当它们尚未附加到上下文时。
我有以下 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
的循环,绝对是最好的方法。它是轻量级的,因为您不需要从数据库中获取 Country
s。相反,由映射器部分...
y.Countries.Select(z => new Country() { CountryId = z })
...您创建 存根实体,即作为真实事物占位符的不完整实体。这是一个常见的 recommended approach to reduce network traffic.
将状态设置为 UnChanged
是将存根 Country
附加到上下文的几种方法之一。您必须在调用 base.Add(region)
之前附加它们(我假设将区域添加到上下文的 Regions
),因为 Add
将对象图中的所有实体标记为新添加的实体(Added
) 当它们尚未附加到上下文时。