Compartir a través de


Características adicionales de seguimiento de cambios

En este documento se tratan varias características y escenarios relacionados con el seguimiento de cambios.

Sugerencia

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 Herramienta de seguimiento de cambios en EF Core para obtener más información sobre estos temas.

Sugerencia

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

Diferencias entre Add y AddAsync

Entity Framework Core (EF Core) proporciona métodos asincrónicos siempre que el uso de ese método pueda dar lugar a una interacción con la base de datos. También se proporcionan métodos sincrónicos para evitar sobrecargas al usar bases de datos que no admiten el acceso asincrónico de alto rendimiento.

Por lo general, DbContext.Add y DbSet<TEntity>.Add no acceden a la base de datos, ya que, de forma inherente, estos métodos simplemente inicial el seguimiento de las entidades. Sin embargo, algunos métodos de generación de valores pueden acceder a la base de datos para generar un valor de clave. El único generador de valores que hace esto y se incluye con EF Core es HiLoValueGenerator<TValue>. El uso de este generador es poco común; nunca está configurado de forma predeterminada. Eso significa que la gran mayoría de las aplicaciones deben usar Add, y no AddAsync.

Otros métodos similares, como Update, Attach y Remove, no tienen sobrecargas asincrónicas porque nunca generan nuevos valores de clave y, por lo tanto, nunca necesitan tener acceso a la base de datos.

AddRange, UpdateRange, AttachRange y RemoveRange.

DbSet<TEntity> y DbContext proporcionan versiones alternativas de Add, Update, Attach y Remove que aceptan varias instancias en una sola llamada. Estos métodos son AddRange, UpdateRange, AttachRange y RemoveRange respectivamente.

Estos métodos se proporcionan por motivos prácticos. El uso de un método "range" tiene la misma funcionalidad que varias llamadas al método equivalente no "range". No hay ninguna diferencia significativa de rendimiento entre los dos enfoques.

Nota:

Esto es diferente de lo que ocurre en EF6, donde tanto AddRange como Add llamaban automáticamente a DetectChanges, pero al llamar a Add varias veces, se llamaba a DetectChanges varias veces en lugar de una vez. Por ello, AddRange era más eficaz en EF6. En EF Core, ninguno de estos métodos llama automáticamente a DetectChanges.

Método DbContext frente a DbSet

Muchos de los métodos, incluidos Add, Update, Attach y Remove, tienen implementaciones tanto en DbSet<TEntity> como en DbContext. Estos métodos tienen exactamente el mismo comportamiento para los tipos de entidad normales. Esto se debe a que el tipo de CLR de la entidad se asigna a uno y solo un tipo de entidad en el modelo de EF Core. Por lo tanto, el tipo de CLR define por completo dónde encaja la entidad en el modelo, con lo que se puede determinar implícitamente el DbSet que se va a usar.

La excepción a esta regla se produce cuando se usan tipos de entidad de tipo compartido, que se usan principalmente para las entidades de combinación de varios a varios. Para usar un tipo de entidad de tipo compartido, primero se debe crear un DbSet para el tipo de modelo de EF Core que se va a usar. De este modo, se podrán usar métodos como Add, Update, Attach y Remove en el DbSet sin que exista ninguna ambigüedad sobre qué tipo de modelo de EF Core se va a usar.

Los tipos de entidad de tipo compartido se usan de forma predeterminada para las entidades de combinación en relaciones de varios a varios. Un tipo de entidad de tipo compartido también se puede configurar explícitamente para su uso en una relación de varios a varios. Por ejemplo, el código siguiente configura Dictionary<string, int> como un tipo de entidad de combinación:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .SharedTypeEntity<Dictionary<string, int>>(
            "PostTag",
            b =>
            {
                b.IndexerProperty<int>("TagId");
                b.IndexerProperty<int>("PostId");
            });

    modelBuilder.Entity<Post>()
        .HasMany(p => p.Tags)
        .WithMany(p => p.Posts)
        .UsingEntity<Dictionary<string, int>>(
            "PostTag",
            j => j.HasOne<Tag>().WithMany(),
            j => j.HasOne<Post>().WithMany());
}

