InvalidOperationException inesperado ao tentar alterar o relacionamento via valor padrão da propriedade

10

No código de exemplo abaixo, recebo a seguinte exceção ao fazer db.Entry(a).Collection(x => x.S).IsModified = true:

System.InvalidOperationException: 'A instância do tipo de entidade' B 'não pode ser rastreada porque outra instância com o valor da chave' {Id: 0} 'já está sendo rastreada. Ao anexar entidades existentes, verifique se apenas uma instância da entidade com um determinado valor de chave está anexada.

Por que não adiciona, em vez de anexar, as instâncias de B?

Estranhamente, a documentação para IsModifiednão especifica InvalidOperationExceptioncomo uma possível exceção. Documentação inválida ou um bug?

Eu sei que esse código é estranho, mas eu o escrevi apenas para entender como o ef core funciona em alguns casos estranhos de egde. O que eu quero é uma explicação, não uma solução alternativa.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    public class A
    {
        public int Id { get; set; }
        public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };
    }

    public class B
    {
        public int Id { get; set; }
    }

    public class Db : DbContext {
        private const string connectionString = @"Server=(localdb)\mssqllocaldb;Database=Apa;Trusted_Connection=True";

        protected override void OnConfiguring(DbContextOptionsBuilder o)
        {
            o.UseSqlServer(connectionString);
            o.EnableSensitiveDataLogging();
        }

        protected override void OnModelCreating(ModelBuilder m)
        {
            m.Entity<A>();
            m.Entity<B>();
        }
    }

    static void Main(string[] args)
    {
        using (var db = new Db()) {
            db.Database.EnsureDeleted();
            db.Database.EnsureCreated();

            db.Add(new A { });
            db.SaveChanges();
        }

        using (var db = new Db()) {
            var a = db.Set<A>().Single();
            db.Entry(a).Collection(x => x.S).IsModified = true;
            db.SaveChanges();
        }
    }
}
Supremo
fonte
Como estão relacionados A e B? significando qual é a propriedade de relacionamento?
sam

Respostas:

8

A razão do erro no código fornecido é a seguir.

Quando você cria uma entidade Ado banco de dados, sua propriedade Sé inicializada com uma coleção que contém dois novos registros B. Idde cada uma dessas novas Bentidades é igual a 0.

// This line of code reads entity from the database
// and creates new instance of object A from it.
var a = db.Set<A>().Single();

// When new entity A is created its field S initialized
// by a collection that contains two new instances of entity B.
// Property Id of each of these two B entities is equal to 0.
public ICollection<B> S { get; set; } = new List<B>() { new B {}, new B {} };

Após a execução da linha de var a = db.Set<A>().Single()coleção de código Sda entidade, Aela não contém Bentidades do banco de dados, porque DbContext Dbnão usa carregamento lento e não há carregamento explícito da coleção S. A entidade Acontém apenas novas Bentidades que foram criadas durante a inicialização da coleção S.

Quando você chama IsModifed = truea Sestrutura da entidade de coleção , tenta adicionar esses dois novos serviços Bao controle de alterações. Mas falha porque ambas as novas Bentidades têm o mesmo Id = 0:

// This line tries to add to change tracking two new B entities with the same Id = 0.
// As a result it fails.
db.Entry(a).Collection(x => x.S).IsModified = true;

Você pode ver no rastreamento da pilha que a estrutura da entidade tenta adicionar Bentidades ao IdentityMap:

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.IdentityMap`1.Add(TKey key, InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetPropertyModified(IProperty property, Boolean changeState, Boolean isModified, Boolean isConceptualNull, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(InternalEntityEntry internalEntityEntry, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.SetFkPropertiesModified(Object relatedEntity, Boolean modified)
at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.set_IsModified(Boolean value)

E a mensagem de erro também informa que ele não pode rastrear a Bentidade Id = 0porque outra Bentidade com o mesmo Idjá está rastreada.


Como resolver este problema.

Para resolver esse problema, você deve excluir o código que cria Bentidades ao inicializar a Scoleção:

public ICollection<B> S { get; set; } = new List<B>();

Em vez disso, você deve preencher a Scoleção no local em que Aé criada. Por exemplo:

db.Add(new A {S = {new B(), new B()}});

Se você não usar o carregamento lento, deverá carregar explicitamente a Scoleção para adicionar seus itens ao rastreamento de alterações:

// Use eager loading, for example.
A a = db.Set<A>().Include(x => x.S).Single();
db.Entry(a).Collection(x => x.S).IsModified = true;

Por que não adiciona, em vez de anexar, as instâncias de B?

Em suma , eles são anexados a serem adicionados porque têm Detachedestado.

Depois de executar a linha de código

var a = db.Set<A>().Single();

instâncias criadas da entidade Btêm estado Detached. Pode ser verificado usando o próximo código:

Console.WriteLine(db.Entry(a.S[0]).State);
Console.WriteLine(db.Entry(a.S[1]).State);

Então, quando você define

db.Entry(a).Collection(x => x.S).IsModified = true;

O EF tenta adicionar Bentidades para alterar o rastreamento. No código-fonte do EFCore, você pode ver que isso nos leva ao método InternalEntityEntry.SetPropertyModified com os próximos valores de argumento:

  • property- uma das nossas Bentidades,
  • changeState = true,
  • isModified = true,
  • isConceptualNull = false,
  • acceptChanges = true.

Esse método com esses argumentos altera o estado dos Detached Batributos para Modifiede tenta iniciar o rastreamento deles (consulte as linhas 490 - 506). Como as Bentidades agora têm estado, Modifiedisso as leva a serem anexadas (não adicionadas).

Iliar Turdushev
fonte
Onde está a resposta para "Por que não adiciona em vez de anexar as instâncias de B?" Você está dizendo "falha porque as duas novas entidades B têm o mesmo Id = 0". Eu acho que está errado, porque ef core salva ambos com 1 e 2 IDs. Eu não acho que é a resposta correta para esta pergunta
DIlshod K
@DIlshod K Obrigado pelo comentário. Na seção "Como resolver esse problema", escrevi que a coleção Sdeve ser carregada explicitamente, porque o código fornecido não usa carregamento lento. Obviamente, a EF salvou Bentidades criadas anteriormente no banco de dados. Mas a linha de código A a = db.Set<A>().Single()carrega apenas entidades Asem entidades na coleção S. Para carregar a coleção, Sdeve-se usar um carregamento rápido. Vou mudar meu asnwer para incluir explicitamente a resposta à pergunta "Por que não adiciona em vez de anexar as instâncias de B?".
Iliar Turdushev 11/03