Controlar explicitamente as entidades

Cada instância DbContext controla as alterações feitas em entidades. Essas entidades controladas, por sua vez, conduzem as alterações ao banco de dados quando SaveChanges é chamado.

O controle de alterações do EF Core (Entity Framework Core) funciona melhor quando a mesma instância de DbContext é usada para consultar entidades e atualizá-las chamando SaveChanges. Isso ocorre porque o EF Core controla automaticamente o estado das entidades consultadas e detecta as alterações feitas nessas entidades quando SaveChanges é chamado. Essa abordagem está abrangida no Controle de Alterações no EF Core.

Dica

Esse documento pressupõe que os estados de entidade e as noções básicas do controle de alterações do EF Core sejam compreendidos. Consulte Controle de Alterações no EF Core para obter mais informações sobre esses tópicos.

Dica

Você pode executar e depurar em todo o código neste documento baixando o código de exemplo do GitHub.

Dica

Para simplificar, este documento usa e referencia métodos síncronos, como SaveChanges, em vez de seus equivalentes assíncronos, como SaveChangesAsync. É possível substituir a ação de chamar e aguardar o método assíncrono, a menos que seja indicado de outra forma.

Introdução

As entidades podem ser explicitamente "anexadas" a um DbContext de modo que o contexto controle essas entidades. Isso é útil principalmente ao:

  1. Criar novas entidades que serão inseridas no banco de dados.
  2. Anexar novamente entidades desconectadas que foram consultadas anteriormente por uma instância de DbContext diferente.

O primeiro deles será necessário para a maioria dos aplicativos e é tratado principalmente pelos métodos DbContext.Add.

O segundo só é necessário para aplicativos que alteram entidades ou suas relações enquanto as entidades não estão sendo controladas. Por exemplo, um aplicativo Web pode enviar entidades para o cliente Web em que o usuário faz alterações e envia as entidades de volta. Essas entidades são conhecidas como "desconectadas", pois foram consultadas originalmente de um DbContext, mas foram desconectadas desse contexto quando enviadas ao cliente.

O aplicativo Web agora deve anexar novamente essas entidades para que elas sejam novamente controladas e indicar as alterações que foram feitas de modo que SaveChanges possa fazer atualizações apropriadas ao banco de dados. Isso é tratado principalmente pelos métodos DbContext.Attach e DbContext.Update.

Dica

Normalmente não deve ser necessário anexar entidades à mesma instância de DbContext das quais elas foram consultadas. Não execute rotineiramente uma consulta sem acompanhamento e anexe as entidades retornadas ao mesmo contexto. Isso será mais lento do que usar uma consulta de acompanhamento e também poderá resultar em problemas como valores de propriedade de sombra ausentes, tornando mais difícil acertar.

Valores de chave gerados versus explícitos

Por padrão, as propriedades de chave inteiras e de GUID são configuradas para usar valores de chave gerados automaticamente. Isso tem uma grande vantagem para o controle de alterações: um valor de chave não definido indica que a entidade é "nova". "Nova" significa que ainda não foi inserida no banco de dados.

Os dois modelos são usados nas seguintes seções. O primeiro é configurado para não usar valores de chave gerados:

public class Blog
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Os valores de chave não gerados (ou seja, definidos explicitamente) são mostrados primeiro em cada exemplo porque tudo é muito explícito e fácil de seguir. Depois, isso é seguido por um exemplo em que os valores de chave gerados são usados:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<Post> Posts { get; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Observe que as propriedades de chave nesse modelo não precisam de nenhuma configuração adicional aqui, pois o uso de valores de chave gerados é o padrão para chaves inteiras simples.

Inserir novas entidades

Valores de chave explícitos

Uma entidade deve ser controlada no estado Added a ser inserida por SaveChanges. As entidades normalmente são colocadas no estado Adicionado chamando um dos métodos DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync ou equivalentes em DbSet<TEntity>.

Dica

Todos esses métodos funcionam da mesma forma no contexto do controle de alterações. Consulte Recursos adicionais do Controle de Alterações para obter mais informações.

Por exemplo, para começar a acompanhar um novo blog:

context.Add(
    new Blog { Id = 1, Name = ".NET Blog", });

Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que o contexto está controlando a nova entidade no estado Added:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

No entanto, os métodos Add não funcionam apenas em uma entidade individual. Na verdade, eles começam a controlar um grafo inteiro de entidades relacionadas, colocando-os todos no estado Added. Por exemplo, para inserir um novo blog e novas postagens associadas:

context.Add(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

O contexto agora está controlando todas essas entidades como Added:

Blog {Id: 1} Added
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Added
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Added
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Observe que valores explícitos foram definidos para as propriedades de chave Id nos exemplos acima. Isso ocorre porque o modelo aqui foi configurado para usar valores de chave definidos explicitamente, em vez de valores de chave gerados automaticamente. Ao não usar chaves geradas, as propriedades de chave devem ser definidas explicitamente antes de chamar Add. Esses valores de chave são inseridos quando SaveChanges é chamado. Por exemplo, ao usar o SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Id", "Name")
VALUES (@p0, @p1);

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p3='1' (DbType = String), @p4='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p5='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p2, @p3, @p4, @p5);

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String), @p1='1' (DbType = String), @p2='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p3='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("Id", "BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2, @p3);

Todas essas entidades são controladas no estado Unchanged após a conclusão de SaveChanges, já que essas entidades agora existem no banco de dados:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Valores de chave gerados

Como mencionado anteriormente, as propriedades de chave inteiras e de GUID são configurados para usar valores de chave gerados automaticamente por padrão. Isso significa que o aplicativo não deve definir nenhum valor de chave explicitamente. Por exemplo, para inserir um novo blog e postagens todos com valores de chave gerados:

context.Add(
    new Blog
    {
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

Como no caso de valores de chave explícitos, o contexto agora está controlando todas essas entidades como Added:

Blog {Id: -2147482644} Added
  Id: -2147482644 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -2147482637}, {Id: -2147482636}]
Post {Id: -2147482637} Added
  Id: -2147482637 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: -2147482644}
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: -2147482644 FK Temporary
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: -2147482644}

Observe, nesse caso, que valores de chave temporária foram gerados para cada entidade. Esses valores são usados pelo EF Core até que SaveChanges seja chamado, momento em que os valores de chave reais são lidos novamente do banco de dados. Por exemplo, ao usar o SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Blogs" ("Name")
VALUES (@p0);
SELECT "Id"
FROM "Blogs"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p2='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p3='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p1, @p2, @p3);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Após a conclusão de SaveChanges, todas as entidades foram atualizadas com seus valores de chave reais e são controladas no estado Unchanged, pois agora correspondem ao estado no banco de dados:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Esse é exatamente o mesmo estado final do exemplo anterior que usou valores de chave explícitos.

Dica

Um valor de chave explícito ainda pode ser definido mesmo ao usar valores de chave gerados. O EF Core tentará fazer a inserção usando esse valor de chave. Algumas configurações de banco de dados, incluindo o SQL Server com colunas de Identidade, não dão suporte a essas inserções e vão gerar (confira esses documentos para uma solução alternativa).

Anexar entidades existentes

Valores de chave explícitos

As entidades retornadas de consultas são controladas no estado Unchanged. O estado Unchanged significa que a entidade não foi modificada desde que foi consultada. Uma entidade desconectada, talvez retornada de um cliente Web em uma solicitação HTTP, pode ser colocada nesse estado usando os métodos DbContext.Attach, DbContext.AttachRange ou equivalentes em DbSet<TEntity>. Por exemplo, para começar a acompanhar um blog existente:

context.Attach(
    new Blog { Id = 1, Name = ".NET Blog", });

Observação

Os exemplos aqui estão criando entidades explicitamente com new para simplificar. Normalmente, as instâncias de entidade terão vindo de outra fonte, como ser desserializada de um cliente ou ser criada a partir de dados em um HTTP Post.

Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que a entidade é controlada no estado Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: []

Assim como Add, Attach realmente define um grafo inteiro de entidades conectadas ao estado Unchanged. Por exemplo, para anexar um blog existente e postagens existentes associadas:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

O contexto agora está controlando todas essas entidades como Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Chamar SaveChanges neste momento não terá efeito. Todas as entidades são marcadas como Unchanged, portanto, não há nada para atualizar no banco de dados.

Valores de chave gerados

