From b3ff4ae72fe6c506b63910ee5ae7af20ceb608ac Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 29 Jul 2026 18:33:07 -0700 Subject: [PATCH] Add documentation for complex types Fixes #4413 --- .github/workflows/build-samples.yml | 11 +- .../core/modeling/complex-types.md | 287 ++++++++++++++++++ .../core/modeling/owned-entities.md | 3 + .../core/what-is-new/ef-core-10.0/whatsnew.md | 3 + .../core/what-is-new/ef-core-11.0/whatsnew.md | 3 + .../core/what-is-new/ef-core-8.0/whatsnew.md | 3 + .../core/what-is-new/ef-core-9.0/whatsnew.md | 3 + entity-framework/toc.yml | 2 + .../Modeling/ComplexTypes/ComplexTypes.csproj | 19 ++ .../ComplexTypes/ComplexTypesContext.cs | 113 +++++++ .../Modeling/ComplexTypes/DataAnnotations.cs | 35 +++ samples/core/Modeling/ComplexTypes/Model.cs | 70 +++++ samples/core/Modeling/ComplexTypes/Program.cs | 100 ++++++ samples/core/Samples.sln | 7 + samples/global.json | 3 +- 15 files changed, 658 insertions(+), 4 deletions(-) create mode 100644 entity-framework/core/modeling/complex-types.md create mode 100644 samples/core/Modeling/ComplexTypes/ComplexTypes.csproj create mode 100644 samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs create mode 100644 samples/core/Modeling/ComplexTypes/DataAnnotations.cs create mode 100644 samples/core/Modeling/ComplexTypes/Model.cs create mode 100644 samples/core/Modeling/ComplexTypes/Program.cs diff --git a/.github/workflows/build-samples.yml b/.github/workflows/build-samples.yml index 50bfd5fd7d..6aa1868677 100644 --- a/.github/workflows/build-samples.yml +++ b/.github/workflows/build-samples.yml @@ -2,13 +2,13 @@ name: Build Samples on: push: - branches: [live] + branches: [live, main] paths: - "samples/core/**" - "samples/end2end/**" - ".github/workflows/build-samples.yml" pull_request: - branches: [live] + branches: [live, main] paths: - "samples/core/**" - "samples/end2end/**" @@ -26,7 +26,12 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: 10.0.x - include-prerelease: true + + - name: Setup .NET 11.0 SDK (preview) + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 11.0.x + dotnet-quality: preview - name: Build samples working-directory: samples/core diff --git a/entity-framework/core/modeling/complex-types.md b/entity-framework/core/modeling/complex-types.md new file mode 100644 index 0000000000..db0c8efffe --- /dev/null +++ b/entity-framework/core/modeling/complex-types.md @@ -0,0 +1,287 @@ +--- +title: Complex Types - EF Core +description: How to configure and use complex types to model value objects in Entity Framework Core +author: AndriySvyryd +ms.date: 07/27/2026 +uid: core/modeling/complex-types +--- +# Complex Types + +Objects saved to the database can be split into three broad categories: + +- Objects that are unstructured and hold a single value. For example, `int`, `Guid`, `string`, `IPAddress`. These are (somewhat loosely) called _primitive types_. +- Objects that are structured to hold multiple values, and where the identity of the object is defined by a key value. For example, `Blog`, `Post`, `Customer`. These are called _entity types_. +- Objects that are structured to hold multiple values, but the object has no key defining its identity. For example, `Address`, `Coordinate`, `Money`. These are called _value objects_, and EF Core maps them as _complex types_. + +A _complex type_ groups several properties into a single .NET type that is contained within an entity type; it doesn't have an identity of its own and cannot be tracked or queried independently. This makes complex types the natural way to model [value objects](https://martinfowler.com/bliki/ValueObject.html). + +> [!TIP] +> You can run and debug into the [full sample project](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Modeling/ComplexTypes) for this article on GitHub. + +> [!NOTE] +> Complex types were introduced in EF Core 8, and have been extended substantially in later releases. Features are annotated below with the version that introduced them. + +## Complex types vs. owned entity types + +Before complex types existed, [owned entity types](xref:core/modeling/owned-entities) were the recommended way to model objects without key properties. However, owned types are still _entity types_ behind the scenes: they have a hidden key and identity, and therefore operate with reference semantics. This causes a number of friction points that complex types are designed to solve. + +The key differences are: + +| Aspect | Owned entity types | Complex types | +| --- | --- | --- | +| Identity | Have a hidden key and identity | No identity; compared by value | +| Instance sharing | The same instance can't be referenced twice | The same instance can be assigned to multiple properties | +| Assignment semantics | Reference semantics | Value semantics (properties are copied) | +| .NET type | Reference types only | Reference _or_ value types | +| Table mapping | Own table, table splitting, or JSON | Container's table (table splitting) or JSON | +| Navigations | Can contain navigations to other entities | Cannot contain navigations | +| Bulk update (`ExecuteUpdate`) | Not supported | Supported | + +For example, assigning a customer's billing address to be the same as their shipping address fails with owned entity types, because the same entity instance can't be referenced more than once: + +```csharp +var customer = await context.Customers.SingleAsync(c => c.Id == someId); +customer.BillingAddress = customer.ShippingAddress; +await context.SaveChangesAsync(); // Throws with owned entity types +``` + +Because complex types have value semantics, the same assignment simply copies the properties over, and works as expected. Similarly, comparing two complex values in a LINQ query compares their contents, whereas comparing two owned entities compares their identities. + +For these reasons, complex types are generally the better choice for modeling value objects with table splitting or JSON mapping. Users currently using owned entity types for these scenarios are encouraged to consider switching to complex types. + +## A simple example + +Consider an `Address` type that holds several related values but has no identity of its own: + +[!code-csharp[Address](../../../samples/core/Modeling/ComplexTypes/Model.cs?name=Address)] + +`Address` can then be used in several places across a customer/orders model: + +[!code-csharp[CustomerOrders](../../../samples/core/Modeling/ComplexTypes/Model.cs?name=CustomerOrders)] + +Creating and saving a customer works as usual: + +[!code-csharp[SaveCustomer](../../../samples/core/Modeling/ComplexTypes/Program.cs?name=SaveCustomer)] + +On a relational database, the complex type does not get its own table. Instead, its properties are saved inline as additional columns on the containing entity's table (this is known as _table splitting_): + +```sql +INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode]) +OUTPUT INSERTED.[Id] +VALUES (@p0, @p1, @p2, @p3, @p4, @p5); +``` + +Because complex types have value semantics, the same `Address` instance can be shared across multiple properties without any issues: + +[!code-csharp[SharedInstance](../../../samples/core/Modeling/ComplexTypes/Program.cs?name=SharedInstance)] + +## Configuring complex types + +Unlike most entity types, complex types are **not discovered by convention**. You must configure them explicitly, either by annotating the type with , or by calling the `ComplexProperty` Fluent API in `OnModelCreating` for each property that should be mapped as a complex type: + +### [Data Annotations](#tab/data-annotations) + +[!code-csharp[AddressAttribute](../../../samples/core/Modeling/ComplexTypes/DataAnnotations.cs?name=AddressAttribute)] + +### [Fluent API](#tab/fluent-api) + +[!code-csharp[ComplexPropertyConfig](../../../samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs?name=ComplexPropertyConfig)] + +*** + +### Configuring facets of complex type properties + +The nested `Property` builder can be used to configure the scalar properties of a complex type, just like properties of an entity type - for example, to set the column name or maximum length: + +[!code-csharp[ComplexPropertyFacets](../../../samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs?name=ComplexPropertyFacets)] + +Starting with EF Core 11, you can configure a property nested inside a complex type directly by chaining member access in the lambda, without first obtaining the complex-type builder: + +[!code-csharp[PropertyChaining](../../../samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs?name=PropertyChaining)] + +## Reference and value types + +A complex type can be a .NET [reference type](/dotnet/csharp/language-reference/keywords/reference-types) (a `class` or `record`) or a [value type](/dotnet/csharp/language-reference/builtin-types/value-types) (a `struct` or `record struct`, introduced in EF Core 10). + +### Mutability + +Because a reference-type instance can be shared by multiple properties, mutating one of its properties changes the value everywhere it is used. This is usually not what you want. A good way to avoid it - and a natural fit for value objects - is to make the complex type **immutable**, so that changing a value requires creating a new instance. The `Address` type used throughout this article is an immutable `record`; changing an address is therefore done with a `with` expression: + +[!code-csharp[ChangeImmutableRecord](../../../samples/core/Modeling/ComplexTypes/Program.cs?name=ChangeImmutableRecord)] + +Even though a whole new `Address` instance is assigned, EF still tracks changes at the individual property level, so only the columns whose values actually changed are updated: + +```sql +UPDATE [Customers] SET [Address_Line1] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +``` + +Immutability can be expressed with an immutable `class` (init-only or read-only properties), a `record`, a `readonly struct`, or a `readonly record struct`. Value types (`struct`) have copy semantics, so assigning them always copies the values and avoids the accidental-sharing problem, even when mutable - but [mutable structs are generally discouraged in C#](/archive/blogs/ericlippert/mutating-readonly-structs), so an immutable form is still recommended. + +> [!TIP] +> If several entities really should observe the same address and update together when it changes, then model the address as an _entity type_ with its own identity and reference it via a navigation, rather than using a complex type. + +## Nested complex types + +A complex type can contain properties of other complex types, allowing you to build up structured objects to any depth. For example, a `Contact` complex type might contain both an `Address` and one or more `PhoneNumber` complex types: + +```csharp +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; } +} +``` + +When mapped via table splitting, the columns of a nested complex type are prefixed with the full path to the property (for example, `Contact_HomePhone_Number`). + +## Optional complex types + +By default, a complex property is **required**: the CLR property must always have a value, and it maps to non-nullable columns. Starting with EF Core 10, a complex property can be made optional by declaring it as nullable: + +[!code-csharp[CustomerOrders](../../../samples/core/Modeling/ComplexTypes/Model.cs?name=CustomerOrders)] + +An optional complex property that is `null` results in `NULL` values in all of its columns. + +> [!NOTE] +> An optional complex type currently requires at least one **required** property to be defined on the complex type. This is because EF needs at least one non-nullable column to distinguish a `null` complex value from a complex value whose properties all happen to be `null`. + +If the complex type has no required property of its own, you can instead configure a discriminator property. While EF Core does not yet support inheritance for complex types, the discriminator is created as a **required shadow property** by default, which satisfies the requirement above: + +[!code-csharp[OptionalWithDiscriminator](../../../samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs?name=OptionalWithDiscriminator)] + +## Collections of complex types + +Starting with EF Core 10, a property can hold a _collection_ of complex types. On relational databases, complex collections must be mapped to a single JSON column using `ToJson` - they cannot be mapped to a different table: + +[!code-csharp[Distributor](../../../samples/core/Modeling/ComplexTypes/Model.cs?name=Distributor)] + +[!code-csharp[ComplexCollectionJson](../../../samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs?name=ComplexCollectionJson)] + +Each element of the collection is stored as a JSON object inside the array, and the entire collection maps to one column: + +```sql +CREATE TABLE [Distributors] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + [ShippingCenters] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id]) +); +``` + +> [!NOTE] +> Collections of value types (`struct`) are not currently supported; use a reference type (`class` or `record`) for complex collection elements. + +## Mapping complex types to JSON + +In addition to table splitting, EF Core 10 allows mapping a (non-collection) complex property to a single JSON column with `ToJson`: + +```csharp +modelBuilder.Entity(b => +{ + b.ComplexProperty(c => c.Address, c => c.ToJson()); + b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson()); +}); +``` + +Each complex value is then serialized into a single JSON column rather than spread across multiple columns: + +```sql +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]) +); +``` + +On SQL Server 2025 and Azure SQL, EF uses the native [`json` data type](/sql/t-sql/data-types/json-data-type) by default; on other databases and older SQL Server versions, JSON is stored in a text column. You can override the column type with `HasColumnType` if needed. + +Unlike table splitting, JSON mapping allows collections _within_ the mapped type, and lets you query and update individual properties inside the document just like any other property. Values inside JSON columns can also be efficiently updated in bulk with . + +## Keys and indexes on complex type properties + +Starting with EF Core 11, keys and indexes can target scalar properties nested inside non-collection complex types. This can be done with a lambda: + +[!code-csharp[IndexOnComplexProperty](../../../samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs?name=IndexOnComplexProperty)] + +The same paths can be configured by name, using `.` to navigate into a complex property: + +```csharp +modelBuilder.Entity() + .HasIndex("Address.PostCode"); +``` + +For relational providers, indexes can also target paths inside complex types mapped to JSON columns. Complex collection paths use `[]` to refer to all elements, or a numeric indexer for a specific element: + +[!code-csharp[IndexOnComplexCollection](../../../samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs?name=IndexOnComplexCollection)] + +> [!NOTE] +> Indexing into a JSON-mapped complex collection requires a database that supports JSON indexes, such as SQL Server 2025. + +For more information, see [Keys](xref:core/modeling/keys) and [Indexes and constraints](xref:core/modeling/indexes). + +## Complex types with entity inheritance + +Starting with EF Core 11, complex types and JSON columns can be used on entity types that use [TPT (table-per-type) or TPC (table-per-concrete-type)](xref:core/modeling/inheritance). This lets you combine the flexibility of these inheritance strategies with the modeling power of complex types. + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + modelBuilder.Entity() + .UseTptMappingStrategy() + .ComplexProperty(a => a.Details); +} +``` + +## Change tracking + +EF Core tracks changes to the individual properties of a complex type, so only the affected columns are updated when you call `SaveChanges`. You can inspect and manipulate this tracking state through the change tracker. + +Use `EntityEntry.ComplexProperty` to reach a complex property, and then drill into its scalar properties: + +[!code-csharp[ChangeTracking](../../../samples/core/Modeling/ComplexTypes/Program.cs?name=ChangeTracking)] + +The API mirrors the entity `EntityEntry` API: you can read and set `CurrentValue`, check and set `IsModified`, and navigate into further nested complex properties or complex collections. Complex properties are also exposed through the entity's [property values](xref:core/change-tracking/entity-entries) APIs (`CurrentValues`/`OriginalValues`). + +## Querying complex types + +Complex type members can be used in LINQ queries just like properties of the entity itself - you can filter on them, project them, and order by them: + +[!code-csharp[QueryComplexMember](../../../samples/core/Modeling/ComplexTypes/Program.cs?name=QueryComplexMember)] + +Because complex types have value semantics, you can also compare an entire complex value in a query, and EF will compare all of its properties: + +```csharp +var ordersToHomeAddress = await context.Orders + .Where(o => o.ShippingAddress == o.BillingAddress) + .ToListAsync(); +``` + +For complex types mapped to JSON, EF Core 11 adds `EF.Functions.JsonPathExists`, which checks whether a given JSON path exists in the document: + +```csharp +var withPostCode = await context.Customers + .Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode")) + .ToListAsync(); +``` + +## Limitations + +Complex types are designed for modeling value objects, and intentionally do not support every capability of entity types. The main limitations are: + +- **No identity or tracking of their own.** A complex type can only exist as part of an entity; you cannot have a `DbSet` of a complex type, nor track or query it independently. +- **No navigations.** A complex type cannot contain navigation properties to entity types. +- **No separate table.** On relational databases, a complex type is always stored in its container's table (via table splitting) or in a JSON column - never in its own table. +- **Collections require JSON.** On relational providers, complex collections must be mapped to JSON with `ToJson`; they cannot be mapped via table splitting. +- **Collections of value types are not supported.** Complex collection elements must be reference types. +- **Optional complex types require a required property.** An optional (nullable) complex type must define at least one required property. + +Complex type support continues to be broadened across releases; see the [what's new](xref:core/what-is-new/ef-core-11.0/whatsnew#complex-types) pages for the latest additions. diff --git a/entity-framework/core/modeling/owned-entities.md b/entity-framework/core/modeling/owned-entities.md index b0b9817afd..80a12e98a6 100644 --- a/entity-framework/core/modeling/owned-entities.md +++ b/entity-framework/core/modeling/owned-entities.md @@ -11,6 +11,9 @@ EF Core allows you to model entity types that can only ever appear on navigation Owned entities are essentially a part of the owner and cannot exist without it, they are conceptually similar to [aggregates](https://martinfowler.com/bliki/DDD_Aggregate.html). This means that the owned entity is by definition on the dependent side of the relationship with the owner. +> [!TIP] +> If you are modeling a [value object](https://martinfowler.com/bliki/ValueObject.html) - an object without its own identity, such as an `Address` or `Coordinate` - consider using a [complex type](xref:core/modeling/complex-types) instead of an owned entity type. Unlike owned types, complex types have value semantics and no hidden key, which avoids a number of pitfalls; see [Complex types vs. owned entity types](xref:core/modeling/complex-types#complex-types-vs-owned-entity-types) for a comparison. + ## Configuring types as owned In most providers, entity types are never configured as owned by convention - you must explicitly use the `OwnsOne` method in `OnModelCreating` or annotate the type with `OwnedAttribute` to configure the type as owned. The Azure Cosmos DB provider is an exception to this. Because Azure Cosmos DB is a document database, the provider configures all related entity types as owned by default. diff --git a/entity-framework/core/what-is-new/ef-core-10.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-10.0/whatsnew.md index 46151cdc1d..e7526fab69 100644 --- a/entity-framework/core/what-is-new/ef-core-10.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-10.0/whatsnew.md @@ -239,6 +239,9 @@ In EF 10 we improved this experience - EF will now materialize a default value f Complex types are used to model types which are contained within your entity types and have no identity of their own; while entity types are (usually) mapped to a database table, complex types can be mapped to columns in their container table ("table splitting"), or to a single JSON column. Complex types introduce document modeling techniques, which can bring substantial performance benefits as traditional JOINs are avoided, and can make your database modeling much simpler and more natural. +> [!TIP] +> For comprehensive documentation on complex types, see [Complex types](xref:core/modeling/complex-types). + ### Table splitting For example, the following maps a customer's addresses as complex types: diff --git a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md index 8aef322ac0..e02af52df9 100644 --- a/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-11.0/whatsnew.md @@ -17,6 +17,9 @@ EF11 requires the .NET 11 SDK to build and requires the .NET 11 runtime to run. ## Complex types +> [!TIP] +> For comprehensive documentation on complex types, see [Complex types](xref:core/modeling/complex-types). + ### Complex types and JSON columns on entity types with TPT/TPC inheritance diff --git a/entity-framework/core/what-is-new/ef-core-8.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-8.0/whatsnew.md index d25ca6fe92..c3b5dbf3f5 100644 --- a/entity-framework/core/what-is-new/ef-core-8.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-8.0/whatsnew.md @@ -18,6 +18,9 @@ EF8 requires the [.NET 8 SDK](https://aka.ms/get-dotnet-8) to build and requires ## Value objects using Complex Types +> [!TIP] +> This section introduces complex types. For comprehensive documentation, see [Complex types](xref:core/modeling/complex-types). + Objects saved to the database can be split into three broad categories: - Objects that are unstructured and hold a single value. For example, `int`, `Guid`, `string`, `IPAddress`. These are (somewhat loosely) called "primitive types". diff --git a/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md index e6398b4a44..cb5174832c 100644 --- a/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md @@ -339,6 +339,9 @@ We'd like to call out Andrea Canciani ([@ranma42](https://github.com/ranma42)) f ### Complex types: GroupBy and ExecuteUpdate support +> [!TIP] +> For comprehensive documentation on complex types, see [Complex types](xref:core/modeling/complex-types). + #### GroupBy > [!TIP] diff --git a/entity-framework/toc.yml b/entity-framework/toc.yml index c52a89e4ce..884ae792ee 100644 --- a/entity-framework/toc.yml +++ b/entity-framework/toc.yml @@ -199,6 +199,8 @@ href: core/modeling/table-splitting.md - name: Owned entity types href: core/modeling/owned-entities.md + - name: Complex types + href: core/modeling/complex-types.md - name: Keyless entity types href: core/modeling/keyless-entity-types.md - name: Spatial data diff --git a/samples/core/Modeling/ComplexTypes/ComplexTypes.csproj b/samples/core/Modeling/ComplexTypes/ComplexTypes.csproj new file mode 100644 index 0000000000..526f65f989 --- /dev/null +++ b/samples/core/Modeling/ComplexTypes/ComplexTypes.csproj @@ -0,0 +1,19 @@ + + + + Exe + net11.0 + EFModeling.ComplexTypes + EFModeling.ComplexTypes + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs b/samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs new file mode 100644 index 0000000000..112102df25 --- /dev/null +++ b/samples/core/Modeling/ComplexTypes/ComplexTypesContext.cs @@ -0,0 +1,113 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFModeling.ComplexTypes; + +public class ComplexTypesContext : DbContext +{ + public DbSet Customers { get; set; } + public DbSet Orders { get; set; } + public DbSet Distributors { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=EFComplexTypes;Trusted_Connection=True;ConnectRetryCount=0"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region ComplexPropertyConfig + modelBuilder.Entity(b => + { + // A required complex property; mapped to columns in the Customers table. + b.ComplexProperty(c => c.Address); + + // An optional complex property (EF Core 10+). + b.ComplexProperty(c => c.SecondaryAddress); + }); + + modelBuilder.Entity(b => + { + b.ComplexProperty(o => o.ShippingAddress); + b.ComplexProperty(o => o.BillingAddress); + }); + #endregion + + #region ComplexPropertyFacets + modelBuilder.Entity() + .ComplexProperty( + o => o.ShippingAddress, + b => + { + b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100); + b.Property(a => a.City).HasColumnName("ShipsToCity"); + }); + #endregion + + #region ComplexCollectionJson + // Collections of complex types must be mapped to JSON on relational providers. + modelBuilder.Entity() + .ComplexCollection(d => d.ShippingCenters, b => b.ToJson()); + #endregion + + #region PropertyChaining + // EF Core 11 allows configuring a complex-type property directly by chaining + // member access, without first obtaining the complex-type builder. + modelBuilder.Entity() + .Property(c => c.Address.Line1) + .HasMaxLength(200); + #endregion + + #region IndexOnComplexProperty + // EF Core 11 allows keys and indexes to target scalar properties nested + // inside non-collection complex types. + modelBuilder.Entity() + .HasIndex(c => c.Address.PostCode); + #endregion + } +} + +// Indexing into a JSON-mapped complex collection requires a database that supports +// JSON indexes (for example, SQL Server 2025). This context hosts that example +// separately so the main sample can run against LocalDB. +public class ComplexTypesJsonIndexContext : DbContext +{ + public DbSet Distributors { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=EFComplexTypesJsonIndex;Trusted_Connection=True;ConnectRetryCount=0"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .ComplexCollection(d => d.ShippingCenters, b => b.ToJson()); + + #region IndexOnComplexCollection + // 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() + .HasIndex("ShippingCenters[].City"); + #endregion + } +} + +public class ComplexTypesDiscriminatorContext : DbContext +{ + public DbSet Places { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=EFComplexTypesDiscriminator;Trusted_Connection=True;ConnectRetryCount=0"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region OptionalWithDiscriminator + // 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() + .ComplexProperty(p => p.Location, b => b.HasDiscriminator()); + #endregion + } +} + diff --git a/samples/core/Modeling/ComplexTypes/DataAnnotations.cs b/samples/core/Modeling/ComplexTypes/DataAnnotations.cs new file mode 100644 index 0000000000..a3b5eb6022 --- /dev/null +++ b/samples/core/Modeling/ComplexTypes/DataAnnotations.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace EFModeling.ComplexTypes.DataAnnotations; + +#region AddressAttribute +[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; } +} +#endregion + +// An entity referencing an attribute-decorated complex type maps it as a complex +// property without any further configuration in OnModelCreating. +public class Customer +{ + public int Id { get; set; } + public required string Name { get; set; } + public required Address Address { get; set; } + public List 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!; +} diff --git a/samples/core/Modeling/ComplexTypes/Model.cs b/samples/core/Modeling/ComplexTypes/Model.cs new file mode 100644 index 0000000000..ccda7df08b --- /dev/null +++ b/samples/core/Modeling/ComplexTypes/Model.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; + +namespace EFModeling.ComplexTypes; + +#region Address +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; } +} +#endregion + +#region CustomerOrders +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 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!; +} +#endregion + +#region Distributor +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
ShippingCenters { get; set; } = new(); +} +#endregion + +#region GeoLocation +public class GeoLocation +{ + // All properties are optional, so this complex type has no required property + // of its own to anchor an optional (nullable) usage. + public double? Latitude { get; set; } + public double? Longitude { get; set; } +} + +public class Place +{ + public int Id { get; set; } + public required string Name { get; set; } + + // An optional complex property whose type has no required properties. + public GeoLocation? Location { get; set; } +} +#endregion + diff --git a/samples/core/Modeling/ComplexTypes/Program.cs b/samples/core/Modeling/ComplexTypes/Program.cs new file mode 100644 index 0000000000..0a06587cd2 --- /dev/null +++ b/samples/core/Modeling/ComplexTypes/Program.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace EFModeling.ComplexTypes; + +public static class Program +{ + private static async Task Main() + { + using (var context = new ComplexTypesContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + #region SaveCustomer + 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(); + #endregion + + #region SharedInstance + // 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(); + #endregion + } + + using (var context = new ComplexTypesContext()) + { + var customer = await context.Customers.FirstAsync(c => c.Name == "Willow"); + + #region ChangeImmutableRecord + // 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(); + #endregion + } + + using (var context = new ComplexTypesContext()) + { + var customer = await context.Customers.FirstAsync(c => c.Name == "Willow"); + + #region ChangeTracking + 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}"); + #endregion + } + + using (var context = new ComplexTypesContext()) + { + #region QueryComplexMember + // 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(); + #endregion + + Console.WriteLine($"Found {ukCities.Count} customer(s) in the UK."); + } + + using (var context = new ComplexTypesContext()) + { + context.Add( + new Distributor + { + Name = "Acme", + ShippingCenters = + { + new Address { Line1 = "1 Main St", City = "Metropolis", Country = "US", PostCode = "10001" }, + new Address { Line1 = "2 Side St", City = "Gotham", Country = "US", PostCode = "10002" } + } + }); + + await context.SaveChangesAsync(); + } + } +} diff --git a/samples/core/Samples.sln b/samples/core/Samples.sln index 1b483456a1..aca0882b06 100644 --- a/samples/core/Samples.sln +++ b/samples/core/Samples.sln @@ -201,6 +201,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Miscellaneous\NewI EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NewInEFCore9.Cosmos", "Miscellaneous\NewInEFCore9.Cosmos\NewInEFCore9.Cosmos.csproj", "{C636065F-C124-4051-90DA-3EA2A0817471}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ComplexTypes", "Modeling\ComplexTypes\ComplexTypes.csproj", "{114CFCD7-6755-4D74-BA81-E3C46D8FD4D8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -555,6 +557,10 @@ Global {C636065F-C124-4051-90DA-3EA2A0817471}.Debug|Any CPU.Build.0 = Debug|Any CPU {C636065F-C124-4051-90DA-3EA2A0817471}.Release|Any CPU.ActiveCfg = Release|Any CPU {C636065F-C124-4051-90DA-3EA2A0817471}.Release|Any CPU.Build.0 = Release|Any CPU + {114CFCD7-6755-4D74-BA81-E3C46D8FD4D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {114CFCD7-6755-4D74-BA81-E3C46D8FD4D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {114CFCD7-6755-4D74-BA81-E3C46D8FD4D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {114CFCD7-6755-4D74-BA81-E3C46D8FD4D8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -643,6 +649,7 @@ Global {22548A1B-0833-49E9-A04E-A7E8690EAE03} = {26184A10-7CF8-4ED2-9F70-E00A55BE063B} {8E9DD759-B6D0-4424-BC6C-97ECE559CE02} = {26184A10-7CF8-4ED2-9F70-E00A55BE063B} {C636065F-C124-4051-90DA-3EA2A0817471} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} + {114CFCD7-6755-4D74-BA81-E3C46D8FD4D8} = {CA5046EC-C894-4535-8190-A31F75FDEB96} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {20C98D35-54EF-46A6-8F3B-1855C1AE4F70} diff --git a/samples/global.json b/samples/global.json index c6cbb9a0aa..16d276bb5d 100644 --- a/samples/global.json +++ b/samples/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "10.0.100" + "version": "11.0.100-preview.6.26359.118", + "rollForward": "latestMinor" } } \ No newline at end of file