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

Add versus 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, Attach e 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, AttachRange e RemoveRange

DbSet<TEntity> e DbContext forneça versões alternativas de Add, Update, Attach e Remove que aceitam várias instâncias em uma única chamada. Esses métodos são AddRange, UpdateRange, AttachRange e RemoveRange 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 de não intervalo equivalente. 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 automaticamente DetectChanges, 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 automaticamente DetectChanges.

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 de tipo compartilhado, 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, Update, Attach e Remove em seguida 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 de tipo compartilhado também pode ser configurado explicitamente para uso em uma relação 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());
}

Alterar chaves estrangeiras e navegação mostra como associar duas entidades acompanhando uma nova instância de entidade de junção. O código abaixo faz isso para o tipo de entidade Dictionary<string, int> de tipo compartilhado usado para a entidade de união:

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

Observe que DbContext.Set<TEntity>(String) é usado para criar um DbSet para o tipo de entidade PostTag. 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 como é feito Dictionary<string, int> no código acima.

Acesso de propriedade versus campo

O acesso às propriedades da entidade usa o campo de backup da propriedade por padrão. Isso é eficiente e evita que você acione efeitos colaterais ao chamar getters e setters de propriedades. Por exemplo, é assim que o carregamento lento consegue evitar o acionamento de 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 propriedade. 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 de acesso à propriedade Field 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á com que o EF Core acesse o valor da propriedade por meio de seu 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 a propriedade ou o campo de apoio, respectivamente, 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á se uma propriedade precisar ser acessada por meio de seu getter ou setter.

FieldDuringConstruction e configure o PreferFieldDuringConstruction EF Core para usar campos de backup somente ao criar instâncias de entidade. Isso permite que as consultas sejam executadas sem efeitos colaterais getter e setter, enquanto as alterações de propriedade posteriores pelo EF Core causarão esses efeitos colaterais.

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

PropertyAccessMode Preferência Preferência de criação de entidades Fallback 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 SaveChanges for chamado. 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 quando você usa os vários mecanismos para Acessar as entidades rastreadas. 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);

context.SaveChanges();

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 propriedade Post.BlogId FK recebe o mesmo valor negativo que o PK do blog associado.
  • Os valores de PK são marcados como temporários definindo IsTemporary depois que cada entidade é rastreada. Isso é necessário porque qualquer valor de chave fornecido pelo aplicativo é considerado um valor de chave real.

Se você observar a exibição de depuração do rastreador de alterações antes de chamar SaveChanges, verá que os valores de PK estão marcados como temporários e as postagens estã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 obtenha seu valor padrão do banco de dados quando SaveChanges for chamada. 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 propriedade ValidFrom 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) });

context.SaveChanges();

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

A visualização de depuração do rastreador de alterações mostra que o primeiro token tinha ValidFrom gerados pelo banco de dados, enquanto o segundo token usava o valor definido 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. Crie a restrição padrão na coluna de alguma outra forma ao não usar 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 significa que o padrão CLR não pode ser inserido explicitamente no banco de dados. Por exemplo, considere uma entidade com uma propriedade inteiro:

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);
context.SaveChanges();

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 que Count 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 propriedade Count 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);
context.SaveChanges();

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 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 backup 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);
context.SaveChanges();

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

Campos de backup anuláveis para propriedades bool

Esse padrão é especialmente útil ao usar propriedades bool com padrões gerados pelo repositório. Como o padrão CLR para bool é "false", isso 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 propriedade IsAuthorized é configurada com um valor padrão de banco de dados "true":

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

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

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

A saída do SaveChanges ao usar SQLite mostra que o padrão do banco de dados é usado para Mac, enquanto os valores explícitos são definidos 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();