Como mencionado anteriormente, as propriedades de chave inteiras e de GUID são configurados para usar valores de chave gerados automaticamente por padrão. Isso tem uma grande vantagem ao trabalhar com entidades desconectadas: um valor de chave não definido indica que a entidade ainda não foi inserida no banco de dados. Isso permite que o controlador de alterações detecte automaticamente novas entidades e as coloque no estado Added. Por exemplo, considere anexar esse grafo de um blog e postagens:

context.Attach(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

O blog tem um valor de chave de 1, indicando que ele já existe no banco de dados. Duas das postagens também têm valores de chave definidos, mas a terceira não. O EF Core verá esse valor de chave como 0, o padrão CLR para um inteiro. Isso resulta no EF Core marcando a nova entidade como Added em vez de Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482636}]
Post {Id: -2147482636} Added
  Id: -2147482636 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'

Chamar SaveChanges nesse momento não resulta em nada com as entidades Unchanged, mas insere a nova entidade no banco de dados. Por exemplo, ao usar o SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

O ponto importante a observar aqui é que, com os valores de chave gerados, o EF Core é capaz de distinguir automaticamente novas entidades existentes em um grafo desconectado. Em poucas palavras, ao usar chaves geradas, o EF Core sempre inserirá uma entidade quando essa entidade não tiver nenhum conjunto de valores de chave.

Atualizar entidades existentes

Valores de chave explícitos

Os métodos DbContext.Update, DbContext.UpdateRange e equivalentes em DbSet<TEntity> se comportam exatamente como os métodos Attach descritos acima, exceto que as entidades são colocadas no estado Modified em vez de Unchanged. Por exemplo, para começar a acompanhar um blog existente como Modified:

context.Update(
    new Blog { Id = 1, Name = ".NET Blog", });

Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que o contexto está controlando essa entidade no estado Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: []

Assim como ocorre com Add e Attach, Update realmente marca um grafo inteiro de entidades relacionadas como Modified. Por exemplo, para anexar um blog existente e postagens existentes associadas como Modified:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            }
        }
    });

O contexto agora está controlando todas essas entidades como Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Chamar SaveChanges nesse momento fará com que as atualizações sejam enviadas ao banco de dados para todas essas entidades. Por exemplo, ao usar o SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

Valores de chave gerados

Assim como acontece com Attach, os valores de chave gerados têm o mesmo benefício principal para Update: um valor de chave não definido indica que a entidade é nova e ainda não foi inserida no banco de dados. Como com Attach, isso permite que o DbContext detecte automaticamente novas entidades e as coloque no estado Added. Por exemplo, considere chamar Update com esse grafo de um blog e postagens:

context.Update(
    new Blog
    {
        Id = 1,
        Name = ".NET Blog",
        Posts =
        {
            new Post
            {
                Id = 1,
                Title = "Announcing the Release of EF Core 5.0",
                Content = "Announcing the release of EF Core 5.0, a full featured cross-platform..."
            },
            new Post
            {
                Id = 2,
                Title = "Announcing F# 5",
                Content = "F# 5 is the latest version of F#, the functional programming language..."
            },
            new Post
            {
                Title = "Announcing .NET 5.0",
                Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
            },
        }
    });

Assim como no exemplo de Attach, a postagem sem nenhum valor de chave é detectada como nova e definida com o estado Added. As outras entidades são marcadas como Modified:

Blog {Id: 1} Modified
  Id: 1 PK
  Name: '.NET Blog' Modified
  Posts: [{Id: 1}, {Id: 2}, {Id: -2147482633}]
Post {Id: -2147482633} Added
  Id: -2147482633 PK Temporary
  BlogId: 1 FK
  Content: '.NET 5.0 includes many enhancements, including single file a...'
  Title: 'Announcing .NET 5.0'
  Blog: {Id: 1}
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...' Modified
  Title: 'Announcing the Release of EF Core 5.0' Modified
  Blog: {Id: 1}
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: 1 FK Modified Originally <null>
  Content: 'F# 5 is the latest version of F#, the functional programming...' Modified
  Title: 'Announcing F# 5' Modified
  Blog: {Id: 1}

