Compartir a través de


Seguimiento explícito de entidades

Cada DbContext instancia realiza un seguimiento de los cambios realizados en las entidades. Estas entidades de las que se realiza un seguimiento, a su vez, impulsan los cambios en la base de datos cuando se llama a SaveChanges.

El seguimiento de cambios de Entity Framework Core (EF Core) funciona mejor cuando se usa la misma DbContext instancia para consultar entidades y actualizarlas llamando a SaveChanges. Esto se debe a que EF Core realiza un seguimiento automático del estado de las entidades consultadas y, a continuación, detecta los cambios realizados en estas entidades cuando se llama a SaveChanges. Este enfoque se aborda en Change Tracking en EF Core.

Tip

En este documento se da por supuesto que se comprenden los estados de entidad y los conceptos básicos del seguimiento de cambios de EF Core. Consulte Change Tracking en EF Core para obtener más información sobre estos temas.

Tip

Puede ejecutar y depurar todo el código de este documento descargando el código de ejemplo de GitHub.

Introduction

Las entidades se pueden "adjuntar" explícitamente a un DbContext de manera que el contexto sigue a esas entidades. Esto es principalmente útil cuando:

  1. Crear nuevas entidades que se insertarán en la base de datos.
  2. Volver a adjuntar entidades desconectadas que fueron consultadas previamente por una instancia de DbContext diferente.

La primera de ellas será necesaria para la mayoría de las aplicaciones y principalmente será gestionada por los métodos DbContext.Add.

El segundo solo lo necesitan las aplicaciones que cambian las entidades o sus relaciones mientras no se realiza el seguimiento de las entidades. Por ejemplo, una aplicación web puede enviar entidades al cliente web donde el usuario realiza cambios y devuelve las entidades. Estas entidades se conocen como "desconectadas", ya que originalmente se consultaron desde dbContext, pero luego se desconectaron de ese contexto cuando se enviaron al cliente.

La aplicación web ahora debe volver a adjuntar estas entidades para que se realice un seguimiento de ellas de nuevo e indicar los cambios realizados de forma que SaveChanges puedan realizar actualizaciones adecuadas en la base de datos. Esto se gestiona principalmente mediante los métodos DbContext.Attach y DbContext.Update.

Tip

Normalmente, no debería ser necesario adjuntar entidades a la misma instancia DbContext desde la que se consultaron. No realice rutinariamente una consulta sin seguimiento y, a continuación, adjunte las entidades devueltas al mismo contexto. Esto será más lento que usar una consulta de seguimiento y también puede dar lugar a problemas como la falta de valores de propiedades ocultas, lo que hace más difícil hacerlo correctamente.

Valores de clave generados frente a definidos explícitamente

De forma predeterminada, las propiedades de tipo entero y GUID clave están configuradas para usar valores de clave generados automáticamente. Esto tiene una ventaja importante para el seguimiento de cambios: un valor de clave sin establecer indica que la entidad es "nueva". Por "nuevo", significamos que aún no se ha insertado en la base de datos.

En las secciones siguientes se usan dos modelos. La primera está configurada para no usar valores de clave generados:

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; }
}

Los valores de clave no generados (es decir, establecidos explícitamente) se muestran primero en cada ejemplo porque todo es muy explícito y fácil de seguir. A continuación, se sigue un ejemplo en el que se usan los valores de clave generados:

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; }
}

Tenga en cuenta que las propiedades de clave de este modelo no necesitan ninguna configuración adicional, ya que el uso de valores de clave generados es el valor predeterminado para las claves de entero simples.

Inserción de nuevas entidades

Valores de clave explícitos

Se debe realizar un seguimiento de una entidad en el estado Added para ser insertada por SaveChanges. Normalmente, las entidades se colocan en el estado Agregado llamando a uno de los métodos DbContext.Add, DbContext.AddRange, DbContext.AddAsync, DbContext.AddRangeAsync, o los métodos equivalentes en DbSet<TEntity>.

Tip

Todos estos métodos funcionan de la misma manera en el contexto del seguimiento de cambios. Consulte Características de seguimiento de cambios adicionales para obtener más información.

Por ejemplo, para iniciar el seguimiento de un nuevo blog:

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

