Saving AutoMapper mapped Collections of Entities using Entity Framework

.net automapper c# entity-framework entity-framework-6

Question

The following Entity Framework Entities I have are:

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 use AutoMapper to map these to 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; }
}

In order to save a new Region, I want to use AutoMapper to convert my ViewModels to Entities. I'll map using this code:

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

Due to the fact that it also tries to construct a new instance of Country with a null Name, this results in an exception when adding the area to the repository. One remedy is to alter theAdd technique to set the State of the nation objects in my repository to Unchanged.

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

The other possibility is to employ more intricate translation logic that accesses real country objects from a different repository. Due to the additional database call required, this method performs longer, but the result is a more comprehensive 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 option, but what is the best course of action?

1
6
3/23/2015 2:49:14 PM

Accepted Answer

The first approach and the loop that sets the states toUnChanged is unquestionably the greatest. It is light since you don't need to fetch anything extra.Country s from the data source. Rather, by the mapper component...

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

You produce blank entities, which are incomplete entities used to represent the real items. This is a typical advisable method to lessen network traffic.

states being set toUnChanged is one of various methods for securing the stub.Country Adapting to the situation. You must connect them before dialing.base.Add(region) (which, I believe, includes the region in theRegions Because of the context,Add marks the newly inserted item as new, marking all entities in an object graph (Added ) when they haven't yet been integrated into the context.

4
3/24/2015 8:52:06 PM

Popular Answer

I believe that the wrong strategy is to link a graph of entities to the DbContext because doing so compels you to write a lot of code to correct entity states and prevent EF from duplicating your entities.

I believe that loading your Region entity from the DbContext, adding or removing Country entities from the Countries collection, and then using SaveChanges is a safer and easier solution.

A general collection mapping approach could be written as follows (untested):

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 assumed that editable Country view-models were available in the Countries collection. However, it truly includes the country IDs. The same add/remove pattern would then need to be used:

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


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow