Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. 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 (de forma algo vaga) chamados tipos primitivos. - Objetos que são estruturados para conter vários valores e onde a identidade do objeto é definida por um valor de chave. Por exemplo,
Blog, ,PostCustomer. Estes são chamados tipos de entidade. - Objetos estruturados para conter múltiplos valores, mas cujo objeto não tem uma chave que defina a sua identidade. Por exemplo,
Address, ,CoordinateMoney. Estes são chamados objetos valor, e o EF Core mapeia-os como tipos complexos.
Um tipo complexo agrupa várias propriedades num único tipo .NET que está contido dentro de um tipo de entidade; não tem identidade própria e não pode ser rastreado ou consultado de forma independente. Isto faz com que os tipos complexos sejam a forma natural de modelar objetos de valor.
Tip
Pode correr e depurar o projeto de exemplo completo deste artigo no GitHub.
Note
Tipos complexos foram introduzidos no EF Core 8 e foram substancialmente estendidos em versões posteriores. As funcionalidades estão anotadas abaixo com a versão que as introduziu.
Tipos complexos vs. tipos de entidades próprias
Antes da existência dos tipos complexos, os tipos de entidade propriedade eram a forma recomendada de modelar objetos sem propriedades-chave. No entanto, os tipos de propriedade continuam a ser tipos de entidade nos bastidores: têm uma chave e identidade ocultas, e por isso operam com semântica de referência. Isto causa vários pontos de atrito que os tipos complexos são concebidos para resolver.
As principais diferenças são:
| Aspect | Tipos de entidades próprias | Tipos complexos |
|---|---|---|
| Identity | Ter uma chave e identidade ocultas | Sem identidade; comparado por valor |
| Partilha de instâncias | A mesma instância não pode ser referenciada duas vezes | A mesma instância pode ser atribuída a múltiplas propriedades |
| Semântica de atribuição | Semântica de referência | Semântica de valores (as propriedades são copiadas) |
| Tipo .NET | Apenas tipos de referência | Tipos de referência ou de valor |
| Mapeamento de tabelas | Tabela própria, divisão de tabela ou JSON | Tabela do contentor (divisão de tabelas) ou JSON |
| Navigations | Pode conter navegações para outras entidades | Não pode conter navegações |
Atualização em massa (ExecuteUpdate) |
Não suportado | Suportado |
Por exemplo, atribuir o endereço de faturação de um cliente igual ao seu endereço de envio falha com os tipos de entidade detida, porque a mesma instância de entidade não pode ser referenciada mais do que 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 os tipos complexos têm semântica de valores, a mesma atribuição simplesmente copia as propriedades e funciona como esperado. De forma semelhante, comparar dois valores complexos numa consulta LINQ compara o seu conteúdo, enquanto comparar duas entidades detidas compara as suas identidades.
Por estas razões, os tipos complexos são geralmente a melhor escolha para modelar objetos de valor com divisão de tabelas ou mapeamento JSON. Os utilizadores que atualmente utilizam tipos de entidades detidas para estes 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 podem então ser usados em vários locais ao longo de um modelo cliente/encomendas:
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 guardar 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();
Numa base de dados relacional, o tipo complexo não recebe a sua própria tabela. Em vez disso, as suas propriedades são guardadas em linha como colunas adicionais na tabela da entidade que o contém (isto é 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 valores, a mesma Address instância pode ser partilhada por múltiplas propriedades sem quaisquer 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();
Configuração de tipos complexos
Ao contrário da maioria dos tipos de entidades, os tipos complexos não são descobertos por convenção. Deve configurá-las explicitamente, seja anotando o tipo com ComplexTypeAttribute, ou chamando a ComplexProperty API Fluent em OnModelCreating 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; }
}
Configuração de facetas de propriedades de tipo complexo
O construtor aninhado Property pode ser usado para configurar as propriedades escalares de um tipo complexo, tal como as propriedades de um tipo de entidade – por exemplo, para definir o nome da coluna ou 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, pode configurar uma propriedade aninhada dentro de um tipo complexo diretamente, encadeando o acesso a membros na lambda, sem antes obter o construtor de tipos complexos:
// 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);
Referências e tipos de valor
Um tipo complexo pode ser um tipo de referência .NET (a class ou record) ou um tipo de valor (a struct ou record struct, introduzido no EF Core 10).
Mutability
Como uma instância do tipo referência pode ser partilhada por múltiplas propriedades, a mutação de uma das suas propriedades altera o valor em todos os locais onde é utilizada. Isto normalmente não é o que queres. Uma boa forma de o evitar – e um ajuste natural para objetos de valor – é tornar o tipo complexo imutável, de modo que alterar um valor exija criar uma nova instância. O Address tipo usado ao longo deste artigo é imutável record; mudar um endereço é, portanto, feito 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();
Apesar de uma instância totalmente Address nova ser atribuída, o EF continua a acompanhar as alterações ao nível da propriedade individual, pelo que apenas as colunas cujos valores realmente alterados são atualizadas:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
A imutabilidade pode ser expressa com propriedades imutáveis class (apenas de init ou só de leitura), a record, a readonly struct, ou a readonly record struct. Os tipos de valor (struct) têm semântica de cópia, por isso atribuir-lhes sempre copia os valores e evita o problema da partilha acidental, mesmo quando mutável – mas estruturas mutáveis são geralmente desencorajadas em C#, pelo que uma forma imutável continua a ser recomendada.
Tip
Se várias entidades realmente devem observar o mesmo endereço e atualizar em conjunto quando este muda, então modele o endereço como um tipo de entidade com identidade própria e referencia-o através 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-lhe construir objetos estruturados a qualquer profundidade. Por exemplo, um Contact tipo complexo pode conter tanto um Address como um 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 via divisão de tabelas, as colunas de um tipo complexo aninhado recebem o prefixo do caminho completo até à propriedade (por exemplo, Contact_HomePhone_Number).
Tipos de complexos opcionais
Por defeito, é necessária uma propriedade complexa: a propriedade CLR deve sempre ter um valor, e mapeia para colunas não anuláveis. A partir do EF Core 10, uma propriedade complexa pode ser tornada opcional ao declará-la 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 é null resulta em NULL valores em todas as suas colunas.
Note
Um tipo de complexo opcional exige atualmente que pelo menos uma propriedade obrigatória seja definida sobre o tipo de complexo. Isto deve-se ao facto de EF precisar de pelo menos uma coluna não anulável para distinguir um null valor complexo de um valor complexo cujas propriedades são todas null.
Se o tipo complexo não tiver uma propriedade obrigatória própria, pode-se configurar uma propriedade discriminadora. Embora o EF Core ainda não suporte herança para tipos complexos, o discriminador é criado como uma propriedade sombra obrigatória por defeito, o que satisfaz o 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 bases de dados relacionais, coleções complexas devem ser mapeadas para uma única coluna JSON usando ToJson – 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 do array, 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])
);
Note
Coleções de tipos de valor (struct) não são atualmente suportadas; use um tipo de referência (class ou record) para elementos complexos da coleção.
Mapear tipos complexos para JSON
Além da divisão de tabelas, o EF Core 10 permite mapear uma propriedade complexa (não de 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 é então serializado numa única coluna JSON em vez de espalhado 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 no SQL do Azure, o EF utiliza o tipo de dado nativojson por defeito; noutras bases de dados e versões mais antigas do SQL Server, o JSON é armazenado numa coluna de texto. Podes sobrescrever o tipo de coluna se HasColumnType necessário.
Ao contrário da divisão de tabelas, o mapeamento JSON permite coleções dentro do tipo mapeado e permite consultar e atualizar propriedades individuais dentro do documento tal como qualquer outra propriedade. Os valores dentro das colunas JSON também podem ser eficientemente atualizados em massa com ExecuteUpdateAsync.
Chaves e índices sobre propriedades de tipos complexos
A partir do EF Core 11, as chaves e índices podem direcionar-se para propriedades escalares aninhadas dentro de tipos complexos que não são de coleção. Isto 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 pelo nome, usando . para navegar numa propriedade complexa:
modelBuilder.Entity<Customer>()
.HasIndex("Address.PostCode");
Para fornecedores relacionais, os índices também podem direcionar caminhos dentro de tipos complexos mapeados para colunas JSON. Caminhos complexos de colecção referem-se [] 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");
Note
A indexação numa coleção complexa mapeada em JSON requer uma base de dados que suporte índices JSON, como o SQL Server 2025.
Para mais informações, consulte Chaves e Í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 utilizam TPT (tabela por tipo) ou TPC (tabela por tipo de betão). Isto permite-lhe combinar a flexibilidade destas estratégias de herança com o poder de modelação dos tipos complexos.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>()
.UseTptMappingStrategy()
.ComplexProperty(a => a.Details);
}
Acompanhamento de alterações
O EF Core acompanha alterações às propriedades individuais de um tipo complexo, por isso apenas as colunas afetadas são atualizadas quando chama SaveChanges. Pode inspecionar e manipular este estado de rastreio através do rastreador de mudanças.
Use EntityEntry.ComplexProperty para alcançar uma propriedade complexa e depois perfure as 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 da entidade EntityEntry : pode ler e definir CurrentValue, verificar e definir IsModified, e navegar para propriedades complexas aninhadas ou coleções complexas. Propriedades complexas também são expostas através das APIsCurrentValues/OriginalValues ().
Consulta a tipos complexos
Membros de tipos complexos podem ser usados em consultas LINQ tal como as propriedades da própria entidade – pode filtrá-los, 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 os tipos complexos têm semântica de valores, também pode comparar um valor complexo inteiro numa consulta, e o EF irá 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 existe um dado caminho JSON no documento:
var withPostCode = await context.Customers
.Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
.ToListAsync();
Limitations
Os tipos complexos são concebidos para modelar objetos de valor e, intencionalmente, não suportam todas as capacidades dos tipos de entidade. As principais limitações são:
- Sem identidade ou rastreio próprio. Um tipo complexo só pode existir como parte de uma entidade; Não se pode ter A
DbSet<T>de um tipo complexo, nem rastreá-lo ou consultá-lo de forma independente. - Sem navegações. Um tipo complexo não pode conter propriedades de navegação para tipos de entidades.
- Não há mesa separada. Em bases de dados relacionais, um tipo complexo é sempre armazenado na tabela do seu contentor (através de divisão de tabelas) ou numa coluna JSON – nunca numa tabela própria.
- As cobranças requerem JSON. Nos fornecedores relacionais, coleções complexas devem ser mapeadas para JSON com
ToJson; não podem ser mapeadas via divisão de tabelas. - Coleções de tipos de valor não são suportadas. Elementos complexos de coleção devem ser tipos de referência.
- Tipos de complexos opcionais requerem uma propriedade obrigatória. Um tipo de complexo opcional (anulável) deve definir pelo menos uma propriedade exigida.
O suporte a tipos complexos continua a ser alargado entre os lançamentos; Consulte as páginas do que há novidades para as últimas adições.