Chamar SaveChanges nesse momento fará com que as atualizações sejam enviadas ao banco de dados para todas as entidades existentes, enquanto a nova entidade é inserida. Por exemplo, ao usar o SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0='.NET Blog' (Size = 9)], CommandType='Text', CommandTimeout='30']
UPDATE "Blogs" SET "Name" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='1' (DbType = String), @p0='1' (DbType = String), @p1='Announcing the release of EF Core 5.0, a full featured cross-platform...' (Size = 72), @p2='Announcing the Release of EF Core 5.0' (Size = 37)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p3='2' (DbType = String), @p0='1' (DbType = String), @p1='F# 5 is the latest version of F#, the functional programming language...' (Size = 72), @p2='Announcing F# 5' (Size = 15)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Content" = @p1, "Title" = @p2
WHERE "Id" = @p3;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String), @p1='.NET 5.0 includes many enhancements, including single file applications, more...' (Size = 80), @p2='Announcing .NET 5.0' (Size = 19)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Posts" ("BlogId", "Content", "Title")
VALUES (@p0, @p1, @p2);
SELECT "Id"
FROM "Posts"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Essa é uma maneira muito fácil de gerar atualizações e inserções de um grafo desconectado. No entanto, isso resulta em atualizações ou inserções sendo enviadas ao banco de dados para cada propriedade de cada entidade controlada, mesmo quando alguns valores de propriedade podem não ter sido alterados. Não se preocupe com isso. Para muitos aplicativos com grafos pequenos, essa pode ser uma maneira fácil e pragmática de gerar atualizações. Dito isso, outros padrões mais complexos às vezes podem resultar em atualizações mais eficientes, conforme descrito em Resolução de Identidade no EF Core.

Excluir entidades existentes

Para que uma entidade seja excluída por SaveChanges, ela deve ser controlada no estado Deleted. As entidades normalmente são colocadas no estado Deleted chamando um dos métodos DbContext.Remove, DbContext.RemoveRange ou equivalentes em DbSet<TEntity>. Por exemplo, para marcar uma postagem existente como Deleted:

context.Remove(
    new Post { Id = 2 });

Inspecionar a exibição de depuração do controle de alterações após essa chamada mostra que o contexto está controlando a entidade no estado Deleted:

Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: <null> FK
  Content: <null>
  Title: <null>
  Blog: <null>

Essa entidade será excluída quando SaveChanges for chamado. Por exemplo, ao usar o SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Após a conclusão do SaveChanges, a entidade excluída será desanexada do DbContext, pois ela não existe mais no banco de dados. A exibição de depuração está, portanto, vazia porque nenhuma entidade está sendo controlada.

Excluir entidades dependentes/filho

Excluir entidades dependentes/filho de um grafo é mais simples do que excluir diretamente entidades principais/pai. Confira a próxima seção e Como alterar chaves estrangeiras e navegações para obter mais informações.

É incomum chamar Remove em uma entidade criada com new. Além disso, ao contrário de Add, Attach e Update, é incomum chamar Remove em uma entidade que ainda não está controlada no estado Unchanged ou Modified. Em vez disso, é comum controlar uma única entidade ou grafo de entidades relacionadas e, em seguida, chamar Remove nas entidades que devem ser excluídas. Esse grafo de entidades controladas normalmente é criado ao:

  1. Executar uma consulta para as entidades
  2. Usar os métodos Attach ou Update em um grafo de entidades desconectadas, conforme descrito nas seções anteriores.

Por exemplo, o código na seção anterior é mais provável para obter uma postagem de um cliente e, em seguida, fazer algo assim:

context.Attach(post);
context.Remove(post);

Isso se comporta exatamente da mesma maneira que o exemplo anterior, já que chamar Remove em uma entidade não controlada faz com que ela seja anexada primeiro e, depois, marcada como Deleted.

Em exemplos mais realistas, um grafo de entidades é anexado primeiro e, em seguida, algumas dessas entidades são marcadas como excluídas. Por exemplo:

// Attach a blog and associated posts
context.Attach(blog);

// Mark one post as Deleted
context.Remove(blog.Posts[1]);

Todas as entidades são marcadas como Unchanged, exceto aquela na qual Remove foi chamado:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Essa entidade será excluída quando SaveChanges for chamado. Por exemplo, ao usar o SQLite:

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

Após a conclusão do SaveChanges, a entidade excluída será desanexada do DbContext, pois ela não existe mais no banco de dados. Outras entidades permanecem no estado Unchanged:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}

Excluir entidades principais/pai

