简体   繁体   中英

Saving AutoMapper mapped Collections of Entities using 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 these using AutoMapper 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; }
}

I want to translate my ViewModels to Entities using AutoMapper so I can save a new Region. This is my mapping code:

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

This causes an exception when adding the region in the repository as it also tries to create a new instance of Country with a null Name. One solution is to change the Add method in my repository to set the State of the country objects 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 alternative solution is to use more complicated translation logic which uses another repository to get the real country objects. This approach has slower performance because it has to make an extra call to the database but you 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 to the first one but what is the correct approach?

The first method, together with the loop to set the states to UnChanged , is definitely the best one. It is lightweight, because you don't needlessly fetch Country s from the database. Instead, by the mapper part...

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

...you create stub entities , ie incomplete entities that serve as a placeholders for the real things. That's a commonly recommended approach to reduce network traffic .

Setting the states to UnChanged is one of several ways to attach the stub Country s to the context. You have to attach them before calling base.Add(region) (which I assume adds the region to the Regions of the context), because Add marks all entities in an object graph off the added entity as new ( Added ) when they're not yet attached to the context.

Well, I think attaching a graph of entities to the DbContext is not the correct approach, because it forces you to write a lot of code to fix entity states to prevent EF from duplicating your entities.

A safer and simpler approach IMO is to load your Region entity from the DbContext, then add/remove Country entities from the Countries collection, then call SaveChanges.

You can write a generic collection mapping method, something like (not tested):

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 the Countries collection contained editable Country view-models. But actually it contains the IDs of the countries. In that case you would 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();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM