Compartilhar via


Recursos adicionais de controle de alterações

Este documento aborda diversos recursos e cenários que envolvem o controle de alterações.

Dica

Este 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 todo o código neste documento baixando o código de exemplo do GitHub.

Add contra AddAsync

O EF Core (Entity Framework Core) fornece métodos assíncronos sempre que o uso desse método pode resultar em uma interação de banco de dados. Métodos síncronos também são fornecidos para evitar sobrecarga ao usar bancos de dados que não dão suporte a acesso assíncrono de alto desempenho.

DbContext.Add e DbSet<TEntity>.Add normalmente não acessam o banco de dados, pois esses métodos inerentemente apenas começam a rastrear entidades. No entanto, algumas formas de geração de valor podem acessar o banco de dados para gerar um valor de chave. O único gerador de valor que faz isso e é fornecido com o EF Core é HiLoValueGenerator<TValue>. Usar esse gerador é incomum; ele nunca é configurado por padrão. Isso significa que a grande maioria dos aplicativos deve usar Add, e não AddAsync.

Outros métodos semelhantes, como Update, Attache Remove não têm sobrecargas assíncronas porque nunca geram novos valores de chave e, portanto, nunca precisam acessar o banco de dados.

AddRange, UpdateRange, AttachRangee RemoveRange

DbSet<TEntity> e DbContext fornecem versões alternativas de Add, Update, Attach e Remove que aceitam várias instâncias em uma única chamada. Esses métodos sãoAddRange, UpdateRangee AttachRangeRemoveRange respectivamente.

Esses métodos são fornecidos como uma conveniência. O uso de um método de "intervalo" tem a mesma funcionalidade que várias chamadas para o método equivalente sem intervalo. Não há diferença significativa de desempenho entre as duas abordagens.

Observação

Isso é diferente do EF6, em que AddRange e Add ambos são chamados DetectChangesautomaticamente, mas chamar Add várias vezes fez com que DetectChanges fosse chamado várias vezes em vez de uma vez. Isso tornou-se AddRange mais eficiente no EF6. No EF Core, nenhum desses métodos chama DetectChanges automaticamente.

Métodos DbContext versus DbSet

Muitos métodos, incluindo Add, Update, Attach e Remove, têm implementações em ambos DbSet<TEntity> e DbContext. Esses métodos têm exatamente o mesmo comportamento para tipos de entidade normais. Isso ocorre porque o tipo CLR da entidade é mapeado para um e apenas um tipo de entidade no modelo EF Core. Portanto, o tipo CLR define completamente onde a entidade se encaixa no modelo e, portanto, o DbSet a ser usado pode ser determinado implicitamente.

A exceção a essa regra é ao usar tipos de entidade compartilhada, que são usados principalmente para entidades de junção muitos-para-muitos. Ao usar um tipo de entidade de tipo compartilhado, um DbSet deve primeiro ser criado para o tipo de modelo EF Core que está sendo usado. Métodos como Add, Updatee Attach, em seguida, Remove podem ser usados no DbSet sem qualquer ambiguidade sobre qual tipo de modelo EF Core está sendo usado.

Os tipos de entidade de tipo compartilhado são usados por padrão para as entidades de junção em relações muitos para muitos. Um tipo de entidade compartilhada também pode ser configurado explicitamente para uso em um relacionamento muitos-para-muitos. Por exemplo, o código abaixo configura Dictionary<string, int> como um tipo de entidade de junção:

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

Mudança de Chaves Estrangeiras e Navegações mostra como associar duas entidades através do acompanhamento de um novo exemplo de entidade de junção. O código a seguir faz isso para o Dictionary<string, int> tipo de entidade de tipo compartilhado usado para a entidade de junção:

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) é usado para criar um DbSet para o PostTag tipo de entidade. Esse DbSet pode ser usado para chamar Add com a nova instância de entidade de junção.

Importante

O tipo CLR usado para unir tipos de entidade por convenção pode mudar em versões futuras para melhorar o desempenho. Não dependa de nenhum tipo de entidade de junção específico, a menos que tenha sido configurado explicitamente, assim como é feito para Dictionary<string, int> no código acima.