Cada relação que conecta dois tipos de entidade tem um lado principal ou pai e um lado dependente ou filho. A entidade dependente/filho é aquela com a propriedade de chave estrangeira. Em uma relação um-para-muitos, o principal/pai está no lado "um" e o dependente/filho está no lado "muitos". Consulte Relacionamentos para obter mais informações.

Nos exemplos anteriores, excluímos uma postagem, que é uma entidade dependente/filho no relacionamento um-para-muitos de blog-postagens. Isso é relativamente simples, pois a remoção de uma entidade dependente/filho não afeta outras entidades. Por outro lado, a exclusão de uma entidade principal/pai também afeta todas as entidades dependentes/filho. Não fazer isso deixaria um valor de chave estrangeira referenciando um valor de chave primária que não existe mais. Esse é um estado de modelo inválido, e resulta em um erro de restrição referencial na maioria dos bancos de dados.

Esse estado de modelo inválido pode ser tratado de duas maneiras:

  1. Definindo os valores de chaves primárias (FK) como nulos. Isso indica que os dependentes/filhos não estão mais relacionados a nenhum principal/pai. Esse é o padrão para relacionamentos opcionais em que a chave estrangeira deve ser anulável. Definir a FK como nula não é válido para os relacionamentos obrigatórios, em que a chave estrangeira normalmente não é anulável.
  2. Excluindo os dependentes/filhos. Esse é o padrão para relacionamentos obrigatórios e também é válido para relacionamentos opcionais.

Consulte Alteração de chaves estrangeiras e navegações para obter informações detalhadas sobre controle de alterações e relacionamentos.

Relações opcionais

A propriedade Post.BlogId de chave estrangeira é anulável no modelo que temos usado. Isso significa que o relacionamento é opcional e, portanto, o comportamento padrão do EF Core é definir as propriedades BlogId de chave estrangeira como nulas quando o blog é excluído. Por exemplo:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Inspecionar a exibição de depuração do controlador de alterações após a chamada para Remove mostra que, conforme o esperado, o blog agora está marcado como Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Modified
  Id: 1 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Modified
  Id: 2 PK
  BlogId: <null> FK Modified Originally 1
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Mais interessante ainda, todas as postagens relacionadas agora estão marcadas como Modified. Isso ocorre porque a propriedade de chave estrangeira em cada entidade foi definida como nula. Chamar SaveChanges atualiza o valor de chave estrangeira para cada postagem como nulo no banco de dados, antes de excluir o blog:

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='2' (DbType = String), @p0=NULL], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0
WHERE "Id" = @p1;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p2;
SELECT changes();

Após a conclusão do SaveChanges, a entidade excluída será desanexada do DbContext, pois ela não existe mais no banco de dados. Outras entidades agora estão marcadas como Unchanged com valores nulos de chave estrangeira, que correspondem ao estado do banco de dados:

Post {Id: 1} Unchanged
  Id: 1 PK
  BlogId: <null> FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: <null>
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: <null> FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: <null>

Relações necessárias

Se a propriedade Post.BlogId de chave estrangeira não for anulável, o relacionamento entre blogs e postagens se tornará "obrigatório". Nessa situação, o EF Core excluirá, por padrão, entidades dependentes/filho quando a entidade principal/pai for excluída. Por exemplo, excluir um blog com postagens relacionadas como no exemplo anterior:

// Attach a blog and associated posts
context.Attach(blog);

// Mark the blog as deleted
context.Remove(blog);

Inspecionar a exibição de depuração do controlador de alterações após a chamada para Remove mostra que, conforme o esperado, o blog está marcado novamente como Deleted:

Blog {Id: 1} Deleted
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}, {Id: 2}]
Post {Id: 1} Deleted
  Id: 1 PK
  BlogId: 1 FK
  Content: 'Announcing the release of EF Core 5.0, a full featured cross...'
  Title: 'Announcing the Release of EF Core 5.0'
  Blog: {Id: 1}
Post {Id: 2} Deleted
  Id: 2 PK
  BlogId: 1 FK
  Content: 'F# 5 is the latest version of F#, the functional programming...'
  Title: 'Announcing F# 5'
  Blog: {Id: 1}

Mais interessante ainda nesse caso é que todas as postagens relacionadas também foram marcadas como Deleted. Chamar SaveChanges faz com que o blog e todas as postagens relacionadas sejam excluídas do banco de dados:

-- Executed DbCommand (0ms) [Parameters=[@p0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p0='2' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Posts"
WHERE "Id" = @p0;
SELECT changes();

-- Executed DbCommand (0ms) [Parameters=[@p1='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
DELETE FROM "Blogs"
WHERE "Id" = @p1;

Após a conclusão do SaveChanges, todas as entidades excluídas serão desanexadas do DbContext, pois elas não existem mais no banco de dados. A saída da exibição de depuração está, portanto, vazia.

Observação

Esse documento apenas arranha a superfície ao trabalhar com relações no EF Core. Consulte Relacionamentos para obter mais informações sobre relacionamentos de modelagem. Confira Alteração de chaves estrangeiras e navegações para obter mais informações sobre como atualizar/excluir entidades dependentes/filho ao chamar SaveChanges.

Acompanhamento personalizado com o TrackGraph

O ChangeTracker.TrackGraph funciona como Add, Attach e Update exceto por gerar um retorno de chamada para cada instância de entidade antes de controla-la. Isso permite que a lógica personalizada seja usada ao determinar como controlar entidades individuais em um grafo.

Por exemplo, considere a regra que o EF Core usa ao controlar entidades com valores de chave gerados: se o valor da chave for zero, a entidade será nova e deverá ser inserida. Vamos estender essa regra para dizer que o valor da chave é negativo, então a entidade deve ser excluída. Isso nos permite alterar os valores de chave primária em entidades de um grafo desconectado para marcar entidades excluídas:

blog.Posts.Add(
    new Post
    {
        Title = "Announcing .NET 5.0",
        Content = ".NET 5.0 includes many enhancements, including single file applications, more..."
    }
);

var toDelete = blog.Posts.Single(e => e.Title == "Announcing F# 5");
toDelete.Id = -toDelete.Id;

Esse grafo desconectado pode ser acompanhado usando o TrackGraph:

public static void UpdateBlog(Blog blog)
{
    using var context = new BlogsContext();

    context.ChangeTracker.TrackGraph(
        blog, node =>
        {
            var propertyEntry = node.Entry.Property("Id");
            var keyValue = (int)propertyEntry.CurrentValue;

            if (keyValue == 0)
            {
                node.Entry.State = EntityState.Added;
            }
            else if (keyValue < 0)
            {
                propertyEntry.CurrentValue = -keyValue;
                node.Entry.State = EntityState.Deleted;
            }
            else
            {
                node.Entry.State = EntityState.Modified;
            }

            Console.WriteLine($"Tracking {node.Entry.Metadata.DisplayName()} with key value {keyValue} as {node.Entry.State}");
        });

    context.SaveChanges();
}

Para cada entidade no grafo, o código acima verifica o valor da chave primária antes de controlar a entidade. Para valores de chave não definidos (zero), o código faz o que o EF Core normalmente faria. Ou seja, se a chave não estiver definida, a entidade será marcada como Added. Se a chave estiver definida e o valor não for negativo, então a entidade será marcada como Modified. No entanto, se um valor de chave negativo for encontrado, seu valor real não negativo será restaurado e a entidade será controlada como Deleted.

A saída da execução desse código é:

Tracking Blog with key value 1 as Modified
Tracking Post with key value 1 as Modified
Tracking Post with key value -2 as Deleted
Tracking Post with key value 0 as Added

Observação

Para simplificar, esse código pressupõe que cada entidade tenha uma propriedade de chave primária inteira chamada Id. Isso pode ser codificado em uma interface ou classe base abstrata. Como alternativa, a propriedade ou as propriedades da chave primária podem ser obtidas dos metadados IEntityType de modo que esse código funcione com qualquer tipo de entidade.

O TrackGraph tem duas sobrecargas. Na sobrecarga simples usada acima, o EF Core determina quando parar de atravessar o grafo. Especificamente, ele interrompe a visita de novas entidades relacionadas de uma determinada entidade quando essa entidade já está sendo controlada ou quando o retorno de chamada não começa a controlar a entidade.

A sobrecarga avançada, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), tem um retorno de chamada que retorna um bool. Se o retorno de chamada retornar false, a travessia do grafo será interrompida, caso contrário, continuará. Deve-se tomar cuidado para evitar loops infinitos ao usar essa sobrecarga.

A sobrecarga avançada também permite que o estado seja fornecido ao TrackGraph e esse estado é então passado para cada retorno de chamada.