Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Los objetos guardados en la base de datos se pueden dividir en tres categorías generales:
- Objetos que no están estructurados y contienen un único valor. Por ejemplo,
int,Guid,string,IPAddress. Estos son (ligeramente sueltos) denominados tipos primitivos. - Objetos que están estructurados para contener múltiples valores, y en los que la identidad del objeto está definida por un valor clave. Por ejemplo,
Blog,Post,Customer. Se denominan tipos de entidad. - Los objetos estructurados para contener varios valores, pero el objeto no tiene ninguna clave que defina su identidad. Por ejemplo,
Address,Coordinate,Money. Estos se denominan objetos de valor y EF Core los asigna como tipos complejos.
Un tipo complejo agrupa varias propiedades en un único tipo de .NET contenido dentro de un tipo de entidad; no tiene una identidad propia y no se puede realizar un seguimiento ni consultarse de forma independiente. Esto hace que los tipos complejos sea la forma natural de modelar objetos de valor.
Tip
Puede ejecutar y depurar en el proyecto de ejemplo completo de este artículo sobre GitHub.
Nota:
Los tipos complejos se introdujeron en EF Core 8 y se han ampliado sustancialmente en versiones posteriores. Las características se anotan a continuación con la versión que los introdujo.
Tipos complejos frente a tipos de entidad propiedad
Antes de que existieran tipos complejos, los tipos de entidad propiedad eran la manera recomendada de modelar objetos sin propiedades clave. Sin embargo, los tipos de propiedad siguen siendo tipos de entidad en segundo plano: tienen una clave y una identidad ocultas y, por lo tanto, funcionan con semántica de referencia. Esto provoca una serie de puntos de fricción que los tipos complejos están diseñados para resolver.
Las diferencias clave son:
| Aspecto | Tipos de entidad en propiedad | Tipos complejos |
|---|---|---|
| Identidad | Tener una clave y una identidad ocultas | Sin identidad; comparado por valor |
| Uso compartido de instancias | No se puede hacer referencia a la misma instancia dos veces | La misma instancia se puede asignar a varias propiedades. |
| Semántica de asignaciones | Semántica de referencia | Semántica de valores (se copian las propiedades) |
| Tipo de .NET | Solo tipos de referencia | Tipos de referencia o valor |
| Asignación de tabla | Propia tabla, división de tablas o JSON | Tabla del contenedor (división de tablas) o JSON |
| Navigations | Puede contener navegaciones a otras entidades. | No se pueden contener navegaciones |
Actualización masiva (ExecuteUpdate) |
No soportado | Soportado |
Por ejemplo, la asignación de la dirección de facturación de un cliente para que sea la misma que la dirección de envío produce un error con los tipos de entidad propiedad, ya que no se puede hacer referencia a la misma instancia de entidad más de una vez:
var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types
Dado que los tipos complejos tienen semántica de valor, la misma asignación simplemente copia las propiedades y funciona según lo previsto. De forma similar, la comparación de dos valores complejos en una consulta LINQ compara su contenido, mientras que la comparación de dos entidades de propiedad compara sus identidades.
Por estas razones, los tipos complejos suelen ser la mejor opción para modelar objetos de valor con división de tablas o asignación JSON. Se recomienda a los usuarios que usan actualmente tipos de entidad propiedad para estos escenarios considerar la posibilidad de cambiar a tipos complejos.
Un ejemplo sencillo
Considere un Address tipo que contiene varios valores relacionados, pero no tiene identidad propia:
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 Después, se puede usar en varios lugares en un modelo de clientes o 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!;
}
Crear y guardar un cliente funciona de la manera habitual:
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();
En una base de datos relacional, el tipo complejo no obtiene su propia tabla. En su lugar, sus propiedades se guardan insertadas como columnas adicionales en la tabla de la entidad contenedora (esto se conoce como división de tablas):
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
Dado que los tipos complejos tienen semántica de valor, la misma Address instancia se puede compartir entre varias propiedades sin 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();
Configuración de tipos complejos
A diferencia de la mayoría de los tipos de entidad, los tipos complejos no se detectan por convención. Debe configurarlos explícitamente, ya sea anotando el tipo con ComplexTypeAttributeo llamando a la ComplexProperty API fluent en OnModelCreating para cada propiedad que se debe asignar como un tipo complejo:
[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; }
}
Configuración de facetas de propiedades de tipo complejo
El generador anidado Property se puede usar para configurar las propiedades escalares de un tipo complejo, al igual que las propiedades de un tipo de entidad; por ejemplo, para establecer el nombre de columna o la longitud máxima:
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 de EF Core 11, puede configurar una propiedad anidada dentro de un tipo complejo directamente mediante el encadenamiento de acceso de miembro en la lambda, sin obtener primero el generador de tipos complejos:
// 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 referencia y valor
Un tipo complejo puede ser un tipo de referencia de .NET (a class o ) o recordun tipo de valor (o structrecord struct, introducido en EF Core 10).
Mutabilidad
Dado que varias propiedades pueden compartir una instancia de tipo de referencia, la mutación de una de sus propiedades cambia el valor en todas partes donde se usa. Esto no suele ser lo que quieras. Una buena manera de evitarlo , y un ajuste natural para los objetos de valor, es hacer que el tipo complejo sea inmutable, de modo que cambiar un valor requiere la creación de una nueva instancia. El Address tipo usado en este artículo es inmutable record; por lo tanto, el cambio de una dirección se realiza con una with expresión:
// 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();
Aunque se asigna una nueva Address instancia completa, EF sigue realizando un seguimiento de los cambios en el nivel de propiedad individual, por lo que solo se actualizan las columnas cuyos valores han cambiado realmente:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
La inmutabilidad se puede expresar con una propiedad inmutable class (solo inicial o de solo lectura), , recordo readonly struct.readonly record struct Los tipos de valor (struct) tienen semántica de copia, por lo que asignarlos siempre copia los valores y evita el problema de uso compartido accidental, incluso cuando las estructuras mutables, pero las estructuras mutables se desaconsejan generalmente en C#, por lo que se recomienda un formulario inmutable.
Tip
Si varias entidades realmente deben observar la misma dirección y actualizarse juntas cuando cambia, modele la dirección como un tipo de entidad con su propia identidad y haga referencia a ella a través de una navegación, en lugar de usar un tipo complejo.
Tipos complejos anidados
Un tipo complejo puede contener propiedades de otros tipos complejos, lo que le permite crear objetos estructurados a cualquier profundidad. Por ejemplo, un Contact tipo complejo puede contener uno Address o varios PhoneNumber tipos complejos:
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; }
}
Cuando se asigna a través de la división de tablas, las columnas de un tipo complejo anidado tienen como prefijo la ruta de acceso completa a la propiedad (por ejemplo, Contact_HomePhone_Number).
Tipos complejos opcionales
De forma predeterminada, se requiere una propiedad compleja: la propiedad CLR siempre debe tener un valor y se asigna a columnas que no aceptan valores NULL. A partir de EF Core 10, se puede hacer opcional una propiedad compleja declarando que admite valores NULL:
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!;
}
Una propiedad compleja opcional que da null como NULL resultado valores en todas sus columnas.
Nota:
Actualmente, un tipo complejo opcional requiere que se defina al menos una propiedad necesaria en el tipo complejo. Esto se debe a que EF necesita al menos una columna que no acepta valores NULL para distinguir un null valor complejo de un valor complejo cuyas propiedades se nullproducen como .
Si el tipo complejo no tiene ninguna propiedad necesaria propia, puede configurar una propiedad discriminador. Aunque EF Core aún no admite la herencia para tipos complejos, el discriminador se crea como una propiedad de sombra necesaria de forma predeterminada, que satisface el requisito anterior:
// 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());
Colecciones de tipos complejos
A partir de EF Core 10, una propiedad puede contener una colección de tipos complejos. En las bases de datos relacionales, las colecciones complejas deben asignarse a una sola columna JSON mediante ToJson : no se pueden asignar a una tabla 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 de la colección se almacena como un objeto JSON dentro de la matriz y toda la colección se asigna a una columna:
CREATE TABLE [Distributors] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[ShippingCenters] json NOT NULL,
CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);
Nota:
Actualmente no se admiten colecciones de tipos de valor (struct); use un tipo de referencia (class o record) para elementos de colección complejos.
Asignación de tipos complejos a JSON
Además de la división de tablas, EF Core 10 permite asignar una propiedad compleja (no colección) a una sola columna JSON con ToJson:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.Address, c => c.ToJson());
b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});
A continuación, cada valor complejo se serializa en una sola columna JSON en lugar de distribuirse entre varias columnas:
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])
);
En SQL Server 2025 y Azure SQL, EF usa el tipo de datos nativo json de forma predeterminada; en otras bases de datos y versiones anteriores de SQL Server, JSON se almacena en una columna de texto. Puede invalidar el tipo de columna con HasColumnType si es necesario.
A diferencia de la división de tablas, la asignación de JSON permite colecciones dentro del tipo asignado y permite consultar y actualizar propiedades individuales dentro del documento igual que cualquier otra propiedad. Los valores dentro de las columnas JSON también se pueden actualizar de forma eficaz de forma masiva con ExecuteUpdateAsync.
Claves e índices en propiedades de tipo complejo
A partir de EF Core 11, las claves e índices pueden tener como destino propiedades escalares anidadas dentro de tipos complejos que no son de colección. Esto se puede hacer con una expresión 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);
Las mismas rutas de acceso se pueden configurar por nombre mediante . para navegar a una propiedad compleja:
modelBuilder.Entity<Customer>()
.HasIndex("Address.PostCode");
En el caso de los proveedores relacionales, los índices también pueden apuntar a rutas dentro de tipos complejos asignados a columnas de JSON. Las rutas de acceso de colección complejas usan [] para hacer referencia a todos los elementos o a un indexador numérico para un 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");
Nota:
La indexación en una colección compleja asignada por JSON requiere una base de datos que admita índices JSON, como SQL Server 2025.
Para obtener más información, consulte Claves e índices y restricciones.
Tipos complejos con herencia de entidades
A partir de EF Core 11, se pueden usar tipos complejos y columnas JSON en tipos de entidad que usan TPT (tabla por tipo) o TPC (tipo table-per-concrete). Esto le permite combinar la flexibilidad de estas estrategias de herencia con la potencia de modelado de tipos complejos.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>()
.UseTptMappingStrategy()
.ComplexProperty(a => a.Details);
}
Seguimiento de cambios
EF Core realiza un seguimiento de los cambios realizados en las propiedades individuales de un tipo complejo, por lo que solo se actualizan las columnas afectadas cuando se llama a SaveChanges. Puede inspeccionar y manipular este estado de seguimiento a través del rastreador de cambios.
Use EntityEntry.ComplexProperty para llegar a una propiedad compleja y, a continuación, profundizar en sus propiedades 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}");
La ComplexPropertyEntry API refleja la API de entidad EntityEntry : puede leer y establecer CurrentValue, comprobar y establecer IsModifiedy navegar a propiedades complejas anidadas o colecciones complejas. Las propiedades complejas también se exponen a través de las API de valores de propiedad de la entidad (CurrentValues/OriginalValues).
Consulta de tipos complejos
Los miembros de tipo complejo se pueden usar en consultas LINQ igual que las propiedades de la propia entidad; puede filtrarlas, proyectarlas y ordenarlas:
// 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();
Dado que los tipos complejos tienen semántica de valores, también puede comparar un valor complejo completo en una consulta y EF comparará todas sus propiedades:
var ordersToHomeAddress = await context.Orders
.Where(o => o.ShippingAddress == o.BillingAddress)
.ToListAsync();
Para los tipos complejos asignados a JSON, EF Core 11 agrega EF.Functions.JsonPathExists, que comprueba si existe una ruta de acceso JSON determinada en el documento:
var withPostCode = await context.Customers
.Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
.ToListAsync();
Limitations
Los tipos complejos están diseñados para modelar objetos de valor y no admiten intencionadamente todas las funcionalidades de los tipos de entidad. Las principales limitaciones son:
- No hay identidad ni seguimiento propio. Un tipo complejo solo puede existir como parte de una entidad; no puede tener un
DbSet<T>tipo complejo, ni realizar un seguimiento ni consultarlo de forma independiente. - Sin navegación. Un tipo complejo no puede contener propiedades de navegación a tipos de entidad.
- No hay ninguna tabla independiente. En las bases de datos relacionales, un tipo complejo siempre se almacena en la tabla de su contenedor (a través de la división de tablas) o en una columna JSON, nunca en su propia tabla.
- Las colecciones requieren JSON. En los proveedores relacionales, las colecciones complejas deben asignarse a JSON con
ToJson; no se pueden asignar a través de la división de tablas. - No se admiten colecciones de tipos de valor. Los elementos de colección complejos deben ser tipos de referencia.
- Los tipos complejos opcionales requieren una propiedad necesaria. Un tipo complejo opcional (que acepta valores NULL) debe definir al menos una propiedad necesaria.
La compatibilidad con tipos complejos se sigue ampliando en todas las versiones; consulte las páginas nuevas para las adiciones más recientes.