Compartir a través de


Funciones 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 Change Tracking en EF Core para obtener más información sobre estos temas.

Sugerencia

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

Add frente a AddAsync

Entity Framework Core (EF Core) proporciona métodos asincrónicos cada vez que usar un método puede 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.

DbContext.Add y DbSet<TEntity>.Add normalmente no acceden a la base de datos, ya que estos métodos simplemente inician el seguimiento de entidades. Sin embargo, algunas formas de generación de valores pueden tener acceso 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. Esto significa que la gran mayoría de las aplicaciones deben usar Add, y no AddAsync.

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

AddRange, UpdateRange, AttachRangey RemoveRange

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

Estos métodos se proporcionan como comodidad. El uso de un método "range" tiene la misma funcionalidad que varias llamadas al método correspondiente que no sea de rango. No hay ninguna diferencia significativa de rendimiento entre los dos enfoques.

Nota:

Esto es diferente de EF6, donde AddRange y Add ambos llamaban a DetectChanges automáticamente, pero llamar a Add varias veces provocaba que DetectChanges se llamara múltiples veces en lugar de solo una. Esto hizo AddRange más eficaz en EF6. En EF Core, ninguno de estos métodos llama automáticamente a DetectChanges.

Métodos DbContext frente a DbSet

Muchos métodos, incluidos Add, Update, Attach y Remove, tienen implementaciones en DbSet<TEntity> y DbContext. Estos métodos tienen exactamente el mismo comportamiento para los tipos de entidad normales. Esto se debe a que el tipo 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 CLR define por completo dónde encaja la entidad en el modelo y, por tanto, el DbSet que se va a usar se puede determinar implícitamente.

La excepción a esta regla es cuando se utilizan tipos de entidad compartidos, que se emplean principalmente para entidades de unión de muchos a muchos. Cuando se usa un tipo de entidad de tipo compartido, primero se debe crear un DbSet para el tipo de modelo de EF Core que se usa. A continuación, se pueden usar métodos como Add, UpdateAttach, y Remove en DbSet sin ambigüedad alguna en cuanto al tipo de modelo de EF Core que se usa.

Los tipos de entidad de tipo compartido se usan de forma predeterminada para las entidades de unión en relaciones de muchos a muchos. 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 siguiente código configura Dictionary<string, int> como un tipo de entidad de unió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());
}

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

using var context = new BlogsContext();

var post = await context.Posts.SingleAsync(e => e.Id == 3);
var tag = await context.Tags.SingleAsync(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);

await context.SaveChangesAsync();

Observe que DbContext.Set<TEntity>(String) se usa para crear un DbSet para el tipo de PostTag entidad. A continuación, este DbSet se puede usar para llamar a Add con la nueva instancia de entidad de combinación.

Importante

El tipo CLR que se usa para combinar tipos de entidad por convenció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 se haya configurado explícitamente como se hace para Dictionary<string, int> en el código anterior.

Propiedad frente al acceso a campos

El acceso a las propiedades de una entidad utiliza el campo de soporte de la propiedad por defecto. Esto es eficaz y evita desencadenar efectos secundarios de llamar a captadores de propiedades y establecedores. Por ejemplo, así es como la carga diferida puede evitar provocar 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 deseable que EF Core genere efectos secundarios cuando modifica los valores de las propiedades. Por ejemplo, cuando los datos se enlazan a entidades, establecer una propiedad puede generar notificaciones a la U.I. que no se producen al establecer el campo directamente. Esto se puede lograr cambiando el PropertyAccessMode por:

Los modos de acceso a las propiedades Field y PreferField permiten que EF Core acceda al valor de la propiedad a través de su campo auxiliar. Del mismo modo, Property y PreferProperty harán que EF Core acceda al valor de propiedad a través de su getter y setter.

Si Field o Property se usan y EF Core no puede acceder al valor a través del campo o del captador/modificador de propiedades, respectivamente, EF Core generará una excepción. Esto asegura que EF Core siempre use el acceso a campos o propiedades cuando piensas que lo está haciendo.

