TLDR: добавление нового неотслеживаемого типа сущности во вложенную коллекцию существующего DTO => существующий DTO сопоставляется с существующим отслеживаемым типом сущности => EF Core tracker теперь видит состояние новой сущности как измененное, а не ошибка Added =>, там обновлять нечего.
Я столкнулся с проблемами с AutoMapper и Entity Framework Core после того, как мы обновили эти пакеты:
- . NET Core 3.1
- AutoMapper 9.0.0
- EF Core 3.1.3
Существует простой сценарий:
У нас есть DTO D, представляющий сущность E. Оба DTO и entity имеют коллекцию дочерних объектов типа R, которые также являются EF-сущностями (не самый лучший дизайн, но держите его так, пожалуйста).
- Я создаю новый DTO D (с пустым коллекция), сопоставьте его с сущностью E и сохраните в базе данных. ОК
- Я загружаю E из базы данных, сопоставляю его с DTO D (E теперь отслеживается в EF Core). OK
- Я создаю новый объект R и добавляю его в коллекцию DTO D. OK
- Я сопоставляю DTO D с существующим объектом E, который теперь отслеживается ядром EF
- Теперь в EF-трекере есть элемент для объекта R, который говорит, что R изменено , но определенно должно быть добавлено .
Существует минимальный рабочий пример такого поведения:
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using System;
using System.Collections.Generic;
using System.Linq;
namespace AutomapperEFTest
{
class TestContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string databaseName = "TestDB";
optionsBuilder
.UseInMemoryDatabase(databaseName: databaseName)
.ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<TestEntity>(e =>
{
e.ToTable("TestEntity");
e.HasKey(t => t.Id);
e.Property(t => t.SomeProperty);
e.HasMany(t => t.RefValues)
.WithOne()
.HasForeignKey(r => r.EntityId);
});
modelBuilder.Entity<RefValue>(e =>
{
e.ToTable("RefValue");
e.HasKey(r => r.Id);
e.Property(r => r.EntityId)
.HasColumnName("EntityId");
e.Property(r => r.Value)
.HasColumnName("Value");
});
}
public DbSet<TestEntity> Entities => Set<TestEntity>();
public DbSet<RefValue> RefValues => Set<RefValue>();
}
class TestEntity
{
public Guid Id { get; set; }
public string SomeProperty { get; set; }
public ICollection<RefValue> RefValues { get; set; }
}
class TestDto
{
public TestDto()
{
RefValues = new List<RefValue>();
}
public Guid Id { get; set; }
public string SomeProperty { get; set; }
public List<RefValue> RefValues { get; set; }
}
class RefValue
{
public Guid Id { get; set; }
public Guid EntityId { get; set; }
public string Value { get; set; }
}
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<TestDto, TestEntity>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.SomeProperty, o => o.MapFrom(s => s.SomeProperty))
.ForMember(d => d.RefValues, o => o.MapFrom(s => s.RefValues))
.ReverseMap();
});
using (var context = new TestContext())
{
var mapper = new Mapper(config);
#region Works
/*
* Adding both parent and child in one step works.
var newDto = new TestDto() { Id = Guid.Parse("4C1596D7-C080-4E56-B2F6-1BAEBDC1CF7D"), SomeProperty = "Whatever" };
var newRef = new RefValue() { Id = Guid.Parse("526DC1A6-130B-41DC-9F74-32F9C807D7F5"), EntityId = Guid.Parse("4C1596D7-C080-4E56-B2F6-1BAEBDC1CF7D"), Value = "New Ref Value" };
newDto.RefValues.Add(newRef);
var entityToSave = mapper.Map<TestEntity>(newDto);
context.Add(entityToSave);
context.SaveChanges();
*/
#endregion
#region Data preparation
var newDto = new TestDto(){Id = Guid.Parse("4C1596D7-C080-4E56-B2F6-1BAEBDC1CF7D"),SomeProperty = "Whatever"};
var entity = mapper.Map<TestEntity>(newDto);
context.Entities.Add(entity);
context.SaveChanges();
#endregion
//Simple scenario - add a new item to a collection of an existing item
var loadedEntity = context.Entities.FirstOrDefault();
var loadedDto = mapper.Map<TestDto>(loadedEntity);
var newRef = new RefValue(){Id = Guid.Parse("526DC1A6-130B-41DC-9F74-32F9C807D7F5"),EntityId = Guid.Parse("4C1596D7-C080-4E56-B2F6-1BAEBDC1CF7D"),Value = "New Ref Value"};
loadedDto.RefValues.Add(newRef);
//ensure it will be mapped to existing tracked item
var entityToSave = mapper.Map(loadedDto, loadedEntity, typeof(TestDto), typeof(TestEntity));
//Now the inner state of the newRef in the tracker is Modified but definitely should be Added
//check it out: context.ChangeTracker.Entries<RefValue>().ToArray();
//throws exception
context.SaveChanges();
}
}
}
}
Как должен быть настроен AutoMapper для предотвращения этого поведения? У нас это работало в предыдущих версиях - NET Core 2.1, EF Core 1.x, AutoMapper 8.