Saving AutoMapper Mappings Object Collections Using the Entity Framework

I have the following Entity Framework Entities:

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 } 

I map them using AutoMapper for the following ViewModels:

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

I want to transfer my ViewModels to objects using AutoMapper to save a new region. This is my display code:

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

This throws an exception when adding a region to the repository, as it also tries to create a new instance of the country with a null name. One solution is to change the Add method in my repository to set the state of country objects in Unchanged.

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

Another alternative solution is to use a more complex translation logic, which uses a different repository to get objects from a real country. This approach has lower performance because it should make an extra call to the database, but you will also get a more complete Region object.

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

I lean toward the first, but what's the right approach?

+5
source share
2 answers

The first method, along with a loop for setting UnChanged states, is definitely the best. This is light weight because you do not need to select Country from the database. Instead, using the display part ...

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

... you create stub objects, that is, incomplete objects that serve as placeholders for real things. This is usually the recommended approach to reduce network traffic .

Setting up states on UnChanged is one of several ways to attach a Country stub to a context. You must attach them before calling base.Add(region) (which, I believe, adds a region to the Regions context), because Add places all the objects in the graph of objects with the added entity as new ( Added ) when they are not yet attached to context.

+3
source

Well, I think that linking an entity graph to a DbContext is not the right approach, because it forces you to write a lot of code to fix entity states to prevent duplication of your EF objects.

A safer and simpler IMO approach is to load a Region object from DbContext, then add / remove country entities from the collection of countries, and then call SaveChanges.

You can write a generic collection matching method, something like (not verified):

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

UPDATE

I suggested that the country compilation contains editable country models. But actually it contains country identifiers. In this case, you will need to apply the same add / remove pattern:

 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(); 
+1
source

Source: https://habr.com/ru/post/1215596/


All Articles