En Cambio de las claves externas y las navegaciones se muestra cómo asociar dos entidades mediante el seguimiento de una nueva instancia de entidad de combinación. El código siguiente lo hace para el tipo de entidad de tipo compartido Dictionary<string, int> que se usa para la entidad de combinación:

using var context = new BlogsContext();

var post = context.Posts.Single(e => e.Id == 3);
var tag = context.Tags.Single(e => e.Id == 1);

var joinEntitySet = context.Set<Dictionary<string, int>>("PostTag");
var joinEntity = new Dictionary<string, int> { ["PostId"] = post.Id, ["TagId"] = tag.Id };
joinEntitySet.Add(joinEntity);

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Se debe tener en cuenta que DbContext.Set<TEntity>(String) se usa para crear un DbSet para el tipo de entidad PostTag. Este DbSet se puede usar después para llamar a Add con la nueva instancia de entidad de combinación.

Importante

El tipo de CLR que se usa para combinar tipos de entidad de combinación puede cambiar en versiones futuras para mejorar el rendimiento. No dependa de ningún tipo de entidad de combinación específico a menos que esté configurado explícitamente tal como se ha hecho para Dictionary<string, int> en el código anterior.

Acceso a propiedad frente a acceso a campo

El acceso a las propiedades de entidad usa el campo de respaldo de la propiedad de forma predeterminada. Esto es eficaz y evita el desencadenamiento de efectos secundarios al llamar a los captadores y establecedores de propiedad. Por ejemplo, así es como la carga diferida puede evitar el desencadenamiento de bucles infinitos. Consulte Campos de respaldo para obtener más información sobre cómo configurar campos de respaldo en el modelo.

A veces puede ser preferible que EF Core genere efectos secundarios al modificar los valores de propiedad. Por ejemplo, cuando los datos se enlazan a entidades, establecer una propiedad puede generar notificaciones a la interfaz de usuario que no se producirían al establecer el campo directamente. Esto también se puede lograr cambiando PropertyAccessMode por:

Los modos de acceso a propiedades Field y PreferField harán que EF Core acceda al valor de propiedad a través de su campo de respaldo. Del mismo modo, Property y PreferProperty harán que EF Core acceda al valor de propiedad a través de su captador y establecedor.

Si se usan Field o Property y EF Core no puede tener acceso al valor a través del campo o del captador o establecedor de propiedad, respectivamente, EF Core producirá una excepción. Esto garantiza que EF Core siempre use el acceso al campo o propiedad cuando se considera que es así.

Por otro lado, los modos PreferField y PreferProperty pasarán a usar la propiedad o el campo de respaldo respectivamente si no es posible usar el acceso preferido. PreferField es el valor predeterminado. Esto significa que EF Core usará los campos siempre que pueda, pero no generará ningún error si en lugar de ello es necesario acceder a una propiedad a través de su captador o establecedor.

FieldDuringConstruction y PreferFieldDuringConstruction configuran EF Core para usar campos de respaldo solo al crear instancias de entidad. Esto permite que las consultas se ejecuten sin efectos secundarios del captador y el establecedor, mientras que los cambios de propiedad posteriores por parte de EF Core provocarán estos efectos secundarios.

Los distintos modos de acceso a propiedades se resumen en la tabla siguiente:

PropertyAccessMode Referencia Preferencia de creación de entidades Tema alternativo Fallback de creación de entidades
Field Campo Campo Produce Produce
Property Propiedad Propiedad Produce Produce
PreferField Campo Campo Propiedad Propiedad
PreferProperty Propiedad Propiedad Campo Campo
FieldDuringConstruction Propiedad Campo Campo Produce
PreferFieldDuringConstruction Propiedad Campo Campo Propiedad

