Uma restrição de unicidade (unique constraint) no EF Core é um índice único no nosso modelo: declaramos esse índice com o atributo [Index] ou com HasIndex().IsUnique(), e o banco de dados recusa a segunda linha que repete o valor.
Os índices permitem duplicatas até que digamos o contrário, e essa é a única coisa que vale a pena saber antes de escrever qualquer uma das duas formas. As mesmas duas opções servem para uma única propriedade e para uma combinação de propriedades, e as duas produzem o mesmo objeto no banco de dados.
Configuração da API mínima
Para este artigo, vamos criar uma miniversão do nosso sistema solar dentro de uma API mínima:
app.MapPost("/planets", async (Planet planet, SolarSystemDbContext context) =>
{
try
{
context.Planets.Add(planet);
await context.SaveChangesAsync();
return Results.Created($"/planets/{planet.Id}", planet);
}
catch (DbUpdateException)
{
return Results.BadRequest();
}
})
.WithName("AddPlanet");
Temos um endpoint POST que retorna 201 Created se o planeta for adicionado com sucesso ao banco de dados, ou 400 Bad Request se o banco de dados rejeitar o salvamento. Capturar DbUpdateException, em vez de qualquer exceção, mantém o endpoint respondendo à falha de que este artigo trata: um segundo planeta chegando com um nome que a tabela já tem. DbUpdateException fica no namespace Microsoft.EntityFrameworkCore, então o Program.cs precisa dessa diretiva using.
Nossa API usa o SQL Server como banco de dados por meio do pacote Microsoft.EntityFrameworkCore.SqlServer (dotnet add package Microsoft.EntityFrameworkCore.SqlServer), então também vamos usar o Docker para hospedá-lo:
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=yourStrong(!)Password" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest
Com o comando docker run, iniciamos um novo contêiner do SQL Server. É também assim que nosso projeto de testes obtém um banco de dados. Há um passo a passo completo sobre como executar o SQL Server em um contêiner a partir dos nossos testes, se quisermos ir além. No bash ou no zsh, colocamos MSSQL_SA_PASSWORD=yourStrong(!)Password entre aspas simples, em vez de duplas, já que um ! dentro de aspas duplas pode iniciar uma expansão do histórico nesses shells e interromper o comando com o erro event not found.
Usamos um banco de dados relacional real porque o provedor em memória não cria índices no banco de dados e, portanto, não impõe um índice único. A Microsoft desaconselha usá-lo como dublê de teste justamente por esse tipo de diferença.
O endpoint obtém o SolarSystemDbContext por injeção de dependência, então o contexto recebe as opções pelo construtor e expõe os planetas como um DbSet:
using Microsoft.EntityFrameworkCore;
public class SolarSystemDbContext(DbContextOptions<SolarSystemDbContext> options)
: DbContext(options)
{
public DbSet<Planet> Planets { get; set; }
}
Registramos o contexto no Program.cs, antes de builder.Build(), e lemos a string de conexão na configuração:
builder.Services.AddDbContext<SolarSystemDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("SolarSystemDatabase")));
A string de conexão fica no appsettings.json e aponta para o nosso contêiner. TrustServerCertificate=True está ali porque o cliente do SQL Server usa criptografia por padrão e recusa um certificado de servidor em que nossa máquina não confia, como o do contêiner:
"ConnectionStrings": {
"SolarSystemDatabase": "Server=127.0.0.1,1433;Database=SolarSystem;User Id=sa;Password=yourStrong(!)Password;TrustServerCertificate=True"
}
Vale conhecer o comportamento padrão antes de escrever qualquer uma das formas: a documentação do EF Core sobre a unicidade de índices diz que um índice permite valores duplicados até que o marquemos como único.
Em seguida, vamos ver como adicionar restrições de unicidade à propriedade Name do planeta.
O que é uma restrição de unicidade no EF Core?
Uma restrição de unicidade impede que duas linhas tenham o mesmo valor em uma coluna e, no EF Core, configuramos essa restrição como um índice único no modelo.
Um índice permite duplicatas até que digamos o contrário. O Microsoft Learn diz isso claramente: “Por padrão, os índices não são únicos: várias linhas podem ter o(s) mesmo(s) valor(es) no conjunto de colunas do índice.” O que transforma o índice em uma restrição é marcá-lo como único.
Existem duas formas, e elas produzem o mesmo objeto no banco de dados. O atributo [Index] vai na classe de entidade, com IsUnique = true. A Fluent API chama HasIndex() e depois IsUnique(), seja em uma classe de configuração, seja diretamente em OnModelCreating().
Quem impõe a regra é o banco de dados, e não o nosso código. Depois que o índice único passa a existir, a segunda inserção com um valor duplicado falha quando chamamos SaveChangesAsync(), e é por isso que a garantia continua valendo mesmo para duas requisições que chegam no mesmo instante.
Como adicionar uma restrição de unicidade com o atributo Index?
O atributo [Index] vai na classe de entidade, e não na propriedade, porque um índice pode abranger várias propriedades.
Dentro dele, nameof(Name) indica a propriedade a indexar, e IsUnique = true transforma esse índice em uma restrição. Usar nameof em vez de um literal de string mantém a configuração ligada à propriedade, de modo que uma renomeação é detectada em tempo de compilação, e não quando o EF Core constrói o modelo.
O atributo fica no namespace Microsoft.EntityFrameworkCore, e não em System.ComponentModel.DataAnnotations, no qual ficam [Key] e [Required], então o arquivo precisa dessa diretiva using.

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
Deixamos a chave primária de lado. Uma propriedade chamada Id já é única por convenção, e marcá-la como required obriga todo corpo de requisição a trazer um valor que cabe ao banco de dados gerar.
O atributo é a forma mais curta e atende à maioria dos modelos. Entre o que ele não consegue expressar estão um índice filtrado e colunas incluídas, e é aí que a Fluent API mostra seu valor.
Vamos decorar a classe de entidade com ele:
[Index(nameof(Name), IsUnique = true)]
public class Planet
{
public int Id { get; set; }
public required string Name { get; set; }
public required double Mass { get; set; }
public required double Radius { get; set; }
public required double OrbitalPeriod { get; set; }
}
Mais especificamente, usamos o atributo Index para decorar a classe Planet. Passamos duas coisas para o atributo. A primeira é o nome da propriedade para a qual queremos criar um índice. Já a segunda define a propriedade IsUnique do índice como true. Dessa forma, garantimos que não teremos mais de um planeta com o mesmo nome.
Como adicionar uma restrição de unicidade com a Fluent API?
A Fluent API mantém a configuração em uma classe, e não em um atributo, o que deixa a entidade livre de detalhes de persistência.
A classe PlanetConfiguration implementa IEntityTypeConfiguration<Planet> e, dentro de Configure(), a chamada builder.HasIndex(p => p.Name).IsUnique() cria o mesmo índice único que o atributo cria.
Nada tem efeito até que a configuração seja descoberta. O método ApplyConfigurationsFromAssembly(GetType().Assembly), chamado dentro de OnModelCreating(), encontra todas as implementações de IEntityTypeConfiguration<T> no assembly, então uma nova classe de configuração não precisa de nenhum registro próprio.
Escrever as mesmas duas chamadas diretamente em OnModelCreating() tem exatamente o mesmo comportamento. Classes separadas só servem para manter o contexto legível quando o modelo passa de um punhado de entidades.
O índice ainda precisa chegar ao banco de dados. Uma mudança no modelo não é uma mudança no esquema: o que executa a instrução CREATE UNIQUE INDEX é adicionar e aplicar uma migração depois dessa alteração, e um modelo sem a migração correspondente deixa a restrição no nosso código e fora da tabela.
Vamos escrever essa configuração:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class PlanetConfiguration : IEntityTypeConfiguration<Planet>
{
public void Configure(EntityTypeBuilder<Planet> builder)
{
builder.HasIndex(p => p.Name)
.IsUnique();
}
}
Primeiro, criamos uma classe PlanetConfiguration que implementa a interface IEntityTypeConfiguration<TEntity>, que tem um método Configure(). Dentro dele, usamos o método HasIndex(), com o qual especificamos qual propriedade queremos indexar. Em seguida, usamos o método IsUnique() para informar ao EF Core que também queremos que o índice seja único.
O passo que coloca o índice no banco de dados é adicionar uma migração e aplicá-la, e vale a pena fazer isso assim que a configuração estiver pronta, depois do último passo abaixo.
Falta uma última coisa:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly);
}
Aqui, na classe SolarSystemDbContext, sobrescrevemos (override) o método OnModelCreating(). Nesse método, usamos o método ApplyConfigurationsFromAssembly() do ModelBuilder para aplicar todas as configurações do assembly atual.
Observe que podemos colocar a configuração dentro do método OnModelCreating(), e não em uma classe separada, mas, ao extrair as configurações para classes separadas, ganhamos legibilidade. O EF Core oferece a mesma escolha entre convenções, atributos e a Fluent API para relacionamentos.
Como tornar única uma combinação de propriedades?
Um índice composto único garante a unicidade da combinação, e não de cada propriedade isoladamente. Dois planetas podem ter o mesmo nome e dois podem ter o mesmo período orbital, mas nenhum par pode ter os dois valores iguais.
O atributo recebe vários nomes de propriedades, e a Fluent API recebe um tipo anônimo com as propriedades. As duas formas adicionam IsUnique, e as duas produzem um único índice sobre duas colunas.
A ordem das colunas importa. Um índice composto acelera as consultas que filtram pelas suas colunas e também as que filtram apenas pelas primeiras colunas que ele cobre, então a propriedade pela qual filtramos com mais frequência deve vir primeiro.

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
Cada propriedade isoladamente ainda pode se repetir, e é essa parte que pega as pessoas de surpresa. Se a propriedade Name também precisar ser única por si só, isso exige um segundo índice único, de uma só coluna, que o índice composto não fornece.
Criação de índices compostos no EF Core com atributos
Os atributos são uma ferramenta poderosa que pode nos ajudar a definir índices compostos:
[Index(nameof(Name), nameof(OrbitalPeriod), IsUnique = true)]
public class Planet
{
public int Id { get; set; }
public required string Name { get; set; }
public required double Mass { get; set; }
public required double Radius { get; set; }
public required double OrbitalPeriod { get; set; }
}
Com o atributo Index, usamos o operador nameof para especificar quais propriedades devem fazer parte do índice. No nosso caso, são Name e OrbitalPeriod. Consequentemente, com essa abordagem garantimos que haverá no máximo um planeta com os mesmos valores nessas propriedades.
Criação de índices compostos no EF Core com a Fluent API
Também podemos obter o mesmo resultado com a Fluent API:
public class PlanetConfiguration : IEntityTypeConfiguration<Planet>
{
public void Configure(EntityTypeBuilder<Planet> builder)
{
builder.HasIndex(p => new
{
p.Name,
p.OrbitalPeriod
})
.IsUnique();
}
}
Atualizamos o método Configure() na classe PlanetConfiguration, criando um novo tipo anônimo que contém as propriedades para as quais queremos criar um índice. Além de adicionar e aplicar uma nova migração, é tudo o que precisamos fazer para criar índices compostos únicos.
Toda opção do atributo tem um equivalente na Fluent API, e algumas opções da Fluent API não têm atributo nenhum:
| O que queremos | Anotação de dados | Fluent API |
|---|---|---|
| Um índice em uma propriedade | [Index(nameof(Name))] | HasIndex(p => p.Name) |
| Tornar esse índice único | [Index(nameof(Name), IsUnique = true)] | HasIndex(p => p.Name).IsUnique() |
| Um índice sobre várias propriedades | [Index(nameof(Name), nameof(OrbitalPeriod))] | HasIndex(p => new { p.Name, p.OrbitalPeriod }) |
| Escolher o nome do índice no banco de dados | [Index(nameof(Name), Name = "Index_Name")] | HasIndex(p => p.Name).HasDatabaseName("Index_Name") |
| Ordenar todas as colunas em ordem decrescente | [Index(nameof(Name), nameof(Mass), AllDescending = true)] | HasIndex(p => new { p.Name, p.Mass }).IsDescending() |
| Definir a ordem coluna por coluna | [Index(nameof(Name), nameof(Mass), IsDescending = new[] { false, true })] | HasIndex(p => new { p.Name, p.Mass }).IsDescending(false, true) |
| Indexar apenas algumas linhas | não disponível | HasIndex(p => p.Name).HasFilter("[Name] IS NOT NULL") |
| Permitir ou não que uma coluna anulável repita null | não disponível | HasIndex(p => p.Name).IsUnique().HasFilter(null) |
| Incluir colunas extras no índice | não disponível | HasIndex(p => p.Name).IncludeProperties(p => new { p.Mass }) |
| Dois índices sobre as mesmas propriedades | [Index(nameof(Name), Name = "IX_Second")] | HasIndex(p => new { p.Name }, "IX_Second") |
Qual é a diferença entre um índice único e uma restrição de unicidade no EF Core?
Em um banco de dados relacional, os dois dão a mesma garantia, e o EF Core modela essa garantia como um índice único. O Microsoft Learn é direto sobre qual usar: “Se você só quer impor a unicidade em uma coluna, defina um índice único em vez de uma chave alternativa.”
HasAlternateKey() é a outra opção, e ela existe para outra finalidade. Uma chave alternativa é um índice único com semântica adicional: o EF a trata como somente leitura, e uma chave estrangeira pode apontar para ela. Recorremos a ela quando outra entidade referencia a propriedade, e não apenas para proibir duplicatas.
É nos valores null que as expectativas falham. No SQL Server, o EF adiciona um filtro IS NOT NULL a um índice único sobre uma coluna anulável, então várias linhas podem conter null sem conflito. Passar HasFilter(null) remove esse filtro e faz o null se comportar como qualquer outro valor.
Vale acompanhar no diagrama abaixo: os dois pontos de entrada convergem para um único objeto no banco de dados, e tudo o que vem depois desse ponto é resposta do banco de dados, e não do nosso código.

Qual das duas formas escrevemos é uma questão de estilo, e o restante das práticas que seguimos ao configurar um modelo se aplica às duas da mesma forma. Nas suas orientações sobre chaves alternativas, o Microsoft Learn dá preferência ao índice único.
Conclusão
Neste artigo, vimos duas formas de aplicar restrições de unicidade a propriedades no EF Core com a abordagem code-first. Usando atributos ou a Fluent API do EF, podemos garantir a unicidade das propriedades nos nossos bancos de dados. Quer optemos pela simplicidade dos atributos, quer pelo caráter intuitivo da Fluent API, essas técnicas ajudam a modelar nossas classes de dados de forma eficaz.
Testado com .NET 10.0.10 e EF Core 10.0.12.