Al inspeccionar la vista de depuración de seguimiento de cambios después de esta llamada muestra que el contexto está realizando el seguimiento de la nueva entidad en el estado Added:

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

Sin embargo, los métodos Add no solo funcionan en una entidad individual. En realidad, comienzan a realizar un seguimiento de un grafo de entidades relacionadas, poniéndolas a todas en el estado Added. Por ejemplo, para insertar un nuevo blog y entradas nuevas asociadas:

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..."
            }
        }
    });

El contexto ahora realiza el seguimiento de todas estas 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 se han establecido valores explícitos para las Id propiedades clave en los ejemplos anteriores. Esto se debe a que el modelo aquí se ha configurado para usar valores de clave establecidos explícitamente, en lugar de valores de clave generados automáticamente. Cuando no se usan claves generadas, las propiedades de clave deben establecerse explícitamente antes de llamar a Add. A continuación, estos valores de clave se insertan cuando se llama a SaveChanges. Por ejemplo, al usar 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 estas entidades se les realiza un seguimiento en el estado Unchanged después de completar SaveChanges, ya que ahora estas entidades existen en la base de datos.

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 clave generados

Como se mencionó anteriormente, las propiedades enteros y clave GUID se configuran para usar valores de clave generados automáticamente de forma predeterminada. Esto significa que la aplicación no debe establecer ningún valor de clave explícitamente. Por ejemplo, para insertar un nuevo blog y todas las entradas con valores de clave generados:

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..."
            }
        }
    });

Al igual que con los valores de clave explícitos, el contexto ahora realiza el seguimiento de todas estas 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 en este caso que se han generado valores de clave temporales para cada entidad. EF Core usa estos valores hasta que se llama a SaveChanges, en cuyo punto se leen los valores de clave reales de la base de datos. Por ejemplo, al usar 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();

Una vez completada la operación SaveChanges, todas las entidades se han actualizado con sus valores de clave reales y se rastrean en el estado Unchanged ya que ahora coinciden con el estado de la base de datos.

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}

Este es exactamente el mismo estado final que el ejemplo anterior que usó valores de clave explícitos.

Tip

Todavía se puede establecer un valor de clave explícito incluso cuando se usan valores de clave generados. A continuación, EF Core intentará insertar usando este valor de clave. Algunas configuraciones de bases de datos, incluido SQL Server con columnas de identidad, no admiten dichas inserciones y generarán errores (consulte estos documentos para obtener una solución alternativa).

Adjuntar entidades existentes

Valores de clave explícitos

Se realiza un seguimiento de las entidades en el estado Unchanged devueltas por las consultas. El Unchanged estado significa que la entidad no se ha modificado desde que se ha consultado. Una entidad desconectada, quizás devuelta de un cliente web en una solicitud HTTP, se puede colocar en este estado mediante DbContext.Attach, DbContext.AttachRangeo los métodos equivalentes en DbSet<TEntity>. Por ejemplo, para iniciar el seguimiento de un blog existente:

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

Note

Los ejemplos aquí están creando entidades explícitamente con new por motivos de simplicidad. Normalmente, las instancias de la entidad provendrán de otro origen, como por ejemplo, si se han descentralizado desde un cliente o si se han creado a partir de datos en una publicación HTTP.

La inspección de la vista de depuración del rastreador de cambios después de esta llamada muestra que se le hace un seguimiento a la entidad en el estado Unchanged:

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

Al igual que Add, Attach de hecho establece un grafo completo de entidades conectadas al estado Unchanged. Por ejemplo, para adjuntar un blog existente y entradas existentes asociadas:

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..."
            }
        }
    });

El contexto ahora realiza el seguimiento de todas estas 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}

Llamar a SaveChanges en este momento no tendrá ningún efecto. Todas las entidades se marcan como Unchanged, por lo que no hay nada que actualizar en la base de datos.

Valores de clave generados

Como se mencionó anteriormente, las propiedades enteros y clave GUID se configuran para usar valores de clave generados automáticamente de forma predeterminada. Esto tiene una ventaja importante al trabajar con entidades desconectadas: un valor de clave sin establecer indica que la entidad aún no se ha insertado en la base de datos. Esto permite al rastreador de cambios detectar automáticamente nuevas entidades y colocarlas en el Added estado. Por ejemplo, considere la posibilidad de adjuntar este gráfico de un blog y entradas:

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..."
            },
        }
    });