Valores temporales

EF Core crea valores de clave temporales al realizar el seguimiento de nuevas entidades, que tendrán valores de clave reales generados por la base de datos cuando se llame a SaveChanges. Consulte Herramienta de seguimiento de cambios en EF Core para obtener información general sobre cómo se usan estos valores temporales.

Acceso a valores temporales

Los valores temporales se almacenan en el seguimiento de cambios y no se establecen directamente en instancias de entidad. Sin embargo, estos valores temporales están expuestos cuando se usan los distintos mecanismos para el acceso a entidades sometidas a seguimiento. Por ejemplo, el código siguiente accede a un valor temporal mediante EntityEntry.CurrentValues:

using var context = new BlogsContext();

var blog = new Blog { Name = ".NET Blog" };

context.Add(blog);

Console.WriteLine($"Blog.Id set on entity is {blog.Id}");
Console.WriteLine($"Blog.Id tracked by EF is {context.Entry(blog).Property(e => e.Id).CurrentValue}");

El resultado de este código es el siguiente:

Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643

PropertyEntry.IsTemporary se puede usar para comprobar si hay valores temporales.

Manipulación de valores temporales

A veces resulta útil trabajar explícitamente con valores temporales. Por ejemplo, se puede crear una colección de nuevas entidades en un cliente web y serializarla posteriormente en el servidor. Los valores de clave externa son una de las maneras de establecer relaciones entre estas entidades. El código siguiente usa este enfoque para asociar un grafo de nuevas entidades mediante clave externa, a la vez que permite que se generen valores de clave reales al llamar a SaveChanges.

var blogs = new List<Blog> { new Blog { Id = -1, Name = ".NET Blog" }, new Blog { Id = -2, Name = "Visual Studio Blog" } };

var posts = new List<Post>
{
    new Post
    {
        Id = -1,
        BlogId = -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,
        BlogId = -2,
        Title = "Disassembly improvements for optimized managed debugging",
        Content = "If you are focused on squeezing out the last bits of performance for your .NET service or..."
    }
};

using var context = new BlogsContext();

foreach (var blog in blogs)
{
    context.Add(blog).Property(e => e.Id).IsTemporary = true;
}

foreach (var post in posts)
{
    context.Add(post).Property(e => e.Id).IsTemporary = true;
}

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Tenga en lo siguiente:

  • Los números negativos se usan como valores de clave temporales. Esto no es necesario, pero es una convención común para evitar conflictos de claves.
  • A la propiedad de clave externa Post.BlogId se le asigna el mismo valor negativo que a la clave principal del blog asociado.
  • Los valores de clave principal se marcan como temporales mediante el establecimiento de IsTemporary una vez que se realiza el seguimiento de cada entidad. Esto es necesario porque se supone que cualquier valor de clave proporcionado por la aplicación es un valor de clave real.

Al examinar la vista de depuración de seguimiento de cambios antes de llamar a SaveChanges, se puede ver que los valores de clave principal se marcan como temporales y que las publicaciones están asociadas a los blogs correctos, incluida la corrección de las navegaciones:

Blog {Id: -2} Added
  Id: -2 PK Temporary
  Name: 'Visual Studio Blog'
  Posts: [{Id: -2}]
Blog {Id: -1} Added
  Id: -1 PK Temporary
  Name: '.NET Blog'
  Posts: [{Id: -1}]
Post {Id: -2} Added
  Id: -2 PK Temporary
  BlogId: -2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: -2}
  Tags: []
Post {Id: -1} Added
  Id: -1 PK Temporary
  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}

Después de llamar a SaveChanges, estos valores temporales se han reemplazado por valores reales generados por la base de datos:

Blog {Id: 1} Unchanged
  Id: 1 PK
  Name: '.NET Blog'
  Posts: [{Id: 1}]