Por otro lado, los modos PreferField y PreferProperty se revertirán al uso de la propiedad o el campo de respaldo respectivamente si el acceso preferido no es posible. PreferField es el valor predeterminado. Esto significa que EF Core usará los campos siempre que pueda, pero no producirá un error si se debe acceder a una propiedad usando sus métodos de acceso, como `getter` o `setter`, en vez de ello.

FieldDuringConstruction y PreferFieldDuringConstruction configure EF Core para usar campos de respaldo solo al crear instancias de entidad. Esto permite que las consultas se ejecuten sin los efectos secundarios de los métodos 'getter' y 'setter', mientras que los cambios posteriores en la propiedad por parte de EF Core causarán que se produzcan estos efectos secundarios.

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

ModoDeAccesoAPropiedad Preferencia Entidades creadoras de preferencias Alternativa Mecanismo alternativo para la 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 Change Tracking 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 se exponen al usar los distintos mecanismos para acceder a entidades con 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}");

La salida de este código es:

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.

Manipular valores temporales

A veces resulta útil trabajar explícitamente con valores temporales. Por ejemplo, una colección de nuevas entidades se puede crear en un cliente web y, a continuación, volver a serializarla en el servidor. Los valores de clave externa son una manera de configurar relaciones entre estas entidades. El código siguiente usa este enfoque para asociar un gráfico de entidades nuevas por clave externa, a la vez que permite generar valores de clave reales cuando se llama 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);

await context.SaveChangesAsync();

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

Tenga en lo siguiente:

  • Los números negativos se usan como valores de clave temporal; esto no es necesario, pero es una convención común para evitar conflictos clave.
  • A la Post.BlogId propiedad FK se le asigna el mismo valor negativo que el PK del blog asociado.
  • Los valores PK se marcan como temporales estableciendo IsTemporary después de realizar 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 del rastreador de cambios antes de llamar a SaveChanges se muestra que los valores de PK se marcan como temporales y 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: []

Trabajar con valores predeterminados

EF Core permite que una propiedad obtenga su valor predeterminado de la base de datos cuando se invoca 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 el siguiente tipo de entidad:

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

La ValidFrom propiedad 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 en su lugar. 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) });

await context.SaveChangesAsync();

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

Al observar se muestra que en la vista de depuración de seguimiento de cambios el primer token fue generado por la base de datos, mientras que el segundo token usó 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:

El uso de valores predeterminados de base de datos requiere que la columna de 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 aceptan valores NULL

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

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 el valor predeterminado de -1 se usará siempre que no se establezca un valor explícito. Sin embargo, establecer el valor en 0 (el valor predeterminado clR para enteros) no se puede distinguir en EF Core de no establecer ningún valor, lo que significa 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);
await context.SaveChangesAsync();

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

Observe que la instancia en Count la que se estableció explícitamente en 0 sigue recibiendo el valor predeterminado de la base de datos, que no es lo que se pretende. Una manera fácil de tratar con esto es hacer que la Count propiedad acepta valores NULL:

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

Esto hace que el valor predeterminado del CLR sea NULL en lugar de 0, lo que significa que 0 se insertará ahora 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);
await context.SaveChangesAsync();

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

Uso de campos de respaldo que aceptan valores nulos

El problema de hacer que la propiedad sea nullable es que, conceptualmente, puede no ser nullable en el modelo de dominio. Forzar que la propiedad sea nullable, por lo tanto, pone en peligro el modelo.

La propiedad puede dejarse no anulable, con solo el campo de soporte siendo anulable. 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 que acepta valores 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);
await context.SaveChangesAsync();

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

Campos de respaldo que aceptan valores NULL para las propiedades bool

Este patrón es especialmente ú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 "false" no se puede insertar explícitamente 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 IsAuthorized propiedad se configura con un valor predeterminado de la base de datos "true":

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

La IsAuthorized propiedad se puede establecer en "true" o "false" explícitamente antes de insertar o 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);

await context.SaveChangesAsync();

La salida de SaveChanges cuando se usa SQLite muestra que el valor predeterminado de la base de datos se usa para Mac, mientras que los valores explícitos se establecen 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 migraciones de EF Core sin que EF Core use estos valores para inserciones. Esto se puede lograr configurando la propiedad como PropertyBuilder.ValueGeneratedNever por ejemplo:

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