El blog tiene un valor de clave de 1, lo que indica que ya existe en la base de datos. Dos de las publicaciones también tienen valores clave establecidos, pero el tercero no. EF Core verá este valor de clave como 0, el valor predeterminado de CLR para un entero. Esto da como resultado que EF Core marque la nueva entidad como Added en lugar 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...'

Llamar a SaveChanges en este momento no hace nada con las Unchanged entidades, pero inserta la nueva entidad en la base de datos. Por ejemplo, al usar 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();

El punto importante que hay que tener en cuenta aquí es que, con los valores de clave generados, EF Core puede distinguir automáticamente las nuevas entidades existentes en un gráfico desconectado. En pocas palabras, cuando se usan claves generadas, EF Core siempre insertará una entidad cuando esa entidad no tenga ningún valor de clave establecido.

Actualización de entidades existentes

Valores de clave explícitos

DbContext.Update, DbContext.UpdateRange y los métodos equivalentes en DbSet<TEntity> se comportan exactamente como los métodos de Attach descritos anteriormente, excepto que las entidades se colocan en el estado de Modified en lugar de Unchanged. Por ejemplo, para iniciar el seguimiento de un blog existente como Modified:

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

Al inspeccionar la vista de depuración de seguimiento de cambios después de esta llamada muestra que el contexto está realizando el seguimiento de esta entidad en el estado Modified:

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

Al igual que con Add y Attach, Update marca realmente un gráfico completo de entidades relacionadas como Modified. Por ejemplo, para adjuntar un blog existente y entradas existentes asociadas 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..."
            }
        }
    });

El contexto ahora realiza el seguimiento de todas estas 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}

Al llamar a SaveChanges en este momento, las actualizaciones se enviarán a la base de datos para todas estas entidades. Por ejemplo, al usar 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 clave generados

Al igual que con Attach, los valores de clave generados tienen la misma ventaja principal para Update: un valor de clave sin establecer indica que la entidad es nueva y aún no se ha insertado en la base de datos. Al igual que con Attach, esto permite a DbContext detectar automáticamente nuevas entidades y colocarlas en el Added estado . Por ejemplo, considere la posibilidad de llamar a Update con este grafo de un blog y sus publicaciones:

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..."
            },
        }
    });

Al igual que con el ejemplo de Attach, la publicación sin ningún valor de clave se detecta como nuevo y se establece en el estado Added. Las otras entidades se marcan 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}

Llamar a SaveChanges en este momento enviará actualizaciones a la base de datos para todas las entidades existentes, al tiempo que se inserta la nueva entidad. Por ejemplo, al usar 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();

Esta es una manera muy fácil de generar actualizaciones e inserciones desde un grafo desconectado. Sin embargo, da como resultado actualizaciones o inserciones que se envían a la base de datos para cada propiedad de cada entidad con seguimiento, incluso cuando es posible que algunos valores de propiedad no se hayan cambiado. No tengas demasiado miedo por esto; para muchas aplicaciones con gráficos pequeños, esto puede ser una manera fácil y pragmática de generar actualizaciones. Dicho esto, otros patrones más complejos a veces pueden dar lugar a actualizaciones más eficaces, como se describe en Resolución de identidades en EF Core.

Eliminación de entidades existentes

Para que SaveChanges elimine una entidad, debe estar registrada en el estado Deleted. Normalmente, las entidades se colocan en el Deleted estado mediante una llamada a uno de DbContext.Remove, DbContext.RemoveRangeo a los métodos equivalentes en DbSet<TEntity>. Por ejemplo, para marcar una publicación existente como Deleted:

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

Al inspeccionar la vista de depuración de seguimiento de cambios después de esta llamada muestra que el contexto está realizando el seguimiento de la entidad en el estado Deleted:

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

Esta entidad se eliminará cuando se llame a SaveChanges. Por ejemplo, al usar SQLite:

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

Una vez completado SaveChanges, la entidad eliminada se desasocia de DbContext, ya que ya no existe en la base de datos. Por lo tanto, la vista de depuración está vacía porque no se realiza el seguimiento de ninguna entidad.