Blog {Id: 2} Unchanged
  Id: 2 PK
  Name: 'Visual Studio Blog'
  Posts: [{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}
  Tags: []
Post {Id: 2} Unchanged
  Id: 2 PK
  BlogId: 2 FK
  Content: 'If you are focused on squeezing out the last bits of perform...'
  Title: 'Disassembly improvements for optimized managed debugging'
  Blog: {Id: 2}
  Tags: []

Trabajo con valores predeterminados

EF Core permite que una propiedad obtenga su valor predeterminado de la base de datos cuando se llama a SaveChanges. Al igual que con los valores de clave generados, EF Core solo usará un valor predeterminado de la base de datos si no se ha establecido explícitamente ningún valor. Por ejemplo, considere los tipos de entidad siguientes:

public class Token
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime ValidFrom { get; set; }
}

La propiedad ValidFrom está configurada para obtener un valor predeterminado de la base de datos:

modelBuilder
    .Entity<Token>()
    .Property(e => e.ValidFrom)
    .HasDefaultValueSql("CURRENT_TIMESTAMP");

Al insertar una entidad de este tipo, EF Core permitirá que la base de datos genere el valor a menos que se haya establecido un valor explícito. Por ejemplo:

using var context = new BlogsContext();

context.AddRange(
    new Token { Name = "A" },
    new Token { Name = "B", ValidFrom = new DateTime(1111, 11, 11, 11, 11, 11) });

context.SaveChanges();

Console.WriteLine(context.ChangeTracker.DebugView.LongView);

Al examinar la vista de depuración de seguimiento de cambios se puede ver que el primer token tenía ValidFrom generado por la base de datos, mientras que el segundo token usaba el valor establecido explícitamente:

Token {Id: 1} Unchanged
  Id: 1 PK
  Name: 'A'
  ValidFrom: '12/30/2020 6:36:06 PM'
Token {Id: 2} Unchanged
  Id: 2 PK
  Name: 'B'
  ValidFrom: '11/11/1111 11:11:11 AM'

Nota:

Para usar valores predeterminados de la base de datos, es necesario que la columna de la base de datos tenga configurada una restricción de valor predeterminada. Esto se realiza automáticamente mediante migraciones de EF Core al usar HasDefaultValueSql o HasDefaultValue. Asegúrese de crear la restricción predeterminada en la columna de alguna otra manera cuando no use migraciones de EF Core.

Uso de propiedades que admiten un valor NULL

EF Core puede determinar si se ha establecido o no una propiedad comparando el valor de propiedad con el valor predeterminado de CLR para ese tipo. Esto funciona bien en la mayoría de los casos, pero significa que el valor predeterminado de CLR no se puede insertar explícitamente en la base de datos. Por ejemplo, considere una entidad con una propiedad de entero:

public class Foo1
{
    public int Id { get; set; }
    public int Count { get; set; }
}

Donde esa propiedad está configurada para tener un valor predeterminado de -1 en la base de datos:

modelBuilder
    .Entity<Foo1>()
    .Property(e => e.Count)
    .HasDefaultValue(-1);

La intención es que se use el valor predeterminado de -1 siempre que no se establezca un valor explícito. Sin embargo, si se establece el valor 0 (el valor predeterminado de CLR para los enteros), EF Core considerará que no se ha establecido ningún valor, ya que no lo puede distinguir, por lo que no es posible insertar 0 para esta propiedad. Por ejemplo:

using var context = new BlogsContext();

var fooA = new Foo1 { Count = 10 };
var fooB = new Foo1 { Count = 0 };
var fooC = new Foo1();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == -1); // Not what we want!
Debug.Assert(fooC.Count == -1);

Observe que la instancia en la que Count se estableció explícitamente en 0 todavía obtiene el valor predeterminado de la base de datos, que no es lo que se pretende. Una manera fácil de solucionarlo es hacer que la propiedad Count admita un valor NULL:

public class Foo2
{
    public int Id { get; set; }
    public int? Count { get; set; }
}

