Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Gli oggetti salvati nel database possono essere suddivisi in tre categorie generali:
- Oggetti non strutturati e che contengono un singolo valore. Ad esempio,
int,Guid,string,IPAddress. Si tratta di tipi primitivi (un po' meno generici). - Oggetti strutturati per contenere più valori e dove l'identità dell'oggetto è definita da un valore di chiave. Ad esempio
Blog,Post,Customer. Questi tipi sono denominati tipi di entità. - Oggetti strutturati per contenere più valori, ma l'oggetto non ha una chiave che ne definisce l'identità. Ad esempio
Address,Coordinate,Money. Questi oggetti sono denominati oggetti valore e EF Core li esegue il mapping come tipi complessi.
Un tipo complesso raggruppa diverse proprietà in un singolo tipo di .NET contenuto all'interno di un tipo di entità. Non ha un'identità propria e non può essere rilevata o sottoposto a query in modo indipendente. Questo rende i tipi complessi il modo naturale per modellare gli oggetti valore.
Tip
È possibile eseguire ed eseguire il debug nel progetto di esempio completo per questo articolo in GitHub.
Annotazioni
I tipi complessi sono stati introdotti in EF Core 8 e sono stati estesi sostanzialmente nelle versioni successive. Le funzionalità sono annotate di seguito con la versione che le ha introdotte.
Tipi complessi e tipi di entità di proprietà
Prima dell'esistenza di tipi complessi, i tipi di entità di proprietà erano il modo consigliato per modellare gli oggetti senza proprietà chiave. Tuttavia, i tipi di proprietà sono ancora tipi di entità dietro le quinte: hanno una chiave nascosta e un'identità e quindi operano con semantica di riferimento. Questo causa una serie di punti di attrito che i tipi complessi sono progettati per risolvere.
Le differenze principali sono le seguenti:
| Aspect | Tipi di entità di proprietà | Tipi complessi |
|---|---|---|
| Identità | Avere una chiave nascosta e un'identità | Nessuna identità; confrontato per valore |
| Condivisione di istanze | Non è possibile fare riferimento alla stessa istanza due volte | La stessa istanza può essere assegnata a più proprietà |
| Semantica di assegnazione | Semantica di riferimento | Semantica dei valori (le proprietà vengono copiate) |
| Tipo .NET | Solo tipi di riferimento | Tipi riferimento o valore |
| Mapping tabella | Tabella personalizzata, suddivisione di tabelle o JSON | Tabella del contenitore (suddivisione di tabelle) o JSON |
| Navigations | Può contenere spostamenti ad altre entità | Impossibile contenere spostamenti |
Aggiornamento bulk (ExecuteUpdate) |
Non supportato | Supportato |
Ad esempio, l'assegnazione dell'indirizzo di fatturazione di un cliente allo stesso modo dell'indirizzo di spedizione ha esito negativo con i tipi di entità di proprietà, perché non è possibile fare riferimento alla stessa istanza di entità più di una volta:
var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types
Poiché i tipi complessi hanno semantica dei valori, la stessa assegnazione copia semplicemente le proprietà e funziona come previsto. Analogamente, il confronto di due valori complessi in una query LINQ confronta il relativo contenuto, mentre il confronto di due entità di proprietà confronta le relative identità.
Per questi motivi, i tipi complessi sono in genere la scelta migliore per la modellazione di oggetti valore con suddivisione delle tabelle o mapping JSON. Gli utenti che usano attualmente tipi di entità di proprietà per questi scenari sono invitati a prendere in considerazione il passaggio a tipi complessi.
Un semplice esempio
Si consideri un Address tipo che contiene diversi valori correlati, ma che non ha identità propria:
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 può quindi essere usato in diverse posizioni in un modello cliente/ordini:
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 creazione e il salvataggio di un cliente funziona come di consueto:
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();
In un database relazionale, il tipo complesso non ottiene la propria tabella. Al contrario, le relative proprietà vengono salvate inline come colonne aggiuntive nella tabella dell'entità contenitore (nota come suddivisione delle tabelle):
INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
Poiché i tipi complessi hanno una semantica di valori, la stessa Address istanza può essere condivisa tra più proprietà senza problemi:
// 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();
Configurazione di tipi complessi
A differenza della maggior parte dei tipi di entità, i tipi complessi non vengono individuati per convenzione. È necessario configurarli in modo esplicito, annotando il tipo con ComplexTypeAttributeo chiamando l'API Fluent in OnModelCreating per ogni proprietà di cui eseguire il ComplexProperty mapping come tipo complesso:
[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; }
}
Configurazione di facet di proprietà di tipi complessi
Il generatore annidato Property può essere usato per configurare le proprietà scalari di un tipo complesso, esattamente come le proprietà di un tipo di entità, ad esempio per impostare il nome della colonna o la lunghezza massima:
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 partire da EF Core 11, è possibile configurare una proprietà annidata all'interno di un tipo complesso direttamente concatenando l'accesso ai membri nell'espressione lambda, senza prima ottenere il generatore di tipi complessi:
// 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);
Tipi riferimento e valore
Un tipo complesso può essere un tipo di riferimento .NET (o classrecord) o un tipo valore (o structrecord struct, introdotto in EF Core 10).
Modificabilità
Poiché un'istanza di tipo riferimento può essere condivisa da più proprietà, la modifica di una delle relative proprietà modifica il valore ovunque venga usato. Questo di solito non è quello che vuoi. Un buon modo per evitarlo, e una adattabilità naturale per gli oggetti valore, consiste nel rendere il tipo complesso non modificabile, in modo che la modifica di un valore richieda la creazione di una nuova istanza. Il tipo usato in questo articolo è non modificabile. La Address modifica di un indirizzo viene quindi eseguita con un'espressionewith:record
// 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();
Anche se viene assegnata un'intera nuova Address istanza, Entity Framework tiene ancora traccia delle modifiche a livello di singola proprietà, quindi vengono aggiornate solo le colonne i cui valori effettivamente modificati vengono aggiornati:
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
L'immutabilità può essere espressa con una proprietà non modificabile class (solo init o di sola lettura), un recordoggetto , o readonly struct.readonly record struct I tipi valore (struct) hanno una semantica di copia, quindi assegnarli copiano sempre i valori ed evitano il problema di condivisione accidentale, anche se modificabili, ma gli struct modificabili sono in genere sconsigliati in C#, quindi è comunque consigliabile un modulo non modificabile.
Tip
Se più entità devono effettivamente osservare lo stesso indirizzo e aggiornarlo insieme quando cambia, modellare l'indirizzo come tipo di entità con la propria identità e farvi riferimento tramite uno spostamento, anziché usare un tipo complesso.
Tipi complessi annidati
Un tipo complesso può contenere proprietà di altri tipi complessi, consentendo di creare oggetti strutturati a qualsiasi profondità. Ad esempio, un Contact tipo complesso può contenere sia un Address oggetto che uno o più PhoneNumber tipi complessi:
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 viene eseguito il mapping tramite suddivisione di tabelle, le colonne di un tipo complesso annidato sono precedute dal percorso completo della proprietà , ad esempio Contact_HomePhone_Number.
Tipi complessi facoltativi
Per impostazione predefinita, è necessaria una proprietà complessa: la proprietà CLR deve sempre avere un valore e viene mappata a colonne non nullable. A partire da EF Core 10, una proprietà complessa può essere resa facoltativa dichiarandola come 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!;
}
Una proprietà complessa facoltativa che restituisce nullNULL valori in tutte le colonne.
Annotazioni
Un tipo complesso facoltativo richiede attualmente almeno una proprietà obbligatoria da definire nel tipo complesso. Ciò avviene perché EF richiede almeno una colonna non nullable per distinguere un null valore complesso da un valore complesso le cui proprietà sono nulltutte .
Se il tipo complesso non ha proprietà obbligatorie proprie, è invece possibile configurare una proprietà discriminatoria. Anche se EF Core non supporta ancora l'ereditarietà per i tipi complessi, il discriminare viene creato come proprietà shadow obbligatoria per impostazione predefinita, che soddisfa il requisito precedente:
// 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());
Raccolte di tipi complessi
A partire da EF Core 10, una proprietà può contenere una raccolta di tipi complessi. Nei database relazionali, le raccolte complesse devono essere mappate a una singola colonna JSON usando ToJson . Non possono essere mappate a una tabella diversa:
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());
Ogni elemento della raccolta viene archiviato come oggetto JSON all'interno della matrice e l'intera raccolta esegue il mapping a una colonna:
CREATE TABLE [Distributors] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[ShippingCenters] json NOT NULL,
CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);
Annotazioni
Le raccolte di tipi valore (struct) non sono attualmente supportate. Usare un tipo riferimento (class o record) per gli elementi della raccolta complessi.
Mapping di tipi complessi a JSON
Oltre alla suddivisione delle tabelle, EF Core 10 consente di eseguire il mapping di una proprietà complessa (non raccolta) a una singola colonna JSON con ToJson:
modelBuilder.Entity<Customer>(b =>
{
b.ComplexProperty(c => c.Address, c => c.ToJson());
b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});
Ogni valore complesso viene quindi serializzato in una singola colonna JSON anziché distribuito tra più colonne:
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])
);
In SQL Server 2025 e Azure SQL EF usa il tipo di dati nativojson per impostazione predefinita. In altri database e versioni precedenti SQL Server, JSON viene archiviato in una colonna di testo. Se necessario, è possibile eseguire l'override del tipo di HasColumnType colonna.
A differenza della suddivisione delle tabelle, il mapping JSON consente raccolte all'interno del tipo mappato e consente di eseguire query e aggiornare singole proprietà all'interno del documento esattamente come qualsiasi altra proprietà. I valori all'interno delle colonne JSON possono anche essere aggiornati in modo efficiente in blocco con ExecuteUpdateAsync.
Chiavi e indici su proprietà di tipo complesso
A partire da EF Core 11, le chiavi e gli indici possono essere destinate a proprietà scalari annidate all'interno di tipi complessi non di raccolta. Questa operazione può essere eseguita con un'espressione 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);
Gli stessi percorsi possono essere configurati in base al nome, usando . per spostarsi in una proprietà complessa:
modelBuilder.Entity<Customer>()
.HasIndex("Address.PostCode");
Per i provider relazionali, gli indici possono anche indirizzare i percorsi all'interno di tipi complessi mappati alle colonne JSON. I percorsi di raccolta complessi usano [] per fare riferimento a tutti gli elementi o a un indicizzatore numerico per un elemento specifico:
// 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");
Annotazioni
L'indicizzazione in una raccolta complessa mappata a JSON richiede un database che supporta indici JSON, ad esempio SQL Server 2025.
Per altre informazioni, vedere Chiavi e indici e vincoli.
Tipi complessi con ereditarietà delle entità
A partire da EF Core 11, i tipi complessi e le colonne JSON possono essere usati nei tipi di entità che usano TPT (tabella per tipo) o TPC (table-per-concrete-type). In questo modo è possibile combinare la flessibilità di queste strategie di ereditarietà con la potenza di modellazione di tipi complessi.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Animal>()
.UseTptMappingStrategy()
.ComplexProperty(a => a.Details);
}
Tracciamento delle modifiche
EF Core tiene traccia delle modifiche apportate alle singole proprietà di un tipo complesso, pertanto solo le colonne interessate vengono aggiornate quando si chiama SaveChanges. È possibile controllare e modificare questo stato di rilevamento tramite lo strumento di rilevamento delle modifiche.
Usare EntityEntry.ComplexProperty per raggiungere una proprietà complessa e quindi eseguire il drill-in delle proprietà scalari:
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 esegue il mirroring dell'API di entità EntityEntry : è possibile leggere e impostare CurrentValue, controllare e impostare IsModifiede passare a ulteriori proprietà complesse annidate o raccolte complesse. Le proprietà complesse vengono esposte anche tramite le API dei valori delle proprietà dell'entità (CurrentValues/OriginalValues).
Esecuzione di query su tipi complessi
I membri dei tipi complessi possono essere usati nelle query LINQ esattamente come le proprietà dell'entità stessa. È possibile filtrarli, proiettarli e ordinarli in base a essi:
// 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();
Poiché i tipi complessi hanno una semantica di valori, è anche possibile confrontare un intero valore complesso in una query e EF confronta tutte le relative proprietà:
var ordersToHomeAddress = await context.Orders
.Where(o => o.ShippingAddress == o.BillingAddress)
.ToListAsync();
Per i tipi complessi mappati a JSON, EF Core 11 aggiunge EF.Functions.JsonPathExists, che controlla se nel documento esiste un percorso JSON specificato:
var withPostCode = await context.Customers
.Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
.ToListAsync();
Limitations
I tipi complessi sono progettati per la modellazione di oggetti valore e intenzionalmente non supportano tutte le funzionalità dei tipi di entità. Le limitazioni principali sono:
- Nessuna identità o rilevamento dei propri. Un tipo complesso può esistere solo come parte di un'entità; non è possibile disporre di un
DbSet<T>tipo complesso, né tenere traccia o eseguirne una query in modo indipendente. - Nessuna navigazione. Un tipo complesso non può contenere proprietà di navigazione ai tipi di entità.
- Nessuna tabella separata. Nei database relazionali, un tipo complesso viene sempre archiviato nella tabella del contenitore (tramite suddivisione di tabelle) o in una colonna JSON, mai nella propria tabella.
- Le raccolte richiedono JSON. Nei provider relazionali, le raccolte complesse devono essere mappate a JSON con
ToJson. Non possono essere mappate tramite la suddivisione delle tabelle. - Le raccolte di tipi valore non sono supportate. Gli elementi della raccolta complessi devono essere tipi di riferimento.
- I tipi complessi facoltativi richiedono una proprietà obbligatoria. Un tipo complesso facoltativo (nullable) deve definire almeno una proprietà obbligatoria.
Il supporto dei tipi complessi continua ad essere ampliato tra le versioni; vedere le nuove pagine per le aggiunte più recenti.