Eliminación de entidades dependientes o secundarias

La eliminación de entidades dependientes o secundarias de un grafo es más sencilla que eliminar entidades principales o primarias. Consulte la sección siguiente y Cambio de claves externas y navegaciones para obtener más información.

Es poco común llamar a Remove en una entidad creada con new. Además, a diferencia de Add, Attach y Update, es poco habitual llamar a Remove en una entidad que no esté rastreada en el estado Unchanged o Modified. En su lugar, es habitual realizar un seguimiento de una sola entidad o un conjunto de entidades relacionadas y, a continuación, llamar a Remove en las entidades que se deben eliminar. Normalmente, este gráfico de entidades con seguimiento se crea mediante:

  1. Realización de una consulta para las entidades
  2. Usar los Attach métodos o Update en un gráfico de entidades desconectadas, como se describe en las secciones anteriores.

Por ejemplo, es más probable que el código de la sección anterior obtenga una publicación de un cliente y, a continuación, haga algo parecido a esto:

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

Esto se comporta exactamente de la misma manera que el ejemplo anterior, ya que llamar a Remove en una entidad sin seguimiento hace que se asocie primero y, a continuación, se marque como Deleted.

En ejemplos más realistas, primero se adjunta un gráfico de entidades y, a continuación, algunas de esas entidades se marcan como eliminadas. Por ejemplo:

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

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

Todas las entidades se marcan como Unchanged, excepto aquella en la que se llamó Remove.

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}

Esta entidad se eliminará cuando se llame a SaveChanges. Por ejemplo, al usar SQLite:

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

Una vez completado SaveChanges, la entidad eliminada se desasocia de DbContext, ya que ya no existe en la base de datos. Otras entidades permanecen en el 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}

Eliminación de entidades principales o matrices

Cada relación que conecta dos tipos de entidad tiene un extremo principal o primario y un extremo dependiente o secundario. La entidad dependiente o secundaria es la que tiene la propiedad de clave externa. En una relación uno a varios, la entidad principal o primaria se encuentra en el lado "uno" y la secundaria o dependiente está en el lado "varios". Consulte Relaciones para obtener más información.

En los ejemplos anteriores se estaba eliminando una publicación, que es una entidad dependiente o secundaria en la relación de uno a varios de publicaciones de un blog. Esto es relativamente sencillo, ya que la eliminación de una entidad dependiente o secundaria no tiene ningún impacto en otras entidades. Por otro lado, la eliminación de una entidad principal o primaria también debe afectar a las entidades dependientes o secundarias. Si no lo hace, dejaría un valor de clave externa que hace referencia a un valor de clave principal que ya no existe. Este es un estado de modelo no válido y da como resultado un error de restricción referencial en la mayoría de las bases de datos.

Este estado de modelo no válido se puede controlar de dos maneras:

  1. Establecer valores de FK en null. Esto indica que los niños o dependientes ya no están relacionados con ningún principal/padre. Este es el valor predeterminado para las relaciones opcionales en las que la clave externa debe ser nullable. Asignar un valor NULL a la FK no es válido para las relaciones obligatorias, donde la clave externa típicamente no acepta valores NULL.
  2. Eliminar los dependientes/hijos. Este es el valor predeterminado para las relaciones necesarias y también es válido para las relaciones opcionales.

Consulte Cambio de claves externas y navegaciones para obtener información detallada sobre el seguimiento de cambios y las relaciones.

Relaciones opcionales

La Post.BlogId propiedad de clave externa admite valores NULL en el modelo que hemos estado usando. Esto significa que la relación es opcional y, por lo tanto, el comportamiento predeterminado de EF Core es establecer BlogId las propiedades de clave externa en NULL cuando se elimina el blog. Por ejemplo:

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

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

La inspección de la vista de depuración de seguimiento de cambios después de la llamada a Remove muestra que, según lo previsto, el blog ahora 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>

Más interesantemente, todas las publicaciones relacionadas ahora están marcadas como Modified. Esto se debe a que la propiedad de clave externa de cada entidad se ha establecido en NULL. Al llamar a SaveChanges, se actualiza el valor de clave externa de cada publicación en null en la base de datos, antes de eliminar el 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();