Acesso a propriedade versus acesso a campo

O acesso às propriedades da entidade usa o campo de backup da propriedade por padrão. Isso é eficiente e evita disparar efeitos colaterais das propriedades ao chamar os métodos getters e setters. Por exemplo, é assim que o carregamento tardio evita disparar loops infinitos. Consulte Campos de Backup para obter mais informações sobre como configurar campos de backup no modelo.

Às vezes, pode ser desejável que o EF Core gere efeitos colaterais quando modifica valores de propriedades. Por exemplo, ao associar dados a entidades, a configuração de uma propriedade pode gerar notificações para a IU, o que não acontece ao definir o campo diretamente. Isso pode ser feito alterando o PropertyAccessMode para:

Os modos Field de acesso à propriedade e PreferField farão com que o EF Core acesse o valor da propriedade por meio de seu campo de backup. Da mesma forma, Property e PreferProperty farão com que o EF Core acesse o valor da propriedade através de seus métodos de 'getter' e 'setter'.

Se Field ou Property forem usados e o EF Core não puder acessar o valor por meio do campo ou do getter/setter de propriedade, respectivamente, o EF Core gerará uma exceção. Isso garante que o EF Core esteja sempre usando o acesso de campo/propriedade quando você achar que está.

Por outro lado, os modos PreferField e PreferProperty voltarão a usar, respectivamente, a propriedade ou o campo de apoio, se não for possível usar o acesso preferencial. PreferField é o padrão. Isso significa que o EF Core usará campos sempre que puder, mas não falhará caso um atributo precise ser acessado por meio de seu getter ou setter.

FieldDuringConstruction e PreferFieldDuringConstruction configuram o EF Core para usar campos de suporte somente ao criar instâncias de entidade. Isso permite que as consultas sejam executadas sem os efeitos colaterais dos métodos "getter" e "setter", enquanto alterações posteriores nas propriedades feitas pelo EF Core provocarão esses efeitos colaterais.

Os diferentes modos de acesso à propriedade são resumidos na tabela a seguir:

Modo de Acesso à Propriedade Preferência Preferência de criação de entidades Alternativa Fallback criando entidades
Field Campo Campo Gera Gera
Property Propriedade Propriedade Gera Gera
PreferField Campo Campo Propriedade Propriedade
PreferProperty Propriedade Propriedade Campo Campo
FieldDuringConstruction Propriedade Campo Campo Gera
PreferFieldDuringConstruction Propriedade Campo Campo Propriedade

Valores temporários

O EF Core cria valores de chave temporários ao rastrear novas entidades que terão valores de chave reais gerados pelo banco de dados quando o comando SaveChanges é executado. Consulte o Controle de Alterações no EF Core para obter uma visão geral de como esses valores temporários são usados.

Acessando valores temporários

Os valores temporários são armazenados no rastreador de alterações e não definidos diretamente em instâncias de entidade. No entanto, esses valores temporários são expostos ao usar os vários mecanismos para acessar entidades controladas. Por exemplo, o código a seguir acessa um valor temporário usando 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}");

A saída desse código é:

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

PropertyEntry.IsTemporary pode ser usado para verificar se há valores temporários.

Manipulando valores temporários

Às vezes, é útil trabalhar explicitamente com valores temporários. Por exemplo, uma coleção de novas entidades pode ser criada em um cliente Web e, em seguida, serializada de volta para o servidor. Valores de chave estrangeira são uma maneira de configurar relações entre essas entidades. O código a seguir usa essa abordagem para associar um grafo de novas entidades por chave estrangeira, ao mesmo tempo em que permite que valores de chave reais sejam gerados quando SaveChanges é chamado.

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

Observe que:

  • Números negativos são usados como valores de chave temporários; isso não é necessário, mas é uma convenção comum para evitar conflitos importantes.
  • A Post.BlogId propriedade FK recebe o mesmo valor negativo que o PK do blog associado.
  • Os valores de PK são marcados como temporários ao definir IsTemporary após cada entidade ser rastreada. Isso é necessário porque qualquer valor de chave fornecido pelo aplicativo é considerado um valor de chave real.

