Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Este documento abrange diversos recursos e cenários que envolvem o controle de alterações.
Sugestão
Este documento pressupõe que os estados da entidade e os conceitos básicos 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.
Sugestão
Você pode executar e depurar todo o código neste documento baixando o código de exemplo do GitHub.
Add versus AddAsync
O Entity Framework Core (EF 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 oferecem suporte a acesso assíncrono de alto desempenho.
DbContext.Add e DbSet<TEntity>.Add normalmente não acessam o banco de dados, uma vez que 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 acompanha o EF Core é HiLoValueGenerator<TValue>. Usar este 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, AttachRangee RemoveRange
DbSet<TEntity> e DbContext fornecem versões alternativas de Add, Update, Attach e Remove que aceitam várias instâncias numa única chamada. Esses métodos são AddRange, UpdateRange, AttachRange, e RemoveRange respectivamente.
Estes métodos são fornecidos como uma conveniência. O uso de um método "range" 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, onde AddRange e Add ambos chamavam automaticamente DetectChanges, mas chamar Add várias vezes fazia com que DetectChanges fosse chamado várias vezes em vez de uma única vez. Isso tornou AddRange mais eficiente no EF6. No EF Core, nenhum destes métodos chama DetectChanges automaticamente.
Métodos DbContext versus DbSet
Muitos métodos, incluindo Add, Update, Attach, e Remove, têm implementações tanto em DbSet<TEntity> como em DbContext. Esses métodos têm exatamente o mesmo comportamento para tipos de entidade normais. Isso ocorre porque o tipo CLR da entidade é mapeado em 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 entidades de tipo partilhado, que são usadas principalmente para entidades de junção de 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 podem ser usados no DbSet sem qualquer ambiguidade quanto ao tipo de modelo EF Core que 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 explicitamente configurado 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ções mostra como associar duas entidades rastreando uma nova instância de entidade de associação. O código abaixo faz isso para o Dictionary<string, int> tipo de entidade de tipo compartilhado usado para a entidade de associaçã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 tipo de PostTag entidade. Esse DbSet pode ser usado para chamar Add com a nova instância de entidade de associação.
Importante
O tipo CLR utilizado por convenção para tipos de entidades associados pode mudar em versões futuras para melhorar o desempenho. Não dependa de nenhum tipo específico de entidade de associação, a menos que este tenha sido explicitamente configurado, como é feito com Dictionary<string, int> no código acima.
Propriedade versus acesso ao campo
O acesso às propriedades da entidade usa o campo de suporte da propriedade por padrão. Isso é eficiente e evita provocar efeitos colaterais ao chamar os métodos de obtenção e definição de propriedades. Por exemplo, é assim que o carregamento lento é capaz de evitar o acionamento de loops infinitos. Consulte Campos de suporte para obter mais informações sobre como configurar campos de suporte no modelo.
Às vezes, pode ser desejável que o EF Core gere efeitos secundários quando modifica valores de propriedade. Por exemplo, quando os dados são vinculados a entidades, a definição de uma propriedade pode gerar notificações para a U.I., o que não acontece ao definir o campo diretamente. Isto pode ser conseguido alterando o PropertyAccessMode para:
- Todos os tipos de entidade no modelo usando ModelBuilder.UsePropertyAccessMode
- Todas as propriedades e navegações de um tipo de entidade específico usando EntityTypeBuilder<TEntity>.UsePropertyAccessMode
- Uma propriedade específica usando PropertyBuilder.UsePropertyAccessMode
- Uma navegação específica usando NavigationBuilder.UsePropertyAccessMode
Modos de acesso à propriedade Field e PreferField farão com que o EF Core aceda ao valor da propriedade por meio do seu campo de armazenamento. Da mesma forma, Property e PreferProperty farão com que o EF Core aceda ao valor da propriedade por meio de seus getter e setter.
Se Field ou Property forem usados e o EF Core não puder acessar o valor por meio do campo ou da propriedade getter/setter, respectivamente, o EF Core lançará uma exceção. Isso garante que o EF Core esteja sempre a utilizar o acesso a campos/propriedades quando pensa 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 através de seu getter ou setter.
FieldDuringConstruction e PreferFieldDuringConstruction configuram EF Core para utilizar campos de apoio apenas aquando da criação de instâncias de entidade. Isso permite que as consultas sejam executadas sem os efeitos colaterais dos métodos getter e setter, embora alterações posteriores nas propriedades pelo EF Core venham a causar esses efeitos colaterais.
Os diferentes modos de acesso à propriedade estão resumidos na tabela a seguir:
| ModoDeAcessoÀPropriedade | Preferência | Entidades criadoras de preferências | Contingência | Criação de entidades de fallback |
|---|---|---|---|---|
Field |
Campo | Campo | Lançamentos | Lançamentos |
Property |
Propriedade | Propriedade | Lançamentos | Lançamentos |
PreferField |
Campo | Campo | Propriedade | Propriedade |
PreferProperty |
Propriedade | Propriedade | Campo | Campo |
FieldDuringConstruction |
Propriedade | Campo | Campo | Lançamentos |
PreferFieldDuringConstruction |
Propriedade | Campo | Campo | Propriedade |
Valores temporários
O EF Core cria valores de chave temporários ao rastrear novas entidades às quais o banco de dados gerará valores de chave reais quando o método SaveChanges for chamado. Consulte Controle de alterações no EF Core para obter uma visão geral de como esses valores temporários são usados.
Acesso a valores temporários
Os valores temporários são armazenados no controlador de alterações e não são definidos diretamente nas instâncias da entidade. No entanto, esses valores temporários são expostos ao usar os vários mecanismos de acesso a 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 deste código é:
Blog.Id set on entity is 0
Blog.Id tracked by EF is -2147482643
PropertyEntry.IsTemporary pode ser usado para verificar valores temporários.
Manipulação de 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. Os valores-chave estrangeiros são uma forma de estabelecer relações entre estas entidades. O código a seguir usa essa abordagem para associar um gráfico de novas entidades por chave estrangeira, enquanto ainda 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);
Repare que:
- Números negativos são usados como valores-chave temporários; Isso não é necessário, mas é uma convenção comum para evitar confrontos importantes.
- A
Post.BlogIdpropriedade FK recebe o mesmo valor negativo que a PK do blog associado. - Os valores PK são marcados como temporários definindo IsTemporary após cada entidade ser rastreada. Isso é necessário porque qualquer valor de chave fornecido pelo aplicativo é assumido como um valor de chave real.
Observar a visualização de depuração do controlador de alterações antes de chamar SaveChanges mostra que os valores PK são marcados como temporários e as postagens estão associadas aos blogs corretos, incluindo a correção de 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 é chamada. Assim como acontece com os valores de chave gerados, o EF Core só usará 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 observar a visualização de depuração do rastreio de alterações, percebe-se que o primeiro token foi ValidFrom gerado pelo banco de dados, enquanto o segundo token usou o valor explicitamente definido.
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 do banco de dados tenha uma restrição de valor padrão configurada. Isso é feito automaticamente pelas migrações do EF Core ao usar HasDefaultValueSql ou HasDefaultValue. Certifique-se de criar a restrição padrão na coluna de alguma outra maneira 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 significa que o valor padrão do 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 onde Count foi explicitamente definida como 0 ainda obtém o valor padrão do banco de dados, que não é o que pretendí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 suporte 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, sem 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 suporte anuláveis para propriedades bool
Este padrão é especialmente útil ao usar propriedades booleanas com valores padrão gerados pela base de dados. Como o padrão padrão do CLR para bool é "false", isso significa que "false" não pode ser inserido explicitamente usando o padrão habitual. 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 de "true":
modelBuilder
.Entity<User>()
.Property(e => e.IsAuthorized)
.HasDefaultValue(true);
A IsAuthorized propriedade pode ser definida como "true" ou "false" explicitamente antes de inserir, ou pode ser deixada indefinida, 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 de SaveChanges ao usar SQLite mostra que o padrão do banco de dados é usado para Mac, enquanto 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 conseguido configurando a propriedade como PropertyBuilder.ValueGeneratedNever Por exemplo:
modelBuilder
.Entity<Bar>()
.Property(e => e.Count)
.HasDefaultValue(-1)
.ValueGeneratedNever();