Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Les objets enregistrés dans la base de données peuvent être divisés en trois grandes catégories :
- Objets non structurés et contenant une seule valeur. Par exemple,
int,Guid,string,IPAddress. Il s’agit (un peu légèrement) de types primitifs. - Objets structurés pour contenir plusieurs valeurs, et où l’identité de l’objet est définie par une valeur de clé. Par exemple,
Blog,Post,Customer. Ils sont appelés types d’entités. - Les objets structurés pour contenir plusieurs valeurs, mais l’objet n’a aucune clé définissant son identité. Par exemple,
Address,Coordinate,Money. Ces objets sont appelés objets valeur et EF Core les mappe en tant que types complexes.
Un type complexe regroupe plusieurs propriétés en un seul type .NET contenu dans un type d'entité ; il n'a pas d'identité propre et ne peut pas être suivi ou interrogé indépendamment. Cela rend les types complexes le moyen naturel de modéliser des objets de valeur.
Tip
Vous pouvez exécuter et déboguer dans l’exemple complet de projet pour cet article sur GitHub.
Note
Les types complexes ont été introduits dans EF Core 8 et ont été étendus de manière significative dans les versions ultérieures. Les fonctionnalités sont annotées ci-dessous avec la version qui les a introduites.
Types complexes et types d’entités détenus
Avant l’existence de types complexes, les types d’entités détenus étaient la méthode recommandée pour modéliser des objets sans propriétés clés. Toutefois, les types détenus sont toujours des types d’entités en arrière-plan : ils ont une clé masquée et une identité, et fonctionnent donc avec la sémantique de référence. Cela provoque un certain nombre de points de friction que les types complexes sont conçus pour résoudre.
Les différences clés sont :
| Aspect | Types d’entité détenus | Types complexes |
|---|---|---|
| Identité | Avoir une clé et une identité masquées | Aucune identité ; comparé par valeur |
| Partage d’instances | La même instance ne peut pas être référencée deux fois | La même instance peut être affectée à plusieurs propriétés |
| Sémantique d’affectation | Sémantique de référence | Sémantique des valeurs (les propriétés sont copiées) |
| Type .NET | Types de référence uniquement | Types référence ou valeur |
| Mappage de table | Propre table, fractionnement de table ou JSON | Table du conteneur (fractionnement de table) ou JSON |
| Navigations | Peut contenir des navigations vers d’autres entités | Impossible de contenir des navigations |
Mise à jour en bloc (ExecuteUpdate) |
Non pris en charge | Supported |
Par exemple, l’affectation de l’adresse de facturation d’un client à être identique à celle de son adresse d’expédition échoue avec les types d’entités détenus, car la même instance d’entité ne peut pas être référencée plusieurs fois :
var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types
Étant donné que les types complexes ont une sémantique de valeur, la même affectation copie simplement les propriétés et fonctionne comme prévu. De même, la comparaison de deux valeurs complexes dans une requête LINQ compare leur contenu, tandis que la comparaison de deux entités détenues compare leurs identités.
Pour ces raisons, les types complexes sont généralement le meilleur choix pour modéliser des objets valeur avec le fractionnement de table ou le mappage JSON. Les utilisateurs qui utilisent actuellement des types d’entités appartenant à ces scénarios sont encouragés à envisager de basculer vers des types complexes.
Un exemple simple
Considérez un Address type qui contient plusieurs valeurs associées, mais n’a pas d’identité propre :
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 peut ensuite être utilisé à plusieurs endroits dans un modèle client/commandes :
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!;
}
La création et l’enregistrement d’un client fonctionnent comme d’habitude :
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();
Sur une base de données relationnelle, le type complexe n’obtient pas sa propre table. Au lieu de cela, ses propriétés sont enregistrées inline en tant que colonnes supplémentaires sur la table de l’entité contenant (ce qui est appelé fractionnement de table) :
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
Étant donné que les types complexes ont une sémantique de valeur, la même Address instance peut être partagée entre plusieurs propriétés sans aucun problème :
// 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();
Configuration de types complexes
Contrairement à la plupart des types d’entités, les types complexes ne sont pas découverts par convention. Vous devez les configurer explicitement, soit en annotant le type avec ComplexTypeAttribute, soit en appelant l’API ComplexProperty Fluent dans OnModelCreating chaque propriété qui doit être mappée en tant que type complexe :
[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; }
}
Configuration des facettes des propriétés de type complexe
Le générateur imbriqué Property peut être utilisé pour configurer les propriétés scalaires d’un type complexe, comme les propriétés d’un type d’entité , par exemple pour définir le nom de colonne ou la longueur maximale :
modelBuilder.Entity<Order>()
.ComplexProperty(
o => o.ShippingAddress,
b =>
{
b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100);
b.Property(a => a.City).HasColumnName("ShipsToCity");
});
À compter d’EF Core 11, vous pouvez configurer une propriété imbriquée directement à l’intérieur d’un type complexe en chaînant l’accès des membres dans l’expression lambda, sans obtenir d’abord le générateur de type complexe :
// 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);
Référence et types valeur
Un type complexe peut être un type de référence .NET (a class ou record) ou un type valeur (a struct ou record struct, introduit dans EF Core 10).
Mutabilité
Étant donné qu’une instance de type référence peut être partagée par plusieurs propriétés, la mutation de l’une de ses propriétés modifie la valeur partout où elle est utilisée. Ce n’est généralement pas ce que vous voulez. Une bonne façon de l’éviter , et un ajustement naturel pour les objets valeur, consiste à rendre le type complexe immuable, afin que la modification d’une valeur nécessite la création d’une nouvelle instance. Le Address type utilisé dans cet article est immuable record; la modification d’une adresse est donc effectuée avec une with expression :
// 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();
Même si une nouvelle Address instance entière est affectée, EF effectue toujours le suivi des modifications au niveau de la propriété individuelle, de sorte que seules les colonnes dont les valeurs ont réellement changé sont mises à jour :
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
L’immuabilité peut être exprimée avec des propriétés immuables class (init uniquement ou en lecture seule), a , a record, a readonly structou a readonly record struct. Les types de valeurs (struct) ont une sémantique de copie. Par conséquent, leur affectation copie toujours les valeurs et évite le problème de partage accidentel, même lorsqu’ils sont mutables , mais les structs mutables sont généralement déconseillés en C#, de sorte qu’un formulaire immuable est toujours recommandé.
Tip
Si plusieurs entités doivent vraiment observer la même adresse et la mettre à jour ensemble lorsqu’elles changent, modélisez l’adresse en tant que type d’entité avec sa propre identité et référencez-la via une navigation, plutôt que d’utiliser un type complexe.
Types complexes imbriqués
Un type complexe peut contenir des propriétés d’autres types complexes, ce qui vous permet de créer des objets structurés à n’importe quelle profondeur. Par exemple, un Contact type complexe peut contenir à la fois un Address ou PhoneNumber plusieurs types complexes :
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; }
}
Lorsqu’elles sont mappées via le fractionnement de table, les colonnes d’un type complexe imbriqué sont précédées du chemin d’accès complet à la propriété (par exemple). Contact_HomePhone_Number
Types complexes facultatifs
Par défaut, une propriété complexe est requise : la propriété CLR doit toujours avoir une valeur, et elle est mappée à des colonnes non nullables. À compter d’EF Core 10, une propriété complexe peut être rendue facultative en la déclarant comme nullable :
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!;
}
Une propriété complexe facultative qui génère nullNULL des valeurs dans toutes ses colonnes.
Note
Un type complexe facultatif nécessite actuellement au moins une propriété requise pour être définie sur le type complexe. Cela est dû au fait que EF a besoin d’au moins une colonne non nullable pour distinguer une null valeur complexe d’une valeur complexe dont toutes les propriétés se trouvent null.
Si le type complexe n’a pas de propriété requise de sa propre part, vous pouvez configurer une propriété de discriminateur. Bien qu’EF Core ne prend pas encore en charge l’héritage pour les types complexes, le discriminateur est créé en tant que propriété d’ombre requise par défaut, ce qui répond à l’exigence ci-dessus :
// 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());
Collections de types complexes
À compter d’EF Core 10, une propriété peut contenir une collection de types complexes. Sur les bases de données relationnelles, les collections complexes doivent être mappées à une seule colonne JSON à l’aide ToJson de laquelle elles ne peuvent pas être mappées à une autre table :
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());
Chaque élément de la collection est stocké en tant qu’objet JSON à l’intérieur du tableau, et la collection entière est mappée à une colonne :
CREATE TABLE [Distributors] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[ShippingCenters] json NOT NULL,
CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);
Note
Les collections de types valeur (struct) ne sont actuellement pas prises en charge ; utilisez un type de référence (class ou record) pour les éléments de collection complexes.
Mappage de types complexes à JSON
Outre le fractionnement de table, EF Core 10 permet de mapper une propriété complexe (non collection) à une seule colonne JSON avec ToJson:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.Address, c => c.ToJson());
b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});
Chaque valeur complexe est ensuite sérialisée en une seule colonne JSON plutôt que répartie sur plusieurs colonnes :
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])
);
Sur SQL Server 2025 et Azure SQL, EF utilise le type de données natif json par défaut ; sur d’autres bases de données et versions antérieures SQL Server, JSON est stocké dans une colonne de texte. Vous pouvez remplacer le type HasColumnType de colonne si nécessaire.
Contrairement au fractionnement de table, le mappage JSON autorise les regroupements au sein du type mappé et vous permet d’interroger et de mettre à jour des propriétés individuelles dans le document comme n’importe quelle autre propriété. Les valeurs à l’intérieur des colonnes JSON peuvent également être mises à jour efficacement en bloc avec ExecuteUpdateAsync.
Clés et index sur les propriétés de type complexes
À compter d’EF Core 11, les clés et les index peuvent cibler des propriétés scalaires imbriquées dans des types complexes non collection. Pour ce faire, utilisez une expression 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);
Les mêmes chemins d’accès peuvent être configurés par nom, à l’aide . de la navigation dans une propriété complexe :
modelBuilder.Entity<Customer>()
.HasIndex("Address.PostCode");
Pour les fournisseurs relationnels, les index peuvent également cibler des chemins d’accès à l’intérieur de types complexes mappés à des colonnes JSON. Les chemins de collection complexes sont utilisés [] pour faire référence à tous les éléments ou à un indexeur numérique pour un élément spécifique :
// 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
L’indexation dans une collection complexe mappée JSON nécessite une base de données qui prend en charge les index JSON, tels que SQL Server 2025.
Pour plus d’informations, consultez Clés et index et contraintes.
Types complexes avec héritage d’entité
À compter d’EF Core 11, les types complexes et les colonnes JSON peuvent être utilisés sur les types d’entités qui utilisent TPT (table par type) ou TPC (type table par béton). Cela vous permet de combiner la flexibilité de ces stratégies d’héritage avec la puissance de modélisation des types complexes.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>()
.UseTptMappingStrategy()
.ComplexProperty(a => a.Details);
}
Suivi des modifications
EF Core effectue le suivi des modifications apportées aux propriétés individuelles d’un type complexe, de sorte que seules les colonnes affectées sont mises à jour lorsque vous appelez SaveChanges. Vous pouvez inspecter et manipuler cet état de suivi via le suivi des modifications.
Permet EntityEntry.ComplexProperty d’atteindre une propriété complexe, puis d’explorer ses propriétés scalaires :
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}");
L’API ComplexPropertyEntry met en miroir l’API d’entité EntityEntry : vous pouvez lire et définir, vérifier et définirCurrentValueIsModified, et accéder à d’autres propriétés complexes imbriquées ou collections complexes. Les propriétés complexes sont également exposées via les API de valeurs de propriété de l’entité (CurrentValues/OriginalValues).
Interrogation de types complexes
Les membres de type complexe peuvent être utilisés dans les requêtes LINQ, tout comme les propriétés de l’entité elle-même. Vous pouvez les filtrer, les projeter et les classer par eux :
// 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();
Étant donné que les types complexes ont une sémantique de valeur, vous pouvez également comparer une valeur complexe entière dans une requête, et EF compare toutes ses propriétés :
var ordersToHomeAddress = await context.Orders
.Where(o => o.ShippingAddress == o.BillingAddress)
.ToListAsync();
Pour les types complexes mappés à JSON, EF Core 11 ajoute EF.Functions.JsonPathExists, qui vérifie si un chemin JSON donné existe dans le document :
var withPostCode = await context.Customers
.Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
.ToListAsync();
Limitations
Les types complexes sont conçus pour la modélisation d’objets valeur et ne prennent intentionnellement pas en charge chaque fonctionnalité des types d’entités. Les principales limitations sont les suivantes :
- Aucune identité ni suivi de leur propre identité. Un type complexe peut exister uniquement dans le cadre d’une entité ; vous ne pouvez pas avoir un
DbSet<T>type complexe, ni le suivre ou l’interroger indépendamment. - Aucune navigation. Un type complexe ne peut pas contenir de propriétés de navigation vers des types d’entités.
- Aucune table distincte. Sur les bases de données relationnelles, un type complexe est toujours stocké dans la table de son conteneur (via le fractionnement de table) ou dans une colonne JSON , jamais dans sa propre table.
- Les collections nécessitent JSON. Sur les fournisseurs relationnels, les collections complexes doivent être mappées à JSON avec
ToJson; elles ne peuvent pas être mappées via le fractionnement de table. - Les collections de types valeur ne sont pas prises en charge. Les éléments de collection complexe doivent être des types de référence.
- Les types complexes facultatifs nécessitent une propriété obligatoire. Un type complexe facultatif (nullable) doit définir au moins une propriété requise.
La prise en charge des types complexes continue d’être élargie dans les versions ; découvrez les nouvelles pages pour les derniers ajouts.