Reputation: 248
I'm having a hard time trying to figure it out how to update a collection.
This is my model
public class ProgramacionSemanal
{
public int ProgramacionSemanalId { get; set; }
public int Semana { get; set; }
public DateTime FechaInicio { get; set; }
public DateTime FechaFin { get; set; }
public ICollection<ProgramacionSemanalDetalle> ProgramacionSemanalDetalles { get; set; }
}
public class ProgramacionSemanalDetalle
{
[Key]
public int ProgramacionSemanalDetalleId { get; set; }
public int ProgramacionSemanalId { get; set; }
[Column("ClienteId")]
public Entidad Cliente { get; set; }
public int UbicacionId { get; set; }
public Ubicacion Ubicacion { get; set; }
public int ProductoId { get; set; }
public Producto Producto { get; set; }
public int Lunes { get; set; }
public int Martes { get; set; }
public int Miercoles { get; set; }
public int Jueves { get; set; }
public int Viernes { get; set; }
public int Sabado { get; set; }
public int Domingo { get; set; }
public AppUsuario Usuario { get; set; }
}
This is a route in my api.
[HttpPut("{programacionsemanalId}")]
public async Task<IActionResult> Put(int programacionSemanalId, [FromBody] ProgramacionSemanalViewModel model)
{
try
{
var oldProgramacionSemanal = await _repository.GetProgramacionSemanalAsync(programacionSemanalId);
if (oldProgramacionSemanal == null) return NotFound($"No se encontro la Programación Semanal.");
_mapper.Map(model, oldProgramacionSemanal);
var currentUser = await _userManager.FindByNameAsync(User.Identity.Name);
foreach (var item in oldProgramacionSemanal.ProgramacionSemanalDetalles)
{
var producto = await _repository.GetProductoAsync(item.ProductoId);
if (producto == null) return BadRequest();
item.Producto = producto;
var ubicacion = await _repository.GetUbicacionAsync(item.UbicacionId);
if (ubicacion == null) return BadRequest();
item.Ubicacion = ubicacion;
item.Usuario = currentUser;
}
if (await _repository.SaveAllAsync())
{
return Ok(_mapper.Map<ProgramacionSemanalViewModel>(oldProgramacionSemanal));
}
}
catch (Exception ex)
{
_logger.LogError($"Threw exception while updating: {ex}");
}
return BadRequest("Couldn't update");
}
I think everything works right,
1) I received all the info defined in the model object.
2) I get all the info from db in object oldProgramacionSemanal
3) After calling _mapper.map, object info oldProgramacionSemanal is modified the relations values are set to null, but Cliente
4) I go through the collection objects and update the relation value
5) When it try to save the object, I get this error
{System.InvalidOperationException: The instance of entity type 'ProgramacionSemanalDetalle' cannot be tracked because another instance with the key value '{ProgramacionSemanalDetalleId: 38}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap
1.ThrowIdentityConflict(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap
1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Nullable1 forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode node, Boolean force) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode node, TState state, Func
3 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState entityState, Boolean forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.NavigationCollectionChanged(InternalEntityEntry entry, INavigation navigation, IEnumerable1 added, IEnumerable
1 removed) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationCollectionChanged(InternalEntityEntry entry, INavigation navigation, IEnumerable1 added, IEnumerable
1 removed) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavigationChange(InternalEntityEntry entry, INavigation navigation) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectChanges(IStateManager stateManager) at Microsoft.EntityFrameworkCore.ChangeTracking.ChangeTracker.DetectChanges() at Microsoft.EntityFrameworkCore.DbContext.TryDetectChanges() at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken) at GasApp.Data.GasRepository.SaveAllAsync() in C:\gleintech\GasApp\GasApp.Data\GasRepository.cs:line 37 at GasApp.Controllers.ProgramacionSemanalController.Put(Int32 programacionSemanalId, ProgramacionSemanalViewModel model) in C:\gleintech\GasApp\GasApp\Controllers\ProgramacionSemanalController.cs:line 171}
Does anyone can give me any advice on what I am doing wrong?
Alberto
Upvotes: 1
Views: 2285
Reputation: 542
Your AutoMapper configuration is important here. I guess your problem are the navigation properties in your entity type and model type. If there are collections, then AutoMapper does not update the existing items by key; instead it just clears the target collection and fills new items to it. That is what EF Core does not like, even now. You have to use the packages AutoMapper.Collection and AutoMapper.Collection.EntityFrameworkCore. Then you have to add the following to your mapper configuration, e.g.
using AutoMapper;
using AutoMapper.EquivalencyExpression;
...
var config = new MapperConfiguration(
cfg =>
{
cfg.AddProfile<AutoMapperProfile>();
cfg.AddCollectionMappers();
// And AutoMapper's source for key information:
cfg.UseEntityFrameworkCoreModel<MyDbContext>();
});
var mapper = config.CreateMapper();
such that AutoMapper starts comparing the items on both sides and can update items instead of remove/insert. And by UseEntityFrameworkCoreModel AutoMapper is told how to compare the items. Instead of UseEntityFrameworkCoreModel you could tell AutoMapper how to compare the items by declaring an equality comparison in the mappings, e.g.
using AutoMapper;
using AutoMapper.EquivalencyExpression;
...
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<CustomerModel, CustomerDto>()
.EqualityComparison((model, dto) => model.Primkey == dto.Primkey)
.ReverseMap();
}
}
Anyway, you would still need AddCollectionMappers.
And we have just come up with a solution to the tedious mentioning of the DbContexts in the AutoMapper configuration (or the EqualityComparisons). If your EF Core entity classes use the Key attribute on the key properties, then AutoMapper can be told to use that information instead of asking the DbContext instances. I might post the code for that later.
Upvotes: 3
Reputation: 2439
The error you are getting is to do with a issue in EF Core 2.0 - see https://github.com/aspnet/EntityFrameworkCore/issues/7340 . This occurs if you try to try to replace an entity class with a new entity class with the same primary key.
This happens in your code because you altered the producto/ubicacion navigation properties, which tells EF to update them. EF does this by (trying) to delete the existing entity pointed to and replacing it with the ones you just loaded. The limitation in EF Core 2.0 then throws the error you found.
I'm not totally sure what you are trying to do because I don't know what your repository is doing - is it reading the collection in, are all the entities tracked, are you including the producto/ubicacion? Therefore I can't give you a definitive answer, but at least you now know why you are getting the error.
Note: that the #7340 issue (which I reported) is fixed in 2.1 so this would work, but its inefficient beacause you are deleting/adding a relationship with no or little change. If there is no change to producto/ubicacion then you might consider using the IsModified method to tell EF that they haven't changed and it will keep the original data, e.g.
context.Entry(entity).Navigation("PropertyName").IsModified = false;
I trust that helps.
Upvotes: 2