Resolução de identidade no EF Core
Uma DbContext só pode acompanhar uma instância de entidade com um determinado valor de chave primária fornecido. Isso significa que várias instâncias de uma entidade com o mesmo valor de chave devem ser resolvidas para uma única instância. Isso é chamado de "resolução de identidade". A resolução de identidade garante que o Entity Framework Core (EF Core) esteja acompanhando um grafo consistente sem ambiguidades sobre as relações ou valores de propriedade das entidades.
Dica
Esse 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.
Introdução
O código a seguir consulta uma entidade e tenta anexar uma instância diferente com o mesmo valor de chave primária:
using var context = new BlogsContext();
var blogA = context.Blogs.Single(e => e.Id == 1);
var blogB = new Blog { Id = 1, Name = ".NET Blog (All new!)" };
try
{
context.Update(blogB); // This will throw
}
catch (Exception e)
{
Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}
A execução desse código resulta na seguinte exceção:
System.InvalidOperationException: a instância do tipo de entidade "Blog" não pode ser rastreada porque outra instância com o valor de chave "{Id: 1}" já está sendo controlada. Ao anexar entidades existentes, verifique se apenas uma instância de entidade com um determinado valor de chave está anexada.
O EF Core requer uma única instância porque:
- Os valores de propriedade podem ser diferentes entre várias instâncias. Ao atualizar o banco de dados, o EF Core precisa saber quais valores de propriedade usar.
- As relações com outras entidades podem ser diferentes entre várias instâncias. Por exemplo, "blogA" pode estar relacionado a uma coleção diferente de postagens que "blogB".
A exceção acima é comumente encontrada nessas situações:
- Ao tentar atualizar uma entidade
- Ao tentar rastrear um grafo serializado de entidades
- Ao não definir um valor de chave que não é gerado automaticamente
- Ao reutilizando uma instância de DbContext para várias unidades de trabalho
Cada uma dessas situações é discutida nas seções a seguir.
Como atualizar uma entidade
Há várias abordagens diferentes para atualizar uma entidade com novos valores, conforme abordado em Controle de Alterações no EF Core e em Controle Explícito de Entidades. Essas abordagens são descritas abaixo no contexto de resolução de identidade. Um ponto importante a ser notado é que cada uma das abordagens usa uma consulta ou uma chamada para uma de Update
ou Attach
, exceto , nunca as duas.
Atualização de Chamadas
Muitas vezes, a entidade a ser atualizada não vem de uma consulta no DbContext que usaremos para SaveChanges. Por exemplo, em um aplicativo Web, uma instância de entidade pode ser criada a partir das informações em uma solicitação POST. A maneira mais simples de lidar com isso é usar DbContext.Update ou DbSet<TEntity>.Update. Por exemplo:
public static void UpdateFromHttpPost1(Blog blog)
{
using var context = new BlogsContext();
context.Update(blog);
context.SaveChanges();
}
Nesse caso:
- Somente uma única instância da entidade é criada.
- A instância de entidade não é consultada do banco de dados como parte da atualização.
- Todos os valores de propriedade serão atualizados no banco de dados, independentemente de terem sido realmente alterados ou não.
- É feita uma viagem de ida e volta de banco de dados.
Em seguida, a consulta aplica alterações
Normalmente, não se sabe quais valores de propriedade foram realmente alterados quando uma entidade é criada a partir de informações em uma solicitação POST ou semelhante. Geralmente, é bom apenas atualizar todos os valores no banco de dados, como fizemos no exemplo anterior. No entanto, se o aplicativo estiver tratando muitas entidades e apenas um pequeno número delas tiver alterações reais, poderá ser útil limitar as atualizações enviadas. Isso pode ser feito executando uma consulta para acompanhar as entidades como elas existem atualmente no banco de dados e, em seguida, aplicando alterações a essas entidades controladas. Por exemplo:
public static void UpdateFromHttpPost2(Blog blog)
{
using var context = new BlogsContext();
var trackedBlog = context.Blogs.Find(blog.Id);
trackedBlog.Name = blog.Name;
trackedBlog.Summary = blog.Summary;
context.SaveChanges();
}
Nesse caso:
- Somente uma instância da entidade é rastreada: aquela que é retornada do banco de dados pela consulta
Find
. Update
,Attach
, etc. não são usadas.- Somente os valores de propriedade que realmente foram alterados são atualizados no banco de dados.
- São feitas duas viagens de ida e volta de banco de dados.
O EF Core tem alguns auxiliares para transferir valores de propriedade como este. Por exemplo, PropertyValues.SetValues copiará todos os valores do objeto fornecido e os definirá no objeto rastreado:
public static void UpdateFromHttpPost3(Blog blog)
{
using var context = new BlogsContext();
var trackedBlog = context.Blogs.Find(blog.Id);
context.Entry(trackedBlog).CurrentValues.SetValues(blog);
context.SaveChanges();
}
SetValues
aceita vários tipos de objeto, incluindo objetos de transferência de dados (DTOs) com nomes de propriedade que correspondem às propriedades do tipo de entidade. Por exemplo:
public static void UpdateFromHttpPost4(BlogDto dto)
{
using var context = new BlogsContext();
var trackedBlog = context.Blogs.Find(dto.Id);
context.Entry(trackedBlog).CurrentValues.SetValues(dto);
context.SaveChanges();
}
Ou um dicionário com entradas de nome/valor para os valores da propriedade:
public static void UpdateFromHttpPost5(Dictionary<string, object> propertyValues)
{
using var context = new BlogsContext();
var trackedBlog = context.Blogs.Find(propertyValues["Id"]);
context.Entry(trackedBlog).CurrentValues.SetValues(propertyValues);
context.SaveChanges();
}
Consulte Acessar entidades controladas para obter mais informações sobre como trabalhar com valores de propriedade como este.
Usar valores originais
Até agora, cada abordagem executou uma consulta antes de fazer a atualização ou atualizou todos os valores de propriedade, independentemente de terem sido ou não alterados. Para atualizar apenas os valores que foram alterados sem consultar como parte da atualização, é necessário obter informações específicas sobre quais valores de propriedade foram alterados. Uma maneira comum de obter essas informações é enviar de volta os valores atuais e originais no POST HTTP ou semelhantes. Por exemplo:
public static void UpdateFromHttpPost6(Blog blog, Dictionary<string, object> originalValues)
{
using var context = new BlogsContext();
context.Attach(blog);
context.Entry(blog).OriginalValues.SetValues(originalValues);
context.SaveChanges();
}
Nesse código, a entidade com valores modificados é anexada pela primeira vez. Isso faz com que o EF Core acompanhe a entidade no estado Unchanged
: ou seja, sem valores de propriedade marcados como modificados. Em seguida, o dicionário de valores originais é aplicado a essa entidade controlada. Isso marcará como propriedades modificadas com valores atuais e originais diferentes. As propriedades que têm os mesmos valores atuais e originais não serão marcadas como modificadas.
Nesse caso:
- Apenas uma instância da entidade é rastreada, por meio de Attach.
- A instância de entidade não é consultada do banco de dados como parte da atualização.
- A aplicação dos valores originais garante que somente os valores de propriedade que realmente foram alterados sejam atualizados no banco de dados.
- Uma viagem de ida e volta é feita no banco de dados.
Assim como nos exemplos da seção anterior, os valores originais não precisam ser passados como um dicionário; uma instância de entidade ou DTO também funcionará.
Dica
Embora essa abordagem tenha características atraentes, ela requer o envio dos valores originais da entidade de e para o cliente Web. Considere cuidadosamente se essa complexidade extra vale os benefícios. Para muitos aplicativos, uma das abordagens mais simples é mais pragmática.
Como anexando um grafo serializado
O EF Core funciona com grafos de entidades conectadas por meio de chaves estrangeiras e propriedades de navegação, conforme descrito nemAlteração de chaves e navegação estrangeiras. Se esses grafos forem criados fora do uso do EF Core, por exemplo, a partir de um arquivo JSON, eles poderão ter várias instâncias da mesma entidade. Essas duplicatas precisam ser resolvidas em instâncias simples antes que o grafo possa ser rastreado.
Grafos sem duplicatas
Antes de avançarmos, é importante reconhecer que:
- Os serializadores geralmente têm opções para lidar com loops e instâncias duplicadas no grafo.
- A escolha do objeto usado como raiz do grafo geralmente pode ajudar a reduzir ou remover duplicatas.
Se possível, use opções de serialização e escolha raízes que não resultem em duplicatas. Por exemplo, o código a seguir usa Json.NET para serializar uma lista de blogs cada um com suas postagens associadas:
using var context = new BlogsContext();
var blogs = context.Blogs.Include(e => e.Posts).ToList();
var serialized = JsonConvert.SerializeObject(
blogs,
new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });
Console.WriteLine(serialized);
O JSON gerado com base nesse código é:
[
{
"Id": 1,
"Name": ".NET Blog",
"Summary": "Posts about .NET",
"Posts": [
{
"Id": 1,
"Title": "Announcing the Release of EF Core 5.0",
"Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
"BlogId": 1
},
{
"Id": 2,
"Title": "Announcing F# 5",
"Content": "F# 5 is the latest version of F#, the functional programming language...",
"BlogId": 1
}
]
},
{
"Id": 2,
"Name": "Visual Studio Blog",
"Summary": "Posts about Visual Studio",
"Posts": [
{
"Id": 3,
"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...",
"BlogId": 2
},
{
"Id": 4,
"Title": "Database Profiling with Visual Studio",
"Content": "Examine when database queries were executed and measure how long the take using...",
"BlogId": 2
}
]
}
]
Observe que não há blogs ou postagens duplicadas no JSON. Isso significa que chamadas simples a Update
funcionarão para atualizar essas entidades no banco de dados:
public static void UpdateBlogsFromJson(string json)
{
using var context = new BlogsContext();
var blogs = JsonConvert.DeserializeObject<List<Blog>>(json);
foreach (var blog in blogs)
{
context.Update(blog);
}
context.SaveChanges();
}
Manipulando duplicatas
O código no exemplo anterior serializou cada blog com suas postagens associadas. Se isso for alterado para serializar cada postagem com seu blog associado, as duplicatas serão introduzidas no JSON serializado. Por exemplo:
using var context = new BlogsContext();
var posts = context.Posts.Include(e => e.Blog).ToList();
var serialized = JsonConvert.SerializeObject(
posts,
new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });
Console.WriteLine(serialized);
O JSON serializado agora tem esta aparência:
[
{
"Id": 1,
"Title": "Announcing the Release of EF Core 5.0",
"Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
"BlogId": 1,
"Blog": {
"Id": 1,
"Name": ".NET Blog",
"Summary": "Posts about .NET",
"Posts": [
{
"Id": 2,
"Title": "Announcing F# 5",
"Content": "F# 5 is the latest version of F#, the functional programming language...",
"BlogId": 1
}
]
}
},
{
"Id": 2,
"Title": "Announcing F# 5",
"Content": "F# 5 is the latest version of F#, the functional programming language...",
"BlogId": 1,
"Blog": {
"Id": 1,
"Name": ".NET Blog",
"Summary": "Posts about .NET",
"Posts": [
{
"Id": 1,
"Title": "Announcing the Release of EF Core 5.0",
"Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
"BlogId": 1
}
]
}
},
{
"Id": 3,
"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...",
"BlogId": 2,
"Blog": {
"Id": 2,
"Name": "Visual Studio Blog",
"Summary": "Posts about Visual Studio",
"Posts": [
{
"Id": 4,
"Title": "Database Profiling with Visual Studio",
"Content": "Examine when database queries were executed and measure how long the take using...",
"BlogId": 2
}
]
}
},
{
"Id": 4,
"Title": "Database Profiling with Visual Studio",
"Content": "Examine when database queries were executed and measure how long the take using...",
"BlogId": 2,
"Blog": {
"Id": 2,
"Name": "Visual Studio Blog",
"Summary": "Posts about Visual Studio",
"Posts": [
{
"Id": 3,
"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...",
"BlogId": 2
}
]
}
}
]
Observe que o grafo agora inclui várias instâncias de Blog com o mesmo valor de chave, bem como várias instâncias de Postagem com o mesmo valor de chave. A tentativa de rastrear esse grafo como fizemos no exemplo anterior gerará:
System.InvalidOperationException: a instância do tipo de entidade "Post" não pode ser rastreada porque outra instância com o valor de chave "{Id: 2}" já está sendo rastreada. Ao anexar entidades existentes, verifique se apenas uma instância de entidade com um determinado valor de chave está anexada.
Podemos corrigir isso de duas maneiras:
- Usar opções de serialização JSON que preservam referências
- Executar a resolução de identidade enquanto o grafo está sendo acompanhado
Preservar referências
O Json.NET fornece a opção PreserveReferencesHandling
para lidar com isso. Por exemplo:
var serialized = JsonConvert.SerializeObject(
posts,
new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.All, Formatting = Formatting.Indented
});
O JSON resultante agora tem essa aparência:
{
"$id": "1",
"$values": [
{
"$id": "2",
"Id": 1,
"Title": "Announcing the Release of EF Core 5.0",
"Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
"BlogId": 1,
"Blog": {
"$id": "3",
"Id": 1,
"Name": ".NET Blog",
"Summary": "Posts about .NET",
"Posts": [
{
"$ref": "2"
},
{
"$id": "4",
"Id": 2,
"Title": "Announcing F# 5",
"Content": "F# 5 is the latest version of F#, the functional programming language...",
"BlogId": 1,
"Blog": {
"$ref": "3"
}
}
]
}
},
{
"$ref": "4"
},
{
"$id": "5",
"Id": 3,
"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...",
"BlogId": 2,
"Blog": {
"$id": "6",
"Id": 2,
"Name": "Visual Studio Blog",
"Summary": "Posts about Visual Studio",
"Posts": [
{
"$ref": "5"
},
{
"$id": "7",
"Id": 4,
"Title": "Database Profiling with Visual Studio",
"Content": "Examine when database queries were executed and measure how long the take using...",
"BlogId": 2,
"Blog": {
"$ref": "6"
}
}
]
}
},
{
"$ref": "7"
}
]
}
Observe que esse JSON substituiu duplicatas por referências como "$ref": "5"
que referem-se à instância já existente no grafo. Esse grafo pode ser novamente acompanhado usando as chamadas simples a Update
, conforme mostrado acima.
O suporte a System.Text.Json nas bibliotecas de classes base do .NET (BCL) tem uma opção semelhante que produz o mesmo resultado. Por exemplo:
var serialized = JsonSerializer.Serialize(
posts, new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true });
Como eesolver duplicatas
Se não for possível eliminar duplicatas no processo de serialização, ChangeTracker.TrackGraph fornece uma maneira de lidar com isso. O TrackGraph funciona como Add
, Attach
e Update
exceto por gerar um retorno de chamada para cada instância de entidade antes de rastreá-lo. Esse retorno de chamada pode ser usado para controlar a entidade ou ignorá-la. Por exemplo:
public static void UpdatePostsFromJsonWithIdentityResolution(string json)
{
using var context = new BlogsContext();
var posts = JsonConvert.DeserializeObject<List<Post>>(json);
foreach (var post in posts)
{
context.ChangeTracker.TrackGraph(
post, node =>
{
var keyValue = node.Entry.Property("Id").CurrentValue;
var entityType = node.Entry.Metadata;
var existingEntity = node.Entry.Context.ChangeTracker.Entries()
.FirstOrDefault(
e => Equals(e.Metadata, entityType)
&& Equals(e.Property("Id").CurrentValue, keyValue));
if (existingEntity == null)
{
Console.WriteLine($"Tracking {entityType.DisplayName()} entity with key value {keyValue}");
node.Entry.State = EntityState.Modified;
}
else
{
Console.WriteLine($"Discarding duplicate {entityType.DisplayName()} entity with key value {keyValue}");
}
});
}
context.SaveChanges();
}
Para cada entidade no grafo, esse código irá:
- Localizar o tipo de entidade e o valor da chave da entidade
- Pesquisar a entidade com essa chave no rastreador de alterações
- Se a entidade for encontrada, nenhuma ação adicional será tomada, pois a entidade será duplicada
- Se a entidade não for encontrada, ela será controlada definindo o estado como
Modified
A saída da execução desse código é:
Tracking EntityType: Post entity with key value 1
Tracking EntityType: Blog entity with key value 1
Tracking EntityType: Post entity with key value 2
Discarding duplicate EntityType: Post entity with key value 2
Tracking EntityType: Post entity with key value 3
Tracking EntityType: Blog entity with key value 2
Tracking EntityType: Post entity with key value 4
Discarding duplicate EntityType: Post entity with key value 4
Importante
Esse código pressupõe que todas as duplicatas sejam idênticas. Isso torna seguro escolher arbitrariamente uma das duplicatas a serem rastreadas ao descartar as outras. Se as duplicatas puderem ser diferentes, o código precisará decidir como determinar qual delas usar e como combinar valores de propriedade e navegação.
Observação
Para simplificar, esse código pressupõe que cada entidade tenha uma propriedade de chave primária chamada Id
. Isso pode ser codificado em uma interface ou classe base abstrata. Como alternativa, a propriedade ou as propriedades da chave primária podem ser obtidas dos metadados IEntityType de modo que esse código funcione com qualquer tipo de entidade.
Falha ao definir valores de chave
Os tipos de entidade geralmente são configurados para usar valores de chave gerados automaticamente. Esse é o padrão para propriedades de inteiros e de GUID de chaves não compostas. No entanto, se o tipo de entidade não estiver configurado para usar valores de chave gerados automaticamente, um valor de chave explícito deverá ser definido antes de acompanhar a entidade. Por exemplo, por meio do seguinte tipo de entidade:
public class Pet
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
public string Name { get; set; }
}
Considere o código que tenta rastrear duas novas instâncias de entidade sem definir valores de chave:
using var context = new BlogsContext();
context.Add(new Pet { Name = "Smokey" });
try
{
context.Add(new Pet { Name = "Clippy" }); // This will throw
}
catch (Exception e)
{
Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}
Esse código lançará:
System.InvalidOperationException: a instância do tipo de entidade 'Pet' não pode ser rastreada porque outra instância com o valor de chave "{Id: 0}" já está sendo rastreada. Ao anexar entidades existentes, verifique se apenas uma instância de entidade com um determinado valor de chave está anexada.
A correção para isso é definir valores de chave explicitamente ou configurar a propriedade de chave para usar valores de chave gerados. Consulte Valores Gerados para obter mais informações.
Utilização excessiva de uma única instância DbContext
DbContext foi projetado para representar uma unidade de trabalho de curta duração, conforme descrito em Inicialização e Configuração de DbContext, e elaborado em Controle de Alterações no EF Core. Não seguir essas diretrizes facilita a execução de situações em que é feita uma tentativa de rastrear várias instâncias da mesma entidade. Alguns exemplos comuns são:
- Usando a mesma instância DbContext para configurar o estado de teste e, em seguida, executar o teste. Isso geralmente resulta no DbContext ainda acompanhando uma instância de entidade da configuração de teste e, em seguida, tentando anexar uma nova instância no teste adequado. Em vez disso, use uma instância DbContext diferente para configurar o estado de teste e o código de teste adequado.
- Como usar uma instância DbContext compartilhada em um repositório ou código semelhante. Em vez disso, verifique se o repositório usa uma única instância DbContext para cada unidade de trabalho.
Resolução de identidade e consultas
A resolução de identidade ocorre automaticamente quando as entidades são rastreadas de uma consulta. Isso significa que, se uma instância de entidade com um determinado valor de chave já estiver controlada, essa instância controlada existente será usada em vez de criar uma nova instância. Isso tem uma consequência importante: se os dados tiverem sido alterados no banco de dados, isso não será refletido nos resultados da consulta. Esse é um bom motivo para usar uma nova instância DbContext para cada unidade de trabalho, conforme descrito em Inicialização e Configuração de DbContext, e elaborado em Controle de Alterações no EF Core.
Importante
É importante entender que o EF Core sempre executa uma consulta LINQ em um DbSet no banco de dados e retorna apenas os resultados com base no que está no banco de dados. No entanto, para uma consulta de acompanhamento, se as entidades retornadas já estiverem controladas, as instâncias controladas serão usadas em vez de criar instâncias dos dados no banco de dados.
Reload() ou GetDatabaseValues() pode ser usado quando as entidades controladas precisam ser atualizadas com os dados mais recentes do banco de dados. Consulte Como Acessar entidades controladas para obter mais informações.
Em contraste com o acompanhamento de consultas, as consultas sem acompanhamento não executam a resolução de identidade. Isso significa que as consultas sem acompanhamento podem retornar duplicatas como no caso de serialização JSON descrito anteriormente. Isso geralmente não será um problema se os resultados da consulta forem serializados e enviados para o cliente.
Dica
Não execute rotineiramente uma consulta sem acompanhamento e anexe as entidades retornadas ao mesmo contexto. Isso será mais lento e difícil de acertar do que usar uma consulta de acompanhamento.
Consultas sem acompanhamento não executam a resolução de identidade porque isso afeta o desempenho de streaming de um grande número de entidades de uma consulta. Isso ocorre porque a resolução de identidade requer manter o controle de cada instância retornada para que ela possa ser usada em vez de criar uma duplicata posteriormente.
Consultas sem acompanhamento podem ser forçadas a executar a resolução de identidade por meio de AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>). Em seguida, a consulta manterá o controle das instâncias retornadas (sem rastreá-las da maneira normal) e garantirá que nenhuma duplicata seja criada nos resultados da consulta.
Como substituir a igualdade de objetos
O EF Core usa igualdade de referência ao comparar instâncias de entidade. Esse é o caso, mesmo que os tipos de entidade substituam Object.Equals(Object) ou alterem a igualdade de objetos. No entanto, há um lugar em que a substituição da igualdade pode afetar o comportamento do EF Core: quando as navegações de coleção usam a igualdade substituída em vez da igualdade de referência e, portanto, relatam várias instâncias como a mesma.
Por isso, é recomendável evitar a substituição da igualdade de entidades. Se ela for usada, crie navegações de coleção que forcem a igualdade de referência. Por exemplo, crie um comparador de igualdade que use a igualdade de referência:
public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
private ReferenceEqualityComparer()
{
}
public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer();
bool IEqualityComparer<object>.Equals(object x, object y) => x == y;
int IEqualityComparer<object>.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}
(A partir do .NET 5, isso é incluído no BCL como ReferenceEqualityComparer.)
Esse comparador pode ser usado ao criar navegações de coleção. Por exemplo:
public ICollection<Order> Orders { get; set; }
= new HashSet<Order>(ReferenceEqualityComparer.Instance);
Comparando propriedades de chave
Além das comparações de igualdade, os valores-chave também precisam ser ordenados. Isso é importante para evitar deadlocks ao atualizar várias entidades em uma única chamada a SaveChanges. Todos os tipos usados para propriedades primárias, alternativas ou de chave estrangeira, bem como aqueles usados para índices exclusivos, devem implementar IComparable<T> e IEquatable<T>. Os tipos normalmente usados como chaves (int, Guid, string etc.) já dão suporte a essas interfaces. Tipos de chave personalizados podem adicionar essas interfaces.