Una vez completado SaveChanges, la entidad eliminada se desasocia de DbContext, ya que ya no existe en la base de datos. Otras entidades ahora se marcan como Unchanged con valores de clave externa NULL, que coinciden con el estado de la base de datos:

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>

Relaciones necesarias

Si la propiedad de clave externa Post.BlogId no acepta valores null, la relación entre blogs y publicaciones se convierte en "obligatoria". En esta situación, EF Core, de forma predeterminada, eliminará las entidades dependientes o secundarias cuando se elimine el elemento principal. Por ejemplo, eliminar un blog con entradas relacionadas como en el ejemplo anterior:

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

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

La inspección de la vista de depuración de seguimiento de cambios después de la llamada a Remove muestra que, según lo previsto, el blog vuelve a estar marcado 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}

Más interesantemente en este caso es que todas las publicaciones relacionadas también se han marcado como Deleted. Llamar a SaveChanges hace que el blog y todas las entradas relacionadas se eliminen de la base de datos:

-- 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;

Una vez completado SaveChanges, todas las entidades eliminadas se desasocian de DbContext, ya que ya no existen en la base de datos. Por lo tanto, el resultado de la vista de depuración está vacío.

Note

Este documento solo araña la superficie del trabajo con relaciones en EF Core. Consulte Relaciones para obtener más información sobre las relaciones de modelado y Cambio de claves externas y navegaciones para obtener más información sobre cómo actualizar, eliminar entidades dependientes o secundarias al llamar a SaveChanges.

Seguimiento personalizado con TrackGraph

ChangeTracker.TrackGraph funciona como Add, Attach y Update, excepto que genera una devolución de llamada para cada instancia de entidad antes de realizar el seguimiento. Esto permite usar lógica personalizada al determinar cómo realizar un seguimiento de entidades individuales en un grafo.

Por ejemplo, considere la regla que EF Core usa al realizar el seguimiento de entidades con valores de clave generados: si el valor de clave es cero, la entidad es nueva y debe insertarse. Vamos a extender esta regla para decir si el valor de clave es negativo, se debe eliminar la entidad. Esto nos permite cambiar los valores de clave principal de las entidades de un grafo desconectado para marcar las entidades eliminadas:

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;

A continuación, se puede realizar un seguimiento de este gráfico desconectado mediante TrackGraph:

public static async Task 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}");
        });

    await context.SaveChangesAsync();
}

Para cada entidad del gráfico, el código anterior comprueba el valor de clave principal antes de realizar el seguimiento de la entidad. En el caso de los valores de clave sin establecer (cero), el código hace lo que EF Core haría normalmente. Es decir, si la clave no está establecida, la entidad se marca como Added. Si se establece la clave y el valor no es negativo, la entidad se marca como Modified. Sin embargo, si se encuentra un valor de clave negativo, se restaura su valor real y no negativo y se realiza el seguimiento de la entidad como Deleted.

La salida de la ejecución de este código es:

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

Note

Para simplificar, este código supone que cada entidad tiene una propiedad de clave principal de entero denominada Id. Esto se podría codificar en una interfaz o clase base abstracta. Como alternativa, la propiedad o las propiedades de clave principal se pueden obtener de los IEntityType metadatos de modo que este código funcione con cualquier tipo de entidad.

TrackGraph tiene dos sobrecargas. En la sobrecarga simple usada anteriormente, EF Core determina cuándo detener el recorrido del gráfico. En concreto, deja de visitar nuevas entidades relacionadas desde una entidad determinada cuando ya se realiza un seguimiento de esa entidad o cuando la devolución de llamada no inicia el seguimiento de la entidad.

La sobrecarga avanzada, ChangeTracker.TrackGraph<TState>(Object, TState, Func<EntityEntryGraphNode<TState>,Boolean>), tiene una devolución de llamada que devuelve un bool. Si el callback devuelve false, el recorrido del grafo se detendrá; de lo contrario, continuará. Se debe tener cuidado para evitar bucles infinitos al usar esta sobrecarga.

La sobrecarga avanzada también permite proporcionar el estado a TrackGraph y este estado se pasa a cada devolución de llamada.