De esta manera, el valor predeterminado de CLR será NULL en lugar de 0, por lo que ahora se insertará 0 cuando se establezca explícitamente:

using var context = new BlogsContext();

var fooA = new Foo2 { Count = 10 };
var fooB = new Foo2 { Count = 0 };
var fooC = new Foo2();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Uso de campos de respaldo que acepten un valor NULL

El problema de hacer que la propiedad admita un valor NULL es que puede que no admita un valor NULL conceptualmente en el modelo de dominio. Por tanto, forzar que la propiedad admita un valor NULL pone en peligro el modelo.

La propiedad se puede dejar como propiedad que no acepta valores NULL, con solo el campo de respaldo como campo que admite un valor NULL. Por ejemplo:

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

    private int? _count;

    public int Count
    {
        get => _count ?? -1;
        set => _count = value;
    }
}

Esto permite insertar el valor predeterminado de CLR (0) si la propiedad se establece explícitamente en 0, aunque no es necesario exponer la propiedad como propiedad que admite un valor NULL en el modelo de dominio. Por ejemplo:

using var context = new BlogsContext();

var fooA = new Foo3 { Count = 10 };
var fooB = new Foo3 { Count = 0 };
var fooC = new Foo3();

context.AddRange(fooA, fooB, fooC);
context.SaveChanges();

Debug.Assert(fooA.Count == 10);
Debug.Assert(fooB.Count == 0);
Debug.Assert(fooC.Count == -1);

Campos de respaldo que admiten valores NULL para las propiedades bool

Este patrón es particularmente útil cuando se usan propiedades bool con valores predeterminados generados por el almacén. Dado que el valor predeterminado de CLR para bool es "false", significa que no se puede insertar explícitamente "false" mediante el patrón normal. Por ejemplo, considere un tipo de entidad User:

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

    private bool? _isAuthorized;

    public bool IsAuthorized
    {
        get => _isAuthorized ?? true;
        set => _isAuthorized = value;
    }
}

La propiedad IsAuthorized está configurada con un valor predeterminado de base de datos de "true":

modelBuilder
    .Entity<User>()
    .Property(e => e.IsAuthorized)
    .HasDefaultValue(true);

La propiedad IsAuthorized se puede establecer en "true" o "false" explícitamente antes de la inserción, o bien se puede dejar sin establecer, en cuyo caso se usará el valor predeterminado de la base de datos:

using var context = new BlogsContext();

var userA = new User { Name = "Mac" };
var userB = new User { Name = "Alice", IsAuthorized = true };
var userC = new User { Name = "Baxter", IsAuthorized = false }; // Always deny Baxter access!

context.AddRange(userA, userB, userC);

context.SaveChanges();

El resultado de SaveChanges al usar SQLite muestra que se usa el valor predeterminado de la base de datos para Mac, mientras que se establecen valores explícitos para Alice y Baxter:

-- Executed DbCommand (0ms) [Parameters=[@p0='Mac' (Size = 3)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("Name")
VALUES (@p0);
SELECT "Id", "IsAuthorized"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='True' (DbType = String), @p1='Alice' (Size = 5)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

-- Executed DbCommand (0ms) [Parameters=[@p0='False' (DbType = String), @p1='Baxter' (Size = 6)], CommandType='Text', CommandTimeout='30']
INSERT INTO "User" ("IsAuthorized", "Name")
VALUES (@p0, @p1);
SELECT "Id"
FROM "User"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

Solo valores predeterminados de esquema

A veces resulta útil tener valores predeterminados en el esquema de base de datos creado por las migraciones de EF Core sin que EF Core use estos valores para las inserciones. Esto se puede lograr configurando la propiedad como PropertyBuilder.ValueGeneratedNever. Por ejemplo:

modelBuilder
    .Entity<Bar>()
    .Property(e => e.Count)
    .HasDefaultValue(-1)
    .ValueGeneratedNever();