Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Os objetos salvos no banco de dados podem ser divididos em três grandes categorias:
- Objetos que não são estruturados e contêm um único valor. Por exemplo,
int,Guid,string,IPAddress. Estes são (um pouco vagamente) chamados de tipos primitivos. - Objetos estruturados para conter vários valores e nos quais a identidade do objeto é definida por um valor de chave. Por exemplo,
Blog, ,PostCustomer. Eles são chamados de tipos de entidade. - Objetos estruturados para conter vários valores, mas o objeto não tem nenhuma chave definindo sua identidade. Por exemplo,
Address, ,CoordinateMoney. Eles são chamados de objetos de valor e o EF Core os mapeia como tipos complexos.
Um tipo complexo agrupa várias propriedades em um único tipo de .NET contido em um tipo de entidade; ele não tem uma identidade própria e não pode ser rastreado ou consultado independentemente. Isso torna os tipos complexos a maneira natural de modelar objetos de valor.
Dica
Você pode executar e depurar no projeto de exemplo completo deste artigo sobre GitHub.
Observação
Tipos complexos foram introduzidos no EF Core 8 e foram estendidos substancialmente em versões posteriores. Os recursos são anotados abaixo com a versão que os introduziu.
Tipos complexos versus tipos de entidade de propriedade
Antes dos tipos complexos existirem, os tipos de entidade de propriedade eram a maneira recomendada de modelar objetos sem propriedades de chave. No entanto, os tipos de propriedade ainda são tipos de entidade nos bastidores: eles têm uma chave oculta e uma identidade e, portanto, operam com semântica de referência. Isso causa vários pontos de atrito que tipos complexos foram projetados para resolver.
As principais diferenças são:
| Aspecto | Tipos de entidade de propriedade | Tipos complexos |
|---|---|---|
| Identity | Ter uma chave oculta e uma identidade | Nenhuma identidade; comparado pelo valor |
| Compartilhamento de instância | A mesma instância não pode ser referenciada duas vezes | A mesma instância pode ser atribuída a várias propriedades |
| Semântica de atribuição | Semântica de referência | Semântica de valor (as propriedades são copiadas) |
| Tipo de .NET | Somente tipos de referência | Tipos de referência ou valor |
| Mapeamento de tabelas | Tabela própria, divisão de tabela ou JSON | Tabela do contêiner (divisão de tabela) ou JSON |
| Navigations | Pode conter navegaçãos para outras entidades | Não é possível conter navegação |
Atualização em massa (ExecuteUpdate) |
Sem suporte | Supported |
Por exemplo, atribuir o endereço de cobrança de um cliente para ser o mesmo que o endereço de envio falha com tipos de entidade de propriedade, porque a mesma instância de entidade não pode ser referenciada mais de uma vez:
var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types
Como tipos complexos têm semântica de valor, a mesma atribuição simplesmente copia as propriedades e funciona conforme o esperado. Da mesma forma, comparar dois valores complexos em uma consulta LINQ compara seu conteúdo, enquanto comparar duas entidades de propriedade compara suas identidades.
Por esses motivos, tipos complexos geralmente são a melhor opção para modelar objetos de valor com divisão de tabela ou mapeamento JSON. Atualmente, os usuários que usam tipos de entidade de propriedade para esses cenários são incentivados a considerar a mudança para tipos complexos.
Um exemplo simples
Considere um Address tipo que contém vários valores relacionados, mas não tem identidade própria:
public record Address
{
public required string Line1 { get; init; }
public string? Line2 { get; init; }
public required string City { get; init; }
public required string Country { get; init; }
public required string PostCode { get; init; }
}
Address em seguida, pode ser usado em vários lugares em um modelo de cliente/pedidos:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
// A required (non-nullable) complex property.
public required Address Address { get; set; }
// An optional (nullable) complex property.
public Address? SecondaryAddress { get; set; }
public List<Order> Orders { get; } = new();
}
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
Criar e salvar um cliente funciona normalmente:
var customer = new Customer
{
Name = "Willow",
Address = new Address
{
Line1 = "Barking Gate",
City = "Walpole St Peter",
Country = "UK",
PostCode = "PE14 7AV"
}
};
context.Add(customer);
await context.SaveChangesAsync();
Em um banco de dados relacional, o tipo complexo não obtém sua própria tabela. Em vez disso, suas propriedades são salvas embutidas como colunas adicionais na tabela da entidade que contém (isso é conhecido como divisão de tabela):
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
Como os tipos complexos têm semântica de valor, a mesma Address instância pode ser compartilhada entre várias propriedades sem problemas:
// The same Address instance can be assigned to multiple complex properties.
customer.Orders.Add(
new Order
{
Contents = "Tesco Tasty Treats",
BillingAddress = customer.Address,
ShippingAddress = customer.Address
});
await context.SaveChangesAsync();
Configurando tipos complexos
Ao contrário da maioria dos tipos de entidade, tipos complexos não são descobertos por convenção. Você deve configurá-los explicitamente, anotando o tipo com ComplexTypeAttribute, ou chamando a ComplexProperty API OnModelCreating fluente para cada propriedade que deve ser mapeada como um tipo complexo:
[ComplexType]
public record Address
{
public required string Line1 { get; init; }
public string? Line2 { get; init; }
public required string City { get; init; }
public required string Country { get; init; }
public required string PostCode { get; init; }
}
Configurando facetas de propriedades de tipo complexo
O construtor aninhado Property pode ser usado para configurar as propriedades escalares de um tipo complexo, assim como as propriedades de um tipo de entidade , por exemplo, para definir o nome da coluna ou o comprimento máximo:
modelBuilder.Entity<Order>()
.ComplexProperty(
o => o.ShippingAddress,
b =>
{
b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100);
b.Property(a => a.City).HasColumnName("ShipsToCity");
});
A partir do EF Core 11, você pode configurar uma propriedade aninhada dentro de um tipo complexo diretamente encadeando o acesso de membro no lambda, sem primeiro obter o construtor de tipo complexo:
// EF Core 11 allows configuring a complex-type property directly by chaining
// member access, without first obtaining the complex-type builder.
modelBuilder.Entity<Customer>()
.Property(c => c.Address.Line1)
.HasMaxLength(200);
Tipos de referência e valor
Um tipo complexo pode ser um tipo de referência .NET (a class ourecord) ou um tipo de valor (structa ourecord struct, introduzido no EF Core 10).
Mutabilidade
Como uma instância de tipo de referência pode ser compartilhada por várias propriedades, a mutação de uma de suas propriedades altera o valor em todos os lugares em que ela é usada. Isso geralmente não é o que você quer. Uma boa maneira de evitá-lo - e um ajuste natural para objetos de valor - é tornar o tipo complexo imutável, de modo que alterar um valor requer a criação de uma nova instância. O Address tipo usado ao longo deste artigo é imutável record; a alteração de um endereço é, portanto, feita com uma with expressão:
// Address is an immutable record, so create a new instance to change a value.
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Mesmo que uma nova Address instância seja atribuída, o EF ainda controla as alterações no nível de propriedade individual, portanto, somente as colunas cujos valores realmente foram alterados são atualizadas:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
A imutabilidade pode ser expressa com uma imutável class (propriedades somente init ou somente leitura), um record, um readonly structou um readonly record struct. Tipos de valor (struct) têm semântica de cópia, portanto, atribuir-lhes sempre copia os valores e evita o problema de compartilhamento acidental, mesmo quando mutáveis - mas structs mutáveis geralmente são desencorajados em C#, portanto, um formulário imutável ainda é recomendado.
Dica
Se várias entidades realmente devem observar o mesmo endereço e atualizar juntas quando ele for alterado, modele o endereço como um tipo de entidade com sua própria identidade e referencie-o por meio de uma navegação, em vez de usar um tipo complexo.
Tipos complexos aninhados
Um tipo complexo pode conter propriedades de outros tipos complexos, permitindo que você compile objetos estruturados a qualquer profundidade. Por exemplo, um Contact tipo complexo pode conter um e um Address ou mais PhoneNumber tipos complexos:
public record Address(string Line1, string? Line2, string City, string Country, string PostCode);
public record PhoneNumber(int CountryCode, long Number);
public record Contact
{
public required Address Address { get; init; }
public required PhoneNumber HomePhone { get; init; }
public required PhoneNumber WorkPhone { get; init; }
}
Quando mapeadas por meio da divisão de tabela, as colunas de um tipo complexo aninhado são prefixadas com o caminho completo para a propriedade (por exemplo, Contact_HomePhone_Number).
Tipos complexos opcionais
Por padrão, uma propriedade complexa é necessária: a propriedade CLR sempre deve ter um valor e é mapeada para colunas não anuláveis. A partir do EF Core 10, uma propriedade complexa pode ser opcional declarando-a como anulável:
public class Customer
{
public int Id { get; set; }
public required string Name { get; set; }
// A required (non-nullable) complex property.
public required Address Address { get; set; }
// An optional (nullable) complex property.
public Address? SecondaryAddress { get; set; }
public List<Order> Orders { get; } = new();
}
public class Order
{
public int Id { get; set; }
public required string Contents { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}
Uma propriedade complexa opcional que resulta null em NULL valores em todas as suas colunas.
Observação
Um tipo complexo opcional atualmente requer que pelo menos uma propriedade necessária seja definida no tipo complexo. Isso ocorre porque o EF precisa de pelo menos uma coluna não anulável para distinguir um null valor complexo de um valor complexo cujas propriedades são nulltodas .
Se o tipo complexo não tiver propriedade própria necessária, você poderá configurar uma propriedade discriminatória. Embora o EF Core ainda não dê suporte à herança para tipos complexos, o discriminador é criado como uma propriedade de sombra necessária por padrão, o que atende ao requisito acima:
// GeoLocation has no required property, so configure a discriminator. EF creates
// it as a required shadow property, which satisfies the requirement that an
// optional complex type have at least one required property.
modelBuilder.Entity<Place>()
.ComplexProperty(p => p.Location, b => b.HasDiscriminator());
Coleções de tipos complexos
A partir do EF Core 10, uma propriedade pode conter uma coleção de tipos complexos. Em bancos de dados relacionais, coleções complexas devem ser mapeadas para uma única coluna JSON usando ToJson - elas não podem ser mapeadas para uma tabela diferente:
public class Distributor
{
public int Id { get; set; }
public required string Name { get; set; }
// A collection of complex types, mapped to a single JSON column.
public List<Address> ShippingCenters { get; set; } = new();
}
// Collections of complex types must be mapped to JSON on relational providers.
modelBuilder.Entity<Distributor>()
.ComplexCollection(d => d.ShippingCenters, b => b.ToJson());
Cada elemento da coleção é armazenado como um objeto JSON dentro da matriz e toda a coleção é mapeada para uma coluna:
CREATE TABLE [Distributors] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[ShippingCenters] json NOT NULL,
CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);
Observação
Não há suporte para coleções de tipos de valor (struct) no momento; use um tipo de referência (class ou record) para elementos de coleção complexos.
Mapeando tipos complexos para JSON
Além da divisão de tabela, o EF Core 10 permite mapear uma propriedade complexa (não coleção) para uma única coluna JSON com ToJson:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.Address, c => c.ToJson());
b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});
Cada valor complexo é serializado em uma única coluna JSON em vez de se espalhar por várias colunas:
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Address] json NOT NULL,
[SecondaryAddress] json NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
No SQL Server 2025 e SQL do Azure, o EF usa o tipo de dados nativo json por padrão; em outros bancos de dados e versões de SQL Server mais antigas, o JSON é armazenado em uma coluna de texto. Você pode substituir o tipo de coluna se HasColumnType necessário.
Ao contrário da divisão de tabela, o mapeamento JSON permite coleções dentro do tipo mapeado e permite consultar e atualizar propriedades individuais dentro do documento, assim como qualquer outra propriedade. Valores dentro de colunas JSON também podem ser atualizados com eficiência em massa com ExecuteUpdateAsync.
Chaves e índices em propriedades de tipo complexo
A partir do EF Core 11, chaves e índices podem direcionar propriedades escalares aninhadas dentro de tipos complexos que não são de coleção. Isso pode ser feito com um lambda:
// EF Core 11 allows keys and indexes to target scalar properties nested
// inside non-collection complex types.
modelBuilder.Entity<Customer>()
.HasIndex(c => c.Address.PostCode);
Os mesmos caminhos podem ser configurados por nome, usando . para navegar até uma propriedade complexa:
modelBuilder.Entity<Customer>()
.HasIndex("Address.PostCode");
Para provedores relacionais, os índices também podem direcionar caminhos dentro de tipos complexos mapeados para colunas JSON. Caminhos de coleção complexos usam [] para se referir a todos os elementos ou a um indexador numérico para um elemento específico:
// Index a scalar inside every element of a JSON-mapped complex collection.
// Requires a provider/database with JSON index support, such as SQL Server 2025.
modelBuilder.Entity<Distributor>()
.HasIndex("ShippingCenters[].City");
Observação
A indexação em uma coleção complexa mapeada por JSON requer um banco de dados que dá suporte a índices JSON, como SQL Server 2025.
Para obter mais informações, consulte Chaves , Índices e restrições.
Tipos complexos com herança de entidade
A partir do EF Core 11, tipos complexos e colunas JSON podem ser usados em tipos de entidade que usam TPT (tabela por tipo) ou TPC (tabela por tipo concreto). Isso permite combinar a flexibilidade dessas estratégias de herança com o poder de modelagem de tipos complexos.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>()
.UseTptMappingStrategy()
.ComplexProperty(a => a.Details);
}
Controle de alterações
O EF Core rastreia as alterações nas propriedades individuais de um tipo complexo, portanto, somente as colunas afetadas são atualizadas quando você chama SaveChanges. Você pode inspecionar e manipular esse estado de acompanhamento por meio do rastreador de alterações.
Use EntityEntry.ComplexProperty para alcançar uma propriedade complexa e, em seguida, faça uma busca detalhada em suas propriedades escalares:
var addressEntry = context.Entry(customer).ComplexProperty(c => c.Address);
Console.WriteLine($"City is currently: {addressEntry.Property(a => a.City).CurrentValue}");
Console.WriteLine($"Address was modified: {addressEntry.Property(a => a.City).IsModified}");
A ComplexPropertyEntry API espelha a API de entidade EntityEntry : você pode ler e definir CurrentValue, verificar e definir IsModifiede navegar até outras propriedades complexas aninhadas ou coleções complexas. Propriedades complexas também são expostas por meio das APIs de valores de propriedade da entidade (CurrentValues/OriginalValues).
Consultando tipos complexos
Membros de tipo complexo podem ser usados em consultas LINQ, assim como as propriedades da própria entidade , você pode filtrar neles, projetá-los e ordenar por eles:
// Filter and project members of a complex property.
var ukCities = await context.Customers
.Where(c => c.Address.Country == "UK")
.Select(c => c.Address.City)
.ToListAsync();
Como tipos complexos têm semântica de valor, você também pode comparar um valor complexo inteiro em uma consulta e o EF comparará todas as suas propriedades:
var ordersToHomeAddress = await context.Orders
.Where(o => o.ShippingAddress == o.BillingAddress)
.ToListAsync();
Para tipos complexos mapeados para JSON, o EF Core 11 adiciona EF.Functions.JsonPathExists, que verifica se um determinado caminho JSON existe no documento:
var withPostCode = await context.Customers
.Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
.ToListAsync();
Limitações
Tipos complexos são projetados para modelar objetos de valor e, intencionalmente, não dão suporte a todos os recursos de tipos de entidade. As principais limitações são:
- Nenhuma identidade ou acompanhamento próprio. Um tipo complexo só pode existir como parte de uma entidade; você não pode ter um
DbSet<T>tipo complexo, nem rastreá-lo ou consultá-lo de forma independente. - Sem navegação. Um tipo complexo não pode conter propriedades de navegação para tipos de entidade.
- Nenhuma tabela separada. Em bancos de dados relacionais, um tipo complexo sempre é armazenado na tabela de seu contêiner (por meio da divisão de tabela) ou em uma coluna JSON - nunca em sua própria tabela.
- As coleções exigem JSON. Em provedores relacionais, coleções complexas devem ser mapeadas para JSON com
ToJson; elas não podem ser mapeadas por meio da divisão de tabela. - Não há suporte para coleções de tipos de valor. Elementos de coleção complexos devem ser tipos de referência.
- Tipos complexos opcionais exigem uma propriedade necessária. Um tipo complexo opcional (anulável) deve definir pelo menos uma propriedade necessária.
O suporte a tipos complexos continua a ser ampliado entre versões; confira as novas páginas para as adições mais recentes.