Examinar a exibição de depuração do rastreador de alterações antes de chamar SaveChanges mostra que os valores de PK são marcados como temporários e as postagens são associadas aos blogs corretos, incluindo a correção das navegações.

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}

Após a chamada SaveChanges, esses valores temporários foram substituídos por valores reais gerados pelo banco de dados:

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: []

Trabalhando com valores padrão

O EF Core permite que uma propriedade receba seu valor padrão do banco de dados quando SaveChanges for invocado. Assim como com os valores de chave gerados, o EF Core usará apenas um padrão do banco de dados se nenhum valor tiver sido definido explicitamente. Por exemplo, considere o seguinte tipo de entidade:

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

A ValidFrom propriedade está configurada para obter um valor padrão do banco de dados:

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

Ao inserir uma entidade desse tipo, o EF Core permitirá que o banco de dados gere o valor, a menos que um valor explícito tenha sido definido. Por exemplo:

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

Ao examinar a visualização de depuração do rastreador de alterações, fica evidente que o primeiro token foi ValidFrom gerado pelo banco de dados, enquanto o segundo token utilizou o valor determinado explicitamente.

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'

Observação

O uso de valores padrão do banco de dados requer que a coluna de banco de dados tenha uma restrição de valor padrão configurada. Isso é feito automaticamente por migrações do EF Core ao usar HasDefaultValueSql ou HasDefaultValue. Certifique-se de criar a restrição padrão na coluna por meio de outra abordagem quando não estiver usando migrações do EF Core.

Usando propriedades anuláveis

O EF Core é capaz de determinar se uma propriedade foi definida ou não comparando o valor da propriedade com o padrão CLR para esse tipo. Isso funciona bem na maioria dos casos, mas isso significa que o padrão CLR não pode ser inserido explicitamente no banco de dados. Por exemplo, considere uma entidade com uma propriedade inteira.

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

Onde essa propriedade está configurada para ter um padrão de banco de dados de -1:

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

A intenção é que o padrão de -1 seja usado sempre que um valor explícito não for definido. No entanto, definir o valor como 0 (o padrão CLR para inteiros) é indistinguível para o EF Core de não definir nenhum valor, isso significa que não é possível inserir 0 para essa propriedade. Por exemplo:

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 a instância em Count que foi definida explicitamente como 0 ainda obtém o valor padrão do banco de dados, que não é o que queríamos. Uma maneira fácil de lidar com isso é tornar a Count propriedade anulável:

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

Isso torna o padrão CLR nulo, em vez de 0, o que significa que 0 agora será inserido quando definido explicitamente:

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

Usando campos de backup anuláveis

O problema de tornar a propriedade anulável é que ela pode não ser conceitualmente anulável no modelo de domínio. Forçar a propriedade a ser anulável, portanto, compromete o modelo.

A propriedade pode ser deixada não anulável, com apenas o campo de suporte sendo anulável. Por exemplo:

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

    private int? _count;

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

Isso permite que o padrão CLR (0) seja inserido se a propriedade estiver explicitamente definida como 0, enquanto não precisar expor a propriedade como anulável no modelo de domínio. Por exemplo:

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 apoio anuláveis para propriedades booleanas

Esse padrão é especialmente útil ao usar propriedades bool com padrões gerados pelo repositório. Como o padrão CLR para bool é "false", significa que "false" não pode ser inserido explicitamente usando o padrão normal. Por exemplo, considere um tipo de entidade 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;
    }
}

A IsAuthorized propriedade é configurada com um valor padrão de banco de dados "true":

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

IsAuthorized A propriedade pode ser definida como "true" ou "false" explicitamente antes da inserção, ou pode ser deixada não definida, caso em que o padrão do banco de dados será usado.

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

A saída do SaveChanges ao usar SQLite mostra que o padrão do banco de dados é utilizado para Mac, enquanto valores específicos são atribuídos para Alice e 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();

Somente padrões de esquema

Às vezes, é útil ter padrões no esquema de banco de dados criado por migrações do EF Core sem que o EF Core use esses valores para inserções. Isso pode ser feito configurando a propriedade como PropertyBuilder.ValueGeneratedNever por exemplo:

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