From 6c7dfdf3a06f715f1e502f6bd493d078f9c9a8ce Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 12 Sep 2023 11:46:43 +0100 Subject: [PATCH] Add Complex Types to What's New in EF8 (#4476) --- .github/workflows/build-samples.yml | 6 + .../performance/modeling-for-performance.md | 2 +- .../core/querying/complex-query-operators.md | 21 +- .../ef-core-8.0/breaking-changes.md | 16 +- .../core/what-is-new/ef-core-8.0/whatsnew.md | 2493 +++++++++++------ entity-framework/toc.yml | 10 +- samples/core/Benchmarks/Benchmarks.csproj | 6 +- .../core/CascadeDeletes/CascadeDeletes.csproj | 8 +- .../AccessingTrackedEntities.csproj | 6 +- .../AdditionalChangeTrackingFeatures.csproj | 6 +- .../ChangeDetectionAndNotifications.csproj | 8 +- .../ChangeTrackerDebugging.csproj | 6 +- .../ChangeTrackingInEFCore.csproj | 6 +- .../ChangingFKsAndNavigations.csproj | 6 +- .../IdentityResolutionInEFCore.csproj | 6 +- samples/core/Cosmos/Cosmos.csproj | 4 +- .../DbContextPooling/DbContextPooling.csproj | 4 +- samples/core/GetStarted/EFGetStarted.csproj | 6 +- samples/core/Intro/Intro.csproj | 6 +- samples/core/Miscellaneous/Async/Async.csproj | 6 +- .../AsyncWithSystemInteractive.csproj | 6 +- .../CachingInterception.csproj | 4 +- .../Collations/Collations.csproj | 6 +- .../CommandInterception.csproj | 4 +- .../CommandLine/CommandLine.csproj | 6 +- .../CompiledModels/CompiledModels.csproj | 6 +- .../ConfiguringDbContext.csproj | 4 +- .../ConnectionInterception.csproj | 4 +- .../ConnectionResiliency.csproj | 6 +- .../DiagnosticListeners.csproj | 4 +- .../core/Miscellaneous/Events/Events.csproj | 4 +- .../Logging/Logging/Logging.csproj | 6 +- .../SimpleLogging/SimpleLogging.csproj | 6 +- .../Multitenancy/Common/Common.csproj | 2 +- .../Multitenancy/MultiDb/MultiDb.csproj | 4 +- .../SingleDbSingleTable.csproj | 4 +- .../TenantControls/TenantControls.csproj | 4 +- .../NewInEFCore6.Cosmos.csproj | 4 +- .../NewInEFCore6/NewInEFCore6.csproj | 10 +- .../NewInEFCore7/NewInEFCore7.csproj | 18 +- .../NewInEFCore8/BlogsContext.cs | 6 +- .../NewInEFCore8/ComplexTypesSample.cs | 143 + .../NewInEFCore8/DateOnlyTimeOnlySample.cs | 10 +- .../ImmutableComplexTypesSample.cs | 156 ++ .../ImmutableStructComplexTypesSample.cs | 162 ++ .../NewInEFCore8/NestedComplexTypesSample.cs | 265 ++ .../NewInEFCore8/NewInEFCore8.csproj | 20 +- .../PrimitiveCollectionsSample.cs | 17 +- .../Miscellaneous/NewInEFCore8/Program.cs | 34 +- .../NewInEFCore8/RecordComplexTypesSample.cs | 301 ++ .../NewInEFCore8/StructComplexTypesSample.cs | 163 ++ .../NullableReferenceTypes.csproj | 6 +- .../SaveChangesInterception.csproj | 4 +- .../BackingFields/BackingFields.csproj | 4 +- .../BulkConfiguration.csproj | 4 +- .../ConcurrencyTokens.csproj | 4 +- .../Modeling/DataSeeding/DataSeeding.csproj | 6 +- .../Modeling/DynamicModel/DynamicModel.csproj | 4 +- .../EntityProperties/EntityProperties.csproj | 4 +- .../Modeling/EntityTypes/EntityTypes.csproj | 4 +- .../GeneratedProperties.csproj | 4 +- .../IndexesAndConstraints.csproj | 4 +- .../Modeling/Inheritance/Inheritance.csproj | 4 +- .../KeylessEntityTypes.csproj | 6 +- samples/core/Modeling/Keys/Keys.csproj | 4 +- samples/core/Modeling/Misc/Misc.csproj | 4 +- .../OwnedEntities/OwnedEntities.csproj | 6 +- .../Relationships/Relationships.csproj | 6 +- .../core/Modeling/Sequences/Sequences.csproj | 4 +- .../ShadowAndIndexerProperties.csproj | 4 +- .../TableSplitting/TableSplitting.csproj | 6 +- .../ValueConversions/ValueConversions.csproj | 8 +- .../AspNetContextPooling.csproj | 6 +- .../AspNetContextPoolingWithState.csproj | 6 +- samples/core/Performance/Other/Other.csproj | 8 +- .../ClientEvaluation/ClientEvaluation.csproj | 6 +- .../Querying/ComplexQuery/ComplexQuery.csproj | 6 +- .../NullSemantics/NullSemantics.csproj | 4 +- .../core/Querying/Overview/Overview.csproj | 6 +- .../Querying/Pagination/Pagination.csproj | 6 +- .../Querying/QueryFilters/QueryFilters.csproj | 6 +- .../Querying/RelatedData/RelatedData.csproj | 6 +- .../Querying/SqlQueries/SqlQueries.csproj | 6 +- samples/core/Querying/Tags/Tags.csproj | 4 +- .../core/Querying/Tracking/Tracking.csproj | 6 +- .../UserDefinedFunctionMapping.csproj | 6 +- samples/core/Saving/Saving.csproj | 4 +- .../core/Schemas/Migrations/Migrations.csproj | 4 +- .../WebApplication1.Data.csproj | 6 +- .../WebApplication1.Migrations.csproj | 2 +- .../WebApplication1/WebApplication1.csproj | 8 +- .../SqlServerMigrations.csproj | 2 +- .../SqliteMigrations/SqliteMigrations.csproj | 2 +- .../WorkerService1/WorkerService1.csproj | 10 +- .../Spatial/Projections/Projections.csproj | 2 +- .../core/Spatial/SqlServer/SqlServer.csproj | 4 +- samples/core/SqlServer/SqlServer.csproj | 4 +- .../BloggingWebApi/BloggingWebApi.csproj | 2 +- .../BusinessLogic/BusinessLogic.csproj | 4 +- .../TestingWithTheDatabase.csproj | 2 +- .../TestingWithoutTheDatabase.csproj | 6 +- 101 files changed, 3067 insertions(+), 1198 deletions(-) create mode 100644 samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs create mode 100644 samples/core/Miscellaneous/NewInEFCore8/ImmutableComplexTypesSample.cs create mode 100644 samples/core/Miscellaneous/NewInEFCore8/ImmutableStructComplexTypesSample.cs create mode 100644 samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs create mode 100644 samples/core/Miscellaneous/NewInEFCore8/RecordComplexTypesSample.cs create mode 100644 samples/core/Miscellaneous/NewInEFCore8/StructComplexTypesSample.cs diff --git a/.github/workflows/build-samples.yml b/.github/workflows/build-samples.yml index 49bcd2bb0a..ee0f154c42 100644 --- a/.github/workflows/build-samples.yml +++ b/.github/workflows/build-samples.yml @@ -33,6 +33,12 @@ jobs: dotnet-version: 6.0.x include-prerelease: true + - name: Setup .NET 8.0 SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 8.0.x + include-prerelease: true + - name: Build samples working-directory: samples/core run: dotnet build diff --git a/entity-framework/core/performance/modeling-for-performance.md b/entity-framework/core/performance/modeling-for-performance.md index c46dbae875..f28289917c 100644 --- a/entity-framework/core/performance/modeling-for-performance.md +++ b/entity-framework/core/performance/modeling-for-performance.md @@ -33,7 +33,7 @@ For more perf-sensitive applications, database triggers can be defined to automa Materialized (or indexed) views are similar to regular views, except that their data is stored on disk ("materialized"), rather than calculated every time the view is queried. Such views are conceptually similar to stored computed columns, as they cache the results of potentially expensive calculations; however, they cache an entire query's resultset instead of a single column. Materialized views can be queried just like any regular table, and since they are cached on disk, such queries execute very quickly and cheaply without havnig to constantly perform the expensive calculations of the query which defines the view. -Specific support for materialized views varies across databases. In some databases (e.g. [PostgreSQL](https://www.postgresql.org/docs/current/rules-materializedviews.html)), materialized views must be manually refreshed in order for their values to be synchronized with their underlying tables. This is typically done via a timer - in cases where some data lag is acceptable - or via a trigger or stored procedure call in specific conditions. [SQL Server Indexed Views](https://learn.microsoft.com/sql/relational-databases/views/create-indexed-views), on the other hand, are automatically updated as their underlying tables are modified; this ensures that the view always shows the latest data, at the cost of slower updates. In addition, SQL Server Index Views have various restrictions on what they support; consult the [documentation](https://learn.microsoft.com/sql/relational-databases/views/create-indexed-views) for more information. +Specific support for materialized views varies across databases. In some databases (e.g. [PostgreSQL](https://www.postgresql.org/docs/current/rules-materializedviews.html)), materialized views must be manually refreshed in order for their values to be synchronized with their underlying tables. This is typically done via a timer - in cases where some data lag is acceptable - or via a trigger or stored procedure call in specific conditions. [SQL Server Indexed Views](/sql/relational-databases/views/create-indexed-views), on the other hand, are automatically updated as their underlying tables are modified; this ensures that the view always shows the latest data, at the cost of slower updates. In addition, SQL Server Index Views have various restrictions on what they support; consult the [documentation](/sql/relational-databases/views/create-indexed-views) for more information. EF doesn't currently provide any specific API for creating or maintaining views, materialized/indexed or otherwise; but it's perfectly fine to [create an empty migration and add the view definition via raw SQL](xref:core/managing-schemas/migrations/managing#arbitrary-changes-via-raw-sql). diff --git a/entity-framework/core/querying/complex-query-operators.md b/entity-framework/core/querying/complex-query-operators.md index 516955bb5e..50309c4a6c 100644 --- a/entity-framework/core/querying/complex-query-operators.md +++ b/entity-framework/core/querying/complex-query-operators.md @@ -116,23 +116,22 @@ ORDER BY [p].[AuthorId] The aggregate operators EF Core supports are as follows -.NET | SQL ------------------------- | --- -Average(x => x.Property) | AVG(Property) -Count() | COUNT(*) -LongCount() | COUNT(*) -Max(x => x.Property) | MAX(Property) -Min(x => x.Property) | MIN(Property) -Sum(x => x.Property) | SUM(Property) +| .NET | SQL | +|--------------------------|---------------| +| Average(x => x.Property) | AVG(Property) | +| Count() | COUNT(*) | +| LongCount() | COUNT(*) | +| Max(x => x.Property) | MAX(Property) | +| Min(x => x.Property) | MIN(Property) | +| Sum(x => x.Property) | SUM(Property) | Additional aggregate operators may be supported. Check your provider docs for more function mappings. Even though there is no database structure to represent an `IGrouping`, in some cases, EF Core 7.0 and newer can create the groupings after the results are returned from the database. This is similar to how the [`Include`](xref:core/querying/related-data/eager) operator works when including related collections. The following LINQ query uses the GroupBy operator to group the results by the value of their Price property. - -[!code-csharp[GroupByFinalOperator](../../../../samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs?name=GroupByFinalOperator)] +``` ```sql SELECT [b].[Price], [b].[Id], [b].[AuthorId] diff --git a/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md b/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md index bc83e18f95..1428842e36 100644 --- a/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md +++ b/entity-framework/core/what-is-new/ef-core-8.0/breaking-changes.md @@ -12,11 +12,11 @@ This page documents API and behavior changes that have the potential to break ex ## Summary -| **Breaking change** | **Impact** | -|:---------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -| [`Contains` in LINQ queries may stop working on older SQL Server versions | High | -| [SQL Server `date` and `time` now scaffold to .NET `DateOnly` and `TimeOnly`](#sqlserver-date-time-only) | Medium | -| [SQLite `Math` methods now translate to SQL](#sqlite-math) | Low | +| **Breaking change** | **Impact** | +|:---------------------------------------------------------------------------------------------------------|------------| +| [`Contains` in LINQ queries may stop working on older SQL Server versions | High | +| [SQL Server `date` and `time` now scaffold to .NET `DateOnly` and `TimeOnly`](#sqlserver-date-time-only) | Medium | +| [SQLite `Math` methods now translate to SQL](#sqlite-math) | Low | ## High-impact changes @@ -34,7 +34,7 @@ Previously, when the `Contains` operator was used in LINQ queries with a paramet Starting with EF Core 8.0, EF now generates SQL that is more efficient, but is unsupported on SQL Server 2014 and below. -Note that newer SQL Server versions may be configured with an older [compatibility level](https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-database-transact-sql-compatibility-level), also making them incompatible with the new SQL. This can also occur with an Azure SQL database which was migrated from a previous on-premises SQL Server instance, carrying over the old compatibility level. +Note that newer SQL Server versions may be configured with an older [compatibility level](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level), also making them incompatible with the new SQL. This can also occur with an Azure SQL database which was migrated from a previous on-premises SQL Server instance, carrying over the old compatibility level. #### Why @@ -56,7 +56,7 @@ FROM [Blogs] AS [b] WHERE [b].[Name] IN (N'Blog1', N'Blog2') ``` -Such insertion of constant values into the SQL creates many performance problems, defeating query plan caching and causing unneeded evictions of other queries. The new EF Core 8.0 translation uses the SQL Server [`OPENJSON`](https://learn.microsoft.com/sql/t-sql/functions/openjson-transact-sql) function to instead transfer the values as a JSON array. This solves the performance issues inherent in the previous technique; however, the `OPENJSON` function is unavailable in SQL Server 2014 and below. +Such insertion of constant values into the SQL creates many performance problems, defeating query plan caching and causing unneeded evictions of other queries. The new EF Core 8.0 translation uses the SQL Server [`OPENJSON`](/sql/t-sql/functions/openjson-transact-sql) function to instead transfer the values as a JSON array. This solves the performance issues inherent in the previous technique; however, the `OPENJSON` function is unavailable in SQL Server 2014 and below. For more information about this change, [see this blog post](https://devblogs.microsoft.com/dotnet/announcing-ef8-preview-4/). @@ -68,7 +68,7 @@ If your database is SQL Server 2016 (13.x) or newer, or if you're using Azure SQ SELECT name, compatibility_level FROM sys.databases; ``` -If the compatibility level is below 130 (SQL Server 2016), consider modifying it to a newer value ([documentation]( https://learn.microsoft.com/sql/t-sql/statements/alter-database-transact-sql-compatibility-level#best-practices-for-upgrading-database-compatibility-leve). +If the compatibility level is below 130 (SQL Server 2016), consider modifying it to a newer value ([documentation](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level#best-practices-for-upgrading-database-compatibility-leve). Otherwise, if your database version really is older than SQL Server 2016, or is set to an old compatibility level which you cannot change for some reason, configure EF Core to revert to the older, less efficient SQL as follows: 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 5a72914caf..400f7b89d8 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 @@ -2,7 +2,7 @@ title: What's New in EF Core 8 description: Overview of new features in EF Core 8 author: ajcvickers -ms.date: 05/14/2023 +ms.date: 09/11/2023 uid: core/what-is-new/ef-core-8.0/whatsnew --- @@ -15,773 +15,1278 @@ EF8 is available as [daily builds](https://github.com/dotnet/efcore/blob/main/do > [!TIP] > You can run and debug into the samples by [downloading the sample code from GitHub](https://github.com/dotnet/EntityFramework.Docs). Each section links to the source code specific to that section. -EF8 previews currently target .NET 6, and can therefore be used with either [.NET 6 (LTS)](https://dotnet.microsoft.com/download/dotnet/6.0) or [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). There is a good chance that EF8 will be changed to target .NET 8 before it's released. +EF8 requires the [latest .NET 8 Preview SDK](https://dotnet.microsoft.com/download/dotnet/8.0). EF8 will not run on earlier .NET versions, and will not run on .NET Framework. -## Sample model +## Value objects using Complex Types -Many of the examples below use a simple model with blogs, posts, tags, and authors: +Objects saved to the database can be split into three broad categories: -[!code-csharp[BlogsModel](../../../../samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs?name=BlogsModel)] +- 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 identity. For example, `Address`, `Coordinate`. -Some of the examples also use aggregate types, which are mapped in different ways in different samples. There is one aggregate type for contacts: +Prior to EF8, there was no good way to map the third type of object. [Owned types](xref:core/modeling/owned-entities) can be used, but since owned types are actually entity types, they have semantics based on a key value, even when that key value is hidden. -[!code-csharp[ContactDetailsAggregate](../../../../samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs?name=ContactDetailsAggregate)] +EF8 now supports "Complex Types" to cover this third type of object. Complex type objects: -And a second aggregate type for post metadata: +- Are not identified or tracked by key value. +- Must be defined as part of an entity type. (In other words, you cannot have a `DbSet` of a complex type.) +- Can be either .NET [value types](/dotnet/csharp/language-reference/builtin-types/value-types) or [reference types](/dotnet/csharp/language-reference/keywords/reference-types). +- Instances can be shared by multiple properties. -[!code-csharp[PostMetadataAggregate](../../../../samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs?name=PostMetadataAggregate)] +### Simple example -> [!TIP] -> The sample model can be found in [BlogsContext.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs). - -## New in EF8 Preview 1 +For example, consider an `Address` type: -### Enhancements to JSON column support + +[!code-csharp[Address](../../../../samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs?name=Address)] -EF8 includes improvements to the [JSON column mapping support introduced in EF7](xref:core/what-is-new/ef-core-7.0/whatsnew#json-columns). +`Address` is then used in three places in a simple customer/orders model: -> [!TIP] -> The code shown here comes from [JsonColumnsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs). + +[!code-csharp[CustomerOrders](../../../../samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs?name=CustomerOrders)] -EF8 supports indexing in JSON arrays when executing queries. For example, the following query checks whether the first two updates were made before a given date. +Let's create and save a customer with their address: -[!code-csharp[CollectionIndexPredicate](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=CollectionIndexPredicate)] +[!code-csharp[SaveCustomer](../../../../samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs?name=SaveCustomer)] -This translates into the following SQL when using SQL Server: +This results in the following row being inserted into the database: ```sql -SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata] -FROM [Posts] AS [p] -WHERE CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) < @__cutoff_0 - AND CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) < @__cutoff_0 +INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode]) +OUTPUT INSERTED.[Id] +VALUES (@p0, @p1, @p2, @p3, @p4, @p5); ``` +Notice that the complex types do not get their own tables. Instead, they are saved inline to columns of the `Customers` table. This matches the table sharing behavior of owned types. + > [!NOTE] -> This query will succeed even if a given post does not have any updates, or only has a single update. In such a case, `JSON_VALUE` returns `NULL` and the predicate is not matched. +> We don't plan to allow complex types to be mapped to their own table. However, in a future release, we do plan to allow the complex type to be saved as a JSON document in a single column. Vote for [Issue #31252](https://github.com/dotnet/efcore/issues/31252) if this is important to you. -Indexing into JSON arrays can also be used to project elements from an array into the final results. For example, the following query projects out the `UpdatedOn` date for the first and second updates of each post. +Now let's say we want to ship an order to a customer and use the customer's address as both the default billing an shipping address. The natural way to do this is to copy the `Address` object from the `Customer` into the `Order`. For example: -[!code-csharp[CollectionIndexProjectionNullable](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=CollectionIndexProjectionNullable)] - -This translates into the following SQL when using SQL Server: - -```sql -SELECT [p].[Title], - CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate], - CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate] -FROM [Posts] AS [p] -``` + #region CreateOrder + customer.Orders.Add( + new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, }); -As noted above, `JSON_VALUE` returns null if the element of the array does not exist. This is handled in the query by casting the projected value to a nullable `DateOnly`. An alternative to casting the value is to filter the query results so that `JSON_VALUE` will never return null. For example: + await context.SaveChangesAsync(); - -[!code-csharp[CollectionIndexProjection](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=CollectionIndexProjection)] +[!code-csharp[CreateOrder](../../../../samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs?name=CreateOrder)] -This translates into the following SQL when using SQL Server: +With complex types, this works as expected, and the address is inserted into the `Orders` table: ```sql -SELECT [p].[Title], - CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate], - CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate] -FROM [Posts] AS [p] - WHERE (CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) IS NOT NULL) - AND (CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) IS NOT NULL) +INSERT INTO [Orders] ([Contents], [CustomerId], + [BillingAddress_City], [BillingAddress_Country], [BillingAddress_Line1], [BillingAddress_Line2], [BillingAddress_PostCode], + [ShippingAddress_City], [ShippingAddress_Country], [ShippingAddress_Line1], [ShippingAddress_Line2], [ShippingAddress_PostCode]) +OUTPUT INSERTED.[Id] +VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11); ``` -### Raw SQL queries for unmapped types - -EF7 introduced [raw SQL queries returning scalar types](xref:core/querying/sql-queries#querying-scalar-(non-entity)-types). This is enhanced in EF8 to include raw SQL queries returning any mappable CLR type, without including that type in the EF model. +So far you might be saying, "but I could do this with owned types!" However, the "entity type" semantics of owned types quickly get in the way. For example, running the code above with owned types results in a slew of warnings and then an error: -> [!TIP] -> The code shown here comes from [RawSqlSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs). +```text +warn: 8/20/2023 12:48:01.678 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update) + The same entity is being tracked as different entity types 'Order.BillingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome. +warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update) + The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Customer.Address#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome. +warn: 8/20/2023 12:48:01.687 CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001] (Microsoft.EntityFrameworkCore.Update) + The same entity is being tracked as different entity types 'Order.ShippingAddress#Address' and 'Order.BillingAddress#Address' with defining navigations. If a property value changes, it will result in two store changes, which might not be the desired outcome. +fail: 8/20/2023 12:48:01.709 CoreEventId.SaveChangesFailed[10000] (Microsoft.EntityFrameworkCore.Update) + An exception occurred in the database while saving changes for context type 'NewInEfCore8.ComplexTypesSample+CustomerContext'. + System.InvalidOperationException: Cannot save instance of 'Order.ShippingAddress#Address' because it is an owned entity without any reference to its owner. Owned entities can only be saved as part of an aggregate also including the owner entity. + at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave() +``` -Queries using unmapped types are executed using or . The former uses string interpolation to parameterize the query, which helps ensure that all non-constant values are parameterized. For example, consider the following database table: +This is because a single instance of the `Address` entity type (with the same hidden key value) is being used for three _different_ entity instances. On the other hand, sharing the same instance between complex properties is allowed, and so the code works as expected when using complex types. -```sql -CREATE TABLE [Posts] ( - [Id] int NOT NULL IDENTITY, - [Title] nvarchar(max) NOT NULL, - [Content] nvarchar(max) NOT NULL, - [PublishedOn] date NOT NULL, - [BlogId] int NOT NULL, -); -``` +### Configuration of complex types -`SqlQuery` can be used to query this table and return instances of a `BlogPost` type with properties corresponding to the columns in the table: +Complex types must be configured in the model using either [mapping attributes](xref:core/modeling/index#use-data-annotations-to-configure-a-model) or by calling [`ComplexProperty` API in `OnModelCreating`](xref:core/modeling/index#use-fluent-api-to-configure-a-model). Complex types are not discovered by convention. -For example: +For example, the `Address` type can be configured using the : ```csharp -public class BlogPost +[ComplexType] +public class Address { - public int Id { get; set; } - public string Title { get; set; } - public string Content { get; set; } - public DateOnly PublishedOn { get; set; } - public int BlogId { get; set; } + public required string Line1 { get; set; } + public string? Line2 { get; set; } + public required string City { get; set; } + public required string Country { get; set; } + public required string PostCode { get; set; } } ``` -For example: +Or in `OnModelCreating`: -[!code-csharp[SqlQueryAllColumns](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryAllColumns)] +[!code-csharp[ComplexTypeConfig](../../../../samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs?name=ComplexTypeConfig)] -This query is parameterized and executed as: +### Mutability + +In the example above, we ended up with the same `Address` instance used in three places. This is allowed and doesn't cause any issues for EF Core when using complex types. However, sharing instances of the same reference type means that if a property value on the instance is modified, then that change will be reflected in all three usages. For example, following on from above, let's change `Line1` of the customer address and save the changes: + + +[!code-csharp[ChangeSharedAddress](../../../../samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs?name=ChangeSharedAddress)] + +This results in the following update to the database when using SQL Server: ```sql -SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1 +UPDATE [Customers] SET [Address_Line1] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +UPDATE [Orders] SET [BillingAddress_Line1] = @p2, [ShippingAddress_Line1] = @p3 +OUTPUT 1 +WHERE [Id] = @p4; ``` -The type used for query results can contain common mapping constructs supported by EF Core, such as parameterized constructors and mapping attributes. For example: +Notice that all three `Line1` columns have changed, since they are all sharing the same instance. This is usually not what we want. - -[!code-csharp[BlogPost](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=BlogPost)] +### Reference types as complex types -> [!NOTE] -> Types used in this way do not have keys defined and cannot have relationships to other types. Types with relationships must be mapped in the model. +#### Immutable class -The type used must have a property for every value in the result set, but do not need to match any table in the database. For example, the following type represents only a subset of information for each post, and includes the blog name, which comes from the `Blogs` table: +We used a simple, mutable `class` in the example above. To prevent the issues with accidental mutation described above, we can make the class immutable. For example: - -[!code-csharp[PostSummary](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=PostSummary)] -And can be queried using `SqlQuery` in the same way as before: + public string Line1 { get; } + public string? Line2 { get; } + public string City { get; } + public string Country { get; } + public string PostCode { get; } +} +``` - -[!code-csharp[SqlQueryJoin](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryJoin)] +> [!TIP] +> With C# 12 or above, this class definition can be simplified using a primary constructor: +> +> ```csharp +> public class Address(string line1, string? line2, string city, string country, string postCode) +> { +> public string Line1 { get; } = line1; +> public string? Line2 { get; } = line2; +> public string City { get; } = city; +> public string Country { get; } = country; +> public string PostCode { get; } = postCode; +> } +> ``` -One nice feature of `SqlQuery` is that it returns an `IQueryable` which can be composed on using LINQ. For example, a 'Where' clause can be added to the query above: +It is now not possible to change the `Line1` value on an existing address. Instead, we need to create a new instance with the changed value. For example: -[!code-csharp[SqlQueryJoinComposed](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryJoinComposed)] +[!code-csharp[ChangeImmutableAddress](../../../../samples/core/Miscellaneous/NewInEFCore8/ImmutableComplexTypesSample.cs?name=ChangeImmutableAddress)] -This is executed as: +This time the call to `SaveChangesAsync` only updates the customer address: ```sql -SELECT [n].[BlogName], [n].[PostTitle], [n].[PublishedOn] -FROM ( - SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn - FROM Posts AS p - INNER JOIN Blogs AS b ON p.BlogId = b.Id - ) AS [n] -WHERE [n].[PublishedOn] >= @__cutoffDate_1 AND [n].[PublishedOn] < @__end_2 +UPDATE [Customers] SET [Address_Line1] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; ``` -At this point it is worth remembering that all of the above can be done completely in LINQ without the need to write any SQL. This includes returning instances of an unmapped type like `PostSummary`. For example, the preceding query can be written in LINQ as: +Note that even though the Address object is immutable, and the entire object has been changed, EF is still tracking changes to the individual properties, so only the columns with changed values are updated. -```csharp -var summaries = - await context.Posts.Select( - p => new PostSummary - { - BlogName = p.Blog.Name, - PostTitle = p.Title, - PublishedOn = p.PublishedOn, - }) - .Where(p => p.PublishedOn >= start && p.PublishedOn < end) - .ToListAsync(); -``` +#### Immutable record -Which translates to much cleaner SQL: +C# 9 introduced [record types](/dotnet/csharp/language-reference/builtin-types/record), which makes creating and using immutable objects easier. For example, the `Address` object can be made a record type: -```sql -SELECT [b].[Name] AS [BlogName], [p].[Title] AS [PostTitle], [p].[PublishedOn] -FROM [Posts] AS [p] -INNER JOIN [Blogs] AS [b] ON [p].[BlogId] = [b].[Id] -WHERE [p].[PublishedOn] >= @__start_0 AND [p].[PublishedOn] < @__end_1 +```csharp +public record Address +{ + public Address(string line1, string? line2, string city, string country, string postCode) + { + Line1 = line1; + Line2 = line2; + City = city; + Country = country; + PostCode = postCode; + } + + public string Line1 { get; init; } + public string? Line2 { get; init; } + public string City { get; init; } + public string Country { get; init; } + public string PostCode { get; init; } +} ``` > [!TIP] -> EF is able to generate cleaner SQL when it is responsible for the entire query than it is when composing over user-supplied SQL because, in the former case, the full semantics of the query is available to EF. +> This record definition can be simplified using a primary constructor: +> +> ```csharp +> public record Address(string Line1, string? Line2, string City, string Country, string PostCode); +> ``` -So far, all the queries have been executed directly against tables. `SqlQuery` can also be used to return results from a view without mapping the view type in the EF model. For example: +Replacing the mutable object and calling `SaveChanges` now requires less code: -[!code-csharp[SqlQueryView](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryView)] +[!code-csharp[ChangeImmutableRecord](../../../../samples/core/Miscellaneous/NewInEFCore8/RecordComplexTypesSample.cs?name=ChangeImmutableRecord)] -Likewise, `SqlQuery` can be used for the results of a function: +### Value types as complex types - -[!code-csharp[SqlQueryFunction](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryFunction)] +#### Mutable struct -The returned `IQueryable` can be composed upon when it is the result of a view or function, just like it can be for the result of a table query. Stored procedures can be also be executed using `SqlQuery`, but most databases do not support composing over them. For example: +A simple mutable [value type](/dotnet/csharp/language-reference/builtin-types/value-types) can be used as a complex type. For example, `Address` can be defined as a `struct` in C#: -[!code-csharp[SqlQueryStoredProc](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryStoredProc)] +[!code-csharp[AddressStruct](../../../../samples/core/Miscellaneous/NewInEFCore8/StructComplexTypesSample.cs?name=AddressStruct)] -### Lazy-loading for no-tracking queries +Assigning the customer `Address` object to the shipping and billing `Address` properties results in each property getting a copy of the `Address`, since this is how value types work. This means that modifying the `Address` on the customer will not change the shipping or billing `Address` instances, so mutable structs don't have the same instance-sharing issues that happen with mutable classes. -EF8 adds support for [lazy-loading of navigations](xref:core/querying/related-data/lazy) on entities that are not being tracked by the `DbContext`. This means a no-tracking query can be followed by lazy-loading of navigations on the entities returned by the no-tracking query. +However, [mutable structs are generally discouraged in C#](/archive/blogs/ericlippert/mutating-readonly-structs), so think very carefully before using them. -> [!TIP] -> The code for the lazy-loading examples shown below comes from [LazyLoadingSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs). +#### Immutable struct -For example, consider a no-tracking query for blogs: +Immutable structs work well as complex types, just like immutable classes do. For example, `Address` can be defined such that it can not be modified: -[!code-csharp[NoTrackingForBlogs](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=NoTrackingForBlogs)] +[!code-csharp[AddressImmutableStruct](../../../../samples/core/Miscellaneous/NewInEFCore8/ImmutableStructComplexTypesSample.cs?name=AddressImmutableStruct)] -If `Blog.Posts` is configured for lazy-loading, for example, using lazy-loading proxies, then accessing `Posts` will cause it to load from the database: +The code for changing the address now looks the same as when using immutable class: -[!code-csharp[ChooseABlog](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=ChooseABlog)] +[!code-csharp[UpdateImmutableStruct](../../../../samples/core/Miscellaneous/NewInEFCore8/ImmutableStructComplexTypesSample.cs?name=UpdateImmutableStruct)] -EF8 also reports whether or not a given navigation is loaded for entities not tracked by the context. For example: +#### Immutable struct record + +C# 10 introduced `struct record` types, which makes it easy to create and work with immutable struct records like it is with immutable class records. For example, we can define `Address` as an immutable struct record: -[!code-csharp[IsLoaded](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=IsLoaded)] +[!code-csharp[RecordStructAddress](../../../../samples/core/Miscellaneous/NewInEFCore8/RecordComplexTypesSample.cs?name=RecordStructAddress)] -There are a few important considerations when using lazy-loading in this way: +The code for changing the address now looks the same as when using immutable class record: -- Lazy-loading will only succeed until the `DbContext` used to query the entity is disposed. -- Entities queried in this way a reference to their `DbContext`, even though they are not tracked by it. Care should be taken to avoid memory leaks if the entity instances will have long lifetimes. -- Explicitly detaching the entity by setting its state to `EntityState.Detached` severs the reference to the `DbContext` and lazy-loading will no longer work. -- Remember that all lazy-loading uses synchronous I/O, since there is no way to access a property in an asynchronous manner. + +[!code-csharp[ChangeImmutableRecord](../../../../samples/core/Miscellaneous/NewInEFCore8/RecordComplexTypesSample.cs?name=ChangeImmutableRecord)] -### Explicit loading from untracked entities +### Nested complex types -EF8 supports loading of navigations on untracked entities even when the entity or navigation is not configured for lazy-loading. Unlike with lazy-loading, this [explicit loading](xref:core/querying/related-data/explicit) can be done asynchronously. For example: +A complex type can contain properties of other complex types. For example, let's use our `Address` complex type from above together with a `PhoneNumber` complex type, and nest them both inside another complex type: -[!code-csharp[ExplicitLoad](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=ExplicitLoad)] - -### Opt-out of lazy-loading for specific navigations + #region NestedComplexTypes + public record Address(string Line1, string? Line2, string City, string Country, string PostCode); -EF8 allows configuration of specific navigations to not lazy-load, even when everything else is set up to do so. For example, to configure the `Post.Author` navigation to not lazy-load, do the following: + public record PhoneNumber(int CountryCode, long Number); - -[!code-csharp[NoLazyLoading](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=NoLazyLoading)] +[!code-csharp[NestedComplexTypes](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=NestedComplexTypes)] -Disabling Lazy-loading like this works for both [lazy-loading proxies](xref:core/querying/related-data/lazy#lazy-loading-with-proxies) and [lazy-loading without proxies](xref:core/querying/related-data/lazy#lazy-loading-without-proxies). +We're using immutable records here, since these are a good match for the semantics of our complex types, but nesting of complex types can be done with any flavor of .NET type. -Lazy-loading proxies work by overriding virtual navigation properties. In classic EF6 applications, a common source of bugs is forgetting to make a navigation virtual, since the navigation will then silently not lazy-load. Therefore, EF Core proxies throw by default when a navigation is not virtual. +> [!NOTE] +> We're not using a primary constructor for the `Contact` type because EF Core does not yet support constructor injection of complex type values. Vote for [Issue #31621](https://github.com/dotnet/efcore/issues/31621) if this is important to you. -This can be changed in EF8 to opt-in to the classic EF6 behavior such that a navigation can be made to not lazy-load simply by making the navigation non-virtual. This opt-in is configured as part of the call to `UseLazyLoadingProxies`. For example: +We will add `Contact` as a property of the `Customer`: -[!code-csharp[IgnoreNonVirtualNavigations](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=IgnoreNonVirtualNavigations)] - -### Lookup tracked entities by primary, alternate, or foreign key - -Internally, EF maintains data structures for finding tracked entities by primary, alternate, or foreign key. These data structures are used for efficient fixup between related entities when new entities are tracked or relationships change. +[!code-csharp[CustomerWithContact](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=CustomerWithContact)] -EF8 contains new public APIs so that applications can now use these data structures to efficiently lookup tracked entities. These APIs are accessed through the of the entity type. For example, to lookup a tracked entity by its primary key: +And `PhoneNumber` as properties of the `Order`: -[!code-csharp[LookupByPrimaryKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByPrimaryKey)] +[!code-csharp[OrderWithPhone](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=OrderWithPhone)] -> [!TIP] -> The code shown here comes from [LookupByKeySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs). +Configuration of nested complex types can again be achieved using : -The [`FindEntry`](https://github.com/dotnet/efcore/blob/81886272a761df8fafe4970b895b1e1fe35effb8/src/EFCore/ChangeTracking/LocalView.cs#L543) method returns either the for the tracked entity, or `null` if no entity with the given key is being tracked. Like all methods on `LocalView`, the database is never queried, even if the entity is not found. The returned entry contains the entity itself, as well as tracking information. For example: +```csharp +[ComplexType] +public record Address(string Line1, string? Line2, string City, string Country, string PostCode); - -[!code-csharp[UseEntry](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=UseEntry)] +[ComplexType] +public record PhoneNumber(int CountryCode, long Number); -Looking up an entity by anything other than a primary key requires that the property name be specified. For example, to look up by an alternate key: +[ComplexType] +public record Contact +{ + public required Address Address { get; init; } + public required PhoneNumber HomePhone { get; init; } + public required PhoneNumber WorkPhone { get; init; } + public PhoneNumrequired ber MobilePhone { get; init; } +} +``` + +Or in `OnModelCreating`: -[!code-csharp[LookupByAlternateKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByAlternateKey)] +[!code-csharp[ConfigureNestedTypes](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=ConfigureNestedTypes)] -Or to look up by a unique foreign key: +### Queries + +Properties of complex types on entity types are treated like any other non-navigation property of the entity type. This means that they are always loaded when the entity type is loaded. This is also true of any nested complex type properties. For example, querying for a customer: -[!code-csharp[LookupByUniqueForeignKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByUniqueForeignKey)] +[!code-csharp[QueryCustomer](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=QueryCustomer)] -So far, the lookups have always returned a single entry, or `null`. However, some lookups can return more than one entry, such as when looking up by a non-unique foreign key. The [`GetEntries`](https://github.com/dotnet/efcore/blob/81886272a761df8fafe4970b895b1e1fe35effb8/src/EFCore/ChangeTracking/LocalView.cs#L664) method should be used for these lookups. For example: +Is translated to the following SQL when using SQL Server: - -[!code-csharp[LookupByForeignKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByForeignKey)] +```sql +SELECT TOP(1) [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country], + [c].[Contact_Address_Line1], [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode], + [c].[Contact_HomePhone_CountryCode], [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode], + [c].[Contact_MobilePhone_Number], [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number] +FROM [Customers] AS [c] +WHERE [c].[Id] = @__customerId_0 +``` -In all these cases, the value being used for the lookup is either a primary key, alternate key, or foreign key value. EF uses its internal data structures for these lookups. However, lookups by value can also be used for the value of any property or combination of properties. For example, to find all archived posts: +Notice two things from this SQL: - -[!code-csharp[LookupByAnyProperty](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByAnyProperty)] +- Everything is returned to populate the customer _and_ all the nested `Contact`, `Address`, and `PhoneNumber` complex types. +- All the complex type values are stored as columns in the table for the entity type. Complex types are never mapped to separate tables. -This lookup requires a scan of all tracked `Post` instances, and so will be less efficient than key lookups. However, it is usually still faster than naive queries using . +#### Projections -Finally, it is also possible to perform lookups against composite keys, other combinations of multiple properties, or when the property type is not known at compile time. For example: +Complex types can be projected from a query. For example, selecting just the shipping address from an order: -[!code-csharp[LookupByCompositePrimaryKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByCompositePrimaryKey)] - -### Discriminator columns have max length +[!code-csharp[QueryShippingAddress](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=QueryShippingAddress)] -In EF8, string discriminator columns used for [TPH inheritance mapping](xref:core/modeling/inheritance) are now configured with a max length. This length is calculated as the smallest Fibonacci number that covers all defined discriminator values. For example, consider the following hierarchy: +This translates to the following when using SQL Server: -```csharp -public abstract class Document -{ - public int Id { get; set; } - public string Title { get; set; } -} +```sql +SELECT TOP(2) [o].[ShippingAddress_City], [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1], + [o].[ShippingAddress_Line2], [o].[ShippingAddress_PostCode] +FROM [Orders] AS [o] +WHERE [o].[Id] = @__orderId_0 +``` -public abstract class Book : Document -{ - public string? Isbn { get; set; } -} +Note that projections of complex types cannot be tracked, since complex type objects have no identity to use for tracking. -public class PaperbackEdition : Book -{ -} +### Use in predicates -public class HardbackEdition : Book -{ -} +Members of complex types can be used in predicates. For example, finding all the orders going to a certain city: -public class Magazine : Document -{ - public int IssueNumber { get; set; } -} -``` + +[!code-csharp[QueryOrdersInCity](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=QueryOrdersInCity)] -With the convention of using the class names for discriminator values, the possible values here are "PaperbackEdition", "HardbackEdition", and "Magazine", and hence the discriminator column is configured for a max length of 21. For example, when using SQL Server: +Which translates to the following SQL on SQL Server: ```sql -CREATE TABLE [Documents] ( - [Id] int NOT NULL IDENTITY, - [Title] nvarchar(max) NOT NULL, - [Discriminator] nvarchar(21) NOT NULL, - [Isbn] nvarchar(max) NULL, - [IssueNumber] int NULL, - CONSTRAINT [PK_Documents] PRIMARY KEY ([Id]), +SELECT [o].[Id], [o].[Contents], [o].[CustomerId], [o].[BillingAddress_City], [o].[BillingAddress_Country], + [o].[BillingAddress_Line1], [o].[BillingAddress_Line2], [o].[BillingAddress_PostCode], + [o].[ContactPhone_CountryCode], [o].[ContactPhone_Number], [o].[ShippingAddress_City], + [o].[ShippingAddress_Country], [o].[ShippingAddress_Line1], [o].[ShippingAddress_Line2], + [o].[ShippingAddress_PostCode] +FROM [Orders] AS [o] +WHERE [o].[ShippingAddress_City] = @__city_0 ``` -> [!TIP] -> Fibonacci numbers are used to limit the number of times a migration is generated to change the column length as new types are added to the hierarchy. +A full complex type instance can also be used in predicates. For example, finding all customers with a given phone number: -### DateOnly/TimeOnly supported on SQL Server + +[!code-csharp[QueryWithPhoneNumber](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=QueryWithPhoneNumber)] -The and types were introduced in .NET 6 and have been supported for several database providers (e.g. SQLite, MySQL, and PostgreSQL) since their introduction. For SQL Server, the recent release of a [Microsoft.Data.SqlClient](https://www.nuget.org/packages/Microsoft.Data.SqlClient/) package targeting .NET 6 has allowed [ErikEJ to add support for these types at the ADO.NET level](https://github.com/dotnet/SqlClient/pull/1813). This in turn paved the way for support in EF8 for `DateOnly` and `TimeOnly` as properties in entity types. +This translates to the following SQL when using SQL Server: -> [!TIP] -> `DateOnly` and `TimeOnly` can be used in EF Core 6 and 7 using the [ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly](https://www.nuget.org/packages/ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly) community package from [@ErikEJ](https://github.com/ErikEJ). +```sql +SELECT [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].[Contact_Address_Country], [c].[Contact_Address_Line1], + [c].[Contact_Address_Line2], [c].[Contact_Address_PostCode], [c].[Contact_HomePhone_CountryCode], + [c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode], [c].[Contact_MobilePhone_Number], + [c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number] +FROM [Customers] AS [c] +WHERE ([c].[Contact_MobilePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode + AND [c].[Contact_MobilePhone_Number] = @__entity_equality_phoneNumber_0_Number) +OR ([c].[Contact_WorkPhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode + AND [c].[Contact_WorkPhone_Number] = @__entity_equality_phoneNumber_0_Number) +OR ([c].[Contact_HomePhone_CountryCode] = @__entity_equality_phoneNumber_0_CountryCode + AND [c].[Contact_HomePhone_Number] = @__entity_equality_phoneNumber_0_Number) +``` -For example, consider the following EF model for British schools: +Notice that equality is performed by expanding out each member of the complex type. This aligns with complex types having no key for identity and hence a complex type instance is equal to another complex type instance if and only if all their members are equal. This also aligns with the equality defined by .NET for record types. - -[!code-csharp[BritishSchools](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=BritishSchools)] - -> [!TIP] -> The code shown here comes from [DateOnlyTimeOnlySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs). - -> [!NOTE] -> This model represents only British schools and stores times as local (GMT) times. Handling different timezones would complicate this code significantly. Note that using `DateTimeOffset` would not help here, since opening and closing times have different offsets depending whether daylight saving time is active or not. +[!code-csharp[BillingAddressCurrentValue](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=BillingAddressCurrentValue)] -These entity types map to the following tables when using SQL Server. Notice that the `DateOnly` properties map to `date` columns, and the `TimeOnly` properties map to `time` columns. +A call to `Property` can be added to access a property of the complex type. For example to get the current value of just the billing post code: -```sql -CREATE TABLE [Schools] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NOT NULL, - [Founded] date NOT NULL, - CONSTRAINT [PK_Schools] PRIMARY KEY ([Id])); + +[!code-csharp[PostCodeCurrentValue](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=PostCodeCurrentValue)] -CREATE TABLE [OpeningHours] ( - [SchoolId] int NOT NULL, - [Id] int NOT NULL IDENTITY, - [DayOfWeek] int NOT NULL, - [OpensAt] time NULL, - [ClosesAt] time NULL, - CONSTRAINT [PK_OpeningHours] PRIMARY KEY ([SchoolId], [Id]), - CONSTRAINT [FK_OpeningHours_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE); +Nested complex types are accessed using nested calls to `ComplexProperty`. For example, to get the city from the nested `Address` of the `Contact` on a `Customer`: -CREATE TABLE [Term] ( - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NOT NULL, - [FirstDay] date NOT NULL, - [LastDay] date NOT NULL, - [SchoolId] int NOT NULL, - CONSTRAINT [PK_Term] PRIMARY KEY ([Id]), - CONSTRAINT [FK_Term_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE); -``` + +[!code-csharp[CityCurrentValue](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=CityCurrentValue)] -Queries using `DateOnly` and `TimeOnly` work in the expected manner. For example, the following LINQ query finds schools that are currently open: +Other methods are available for reading and changing state. For example, can be used to set a property of a complex type as modified: -[!code-csharp[OpenSchools](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=OpenSchools)] +[!code-csharp[SetPostCodeIsModified](../../../../samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs?name=SetPostCodeIsModified)] -This query translates to the following SQL, as shown by : +### Current limitations -```sql -DECLARE @__today_0 date = '2023-02-07'; -DECLARE @__dayOfWeek_1 int = 2; -DECLARE @__time_2 time = '19:53:40.4798052'; +Complex types represent a significant investment across the EF stack. We were not able to make everything work in this release, but we plan to close some of the gaps in a future release. Make sure to vote (👍) on the appropriate GitHub issues if fixing any of these limitations is important to you. -SELECT [s].[Id], [s].[Founded], [s].[Name], [o0].[SchoolId], [o0].[Id], [o0].[ClosesAt], [o0].[DayOfWeek], [o0].[OpensAt] -FROM [Schools] AS [s] -LEFT JOIN [OpeningHours] AS [o0] ON [s].[Id] = [o0].[SchoolId] -WHERE EXISTS ( - SELECT 1 - FROM [Term] AS [t] - WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0 AND [t].[LastDay] >= @__today_0) AND EXISTS ( - SELECT 1 - FROM [OpeningHours] AS [o] - WHERE [s].[Id] = [o].[SchoolId] AND [o].[DayOfWeek] = @__dayOfWeek_1 AND [o].[OpensAt] < @__time_2 AND [o].[ClosesAt] >= @__time_2) -ORDER BY [s].[Id], [o0].[SchoolId] -``` +Complex type limitations in EF8 include: -`DateOnly` and `TimeOnly` can also be used in JSON columns. For example, `OpeningHours` can be saved as a JSON document, resulting in data that looks like this: +- Support collections of complex types. ([Issue #31237](https://github.com/dotnet/efcore/issues/31237)) +- Allow complex type properties to be null. ([Issue #31376](https://github.com/dotnet/efcore/issues/31376)) +- Map complex type properties to JSON columns. ([Issue #31252](https://github.com/dotnet/efcore/issues/31252)) +- Constructor injection for complex types. ([Issue #31621](https://github.com/dotnet/efcore/issues/31621)) +- Add seed data support for complex types. ([Issue #31254](https://github.com/dotnet/efcore/issues/31254)) +- Map complex type properties for the Cosmos provider. ([Issue #31253](https://github.com/dotnet/efcore/issues/31253)) +- Implement complex types for the in-memory database. ([Issue #31464](https://github.com/dotnet/efcore/issues/31464)) -| Column | Value | -|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Id | 2 | -| Name | Farr High School | -| Founded | 1964-05-01 | -| OpeningHours |
[
{ "DayOfWeek": "Sunday", "ClosesAt": null, "OpensAt": null },
{ "DayOfWeek": "Monday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Tuesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Wednesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Thursday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Friday", "ClosesAt": "12:50:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Saturday", "ClosesAt": null, "OpensAt": null }
] | +## Primitive collections -Combining two features from EF8, we can now query for opening hours by indexing into the JSON collection. For example: +A persistent question when using relational databases is what to do with collections of primitive types; that is, lists or arrays of integers, date/times, strings, and so on. If you're using PostgreSQL, then its easy to store these things using PostgreSQL's [built-in array type](https://www.postgresql.org/docs/current/arrays.html). For other databases, there are two common approaches: - -[!code-csharp[OpenSchoolsJson](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=OpenSchoolsJson)] +- Create a table with a column for the primitive type value and another column to act as a foreign key linking each value to its owner of the collection. +- Serialize the primitive collection into some column type that is handled by the database--for example, serialize to and from a string. -This query translates to the following SQL, as shown by : +The first option has advantages in many situations--we'll take a quick look at it at the end of this section. However, it's not a natural representation of the data in the model, and if what you really have is a collection of a primitive type, then the second option can be more effective. -```sql -DECLARE @__today_0 date = '2023-02-07'; -DECLARE @__dayOfWeek_1 int = 2; -DECLARE @__time_2 time = '20:14:34.7795877'; +Starting with Preview 4, EF8 now includes built-in support for the second option, using JSON as the serialization format. JSON works well for this since modern relational databases include built-in mechanisms for querying and manipulating JSON, such that the JSON column can, effectively, be treated as a table when needed, without the overhead of actually creating that table. These same mechanisms allow JSON to be passed in parameters and then used in similar way to table-valued parameters in queries--more about this later. -SELECT [s].[Id], [s].[Founded], [s].[Name], [s].[OpeningHours] -FROM [Schools] AS [s] -WHERE EXISTS ( - SELECT 1 - FROM [Term] AS [t] - WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0 - AND [t].[LastDay] >= @__today_0) - AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].OpensAt') AS time) < @__time_2 - AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1 AS int) AS nvarchar(max)) + '].ClosesAt') AS time) >= @__time_2 +> [!TIP] +> The code shown here comes from [PrimitiveCollectionsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs). + +### Primitive collection properties + +EF Core can map any `IEnumerable` property, where `T` is a primitive type, to a JSON column in the database. This is done by convention for public properties which have both a getter and a setter. For example, all properties in the following entity type are mapped to JSON columns by convention: + +```csharp +public class PrimitiveCollections +{ + public IEnumerable Ints { get; set; } + public ICollection Strings { get; set; } + public ISet DateTimes { get; set; } + public IList Dates { get; set; } + public uint[] UnsignedInts { get; set; } + public List Booleans { get; set; } + public List Urls { get; set; } +} ``` -Finally, updates and deletes can be accomplished with [tracking and SaveChanges](xref:core/saving/basic), or using [ExecuteUpdate/ExecuteDelete](xref:core/what-is-new/ef-core-7.0/whatsnew#executeupdate-and-executedelete-bulk-updates). For example: +> [!NOTE] +> What do we mean by "primitive type" in this context? Essentially, something that the database provider knows how to map, using some kind of value conversion if necessary. For example, in the entity type above, the types `int`, `string`, `DateTime`, `DateOnly` and `bool` are all handled without conversion by the database provider. SQL Server does not have native support for unsigned ints or URIs, but `uint` and `Uri` are still treated as primitive types because there are [built-in value converters](xref:core/modeling/value-conversions#built-in-converters) for these types. - -[!code-csharp[UpdateForSnowDay](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=UpdateForSnowDay)] +By default, EF Core uses an unconstrained Unicode string column type to hold the JSON, since this protects against data loss with large collections. However, on some database systems, such as SQL Server, specifying a maximum length for the string can improve performance. This, along with other column configuration, can be done [in the normal way](xref:core/modeling/entity-properties). For example: -This update translates to the following SQL: +```csharp +modelBuilder + .Entity() + .Property(e => e.Booleans) + .HasMaxLength(1024) + .IsUnicode(false); +``` -```sql -UPDATE [t0] -SET [t0].[LastDay] = DATEADD(day, CAST(1 AS int), [t0].[LastDay]) -FROM [Schools] AS [s] -INNER JOIN [Term] AS [t0] ON [s].[Id] = [t0].[SchoolId] -WHERE EXISTS ( - SELECT 1 - FROM [Term] AS [t] - WHERE [s].[Id] = [t].[SchoolId] AND DATEPART(year, [t].[LastDay]) = 2022) +Or, using mapping attributes: + +```csharp +[MaxLength(2500)] +[Unicode(false)] +public uint[] UnsignedInts { get; set; } ``` -### Reverse engineer Synapse and Dynamics 365 TDS +A default column configuration can be used for all properties of a certain type using [pre-convention model configuration](xref:core/modeling/bulk-configuration#pre-convention-configuration). For example: -EF8 reverse engineering (a.k.a. scaffolding from an existing database) now supports [Synapse Serverless SQL Pool](/azure/synapse-analytics/sql/on-demand-workspace-overview) and [Dynamics 365 TDS Endpoint](/power-apps/developer/data-platform/dataverse-sql-query) databases. +```csharp +protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) +{ + configurationBuilder + .Properties>() + .AreUnicode(false) + .HaveMaxLength(4000); +} +``` -> [!WARNING] -> These database systems have differences from normal SQL Server and Azure SQL databases. These differences mean that not all EF Core functionality is supported when writing queries against or performing other operations with these database systems. +### Queries with primitive collections -### Smaller enhancements included in Preview 1 +Let's look at some of the queries that make use of collections of primitive types. For this, we'll need a simple model with two entity types. The first represents a [British public house](https://en.wikipedia.org/wiki/Pub), or "pub": -In addition to the enhancements described above, EF8 Preview 1 also [includes many smaller enhancements](https://github.com/dotnet/efcore/issues?q=is%3Aissue+label%3Atype-enhancement+milestone%3A8.0.0-preview1+is%3Aclosed). Some of these relate to the internal workings of EF Core, even excluding these, there are many that may be of interest to application developers. These include: + +[!code-csharp[Pub](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=Pub)] -## New in EF8 Preview 2 +The `Pub` type contains two primitive collections: -### JSON Columns for SQLite +- `Beers` is an array of strings representing the beer brands available at the pub. +- `DaysVisited` is a list of the dates on which the pub was visited. -EF7 introduced support for mapping to JSON columns when using Azure SQL/SQL Server. EF8 extends this support to SQLite databases. As for the SQL Server support, this includes: +> [!TIP] +> In a real application, it would probably make more sense to create an entity type for beer, and have a table for beers. We're showing a primitive collection here to illustrate how they work. But remember, just because you can model something as a primitive collection doesn't mean that you necessarily should. -- Mapping of aggregates built from .NET types to JSON documents stored in SQLite columns -- Queries into JSON columns, such as filtering and sorting by the elements of the documents -- Queries that project elements out of the JSON document into results -- Updating and saving changes to JSON documents +The second entity type represents a dog walk in the British countryside: -The existing [documentation from What's New in EF7](xref:core/what-is-new/ef-core-7.0/whatsnew#json-columns) provides detailed information on JSON mapping, queries, and updates. This documentation now also applies to SQLite. + +[!code-csharp[DogWalk](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=DogWalk)] -Queries into JSON columns on SQLite use the `json_extract` function. For example, the "authors in Chigley" query from the documentation referenced above: +Like `Pub`, `DogWalk` also contains a collection of the dates visited, and a link to the closest pub since, you know, sometimes the dog needs a saucer of beer after a long walk. + +Using this model, the first query we will do is a simple `Contains` query to find all walks with one of several different terrains: -[!code-csharp[AuthorsInChigley](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=AuthorsInChigley)] +[!code-csharp[WalksWithTerrain](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=WalksWithTerrain)] -Is translated to the following SQL when using SQLite: +This is already translated by current versions of EF Core by inlining the values to look for. For example, when using SQL Server: ```sql -SELECT "a"."Id", "a"."Name", "a"."Contact" -FROM "Authors" AS "a" -WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley' +SELECT [w].[Name] +FROM [Walks] AS [w] +WHERE [w].[Terrain] IN (1, 5, 4) ``` -#### Updating JSON columns - -For updates, EF uses the `json_set` function on SQLite. For example, when updating a single property in a document: - - -[!code-csharp[UpdateProperty](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=UpdateProperty)] +> [!IMPORTANT] +> The inlining of values here is done in such a way that there is no chance of a SQL injection attack. The change to use JSON described below is all about performance, and nothing to do with security. -EF generates the following parameters: +For EF Core 8, the default is now to pass the list of terrains as a single parameter containing a JSON collection. For example: -```text -info: 3/10/2023 10:51:33.127 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) - Executed DbCommand (0ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30'] +```none +@__terrains_0='[1,5,4]' ``` -Which use the `json_set` function on SQLite: +The query then uses `OpenJson` on SQL Server: ```sql -UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country', json_extract(@p0, '$[0]')) -WHERE "Id" = @p1 -RETURNING 1; +SELECT [w].[Name] +FROM [Walks] AS [w] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__terrains_0) AS [t] + WHERE CAST([t].[value] AS int) = [w].[Terrain]) ``` -### SQL Server HierarchyId +Or `json_each` on SQLite: -Azure SQL and SQL Server have a special data type called [`hierarchyid`](/sql/t-sql/data-types/hierarchyid-data-type-method-reference) that is used to store [hierarchical data](/sql/relational-databases/hierarchical-data-sql-server). In this case, "hierarchical data" essentially means data that forms a tree structure, where each item can have a parent and/or children. Examples of such data are: +```sql +SELECT "w"."Name" +FROM "Walks" AS "w" +WHERE EXISTS ( + SELECT 1 + FROM json_each(@__terrains_0) AS "t" + WHERE "t"."value" = "w"."Terrain") +``` -- An organizational structure -- A file system -- A set of tasks in a project -- A taxonomy of language terms -- A graph of links between Web pages +> [!NOTE] +> `OpenJson` is only available on SQL Server 2016 ([compatibility level 130](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level)) and later. You can tell SQL Server that you're using an older version by configuring the compatibility level as part of `UseSqlServer`. For example: +> +> ```csharp +> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) +> => optionsBuilder +> .UseSqlServer( +> @"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow", +> sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120)); +> ``` -The database is then able to run queries against this data using its hierarchical structure. For example, a query can find ancestors and dependents of given items, or find all items at a certain depth in the hierarchy. +Let's try a different kind of `Contains` query. In this case, we'll look for a value of the parameter collection in the column. For example, any pub that stocks Heineken: -#### HierarchyId support in .NET and EF Core + +[!code-csharp[PubsWithHeineken](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsWithHeineken)] -Official support for the SQL Server `hierarchyid` type has only recently come to modern .NET platforms (i.e. ".NET Core"). This support is in the form of the [Microsoft.SqlServer.Types](https://www.nuget.org/packages/Microsoft.SqlServer.Types) NuGet package, which brings in low-level SQL Server-specific types. In this case, the low-level type is called `SqlHierarchyId`. +The existing [documentation from What's New in EF7](xref:core/what-is-new/ef-core-7.0/whatsnew#json-columns) provides detailed information on JSON mapping, queries, and updates. This documentation now also applies to SQLite. -At the next level, a new [Microsoft.EntityFrameworkCore.SqlServer.Abstractions](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer.Abstractions) package has been introduced, which includes a higher-level `HierarchyId` type intended for use in entity types. +```sql +SELECT [p].[Name] +FROM [Pubs] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[Beers]) AS [b] + WHERE [b].[value] = @__beer_0) +``` -> [!TIP] -> The `HierarchyId` type is more idiomatic to the norms of .NET than `SqlHierarchyId`, which is instead modeled after how .NET Framework types are hosted inside the SQL Server database engine. `HierarchyId` is designed to work with EF Core, but it can also be used outside of EF Core in other applications. The `Microsoft.EntityFrameworkCore.SqlServer.Abstractions` package doesn't reference any other packages, and so has minimal impact on deployed application size and dependencies. +`OpenJson` is now used to to extract values from JSON column so that each value can be matched to the passed parameter. -Use of `HierarchyId` for EF Core functionality such as queries and updates requires the [Microsoft.EntityFrameworkCore.SqlServer.HierarchyId](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer.HierarchyId) package. This package brings in `Microsoft.EntityFrameworkCore.SqlServer.Abstractions` and `Microsoft.SqlServer.Types` as transitive dependencies, and so is often the only package needed. Once the package is installed, use of `HierarchyId` is enabled by calling `UseHierarchyId` as part of the application's call to `UseSqlServer`. For example: +We can combine the use of `OpenJson` on the parameter with `OpenJson` on the column. For example, to find pubs that stock any one of a variety of lagers: -```csharp -options.UseSqlServer( + +[!code-csharp[PubsWithLager](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsWithLager)] + +This translates to the following on SQL Server: + +```sql +SELECT [p].[Name] +FROM [Pubs] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson(@__beers_0) AS [b] + WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[Beers]) AS [b0] + WHERE [b0].[value] = [b].[value] OR ([b0].[value] IS NULL AND [b].[value] IS NULL))) +``` + +The `@__beers_0` parameter value here is `["Carling","Heineken","Stella Artois","Carlsberg"]`. + +Let's look at a query that makes use of the column containing a collection of dates. For example, to find pubs visited this year: + + +[!code-csharp[PubsVisitedThisYear](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsVisitedThisYear)] + +This translates to the following on SQL Server: + +```sql +SELECT [p].[Name] +FROM [Pubs] AS [p] +WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[DaysVisited]) AS [d] + WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0) +``` + +Notice that the query makes use of the date-specific function `DATEPART` here because EF _knows that the primitive collection contains dates_. It might not seem like it, but this is actually really important. Because EF knows what's in the collection, it can generate appropriate SQL to use the typed values with parameters, functions, other columns etc. + +Let's use the date collection again, this time to order appropriately for the type and project values extracted from the collection. For example, let's list pubs in the order that they were first visited, and with the first and last date each pub was visited: + + +[!code-csharp[PubsVisitedInOrder](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsVisitedInOrder)] + +This translates to the following on SQL Server: + +```sql +SELECT [p].[Name], ( + SELECT TOP(1) CAST([d0].[value] AS date) + FROM OpenJson([p].[DaysVisited]) AS [d0] + ORDER BY CAST([d0].[value] AS date)) AS [FirstVisited], ( + SELECT TOP(1) CAST([d1].[value] AS date) + FROM OpenJson([p].[DaysVisited]) AS [d1] + ORDER BY CAST([d1].[value] AS date) DESC) AS [LastVisited] +FROM [Pubs] AS [p] +ORDER BY ( + SELECT TOP(1) CAST([d].[value] AS date) + FROM OpenJson([p].[DaysVisited]) AS [d] + ORDER BY CAST([d].[value] AS date)) +``` + +And finally, just how often do we end up visiting the closest pub when taking the dog for a walk? Let's find out: + + +[!code-csharp[WalksWithADrink](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=WalksWithADrink)] + +This translates to the following on SQL Server: + +```sql +SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], ( + SELECT COUNT(*) + FROM OpenJson([w].[DaysVisited]) AS [d] + WHERE EXISTS ( + SELECT 1 + FROM OpenJson([p].[DaysVisited]) AS [d0] + WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], ( + SELECT COUNT(*) + FROM OpenJson([w].[DaysVisited]) AS [d1]) AS [TotalCount] +FROM [Walks] AS [w] +INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id] +``` + +And reveals the following data: + +```none +The Prince of Wales Feathers was visited 5 times in 8 "Ailsworth to Nene" walks. +The Prince of Wales Feathers was visited 6 times in 9 "Caster Hanglands" walks. +The Royal Oak was visited 6 times in 8 "Ferry Meadows" walks. +The White Swan was visited 7 times in 9 "Woodnewton" walks. +The Eltisley was visited 6 times in 8 "Eltisley" walks. +Farr Bay Inn was visited 7 times in 11 "Farr Beach" walks. +Farr Bay Inn was visited 7 times in 9 "Newlands" walks. +``` + +Looks like beer and dog walking are a winning combination! + +### Primitive collections in JSON documents + +In all the examples above, column for primitive collection contains JSON. However, this is not the same as mapping [an owned entity type to a column containing a JSON document](xref:core/what-is-new/ef-core-7.0/whatsnew#json-columns), which was introduced in EF7. But what if that JSON document itself contains a primitive collection? Well, all the queries above still work in the same way! For example, imagine we move the _days visited_ data into an owned type `Visits` mapped to a JSON document: + + +[!code-csharp[Pub](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsInJsonSample.cs?name=Pub)] + +> [!TIP] +> The code shown here comes from [PrimitiveCollectionsInJsonSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsInJsonSample.cs). + +We can now run a variation of our final query that, this time, extracts data from the JSON document, including queries into the primitive collections contained in the document: + + +[!code-csharp[WalksWithADrink](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsInJsonSample.cs?name=WalksWithADrink)] + +This translates to the following on SQL Server: + +```sql +SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], JSON_VALUE([w].[Visits], '$.LocationTag') AS [WalkLocationTag], JSON_VALUE([p].[Visits], '$.LocationTag') AS [PubLocationTag], ( + SELECT COUNT(*) + FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d] + WHERE EXISTS ( + SELECT 1 + FROM OpenJson(JSON_VALUE([p].[Visits], '$.DaysVisited')) AS [d0] + WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], ( + SELECT COUNT(*) + FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d1]) AS [TotalCount] +FROM [Walks] AS [w] +INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id] +``` + +And to a similar query when using SQLite: + +```sql +SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", "w"."Visits" ->> 'LocationTag' AS "WalkLocationTag", "p"."Visits" ->> 'LocationTag' AS "PubLocationTag", ( + SELECT COUNT(*) + FROM json_each("w"."Visits" ->> 'DaysVisited') AS "d" + WHERE EXISTS ( + SELECT 1 + FROM json_each("p"."Visits" ->> 'DaysVisited') AS "d0" + WHERE "d0"."value" = "d"."value")) AS "Count", json_array_length("w"."Visits" ->> 'DaysVisited') AS "TotalCount" +FROM "Walks" AS "w" +INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id" +``` + +> [!TIP] +> Notice that on SQLite EF Core now makes use of the `->>` operator, resulting in queries that are both easier to read and often more performant. + +### Mapping primitive collections to a table + +We mentioned above that another option for primitive collections is to map them to a different table. First class support for this is tracked by [Issue #25163](https://github.com/dotnet/efcore/issues/25163); make sure to vote for this issue if it is important to you. Until this is implemented, the best approach is to create a wrapping type for the primitive. For example, let's create a type for `Beer`: + + +[!code-csharp[Beer](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionToTableSample.cs?name=Beer)] + +Notice that the type simply wraps the primitive value--it doesn't have a primary key or any foreign keys defined. This type can then be used in the `Pub` class: + + +[!code-csharp[Pub](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionToTableSample.cs?name=Pub)] + +EF will now create a `Beer` table, synthesizing primary key and foreign key columns back to the `Pubs` table. For example, on SQL Server: + +```sql +CREATE TABLE [Beer] ( + [PubId] int NOT NULL, + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Beer] PRIMARY KEY ([PubId], [Id]), + CONSTRAINT [FK_Beer_Pubs_PubId] FOREIGN KEY ([PubId]) REFERENCES [Pubs] ([Id]) ON DELETE CASCADE +``` + +## Enhancements to JSON column mapping + +EF8 includes improvements to the [JSON column mapping support introduced in EF7](xref:core/what-is-new/ef-core-7.0/whatsnew#json-columns). + +> [!TIP] +> The code shown here comes from [JsonColumnsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs). + +### Translate element access into JSON arrays + +EF8 supports indexing in JSON arrays when executing queries. For example, the following query checks whether the first two updates were made before a given date. + + +[!code-csharp[CollectionIndexPredicate](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=CollectionIndexPredicate)] + +This translates into the following SQL when using SQL Server: + +```sql +SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata] +FROM [Posts] AS [p] +WHERE CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) < @__cutoff_0 + AND CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) < @__cutoff_0 +``` + +> [!NOTE] +> This query will succeed even if a given post does not have any updates, or only has a single update. In such a case, `JSON_VALUE` returns `NULL` and the predicate is not matched. + +Indexing into JSON arrays can also be used to project elements from an array into the final results. For example, the following query projects out the `UpdatedOn` date for the first and second updates of each post. + + +[!code-csharp[CollectionIndexProjectionNullable](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=CollectionIndexProjectionNullable)] + +This translates into the following SQL when using SQL Server: + +```sql +SELECT [p].[Title], + CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate], + CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate] +FROM [Posts] AS [p] +``` + +As noted above, `JSON_VALUE` returns null if the element of the array does not exist. This is handled in the query by casting the projected value to a nullable `DateOnly`. An alternative to casting the value is to filter the query results so that `JSON_VALUE` will never return null. For example: + + +[!code-csharp[CollectionIndexProjection](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=CollectionIndexProjection)] + +This translates into the following SQL when using SQL Server: + +```sql +SELECT [p].[Title], + CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS [LatestUpdate], + CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS [SecondLatestUpdate] +FROM [Posts] AS [p] + WHERE (CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) IS NOT NULL) + AND (CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) IS NOT NULL) +``` + +### JSON Columns for SQLite + +EF7 introduced support for mapping to JSON columns when using Azure SQL/SQL Server. EF8 extends this support to SQLite databases. As for the SQL Server support, this includes: + +- Mapping of aggregates built from .NET types to JSON documents stored in SQLite columns +- Queries into JSON columns, such as filtering and sorting by the elements of the documents +- Queries that project elements out of the JSON document into results +- Updating and saving changes to JSON documents + +The existing [documentation from What's New in EF7](xref:core/what-is-new/ef-core-7.0/whatsnew#json-columns) provides detailed information on JSON mapping, queries, and updates. This documentation now also applies to SQLite. + +> [!TIP] +> The code shown in the EF7 documentation has been updated to also run on SQLite can can be found in [JsonColumnsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs). + +#### Queries into JSON columns + +Queries into JSON columns on SQLite use the `json_extract` function. For example, the "authors in Chigley" query from the documentation referenced above: + + +[!code-csharp[AuthorsInChigley](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=AuthorsInChigley)] + +Is translated to the following SQL when using SQLite: + +```sql +SELECT "a"."Id", "a"."Name", "a"."Contact" +FROM "Authors" AS "a" +WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley' +``` + +#### Updating JSON columns + +For updates, EF uses the `json_set` function on SQLite. For example, when updating a single property in a document: + + +[!code-csharp[UpdateProperty](../../../../samples/core/Miscellaneous/NewInEFCore8/JsonColumnsSample.cs?name=UpdateProperty)] + +EF generates the following parameters: + +```text +info: 3/10/2023 10:51:33.127 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (0ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30'] +``` + +Which use the `json_set` function on SQLite: + +```sql +UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country', json_extract(@p0, '$[0]')) +WHERE "Id" = @p1 +RETURNING 1; +``` + +## HierarchyId in .NET and EF Core + +Azure SQL and SQL Server have a special data type called [`hierarchyid`](/sql/t-sql/data-types/hierarchyid-data-type-method-reference) that is used to store [hierarchical data](/sql/relational-databases/hierarchical-data-sql-server). In this case, "hierarchical data" essentially means data that forms a tree structure, where each item can have a parent and/or children. Examples of such data are: + +- An organizational structure +- A file system +- A set of tasks in a project +- A taxonomy of language terms +- A graph of links between Web pages + +The database is then able to run queries against this data using its hierarchical structure. For example, a query can find ancestors and dependents of given items, or find all items at a certain depth in the hierarchy. + +### Support in .NET and EF Core + +Official support for the SQL Server `hierarchyid` type has only recently come to modern .NET platforms (i.e. ".NET Core"). This support is in the form of the [Microsoft.SqlServer.Types](https://www.nuget.org/packages/Microsoft.SqlServer.Types) NuGet package, which brings in low-level SQL Server-specific types. In this case, the low-level type is called `SqlHierarchyId`. + +At the next level, a new [Microsoft.EntityFrameworkCore.SqlServer.Abstractions](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer.Abstractions) package has been introduced, which includes a higher-level `HierarchyId` type intended for use in entity types. + +> [!TIP] +> The `HierarchyId` type is more idiomatic to the norms of .NET than `SqlHierarchyId`, which is instead modeled after how .NET Framework types are hosted inside the SQL Server database engine. `HierarchyId` is designed to work with EF Core, but it can also be used outside of EF Core in other applications. The `Microsoft.EntityFrameworkCore.SqlServer.Abstractions` package doesn't reference any other packages, and so has minimal impact on deployed application size and dependencies. + +Use of `HierarchyId` for EF Core functionality such as queries and updates requires the [Microsoft.EntityFrameworkCore.SqlServer.HierarchyId](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer.HierarchyId) package. This package brings in `Microsoft.EntityFrameworkCore.SqlServer.Abstractions` and `Microsoft.SqlServer.Types` as transitive dependencies, and so is often the only package needed. Once the package is installed, use of `HierarchyId` is enabled by calling `UseHierarchyId` as part of the application's call to `UseSqlServer`. For example: + +```csharp +options.UseSqlServer( connectionString, x => x.UseHierarchyId()); ``` @@ -789,7 +1294,7 @@ options.UseSqlServer( > [!NOTE] > Unofficial support for `hierarchyid` in EF Core has been available for many years via the [EntityFrameworkCore.SqlServer.HierarchyId](https://www.nuget.org/packages/EntityFrameworkCore.SqlServer.HierarchyId) package. This package has been maintained as a collaboration between the community and the EF team. Now that there is official support for `hierarchyid` in .NET, the code from this community package forms, with the permission of the original contributors, the basis for the official package described here. Many thanks to all those involved over the years, including [@aljones](https://github.com/aljones), [@cutig3r](https://github.com/cutig3r), [@huan086](https://github.com/huan086), [@kmataru](https://github.com/kmataru), [@mehdihaghshenas](https://github.com/mehdihaghshenas), and [@vyrotek](https://github.com/vyrotek) -#### Modeling hierarchies +### Modeling hierarchies The `HierarchyId` type can be used for properties of an entity type. For example, assume we want to model the paternal family tree of some fictional [halflings](https://en.wikipedia.org/wiki/Halfling). In the entity type for `Halfling`, a `HierarchyId` property can be used to locate each halfling in the family tree. @@ -871,7 +1376,7 @@ The following code inserts this family tree into a database using EF Core: > [!TIP] > If needed, decimal values can be used to create new nodes between two existing nodes. For example, `/3/2.5/2/` goes between `/3/2/2/` and `/3/3/2/`. -#### Querying hierarchies +### Querying hierarchies `HierarchyId` exposes several methods that can be used in LINQ queries. @@ -1060,7 +1565,7 @@ ORDER BY [h].[PathFromPatriarch].GetLevel() DESC Running this query with "Bilbo" and "Frodo" tells us that their common ancestor is "Balbo". -#### Updating hierarchies +### Updating hierarchies The normal [change tracking](xref:core/change-tracking/index) and [SaveChanges](xref:core/saving/basic) mechanisms can be used to update `hierarchyid` columns. @@ -1079,516 +1584,652 @@ For example, I'm sure we all remember the scandal of SR 1752 (a.k.a. "LongoGate" Then `GetReparentedValue` is used to update the `HierarchyId` for Longo and each descendent, followed by a call to `SaveChangesAsync`: +[!code-csharp[GetReparentedValue](../../../../samples/core/Miscellaneous/NewInEFCore8/HierarchyIdSample.cs?name=GetReparentedValue)] + +This results in the following database update: + +```sql +SET NOCOUNT ON; +UPDATE [Halflings] SET [PathFromPatriarch] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +UPDATE [Halflings] SET [PathFromPatriarch] = @p2 +OUTPUT 1 +WHERE [Id] = @p3; +UPDATE [Halflings] SET [PathFromPatriarch] = @p4 +OUTPUT 1 +WHERE [Id] = @p5; +``` + +Using these parameters: + +```text + @p1='9', + @p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object), + @p3='16', + @p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object), + @p5='23', + @p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object) + ``` + +> [!NOTE] +> The parameters values for `HierarchyId` properties are sent to the database in their compact, binary format. + +Following the update, querying for the descendents of "Mungo" returns "Bungo", "Belba", "Linda", "Bingo", "Bilbo", "Falco", and "Poppy", while querying for the descendents of "Ponto" returns "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony", and "Angelica". + +## Raw SQL queries for unmapped types + +EF7 introduced [raw SQL queries returning scalar types](xref:core/querying/sql-queries#querying-scalar-(non-entity)-types). This is enhanced in EF8 to include raw SQL queries returning any mappable CLR type, without including that type in the EF model. + +> [!TIP] +> The code shown here comes from [RawSqlSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs). + +Queries using unmapped types are executed using or . The former uses string interpolation to parameterize the query, which helps ensure that all non-constant values are parameterized. For example, consider the following database table: + +```sql +CREATE TABLE [Posts] ( + [Id] int NOT NULL IDENTITY, + [Title] nvarchar(max) NOT NULL, + [Content] nvarchar(max) NOT NULL, + [PublishedOn] date NOT NULL, + [BlogId] int NOT NULL, +); +``` + +`SqlQuery` can be used to query this table and return instances of a `BlogPost` type with properties corresponding to the columns in the table: + +For example: + +```csharp +public class BlogPost +{ + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public DateOnly PublishedOn { get; set; } + public int BlogId { get; set; } +} +``` + +For example: + + -[!code-csharp[GetReparentedValue](../../../../samples/core/Miscellaneous/NewInEFCore8/HierarchyIdSample.cs?name=GetReparentedValue)] +[!code-csharp[SqlQueryAllColumns](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryAllColumns)] -This results in the following database update: +This query is parameterized and executed as: ```sql -SET NOCOUNT ON; -UPDATE [Halflings] SET [PathFromPatriarch] = @p0 -OUTPUT 1 -WHERE [Id] = @p1; -UPDATE [Halflings] SET [PathFromPatriarch] = @p2 -OUTPUT 1 -WHERE [Id] = @p3; -UPDATE [Halflings] SET [PathFromPatriarch] = @p4 -OUTPUT 1 -WHERE [Id] = @p5; +SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1 ``` -Using these parameters: - -```text - @p1='9', - @p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object), - @p3='16', - @p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object), - @p5='23', - @p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object) - ``` - -> [!NOTE] -> The parameters values for `HierarchyId` properties are sent to the database in their compact, binary format. - -Following the update, querying for the descendents of "Mungo" returns "Bungo", "Belba", "Linda", "Bingo", "Bilbo", "Falco", and "Poppy", while querying for the descendents of "Ponto" returns "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca", "Lotho", "Ponto", "Porto", "Peony", and "Angelica". +The type used for query results can contain common mapping constructs supported by EF Core, such as parameterized constructors and mapping attributes. For example: -### Smaller enhancements included in Preview 2 + +[!code-csharp[BlogPost](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=BlogPost)] -### Collections of primitive types +> [!NOTE] +> Types used in this way do not have keys defined and cannot have relationships to other types. Types with relationships must be mapped in the model. -A persistent question when using relational databases is what to do with collections of primitive types; that is, lists or arrays of integers, date/times, strings, and so on. If you're using PostgreSQL, then its easy to store these things using PostgreSQL's [built-in array type](https://www.postgresql.org/docs/current/arrays.html). For other databases, there are two common approaches: +The type used must have a property for every value in the result set, but do not need to match any table in the database. For example, the following type represents only a subset of information for each post, and includes the blog name, which comes from the `Blogs` table: -- Create a table with a column for the primitive type value and another column to act as a foreign key linking each value to its owner of the collection. -- Serialize the primitive collection into some column type that is handled by the database--for example, serialize to and from a string. + +[!code-csharp[PostSummary](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=PostSummary)] -The first option has advantages in many situations--we'll take a quick look at it at the end of this section. However, it's not a natural representation of the data in the model, and if what you really have is a collection of a primitive type, then the second option can be more effective. +And can be queried using `SqlQuery` in the same way as before: -Starting with Preview 4, EF8 now includes built-in support for the second option, using JSON as the serialization format. JSON works well for this since modern relational databases include built-in mechanisms for querying and manipulating JSON, such that the JSON column can, effectively, be treated as a table when needed, without the overhead of actually creating that table. These same mechanisms allow JSON to be passed in parameters and then used in similar way to table-valued parameters in queries--more about this later. + +[!code-csharp[SqlQueryJoin](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryJoin)] -> [!TIP] -> The code shown here comes from [PrimitiveCollectionsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs). +One nice feature of `SqlQuery` is that it returns an `IQueryable` which can be composed on using LINQ. For example, a 'Where' clause can be added to the query above: -#### Primitive collection properties + +[!code-csharp[SqlQueryJoinComposed](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryJoinComposed)] -EF Core can map any `IEnumerable` property, where `T` is a primitive type, to a JSON column in the database. This is done by convention for public properties which have both a getter and a setter. For example, all properties in the following entity type are mapped to JSON columns by convention: +This is executed as: -```csharp -public class PrimitiveCollections -{ - public IEnumerable Ints { get; set; } - public ICollection Strings { get; set; } - public ISet DateTimes { get; set; } - public IList Dates { get; set; } - public uint[] UnsignedInts { get; set; } - public List Booleans { get; set; } - public List Urls { get; set; } -} +```sql +SELECT [n].[BlogName], [n].[PostTitle], [n].[PublishedOn] +FROM ( + SELECT b.Name AS BlogName, p.Title AS PostTitle, p.PublishedOn + FROM Posts AS p + INNER JOIN Blogs AS b ON p.BlogId = b.Id + ) AS [n] +WHERE [n].[PublishedOn] >= @__cutoffDate_1 AND [n].[PublishedOn] < @__end_2 ``` -> [!NOTE] -> What do we mean by "primitive type" in this context? Essentially, something that the database provider knows how to map, using some kind of value conversion if necessary. For example, in the entity type above, the types `int`, `string`, `DateTime`, `DateOnly` and `bool` are all handled without conversion by the database provider. SQL Server does not have native support for unsigned ints or URIs, but `uint` and `Uri` are still treated as primitive types because there are [built-in value converters](xref:core/modeling/value-conversions#built-in-converters) for these types. - -By default, EF Core uses an unconstrained Unicode string column type to hold the JSON, since this protects against data loss with large collections. However, on some database systems, such as SQL Server, specifying a maximum length for the string can improve performance. This, along with other column configuration, can be done [in the normal way](xref:core/modeling/entity-properties). For example: +At this point it is worth remembering that all of the above can be done completely in LINQ without the need to write any SQL. This includes returning instances of an unmapped type like `PostSummary`. For example, the preceding query can be written in LINQ as: ```csharp -modelBuilder - .Entity() - .Property(e => e.Booleans) - .HasMaxLength(1024) - .IsUnicode(false); +var summaries = + await context.Posts.Select( + p => new PostSummary + { + BlogName = p.Blog.Name, + PostTitle = p.Title, + PublishedOn = p.PublishedOn, + }) + .Where(p => p.PublishedOn >= start && p.PublishedOn < end) + .ToListAsync(); ``` -Or, using mapping attributes: +Which translates to much cleaner SQL: -```csharp -[MaxLength(2500)] -[Unicode(false)] -public uint[] UnsignedInts { get; set; } +```sql +SELECT [b].[Name] AS [BlogName], [p].[Title] AS [PostTitle], [p].[PublishedOn] +FROM [Posts] AS [p] +INNER JOIN [Blogs] AS [b] ON [p].[BlogId] = [b].[Id] +WHERE [p].[PublishedOn] >= @__start_0 AND [p].[PublishedOn] < @__end_1 ``` -A default column configuration can be used for all properties of a certain type using [pre-convention model configuration](xref:core/modeling/bulk-configuration#pre-convention-configuration). For example: +> [!TIP] +> EF is able to generate cleaner SQL when it is responsible for the entire query than it is when composing over user-supplied SQL because, in the former case, the full semantics of the query is available to EF. -```csharp -protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) -{ - configurationBuilder - .Properties>() - .AreUnicode(false) - .HaveMaxLength(4000); -} -``` +So far, all the queries have been executed directly against tables. `SqlQuery` can also be used to return results from a view without mapping the view type in the EF model. For example: -#### Queries with primitive collections + +[!code-csharp[SqlQueryView](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryView)] -Let's look at some of the queries that make use of collections of primitive types. For this, we'll need a simple model with two entity types. The first represents a [British public house](https://en.wikipedia.org/wiki/Pub), or "pub": +Likewise, `SqlQuery` can be used for the results of a function: +[!code-csharp[SqlQueryFunction](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryFunction)] - public int Id { get; set; } - public string Name { get; set; } - public string[] Beers { get; set; } - public List DaysVisited { get; private set; } = new(); - } +The returned `IQueryable` can be composed upon when it is the result of a view or function, just like it can be for the result of a table query. Stored procedures can be also be executed using `SqlQuery`, but most databases do not support composing over them. For example: + + -[!code-csharp[Pub](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=Pub)] +[!code-csharp[SqlQueryStoredProc](../../../../samples/core/Miscellaneous/NewInEFCore8/RawSqlSample.cs?name=SqlQueryStoredProc)] -The `Pub` type contains two primitive collections: +## Enhancements to lazy-loading -- `Beers` is an array of strings representing the beer brands available at the pub. -- `DaysVisited` is a list of the dates on which the pub was visited. +### Lazy-loading for no-tracking queries + +EF8 adds support for [lazy-loading of navigations](xref:core/querying/related-data/lazy) on entities that are not being tracked by the `DbContext`. This means a no-tracking query can be followed by lazy-loading of navigations on the entities returned by the no-tracking query. > [!TIP] -> In a real application, it would probably make more sense to create an entity type for beer, and have a table for beers. We're showing a primitive collection here to illustrate how they work. But remember, just because you can model something as a primitive collection doesn't mean that you necessarily should. +> The code for the lazy-loading examples shown below comes from [LazyLoadingSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs). -The second entity type represents a dog walk in the British countryside: +For example, consider a no-tracking query for blogs: -[!code-csharp[DogWalk](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=DogWalk)] +[!code-csharp[NoTrackingForBlogs](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=NoTrackingForBlogs)] -Like `Pub`, `DogWalk` also contains a collection of the dates visited, and a link to the closest pub since, you know, sometimes the dog needs a saucer of beer after a long walk. +If `Blog.Posts` is configured for lazy-loading, for example, using lazy-loading proxies, then accessing `Posts` will cause it to load from the database: + + +[!code-csharp[ChooseABlog](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=ChooseABlog)] -Using this model, the first query we will do is a simple `Contains` query to find all walks with one of several different terrains: +EF8 also reports whether or not a given navigation is loaded for entities not tracked by the context. For example: -[!code-csharp[WalksWithTerrain](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=WalksWithTerrain)] +[!code-csharp[IsLoaded](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=IsLoaded)] -This is already translated by current versions of EF Core by inlining the values to look for. For example, when using SQL Server: +There are a few important considerations when using lazy-loading in this way: -```sql -SELECT [w].[Name] -FROM [Walks] AS [w] -WHERE [w].[Terrain] IN (1, 5, 4) -``` +- Lazy-loading will only succeed until the `DbContext` used to query the entity is disposed. +- Entities queried in this way a reference to their `DbContext`, even though they are not tracked by it. Care should be taken to avoid memory leaks if the entity instances will have long lifetimes. +- Explicitly detaching the entity by setting its state to `EntityState.Detached` severs the reference to the `DbContext` and lazy-loading will no longer work. +- Remember that all lazy-loading uses synchronous I/O, since there is no way to access a property in an asynchronous manner. -However, this strategy does not work well with database query caching--see [Announcing EF8 Preview 4](https://devblogs.microsoft.com/dotnet/announcing-ef8-preview-4/) on the .NET Blog for a discussion of this issue. +Lazy-loading from untracked entities works for both [lazy-loading proxies](xref:core/querying/related-data/lazy#lazy-loading-with-proxies) and [lazy-loading without proxies](xref:core/querying/related-data/lazy#lazy-loading-without-proxies). -> [!IMPORTANT] -> The inlining of values here is done in such a way that there is no chance of a SQL injection attack. The change to use JSON described below is all about performance, and nothing to do with security. +### Explicit loading from untracked entities -For EF Core 8, the default is now to pass the list of terrains as a single parameter containing a JSON collection. For example: +EF8 supports loading of navigations on untracked entities even when the entity or navigation is not configured for lazy-loading. Unlike with lazy-loading, this [explicit loading](xref:core/querying/related-data/explicit) can be done asynchronously. For example: -```none -@__terrains_0='[1,5,4]' -``` + +[!code-csharp[ExplicitLoad](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=ExplicitLoad)] -The query then uses `OpenJson` on SQL Server: +### Opt-out of lazy-loading for specific navigations -```sql -SELECT [w].[Name] -FROM [Walks] AS [w] -WHERE EXISTS ( - SELECT 1 - FROM OpenJson(@__terrains_0) AS [t] - WHERE CAST([t].[value] AS int) = [w].[Terrain]) -``` +EF8 allows configuration of specific navigations to not lazy-load, even when everything else is set up to do so. For example, to configure the `Post.Author` navigation to not lazy-load, do the following: -Or `json_each` on SQLite: + +[!code-csharp[NoLazyLoading](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=NoLazyLoading)] -```sql -SELECT "w"."Name" -FROM "Walks" AS "w" -WHERE EXISTS ( - SELECT 1 - FROM json_each(@__terrains_0) AS "t" - WHERE "t"."value" = "w"."Terrain") -``` +Disabling Lazy-loading like this works for both [lazy-loading proxies](xref:core/querying/related-data/lazy#lazy-loading-with-proxies) and [lazy-loading without proxies](xref:core/querying/related-data/lazy#lazy-loading-without-proxies). -> [!NOTE] -> `OpenJson` is only available on SQL Server 2016 ([compatibility level 130](/sql/t-sql/statements/alter-database-transact-sql-compatibility-level)) and later. You can tell SQL Server that you're using an older version by configuring the compatibility level as part of `UseSqlServer`. For example: -> -> ```csharp -> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) -> => optionsBuilder -> .UseSqlServer( -> @"Data Source=(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow", -> sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120)); -> ``` +Lazy-loading proxies work by overriding virtual navigation properties. In classic EF6 applications, a common source of bugs is forgetting to make a navigation virtual, since the navigation will then silently not lazy-load. Therefore, EF Core proxies throw by default when a navigation is not virtual. -Let's try a different kind of `Contains` query. In this case, we'll look for a value of the parameter collection in the column. For example, any pub that stocks Heineken: +This can be changed in EF8 to opt-in to the classic EF6 behavior such that a navigation can be made to not lazy-load simply by making the navigation non-virtual. This opt-in is configured as part of the call to `UseLazyLoadingProxies`. For example: -[!code-csharp[PubsWithHeineken](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsWithHeineken)] +[!code-csharp[IgnoreNonVirtualNavigations](../../../../samples/core/Miscellaneous/NewInEFCore8/LazyLoadingSample.cs?name=IgnoreNonVirtualNavigations)] -This translates to the following on SQL Server: +## Access to tracked entities -```sql -SELECT [p].[Name] -FROM [Pubs] AS [p] -WHERE EXISTS ( - SELECT 1 - FROM OpenJson([p].[Beers]) AS [b] - WHERE [b].[value] = @__beer_0) -``` +### Lookup tracked entities by primary, alternate, or foreign key -`OpenJson` is now used to to extract values from JSON column so that each value can be matched to the passed parameter. +Internally, EF maintains data structures for finding tracked entities by primary, alternate, or foreign key. These data structures are used for efficient fixup between related entities when new entities are tracked or relationships change. -We can combine the use of `OpenJson` on the parameter with `OpenJson` on the column. For example, to find pubs that stock any one of a variety of lagers: +EF8 contains new public APIs so that applications can now use these data structures to efficiently lookup tracked entities. These APIs are accessed through the of the entity type. For example, to lookup a tracked entity by its primary key: -[!code-csharp[PubsWithLager](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsWithLager)] +[!code-csharp[LookupByPrimaryKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByPrimaryKey)] -This translates to the following on SQL Server: +> [!TIP] +> The code shown here comes from [LookupByKeySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs). -```sql -SELECT [p].[Name] -FROM [Pubs] AS [p] -WHERE EXISTS ( - SELECT 1 - FROM OpenJson(@__beers_0) AS [b] - WHERE EXISTS ( - SELECT 1 - FROM OpenJson([p].[Beers]) AS [b0] - WHERE [b0].[value] = [b].[value] OR ([b0].[value] IS NULL AND [b].[value] IS NULL))) -``` +The [`FindEntry`](https://github.com/dotnet/efcore/blob/81886272a761df8fafe4970b895b1e1fe35effb8/src/EFCore/ChangeTracking/LocalView.cs#L543) method returns either the for the tracked entity, or `null` if no entity with the given key is being tracked. Like all methods on `LocalView`, the database is never queried, even if the entity is not found. The returned entry contains the entity itself, as well as tracking information. For example: -The `@__beers_0` parameter value here is `["Carling","Heineken","Stella Artois","Carlsberg"]`. + +[!code-csharp[UseEntry](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=UseEntry)] -Let's look at a query that makes use of the column containing a collection of dates. For example, to find pubs visited this year: +Looking up an entity by anything other than a primary key requires that the property name be specified. For example, to look up by an alternate key: -[!code-csharp[PubsVisitedThisYear](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsVisitedThisYear)] - -This translates to the following on SQL Server: +[!code-csharp[LookupByAlternateKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByAlternateKey)] -```sql -SELECT [p].[Name] -FROM [Pubs] AS [p] -WHERE EXISTS ( - SELECT 1 - FROM OpenJson([p].[DaysVisited]) AS [d] - WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0) -``` +Or to look up by a unique foreign key: -Notice that the query makes use of the date-specific function `DATEPART` here because EF _knows that the primitive collection contains dates_. It might not seem like it, but this is actually really important. Because EF knows what's in the collection, it can generate appropriate SQL to use the typed values with parameters, functions, other columns etc. + +[!code-csharp[LookupByUniqueForeignKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByUniqueForeignKey)] -Let's use the date collection again, this time to order appropriately for the type and project values extracted from the collection. For example, let's list pubs in the order that they were first visited, and with the first and last date each pub was visited: +So far, the lookups have always returned a single entry, or `null`. However, some lookups can return more than one entry, such as when looking up by a non-unique foreign key. The [`GetEntries`](https://github.com/dotnet/efcore/blob/81886272a761df8fafe4970b895b1e1fe35effb8/src/EFCore/ChangeTracking/LocalView.cs#L664) method should be used for these lookups. For example: -[!code-csharp[PubsVisitedInOrder](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=PubsVisitedInOrder)] +[!code-csharp[LookupByForeignKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByForeignKey)] -This translates to the following on SQL Server: +In all these cases, the value being used for the lookup is either a primary key, alternate key, or foreign key value. EF uses its internal data structures for these lookups. However, lookups by value can also be used for the value of any property or combination of properties. For example, to find all archived posts: -```sql -SELECT [p].[Name], ( - SELECT TOP(1) CAST([d0].[value] AS date) - FROM OpenJson([p].[DaysVisited]) AS [d0] - ORDER BY CAST([d0].[value] AS date)) AS [FirstVisited], ( - SELECT TOP(1) CAST([d1].[value] AS date) - FROM OpenJson([p].[DaysVisited]) AS [d1] - ORDER BY CAST([d1].[value] AS date) DESC) AS [LastVisited] -FROM [Pubs] AS [p] -ORDER BY ( - SELECT TOP(1) CAST([d].[value] AS date) - FROM OpenJson([p].[DaysVisited]) AS [d] - ORDER BY CAST([d].[value] AS date)) -``` + +[!code-csharp[LookupByAnyProperty](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByAnyProperty)] -And finally, just how often do we end up visiting the closest pub when taking the dog for a walk? Let's find out: +This lookup requires a scan of all tracked `Post` instances, and so will be less efficient than key lookups. However, it is usually still faster than naive queries using . + +Finally, it is also possible to perform lookups against composite keys, other combinations of multiple properties, or when the property type is not known at compile time. For example: -[!code-csharp[WalksWithADrink](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs?name=WalksWithADrink)] +[!code-csharp[LookupByCompositePrimaryKey](../../../../samples/core/Miscellaneous/NewInEFCore8/LookupByKeySample.cs?name=LookupByCompositePrimaryKey)] -This translates to the following on SQL Server: +## Model building + +### Discriminator columns have max length + +In EF8, string discriminator columns used for [TPH inheritance mapping](xref:core/modeling/inheritance) are now configured with a max length. This length is calculated as the smallest Fibonacci number that covers all defined discriminator values. For example, consider the following hierarchy: + +```csharp +public abstract class Document +{ + public int Id { get; set; } + public string Title { get; set; } +} + +public abstract class Book : Document +{ + public string? Isbn { get; set; } +} + +public class PaperbackEdition : Book +{ +} + +public class HardbackEdition : Book +{ +} + +public class Magazine : Document +{ + public int IssueNumber { get; set; } +} +``` + +With the convention of using the class names for discriminator values, the possible values here are "PaperbackEdition", "HardbackEdition", and "Magazine", and hence the discriminator column is configured for a max length of 21. For example, when using SQL Server: ```sql -SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], ( - SELECT COUNT(*) - FROM OpenJson([w].[DaysVisited]) AS [d] - WHERE EXISTS ( - SELECT 1 - FROM OpenJson([p].[DaysVisited]) AS [d0] - WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], ( - SELECT COUNT(*) - FROM OpenJson([w].[DaysVisited]) AS [d1]) AS [TotalCount] -FROM [Walks] AS [w] -INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id] +CREATE TABLE [Documents] ( + [Id] int NOT NULL IDENTITY, + [Title] nvarchar(max) NOT NULL, + [Discriminator] nvarchar(21) NOT NULL, + [Isbn] nvarchar(max) NULL, + [IssueNumber] int NULL, + CONSTRAINT [PK_Documents] PRIMARY KEY ([Id]), ``` -And reveals the following data: +> [!TIP] +> Fibonacci numbers are used to limit the number of times a migration is generated to change the column length as new types are added to the hierarchy. -```none -The Prince of Wales Feathers was visited 5 times in 8 "Ailsworth to Nene" walks. -The Prince of Wales Feathers was visited 6 times in 9 "Caster Hanglands" walks. -The Royal Oak was visited 6 times in 8 "Ferry Meadows" walks. -The White Swan was visited 7 times in 9 "Woodnewton" walks. -The Eltisley was visited 6 times in 8 "Eltisley" walks. -Farr Bay Inn was visited 7 times in 11 "Farr Beach" walks. -Farr Bay Inn was visited 7 times in 9 "Newlands" walks. -``` +### DateOnly/TimeOnly supported on SQL Server -Looks like beer and dog walking are a winning combination! +The and types were introduced in .NET 6 and have been supported for several database providers (e.g. SQLite, MySQL, and PostgreSQL) since their introduction. For SQL Server, the recent release of a [Microsoft.Data.SqlClient](https://www.nuget.org/packages/Microsoft.Data.SqlClient/) package targeting .NET 6 has allowed [ErikEJ to add support for these types at the ADO.NET level](https://github.com/dotnet/SqlClient/pull/1813). This in turn paved the way for support in EF8 for `DateOnly` and `TimeOnly` as properties in entity types. -#### Primitive collections in JSON documents +> [!TIP] +> `DateOnly` and `TimeOnly` can be used in EF Core 6 and 7 using the [ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly](https://www.nuget.org/packages/ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly) community package from [@ErikEJ](https://github.com/ErikEJ). -In all the examples above, column for primitive collection contains JSON. However, this is not the same as mapping [an owned entity type to a column containing a JSON document](xref:core/what-is-new/ef-core-7.0/whatsnew#json-columns), which was introduced in EF7. But what if that JSON document itself contains a primitive collection? Well, all the queries above still work in the same way! For example, imagine we move the _days visited_ data into an owned type `Visits` mapped to a JSON document: +For example, consider the following EF model for British schools: -[!code-csharp[Pub](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsInJsonSample.cs?name=Pub)] +[!code-csharp[BritishSchools](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=BritishSchools)] > [!TIP] -> The code shown here comes from [PrimitiveCollectionsInJsonSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsInJsonSample.cs). - -We can now run a variation of our final query that, this time, extracts data from the JSON document, including queries into the primitive collections contained in the document: +> The code shown here comes from [DateOnlyTimeOnlySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs). - -[!code-csharp[WalksWithADrink](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsInJsonSample.cs?name=WalksWithADrink)] +> [!NOTE] +> This model represents only British schools and stores times as local (GMT) times. Handling different timezones would complicate this code significantly. Note that using `DateTimeOffset` would not help here, since opening and closing times have different offsets depending whether daylight saving time is active or not. -This translates to the following on SQL Server: +These entity types map to the following tables when using SQL Server. Notice that the `DateOnly` properties map to `date` columns, and the `TimeOnly` properties map to `time` columns. ```sql -SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], JSON_VALUE([w].[Visits], '$.LocationTag') AS [WalkLocationTag], JSON_VALUE([p].[Visits], '$.LocationTag') AS [PubLocationTag], ( - SELECT COUNT(*) - FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d] - WHERE EXISTS ( - SELECT 1 - FROM OpenJson(JSON_VALUE([p].[Visits], '$.DaysVisited')) AS [d0] - WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR ([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], ( - SELECT COUNT(*) - FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d1]) AS [TotalCount] -FROM [Walks] AS [w] -INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id] +CREATE TABLE [Schools] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + [Founded] date NOT NULL, + CONSTRAINT [PK_Schools] PRIMARY KEY ([Id])); + +CREATE TABLE [OpeningHours] ( + [SchoolId] int NOT NULL, + [Id] int NOT NULL IDENTITY, + [DayOfWeek] int NOT NULL, + [OpensAt] time NULL, + [ClosesAt] time NULL, + CONSTRAINT [PK_OpeningHours] PRIMARY KEY ([SchoolId], [Id]), + CONSTRAINT [FK_OpeningHours_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE); + +CREATE TABLE [Term] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + [FirstDay] date NOT NULL, + [LastDay] date NOT NULL, + [SchoolId] int NOT NULL, + CONSTRAINT [PK_Term] PRIMARY KEY ([Id]), + CONSTRAINT [FK_Term_Schools_SchoolId] FOREIGN KEY ([SchoolId]) REFERENCES [Schools] ([Id]) ON DELETE CASCADE); ``` -And to a similar query when using SQLite: +Queries using `DateOnly` and `TimeOnly` work in the expected manner. For example, the following LINQ query finds schools that are currently open: + + +[!code-csharp[OpenSchools](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=OpenSchools)] + +This query translates to the following SQL, as shown by : ```sql -SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", "w"."Visits" ->> 'LocationTag' AS "WalkLocationTag", "p"."Visits" ->> 'LocationTag' AS "PubLocationTag", ( - SELECT COUNT(*) - FROM json_each("w"."Visits" ->> 'DaysVisited') AS "d" - WHERE EXISTS ( - SELECT 1 - FROM json_each("p"."Visits" ->> 'DaysVisited') AS "d0" - WHERE "d0"."value" = "d"."value")) AS "Count", json_array_length("w"."Visits" ->> 'DaysVisited') AS "TotalCount" -FROM "Walks" AS "w" -INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id" +DECLARE @__today_0 date = '2023-02-07'; +DECLARE @__dayOfWeek_1 int = 2; +DECLARE @__time_2 time = '19:53:40.4798052'; + +SELECT [s].[Id], [s].[Founded], [s].[Name], [o0].[SchoolId], [o0].[Id], [o0].[ClosesAt], [o0].[DayOfWeek], [o0].[OpensAt] +FROM [Schools] AS [s] +LEFT JOIN [OpeningHours] AS [o0] ON [s].[Id] = [o0].[SchoolId] +WHERE EXISTS ( + SELECT 1 + FROM [Term] AS [t] + WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0 AND [t].[LastDay] >= @__today_0) AND EXISTS ( + SELECT 1 + FROM [OpeningHours] AS [o] + WHERE [s].[Id] = [o].[SchoolId] AND [o].[DayOfWeek] = @__dayOfWeek_1 AND [o].[OpensAt] < @__time_2 AND [o].[ClosesAt] >= @__time_2) +ORDER BY [s].[Id], [o0].[SchoolId] ``` -> [!TIP] -> Notice that on SQLite EF Core now makes use of the `->>` operator, resulting in queries that are both easier to read and often more performant. +`DateOnly` and `TimeOnly` can also be used in JSON columns. For example, `OpeningHours` can be saved as a JSON document, resulting in data that looks like this: -#### Mapping primitive collections to a table +| Column | Value | +|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Id | 2 | +| Name | Farr High School | +| Founded | 1964-05-01 | +| OpeningHours |
[
{ "DayOfWeek": "Sunday", "ClosesAt": null, "OpensAt": null },
{ "DayOfWeek": "Monday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Tuesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Wednesday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Thursday", "ClosesAt": "15:35:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Friday", "ClosesAt": "12:50:00", "OpensAt": "08:45:00" },
{ "DayOfWeek": "Saturday", "ClosesAt": null, "OpensAt": null }
] | -We mentioned above that another option for primitive collections is to map them to a different table. First class support for this is tracked by [Issue #25163](https://github.com/dotnet/efcore/issues/25163); make sure to vote for this issue if it is important to you. Until this is implemented, the best approach is to create a wrapping type for the primitive. For example, let's create a type for `Beer`: +Combining two features from EF8, we can now query for opening hours by indexing into the JSON collection. For example: -[!code-csharp[Beer](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionToTableSample.cs?name=Beer)] +[!code-csharp[OpenSchoolsJson](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=OpenSchoolsJson)] -Notice that the type simply wraps the primitive value--it doesn't have a primary key or any foreign keys defined. This type can then be used in the `Pub` class: +This query translates to the following SQL, as shown by : - -[!code-csharp[Pub](../../../../samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionToTableSample.cs?name=Pub)] +[!code-csharp[UpdateForSnowDay](../../../../samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs?name=UpdateForSnowDay)] -EF will now create a `Beer` table, synthesizing primary key and foreign key columns back to the `Pubs` table. For example, on SQL Server: +This update translates to the following SQL: ```sql -CREATE TABLE [Beer] ( - [PubId] int NOT NULL, - [Id] int NOT NULL IDENTITY, - [Name] nvarchar(max) NOT NULL, - CONSTRAINT [PK_Beer] PRIMARY KEY ([PubId], [Id]), - CONSTRAINT [FK_Beer_Pubs_PubId] FOREIGN KEY ([PubId]) REFERENCES [Pubs] ([Id]) ON DELETE CASCADE +UPDATE [t0] +SET [t0].[LastDay] = DATEADD(day, CAST(1 AS int), [t0].[LastDay]) +FROM [Schools] AS [s] +INNER JOIN [Term] AS [t0] ON [s].[Id] = [t0].[SchoolId] +WHERE EXISTS ( + SELECT 1 + FROM [Term] AS [t] + WHERE [s].[Id] = [t].[SchoolId] AND DATEPART(year, [t].[LastDay]) = 2022) ``` + +### Reverse engineer Synapse and Dynamics 365 TDS + +EF8 reverse engineering (a.k.a. scaffolding from an existing database) now supports [Synapse Serverless SQL Pool](/azure/synapse-analytics/sql/on-demand-workspace-overview) and [Dynamics 365 TDS Endpoint](/power-apps/developer/data-platform/dataverse-sql-query) databases. + +> [!WARNING] +> These database systems have differences from normal SQL Server and Azure SQL databases. These differences mean that not all EF Core functionality is supported when writing queries against or performing other operations with these database systems. + +## Enhancements to Math translations + +[Generic math](/dotnet/standard/generics/math) interfaces were introduced in .NET 7. Concrete types like `double` and `float` implemented these interfaces adding new APIs mirroring the existing functionality of [Math](/dotnet/api/system.math) and [MathF](/dotnet/api/system.mathf). + +EF Core 8 translates calls to these generic math APIs in LINQ using providers' existing SQL translations for `Math` and `MathF`. This means you're now free to choose between calls either like `Math.Sin` or `double.Sin` in your EF queries. + +We worked with the .NET team to add two new generic math methods in .NET 8 that are implemented on `double` and `float`. These are also translated to SQL in EF Core 8. + +| .NET | SQL | +|------------------|---------| +| DegreesToRadians | RADIANS | +| RadiansToDegrees | DEGREES | + +Finally, we worked with Eric Sink in the [SQLitePCLRaw project](https://github.com/ericsink/SQLitePCL.raw) to enable the [SQLite math functions](https://sqlite.org/lang_mathfunc.html) in their builds of the native SQLite library. This includes the native library you get by default when you install the EF Core SQLite provider. This enables several new SQL translations in LINQ. + +| .NET | SQLite | +|------------------|---------------| +| DegreesToRadians | radians | +| RadiansToDegrees | degrees | +| Acos | acos | +| Acosh | acosh | +| Asin | asin | +| Asinh | asinh | +| Atan | atan | +| Atan2 | atan2 | +| Atanh | atanh | +| Ceiling | ceiling | +| Cos | cos | +| Cosh | cosh | +| Exp | exp | +| Floor | floor | +| Log | `ln` or `log` | +| Log2 | log2 | +| Log10 | log10 | +| Pow | pow | +| Sign | sign | +| Sin | sin | +| Sinh | sinh | +| Sqrt | sqrt | +| Tan | tan | +| Tanh | tanh | +| Truncate | trunc | diff --git a/entity-framework/toc.yml b/entity-framework/toc.yml index b7445faeab..e0d2cbf85e 100644 --- a/entity-framework/toc.yml +++ b/entity-framework/toc.yml @@ -30,14 +30,12 @@ items: - name: Welcome! href: core/index.md - - name: "What's new in EF Core 7.0 (EF7)" - href: /ef/core/what-is-new/ef-core-7.0/whatsnew - - name: "Breaking changes in EF Core 7.0 (EF7)" - href: /ef/core/what-is-new/ef-core-7.0/breaking-changes - name: "The plan for EF Core 8 (EF8)" href: /ef/core/what-is-new/ef-core-8.0/plan - - name: "Latest news and progress on EF Core 8 (EF8)" - href: https://aka.ms/efnews + - name: "What's new in EF Core 8.0 (EF8)" + href: /ef/core/what-is-new/ef-core-8.0/whatsnew + - name: "Breaking changes in EF Core 8.0 (EF8)" + href: /ef/core/what-is-new/ef-core-8.0/breaking-changes - name: Getting started items: - name: EF Core Overview diff --git a/samples/core/Benchmarks/Benchmarks.csproj b/samples/core/Benchmarks/Benchmarks.csproj index 656a7c1c3f..7c797bec66 100644 --- a/samples/core/Benchmarks/Benchmarks.csproj +++ b/samples/core/Benchmarks/Benchmarks.csproj @@ -1,13 +1,13 @@  Exe - net6.0 + net8.0 true - - + + diff --git a/samples/core/CascadeDeletes/CascadeDeletes.csproj b/samples/core/CascadeDeletes/CascadeDeletes.csproj index 0d4af30783..d491012a33 100644 --- a/samples/core/CascadeDeletes/CascadeDeletes.csproj +++ b/samples/core/CascadeDeletes/CascadeDeletes.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 - - - + + + diff --git a/samples/core/ChangeTracking/AccessingTrackedEntities/AccessingTrackedEntities.csproj b/samples/core/ChangeTracking/AccessingTrackedEntities/AccessingTrackedEntities.csproj index 5409170f7d..d613c1c96c 100644 --- a/samples/core/ChangeTracking/AccessingTrackedEntities/AccessingTrackedEntities.csproj +++ b/samples/core/ChangeTracking/AccessingTrackedEntities/AccessingTrackedEntities.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/ChangeTracking/AdditionalChangeTrackingFeatures/AdditionalChangeTrackingFeatures.csproj b/samples/core/ChangeTracking/AdditionalChangeTrackingFeatures/AdditionalChangeTrackingFeatures.csproj index 5409170f7d..d613c1c96c 100644 --- a/samples/core/ChangeTracking/AdditionalChangeTrackingFeatures/AdditionalChangeTrackingFeatures.csproj +++ b/samples/core/ChangeTracking/AdditionalChangeTrackingFeatures/AdditionalChangeTrackingFeatures.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/ChangeTracking/ChangeDetectionAndNotifications/ChangeDetectionAndNotifications.csproj b/samples/core/ChangeTracking/ChangeDetectionAndNotifications/ChangeDetectionAndNotifications.csproj index 59e56204b5..55434500ec 100644 --- a/samples/core/ChangeTracking/ChangeDetectionAndNotifications/ChangeDetectionAndNotifications.csproj +++ b/samples/core/ChangeTracking/ChangeDetectionAndNotifications/ChangeDetectionAndNotifications.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 - - - + + + diff --git a/samples/core/ChangeTracking/ChangeTrackerDebugging/ChangeTrackerDebugging.csproj b/samples/core/ChangeTracking/ChangeTrackerDebugging/ChangeTrackerDebugging.csproj index 5409170f7d..d613c1c96c 100644 --- a/samples/core/ChangeTracking/ChangeTrackerDebugging/ChangeTrackerDebugging.csproj +++ b/samples/core/ChangeTracking/ChangeTrackerDebugging/ChangeTrackerDebugging.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/ChangeTracking/ChangeTrackingInEFCore/ChangeTrackingInEFCore.csproj b/samples/core/ChangeTracking/ChangeTrackingInEFCore/ChangeTrackingInEFCore.csproj index 5409170f7d..d613c1c96c 100644 --- a/samples/core/ChangeTracking/ChangeTrackingInEFCore/ChangeTrackingInEFCore.csproj +++ b/samples/core/ChangeTracking/ChangeTrackingInEFCore/ChangeTrackingInEFCore.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/ChangeTracking/ChangingFKsAndNavigations/ChangingFKsAndNavigations.csproj b/samples/core/ChangeTracking/ChangingFKsAndNavigations/ChangingFKsAndNavigations.csproj index 5409170f7d..d613c1c96c 100644 --- a/samples/core/ChangeTracking/ChangingFKsAndNavigations/ChangingFKsAndNavigations.csproj +++ b/samples/core/ChangeTracking/ChangingFKsAndNavigations/ChangingFKsAndNavigations.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/ChangeTracking/IdentityResolutionInEFCore/IdentityResolutionInEFCore.csproj b/samples/core/ChangeTracking/IdentityResolutionInEFCore/IdentityResolutionInEFCore.csproj index 0604c3b22b..3dbbe787f1 100644 --- a/samples/core/ChangeTracking/IdentityResolutionInEFCore/IdentityResolutionInEFCore.csproj +++ b/samples/core/ChangeTracking/IdentityResolutionInEFCore/IdentityResolutionInEFCore.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/Cosmos/Cosmos.csproj b/samples/core/Cosmos/Cosmos.csproj index f30608146e..8e028a04a7 100644 --- a/samples/core/Cosmos/Cosmos.csproj +++ b/samples/core/Cosmos/Cosmos.csproj @@ -2,11 +2,11 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/DbContextPooling/DbContextPooling.csproj b/samples/core/DbContextPooling/DbContextPooling.csproj index 5007941a5c..b819008f23 100644 --- a/samples/core/DbContextPooling/DbContextPooling.csproj +++ b/samples/core/DbContextPooling/DbContextPooling.csproj @@ -1,10 +1,10 @@ Exe - net6.0 + net8.0 Samples - + diff --git a/samples/core/GetStarted/EFGetStarted.csproj b/samples/core/GetStarted/EFGetStarted.csproj index 8334fea527..360cf7bfdc 100644 --- a/samples/core/GetStarted/EFGetStarted.csproj +++ b/samples/core/GetStarted/EFGetStarted.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 $(MSBuildProjectDirectory) - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/samples/core/Intro/Intro.csproj b/samples/core/Intro/Intro.csproj index b123941b86..4a8000a171 100644 --- a/samples/core/Intro/Intro.csproj +++ b/samples/core/Intro/Intro.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 $(MSBuildProjectDirectory) - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/samples/core/Miscellaneous/Async/Async.csproj b/samples/core/Miscellaneous/Async/Async.csproj index 4c85f753eb..c798e60b12 100644 --- a/samples/core/Miscellaneous/Async/Async.csproj +++ b/samples/core/Miscellaneous/Async/Async.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFAsync EFAsync - - + + diff --git a/samples/core/Miscellaneous/AsyncWithSystemInteractive/AsyncWithSystemInteractive.csproj b/samples/core/Miscellaneous/AsyncWithSystemInteractive/AsyncWithSystemInteractive.csproj index 1e4ac35d45..ed2b597f8f 100644 --- a/samples/core/Miscellaneous/AsyncWithSystemInteractive/AsyncWithSystemInteractive.csproj +++ b/samples/core/Miscellaneous/AsyncWithSystemInteractive/AsyncWithSystemInteractive.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFAsyncWithSystemInteractive EFAsyncWithSystemInteractive - - + + diff --git a/samples/core/Miscellaneous/CachingInterception/CachingInterception.csproj b/samples/core/Miscellaneous/CachingInterception/CachingInterception.csproj index 7256c8d5bb..27893eb7a7 100644 --- a/samples/core/Miscellaneous/CachingInterception/CachingInterception.csproj +++ b/samples/core/Miscellaneous/CachingInterception/CachingInterception.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Miscellaneous/Collations/Collations.csproj b/samples/core/Miscellaneous/Collations/Collations.csproj index 7c02e4114d..6365a4929c 100644 --- a/samples/core/Miscellaneous/Collations/Collations.csproj +++ b/samples/core/Miscellaneous/Collations/Collations.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFCollations EFCollations - - + + diff --git a/samples/core/Miscellaneous/CommandInterception/CommandInterception.csproj b/samples/core/Miscellaneous/CommandInterception/CommandInterception.csproj index 5806422bed..f88a585737 100644 --- a/samples/core/Miscellaneous/CommandInterception/CommandInterception.csproj +++ b/samples/core/Miscellaneous/CommandInterception/CommandInterception.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Miscellaneous/CommandLine/CommandLine.csproj b/samples/core/Miscellaneous/CommandLine/CommandLine.csproj index e55cd1d84d..bdda816bab 100644 --- a/samples/core/Miscellaneous/CommandLine/CommandLine.csproj +++ b/samples/core/Miscellaneous/CommandLine/CommandLine.csproj @@ -1,12 +1,12 @@  - net6.0 + net8.0 - - + + diff --git a/samples/core/Miscellaneous/CompiledModels/CompiledModels.csproj b/samples/core/Miscellaneous/CompiledModels/CompiledModels.csproj index 7babbc54ce..f72167d4d5 100644 --- a/samples/core/Miscellaneous/CompiledModels/CompiledModels.csproj +++ b/samples/core/Miscellaneous/CompiledModels/CompiledModels.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/Miscellaneous/ConfiguringDbContext/ConfiguringDbContext.csproj b/samples/core/Miscellaneous/ConfiguringDbContext/ConfiguringDbContext.csproj index 84ac0f077e..7621dc7c32 100644 --- a/samples/core/Miscellaneous/ConfiguringDbContext/ConfiguringDbContext.csproj +++ b/samples/core/Miscellaneous/ConfiguringDbContext/ConfiguringDbContext.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - + diff --git a/samples/core/Miscellaneous/ConnectionInterception/ConnectionInterception.csproj b/samples/core/Miscellaneous/ConnectionInterception/ConnectionInterception.csproj index f8f71baafd..05a9c2ebbc 100644 --- a/samples/core/Miscellaneous/ConnectionInterception/ConnectionInterception.csproj +++ b/samples/core/Miscellaneous/ConnectionInterception/ConnectionInterception.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Miscellaneous/ConnectionResiliency/ConnectionResiliency.csproj b/samples/core/Miscellaneous/ConnectionResiliency/ConnectionResiliency.csproj index 1012e6dd40..3da20731de 100644 --- a/samples/core/Miscellaneous/ConnectionResiliency/ConnectionResiliency.csproj +++ b/samples/core/Miscellaneous/ConnectionResiliency/ConnectionResiliency.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFConnectionResiliency EFConnectionResiliency - - + + diff --git a/samples/core/Miscellaneous/DiagnosticListeners/DiagnosticListeners.csproj b/samples/core/Miscellaneous/DiagnosticListeners/DiagnosticListeners.csproj index 7256c8d5bb..27893eb7a7 100644 --- a/samples/core/Miscellaneous/DiagnosticListeners/DiagnosticListeners.csproj +++ b/samples/core/Miscellaneous/DiagnosticListeners/DiagnosticListeners.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Miscellaneous/Events/Events.csproj b/samples/core/Miscellaneous/Events/Events.csproj index 7256c8d5bb..27893eb7a7 100644 --- a/samples/core/Miscellaneous/Events/Events.csproj +++ b/samples/core/Miscellaneous/Events/Events.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Miscellaneous/Logging/Logging/Logging.csproj b/samples/core/Miscellaneous/Logging/Logging/Logging.csproj index ee940c6e08..8e9201068a 100644 --- a/samples/core/Miscellaneous/Logging/Logging/Logging.csproj +++ b/samples/core/Miscellaneous/Logging/Logging/Logging.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFLogging EFLogging - - + + diff --git a/samples/core/Miscellaneous/Logging/SimpleLogging/SimpleLogging.csproj b/samples/core/Miscellaneous/Logging/SimpleLogging/SimpleLogging.csproj index 042db51030..16861415a5 100755 --- a/samples/core/Miscellaneous/Logging/SimpleLogging/SimpleLogging.csproj +++ b/samples/core/Miscellaneous/Logging/SimpleLogging/SimpleLogging.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 - - + + diff --git a/samples/core/Miscellaneous/Multitenancy/Common/Common.csproj b/samples/core/Miscellaneous/Multitenancy/Common/Common.csproj index 132c02c59c..30402ac0e7 100644 --- a/samples/core/Miscellaneous/Multitenancy/Common/Common.csproj +++ b/samples/core/Miscellaneous/Multitenancy/Common/Common.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/samples/core/Miscellaneous/Multitenancy/MultiDb/MultiDb.csproj b/samples/core/Miscellaneous/Multitenancy/MultiDb/MultiDb.csproj index e1868af2e2..1b674a43ad 100644 --- a/samples/core/Miscellaneous/Multitenancy/MultiDb/MultiDb.csproj +++ b/samples/core/Miscellaneous/Multitenancy/MultiDb/MultiDb.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 enable enable - + diff --git a/samples/core/Miscellaneous/Multitenancy/SingleDbSingleTable/SingleDbSingleTable.csproj b/samples/core/Miscellaneous/Multitenancy/SingleDbSingleTable/SingleDbSingleTable.csproj index e1868af2e2..1b674a43ad 100644 --- a/samples/core/Miscellaneous/Multitenancy/SingleDbSingleTable/SingleDbSingleTable.csproj +++ b/samples/core/Miscellaneous/Multitenancy/SingleDbSingleTable/SingleDbSingleTable.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 enable enable - + diff --git a/samples/core/Miscellaneous/Multitenancy/TenantControls/TenantControls.csproj b/samples/core/Miscellaneous/Multitenancy/TenantControls/TenantControls.csproj index 182eb7e41c..2f158578e5 100644 --- a/samples/core/Miscellaneous/Multitenancy/TenantControls/TenantControls.csproj +++ b/samples/core/Miscellaneous/Multitenancy/TenantControls/TenantControls.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable @@ -12,7 +12,7 @@ - + diff --git a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/NewInEFCore6.Cosmos.csproj b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/NewInEFCore6.Cosmos.csproj index 1048bc2c90..cfdac33b47 100644 --- a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/NewInEFCore6.Cosmos.csproj +++ b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/NewInEFCore6.Cosmos.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Miscellaneous/NewInEFCore6/NewInEFCore6.csproj b/samples/core/Miscellaneous/NewInEFCore6/NewInEFCore6.csproj index 5ff7276c20..5b30aafa61 100644 --- a/samples/core/Miscellaneous/NewInEFCore6/NewInEFCore6.csproj +++ b/samples/core/Miscellaneous/NewInEFCore6/NewInEFCore6.csproj @@ -2,15 +2,15 @@ Exe - net6.0 + net8.0 - - - - + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj index 955638ec8b..62a3a49dda 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj +++ b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj @@ -2,21 +2,21 @@ Exe - net6.0 + net8.0 enable enable NewInEfCore7 - - - - - - - - + + + + + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs b/samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs index f66862583a..c8a72ccc41 100644 --- a/samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs +++ b/samples/core/Miscellaneous/NewInEFCore8/BlogsContext.cs @@ -27,8 +27,8 @@ public Website(Uri uri, string email) } public Guid Id { get; private set; } - public Uri Uri { get; init; } - public string Email { get; init; } + public Uri Uri { get; private set; } + public string Email { get; private set; } public virtual Blog Blog { get; set; } = null!; } @@ -171,7 +171,7 @@ public PostUpdate(IPAddress postedFrom, DateOnly updatedOn) } public IPAddress PostedFrom { get; private set; } - public string? UpdatedBy { get; init; } + public string? UpdatedBy { get; set; } public DateOnly UpdatedOn { get; private set; } public List Commits { get; } = new(); } diff --git a/samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs b/samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs new file mode 100644 index 0000000000..885e532457 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore8/ComplexTypesSample.cs @@ -0,0 +1,143 @@ +namespace NewInEfCore8; + +public static class ComplexTypesSample +{ + public static Task Use_mutable_class_as_complex_type() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + public static Task Use_mutable_class_as_complex_type_SQLite() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + private static async Task ComplexTypeTest() + where TContext : CustomerContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + #region SaveCustomer + var customer = new Customer + { + Name = "Willow", + Address = new() { Line1 = "Barking Gate", City = "Walpole St Peter", Country = "UK", PostCode = "PE14 7AV" } + }; + + context.Add(customer); + await context.SaveChangesAsync(); + #endregion + + #region CreateOrder + customer.Orders.Add( + new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, }); + + await context.SaveChangesAsync(); + #endregion + + #region ChangeSharedAddress + customer.Address.Line1 = "Peacock Lodge"; + await context.SaveChangesAsync(); + #endregion + + context.ChangeTracker.Clear(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + #region CustomerOrders + 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!; + } + #endregion + + #region Address + public class Address + { + public required string Line1 { get; set; } + public string? Line2 { get; set; } + public required string City { get; set; } + public required string Country { get; set; } + public required string PostCode { get; set; } + } + #endregion + + public class CustomerContext : CustomerContextBase + { + } + + public class CustomerContextSqlite : CustomerContextBase + { + public CustomerContextSqlite() + : base(useSqlite: true) + { + } + } + + public abstract class CustomerContextBase(bool useSqlite = false) : DbContext + { + public bool UseSqlite { get; } = useSqlite; + public bool LoggingEnabled { get; set; } + + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + #region ComplexTypeConfig + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .ComplexProperty(e => e.Address); + + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.BillingAddress); + b.ComplexProperty(e => e.ShippingAddress); + }); + } + #endregion + + public async Task Seed() + { + await SaveChangesAsync(); + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs b/samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs index a560178326..d7a1b3edc0 100644 --- a/samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs +++ b/samples/core/Miscellaneous/NewInEFCore8/DateOnlyTimeOnlySample.cs @@ -42,7 +42,15 @@ private static async Task DateOnlyTimeOnlyTest() Console.WriteLine("Current terms:"); foreach (var school in currentTerms) { - Console.WriteLine($" The current term for {school.Name} is '{school.Terms.Single().Name}' "); + var term = school.Terms.SingleOrDefault(); + if (term == null) + { + Console.WriteLine($" {school.Name} is not current in term."); + } + else + { + Console.WriteLine($" The current term for {school.Name} is '{term.Name}'."); + } } Console.WriteLine(); diff --git a/samples/core/Miscellaneous/NewInEFCore8/ImmutableComplexTypesSample.cs b/samples/core/Miscellaneous/NewInEFCore8/ImmutableComplexTypesSample.cs new file mode 100644 index 0000000000..2f113f4fa6 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore8/ImmutableComplexTypesSample.cs @@ -0,0 +1,156 @@ +namespace NewInEfCore8; + +public static class ImmutableComplexTypesSample +{ + public static Task Use_immutable_class_as_complex_type() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + public static Task Use_immutable_class_as_complex_type_SQLite() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + private static async Task ComplexTypeTest() + where TContext : CustomerContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + var customer = new Customer() { Name = "Willow", Address = new("Barking Gate", null, "Walpole St Peter", "UK", "PE14 7AV") }; + + context.Add(customer); + await context.SaveChangesAsync(); + + customer.Orders.Add( + new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, }); + + await context.SaveChangesAsync(); + + #region ChangeImmutableAddress + var currentAddress = customer.Address; + customer.Address = new Address( + "Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode); + + await context.SaveChangesAsync(); + #endregion + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + 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!; + } + + public class Address(string line1, string? line2, string city, string country, string postCode) + { + public string Line1 { get; } = line1; + public string? Line2 { get; } = line2; + public string City { get; } = city; + public string Country { get; } = country; + public string PostCode { get; } = postCode; + } + + public class CustomerContext : CustomerContextBase + { + } + + public class CustomerContextSqlite : CustomerContextBase + { + public CustomerContextSqlite() + : base(useSqlite: true) + { + } + } + + public abstract class CustomerContextBase(bool useSqlite = false) : DbContext + { + public bool UseSqlite { get; } = useSqlite; + public bool LoggingEnabled { get; set; } + + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer( + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + //sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120))) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ComplexProperty(e => e.Address, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.BillingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + b.ComplexProperty(e => e.ShippingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + }); + } + + public async Task Seed() + { + await SaveChangesAsync(); + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore8/ImmutableStructComplexTypesSample.cs b/samples/core/Miscellaneous/NewInEFCore8/ImmutableStructComplexTypesSample.cs new file mode 100644 index 0000000000..b5987b86da --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore8/ImmutableStructComplexTypesSample.cs @@ -0,0 +1,162 @@ +namespace NewInEfCore8; + +public static class ImmutableStructComplexTypesSample +{ + public static Task Use_immutable_struct_as_complex_type() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + public static Task Use_immutable_struct_as_complex_type_SQLite() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + private static async Task ComplexTypeTest() + where TContext : CustomerContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + var customer = new Customer() + { + Name = "Willow", + Address = new("Barking Gate", null, "Walpole St Peter", "UK", "PE14 7AV") + }; + + context.Add(customer); + await context.SaveChangesAsync(); + + customer.Orders.Add( + new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, }); + + await context.SaveChangesAsync(); + + #region UpdateImmutableStruct + var currentAddress = customer.Address; + customer.Address = new Address( + "Peacock Lodge", currentAddress.Line2, currentAddress.City, currentAddress.Country, currentAddress.PostCode); + + await context.SaveChangesAsync(); + #endregion + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + 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!; + } + + #region AddressImmutableStruct + public readonly struct Address(string line1, string? line2, string city, string country, string postCode) + { + public string Line1 { get; } = line1; + public string? Line2 { get; } = line2; + public string City { get; } = city; + public string Country { get; } = country; + public string PostCode { get; } = postCode; + } + #endregion + + public class CustomerContext : CustomerContextBase + { + } + + public class CustomerContextSqlite : CustomerContextBase + { + public CustomerContextSqlite() + : base(useSqlite: true) + { + } + } + + public abstract class CustomerContextBase(bool useSqlite = false) : DbContext + { + public bool UseSqlite { get; } = useSqlite; + public bool LoggingEnabled { get; set; } + + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer( + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + //sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120))) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ComplexProperty(e => e.Address, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.BillingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + b.ComplexProperty(e => e.ShippingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + }); + } + + public async Task Seed() + { + await SaveChangesAsync(); + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs b/samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs new file mode 100644 index 0000000000..5b71bafe06 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore8/NestedComplexTypesSample.cs @@ -0,0 +1,265 @@ +namespace NewInEfCore8; + +public static class NestedComplexTypesSample +{ + public static Task Use_mutable_classes_as_complex_types() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + public static Task Use_mutable_classes_as_complex_types_SQLite() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + private static async Task ComplexTypeTest() + where TContext : CustomerContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + var customerId = 1; + + #region QueryCustomer + var customer = await context.Customers.FirstAsync(e => e.Id == customerId); + #endregion + + var address = customer.Contact.Address; + var phone = customer.Contact.MobilePhone; + + var order = new Order { Contents = "Tesco Tasty Treats", BillingAddress = address, ShippingAddress = address, ContactPhone = phone }; + customer.Orders.Add(order); + + await context.SaveChangesAsync(); + + order.ContactPhone = customer.Contact.WorkPhone; + + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + + var customers = await context.Customers.Where(e => e.Id == customerId).Include(e => e.Orders).ToListAsync(); + + customer = customers.First(); + order = customer.Orders.First(); + + #region BillingAddressCurrentValue + var billingAddress = context.Entry(order) + .ComplexProperty(e => e.BillingAddress) + .CurrentValue; + #endregion + + #region PostCodeCurrentValue + var postCode = context.Entry(order) + .ComplexProperty(e => e.BillingAddress) + .Property(e => e.PostCode) + .CurrentValue; + #endregion + + #region CityCurrentValue + var currentCity = context.Entry(customer) + .ComplexProperty(e => e.Contact) + .ComplexProperty(e => e.Address) + .Property(e => e.City) + .CurrentValue; + #endregion + + #region SetCityCurrentValue + context.Entry(customer) + .ComplexProperty(e => e.Contact) + .ComplexProperty(e => e.Address) + .Property(e => e.City) + .CurrentValue = "Ames"; + #endregion + + #region CityOriginalValue + var originalCity = context.Entry(customer) + .ComplexProperty(e => e.Contact) + .ComplexProperty(e => e.Address) + .Property(e => e.City) + .OriginalValue; + #endregion + + #region SetPostCodeIsModified + context.Entry(customer) + .ComplexProperty(e => e.Contact) + .ComplexProperty(e => e.Address) + .Property(e => e.PostCode) + .IsModified = true; + #endregion + + var orderId = 1; + + #region QueryShippingAddress + var shippingAddress = await context.Orders + .Where(e => e.Id == orderId) + .Select(e => e.ShippingAddress) + .SingleAsync(); + #endregion + + var shippingCity = await context.Orders.Where(e => e.Id == orderId).Select(e => e.ShippingAddress.City).SingleAsync(); + + var addresses = await context.Orders.Select(e => new { e.ShippingAddress, e.BillingAddress }).ToListAsync(); + + #region QueryOrdersInCity + var city = "Walpole St Peter"; + var walpoleOrders = await context.Orders.Where(e => e.ShippingAddress.City == city).ToListAsync(); + #endregion + + var countryCode = 44; + var customersWithUkOrders = await context.Customers + .Where(e => e.Orders.Select(e => e.ContactPhone.CountryCode == countryCode).Any()) + .Select(e => new { e.Name, Phone = e.Orders.FirstOrDefault(e => e.ContactPhone.CountryCode == countryCode) }).ToListAsync(); + + #region QueryWithPhoneNumber + var phoneNumber = new PhoneNumber(44, 7777555777); + var customersWithNumber = await context.Customers + .Where( + e => e.Contact.MobilePhone == phoneNumber + || e.Contact.WorkPhone == phoneNumber + || e.Contact.HomePhone == phoneNumber) + .ToListAsync(); + #endregion + + await context.Customers + .Where( + e => e.Contact.MobilePhone == phoneNumber + || e.Contact.WorkPhone == phoneNumber + || e.Contact.HomePhone == phoneNumber) + .ExecuteDeleteAsync(); + + var allNumbers = await context.Customers + .Select(e => e.Contact.MobilePhone) + .Distinct() + .ToListAsync(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + #region CustomerWithContact + public class Customer + { + public int Id { get; set; } + public required string Name { get; set; } + public required Contact Contact { get; set; } + public List Orders { get; } = new(); + } + #endregion + + #region OrderWithPhone + public class Order + { + public int Id { get; set; } + public required string Contents { get; set; } + public required PhoneNumber ContactPhone { get; set; } + public required Address ShippingAddress { get; set; } + public required Address BillingAddress { get; set; } + public Customer Customer { get; set; } = null!; + } + #endregion + + #region NestedComplexTypes + 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; } + public required PhoneNumber MobilePhone { get; init; } + } + #endregion + + public class CustomerContext : CustomerContextBase + { + } + + public class CustomerContextSqlite : CustomerContextBase + { + public CustomerContextSqlite() + : base(useSqlite: true) + { + } + } + + public abstract class CustomerContextBase(bool useSqlite = false) : DbContext + { + public bool UseSqlite { get; } = useSqlite; + public bool LoggingEnabled { get; set; } + + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + #region ConfigureNestedTypes + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.Contact, + b => + { + b.ComplexProperty(e => e.Address); + b.ComplexProperty(e => e.HomePhone); + b.ComplexProperty(e => e.WorkPhone); + b.ComplexProperty(e => e.MobilePhone); + }); + }); + + modelBuilder.Entity( + b => + { + b.ComplexProperty(e => e.ContactPhone); + b.ComplexProperty(e => e.BillingAddress); + b.ComplexProperty(e => e.ShippingAddress); + }); + } + #endregion + + public async Task Seed() + { + var customer = new Customer + { + Name = "Willow", + Contact = new() + { + Address = new("Barking Gate", null, "Walpole St Peter", "UK", "PE14 7AV"), + HomePhone = new(44, 7777555777), + WorkPhone = new(1, 12345678901), + MobilePhone = new(49, 987654321) + } + }; + + Add(customer); + await SaveChangesAsync(); + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore8/NewInEFCore8.csproj b/samples/core/Miscellaneous/NewInEFCore8/NewInEFCore8.csproj index 8fe2979fc0..6d8a5a6b0b 100644 --- a/samples/core/Miscellaneous/NewInEFCore8/NewInEFCore8.csproj +++ b/samples/core/Miscellaneous/NewInEFCore8/NewInEFCore8.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 enable enable NewInEfCore8 @@ -10,15 +10,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs b/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs index 827aec36dc..ae7ee16223 100644 --- a/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore8/PrimitiveCollectionsSample.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.ObjectModel; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace NewInEfCore8; @@ -109,7 +110,7 @@ private static void PrintSampleName([CallerMemberName] string? methodName = null Console.WriteLine(); } - public class MyCollection : ICollection + public class MyCollection : IList { private readonly List _list = new(); public IEnumerator GetEnumerator() => _list.GetEnumerator(); @@ -121,6 +122,14 @@ public class MyCollection : ICollection public bool Remove(int item) => _list.Remove(item); public int Count => _list.Count; public bool IsReadOnly => ((ICollection)_list).IsReadOnly; + public int IndexOf(int item) => _list.IndexOf(item); + public void Insert(int index, int item) => _list.Insert(index, item); + public void RemoveAt(int index) => _list.RemoveAt(index); + public int this[int index] + { + get => _list[index]; + set => _list[index] = value; + } } public class PrimitiveCollections @@ -128,14 +137,14 @@ public class PrimitiveCollections public int Id { get; set; } public IEnumerable Ints { get; set; } = null!; public ICollection Strings { get; set; } = null!; - public ISet DateTimes { get; set; } = null!; + public Collection DateTimes { get; set; } = null!; public IList Dates { get; set; } = null!; [MaxLength(2500)] [Unicode(false)] public uint[] UnsignedInts { get; set; } = null!; - public HashSet Guids { get; set; } = null!; + public ObservableCollection Guids { get; set; } = null!; public List Booleans { get; set; } = null!; public List Urls { get; set; } = null!; @@ -301,7 +310,7 @@ public async Task Seed() { Ints = new[] { 1, 2, 3 }, Strings = new List { "One", "Two", "Three" }, - DateTimes = new HashSet { new(2023, 1, 1, 1, 1, 1), new(2023, 2, 2, 2, 2, 2), new(2023, 3, 3, 3, 3, 3) }, + DateTimes = new Collection { new(2023, 1, 1, 1, 1, 1), new(2023, 2, 2, 2, 2, 2), new(2023, 3, 3, 3, 3, 3) }, Dates = new List { new(2023, 1, 1), new(2023, 2, 2), new(2023, 3, 3) }, UnsignedInts = new uint[] { 1, 2, 3 }, Guids = new() { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }, diff --git a/samples/core/Miscellaneous/NewInEFCore8/Program.cs b/samples/core/Miscellaneous/NewInEFCore8/Program.cs index 34ad7a8748..ce14f81cd9 100644 --- a/samples/core/Miscellaneous/NewInEFCore8/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore8/Program.cs @@ -4,26 +4,44 @@ public class Program { public static async Task Main() { - await JsonColumnsSample.Json_columns_with_TPH(); + await ComplexTypesSample.Use_mutable_class_as_complex_type(); + await ImmutableComplexTypesSample.Use_immutable_class_as_complex_type(); + await RecordComplexTypesSample.Use_immutable_record_as_complex_type(); + await StructComplexTypesSample.Use_mutable_struct_as_complex_type(); + await ImmutableStructComplexTypesSample.Use_immutable_struct_as_complex_type(); + await NestedComplexTypesSample.Use_mutable_classes_as_complex_types(); + + await ComplexTypesSample.Use_mutable_class_as_complex_type_SQLite(); + await ImmutableComplexTypesSample.Use_immutable_class_as_complex_type_SQLite(); + await RecordComplexTypesSample.Use_immutable_record_as_complex_type_SQLite(); + await StructComplexTypesSample.Use_mutable_struct_as_complex_type_SQLite(); + await ImmutableStructComplexTypesSample.Use_immutable_struct_as_complex_type_SQLite(); + await NestedComplexTypesSample.Use_mutable_classes_as_complex_types_SQLite(); - // https://github.com/dotnet/efcore/issues/30886 - // await JsonColumnsSample.Json_columns_with_TPH_on_SQLite(); + await JsonColumnsSample.Json_columns_with_TPH(); + await JsonColumnsSample.Json_columns_with_TPH_on_SQLite(); await RawSqlSample.SqlQuery_for_unmapped_types(); - await LazyLoadingSample.Lazy_loading_for_no_tracking_queries(); + + // https://github.com/dotnet/efcore/issues/31597 + // await LazyLoadingSample.Lazy_loading_for_no_tracking_queries(); + await InheritanceSample.Discriminator_length_TPH(); + await LookupByKeySample.Lookup_tracked_entities_by_key(); - await DateOnlyTimeOnlySample.Can_use_DateOnly_TimeOnly_on_SQLite(); - // https://github.com/dotnet/efcore/issues/30885 - // await DateOnlyTimeOnlySample.Can_use_DateOnly_TimeOnly_on_SQL_Server(); - // await DateOnlyTimeOnlySample.Can_use_DateOnly_TimeOnly_on_SQL_Server_with_JSON(); + await DateOnlyTimeOnlySample.Can_use_DateOnly_TimeOnly_on_SQLite(); + await DateOnlyTimeOnlySample.Can_use_DateOnly_TimeOnly_on_SQL_Server(); + await DateOnlyTimeOnlySample.Can_use_DateOnly_TimeOnly_on_SQL_Server_with_JSON(); await HierarchyIdSample.SQL_Server_HierarchyId(); + await PrimitiveCollectionsSample.Queries_using_primitive_collections(); await PrimitiveCollectionsSample.Queries_using_primitive_collections_SQLite(); + await PrimitiveCollectionsInJsonSample.Queries_using_primitive_collections_in_JSON_documents(); await PrimitiveCollectionsInJsonSample.Queries_using_primitive_collections_in_JSON_documents_SQLite(); + await PrimitiveCollectionToTableSample.Queries_against_a_table_wrapping_a_primitive_type(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore8/RecordComplexTypesSample.cs b/samples/core/Miscellaneous/NewInEFCore8/RecordComplexTypesSample.cs new file mode 100644 index 0000000000..a779f32d92 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore8/RecordComplexTypesSample.cs @@ -0,0 +1,301 @@ +namespace NewInEfCore8; + +public static class RecordComplexTypesSample +{ + public static Task Use_immutable_record_as_complex_type() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + public static Task Use_immutable_record_as_complex_type_SQLite() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + private static async Task ComplexTypeTest() + where TContext : CustomerContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + var customer = new Customer() + { + Name = "Willow", + Address = new("Barking Gate", null, "Walpole St Peter", "UK", "PE14 7AV") + }; + + context.Add(customer); + await context.SaveChangesAsync(); + + customer.Orders.Add( + new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, }); + + await context.SaveChangesAsync(); + + customer.Address = customer.Address with { Line1 = "Peacock Lodge" }; + + await context.SaveChangesAsync(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + 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!; + } + + public record Address(string Line1, string? Line2, string City, string Country, string PostCode); + + public class CustomerContext : CustomerContextBase + { + } + + public class CustomerContextSqlite : CustomerContextBase + { + public CustomerContextSqlite() + : base(useSqlite: true) + { + } + } + + public abstract class CustomerContextBase(bool useSqlite = false) : DbContext + { + public bool UseSqlite { get; } = useSqlite; + public bool LoggingEnabled { get; set; } + + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer( + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + //sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120))) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ComplexProperty(e => e.Address, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.BillingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + b.ComplexProperty(e => e.ShippingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + }); + } + + public async Task Seed() + { + await SaveChangesAsync(); + } + } +} + +public static class StructRecordComplexTypesSample +{ + public static Task Use_immutable_struct_record_as_complex_type() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + public static Task Use_immutable_struct_record_as_complex_type_SQLite() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + private static async Task ComplexTypeTest() + where TContext : CustomerContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + var customer = new Customer() + { + Name = "Willow", + Address = new("Barking Gate", null, "Walpole St Peter", "UK", "PE14 7AV") + }; + + context.Add(customer); + await context.SaveChangesAsync(); + + customer.Orders.Add( + new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, }); + + await context.SaveChangesAsync(); + + #region ChangeImmutableRecord + customer.Address = customer.Address with { Line1 = "Peacock Lodge" }; + + await context.SaveChangesAsync(); + #endregion + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + 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!; + } + + #region RecordStructAddress + public readonly record struct Address(string Line1, string? Line2, string City, string Country, string PostCode); + #endregion + + public class CustomerContext : CustomerContextBase + { + } + + public class CustomerContextSqlite : CustomerContextBase + { + public CustomerContextSqlite() + : base(useSqlite: true) + { + } + } + + public abstract class CustomerContextBase(bool useSqlite = false) : DbContext + { + public bool UseSqlite { get; } = useSqlite; + public bool LoggingEnabled { get; set; } + + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer( + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + //sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120))) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ComplexProperty(e => e.Address, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.BillingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + b.ComplexProperty(e => e.ShippingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + }); + } + + public async Task Seed() + { + await SaveChangesAsync(); + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore8/StructComplexTypesSample.cs b/samples/core/Miscellaneous/NewInEFCore8/StructComplexTypesSample.cs new file mode 100644 index 0000000000..c117f56d4b --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore8/StructComplexTypesSample.cs @@ -0,0 +1,163 @@ +namespace NewInEfCore8; + +public static class StructComplexTypesSample +{ + public static Task Use_mutable_struct_as_complex_type() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + public static Task Use_mutable_struct_as_complex_type_SQLite() + { + PrintSampleName(); + return ComplexTypeTest(); + } + + private static async Task ComplexTypeTest() + where TContext : CustomerContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + var customer = new Customer() + { + Name = "Willow", + Address = new() { Line1 = "Barking Gate", City = "Walpole St Peter", Country = "UK", PostCode = "PE14 7AV" } + }; + + context.Add(customer); + await context.SaveChangesAsync(); + + customer.Orders.Add( + new Order { Contents = "Tesco Tasty Treats", BillingAddress = customer.Address, ShippingAddress = customer.Address, }); + + await context.SaveChangesAsync(); + + var currentAddress = customer.Address; + currentAddress.Line1 = "Peacock Lodge"; + customer.Address = currentAddress; + await context.SaveChangesAsync(); + + customer.Address = customer.Address with { Line1 = "Peacock Lodge" }; + + await context.SaveChangesAsync(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class Customer + { + public int Id { get; set; } + public required string Name { get; set; } + public 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!; + } + + #region AddressStruct + public struct Address + { + public required string Line1 { get; set; } + public string? Line2 { get; set; } + public required string City { get; set; } + public required string Country { get; set; } + public required string PostCode { get; set; } + } + #endregion + + public class CustomerContext : CustomerContextBase + { + } + + public class CustomerContextSqlite : CustomerContextBase + { + public CustomerContextSqlite() + : base(useSqlite: true) + { + } + } + + public abstract class CustomerContextBase(bool useSqlite = false) : DbContext + { + public bool UseSqlite { get; } = useSqlite; + public bool LoggingEnabled { get; set; } + + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer( + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + //sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseCompatibilityLevel(120))) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ComplexProperty(e => e.Address, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.BillingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + + b.ComplexProperty(e => e.ShippingAddress, + b => + { + b.Property(e => e.Line1); + b.Property(e => e.Line2); + b.Property(e => e.City); + b.Property(e => e.Country); + b.Property(e => e.PostCode); + }); + }); + } + + public async Task Seed() + { + await SaveChangesAsync(); + } + } +} diff --git a/samples/core/Miscellaneous/NullableReferenceTypes/NullableReferenceTypes.csproj b/samples/core/Miscellaneous/NullableReferenceTypes/NullableReferenceTypes.csproj index 4f0eb01205..f868d10c17 100644 --- a/samples/core/Miscellaneous/NullableReferenceTypes/NullableReferenceTypes.csproj +++ b/samples/core/Miscellaneous/NullableReferenceTypes/NullableReferenceTypes.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 8.0 enable - - + + diff --git a/samples/core/Miscellaneous/SaveChangesInterception/SaveChangesInterception.csproj b/samples/core/Miscellaneous/SaveChangesInterception/SaveChangesInterception.csproj index 7256c8d5bb..27893eb7a7 100644 --- a/samples/core/Miscellaneous/SaveChangesInterception/SaveChangesInterception.csproj +++ b/samples/core/Miscellaneous/SaveChangesInterception/SaveChangesInterception.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Modeling/BackingFields/BackingFields.csproj b/samples/core/Modeling/BackingFields/BackingFields.csproj index 1db4205c5d..e1af303ea8 100644 --- a/samples/core/Modeling/BackingFields/BackingFields.csproj +++ b/samples/core/Modeling/BackingFields/BackingFields.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.BackingFields EFModeling.BackingFields - + diff --git a/samples/core/Modeling/BulkConfiguration/BulkConfiguration.csproj b/samples/core/Modeling/BulkConfiguration/BulkConfiguration.csproj index 386ec1c8a7..0d16462245 100644 --- a/samples/core/Modeling/BulkConfiguration/BulkConfiguration.csproj +++ b/samples/core/Modeling/BulkConfiguration/BulkConfiguration.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFModeling.BulkConfiguration EFModeling.BulkConfiguration enable - + diff --git a/samples/core/Modeling/ConcurrencyTokens/ConcurrencyTokens.csproj b/samples/core/Modeling/ConcurrencyTokens/ConcurrencyTokens.csproj index 7273e1a408..dd32a12315 100644 --- a/samples/core/Modeling/ConcurrencyTokens/ConcurrencyTokens.csproj +++ b/samples/core/Modeling/ConcurrencyTokens/ConcurrencyTokens.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.ConcurrencyTokens EFModeling.ConcurrencyTokens - + diff --git a/samples/core/Modeling/DataSeeding/DataSeeding.csproj b/samples/core/Modeling/DataSeeding/DataSeeding.csproj index aa6939cdf0..f676eb0cef 100644 --- a/samples/core/Modeling/DataSeeding/DataSeeding.csproj +++ b/samples/core/Modeling/DataSeeding/DataSeeding.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFModeling.DataSeeding EFModeling.DataSeeding - - + + diff --git a/samples/core/Modeling/DynamicModel/DynamicModel.csproj b/samples/core/Modeling/DynamicModel/DynamicModel.csproj index d951375fa8..1ca60aaba7 100644 --- a/samples/core/Modeling/DynamicModel/DynamicModel.csproj +++ b/samples/core/Modeling/DynamicModel/DynamicModel.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.DynamicModel EFModeling.DynamicModel - + diff --git a/samples/core/Modeling/EntityProperties/EntityProperties.csproj b/samples/core/Modeling/EntityProperties/EntityProperties.csproj index 5ef1e932a9..fb0734508e 100644 --- a/samples/core/Modeling/EntityProperties/EntityProperties.csproj +++ b/samples/core/Modeling/EntityProperties/EntityProperties.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.EntityProperties EFModeling.EntityProperties - + diff --git a/samples/core/Modeling/EntityTypes/EntityTypes.csproj b/samples/core/Modeling/EntityTypes/EntityTypes.csproj index 719ecbd2a4..05f63f2796 100644 --- a/samples/core/Modeling/EntityTypes/EntityTypes.csproj +++ b/samples/core/Modeling/EntityTypes/EntityTypes.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.EntityTypes EFModeling.EntityTypes - + diff --git a/samples/core/Modeling/GeneratedProperties/GeneratedProperties.csproj b/samples/core/Modeling/GeneratedProperties/GeneratedProperties.csproj index 845f092595..86722c7df2 100644 --- a/samples/core/Modeling/GeneratedProperties/GeneratedProperties.csproj +++ b/samples/core/Modeling/GeneratedProperties/GeneratedProperties.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.GeneratedProperties EFModeling.GeneratedProperties - + diff --git a/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj b/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj index dcee4338de..96813eefb1 100644 --- a/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj +++ b/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.IndexesAndConstraints EFModeling.IndexesAndConstraints - + diff --git a/samples/core/Modeling/Inheritance/Inheritance.csproj b/samples/core/Modeling/Inheritance/Inheritance.csproj index b65eae2867..9bc57280b0 100644 --- a/samples/core/Modeling/Inheritance/Inheritance.csproj +++ b/samples/core/Modeling/Inheritance/Inheritance.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.Inheritance EFModeling.Inheritance - + diff --git a/samples/core/Modeling/KeylessEntityTypes/KeylessEntityTypes.csproj b/samples/core/Modeling/KeylessEntityTypes/KeylessEntityTypes.csproj index 3b6ea147aa..91d34d62c3 100644 --- a/samples/core/Modeling/KeylessEntityTypes/KeylessEntityTypes.csproj +++ b/samples/core/Modeling/KeylessEntityTypes/KeylessEntityTypes.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFModeling.KeylessEntityTypes EFModeling.KeylessEntityTypes - - + + diff --git a/samples/core/Modeling/Keys/Keys.csproj b/samples/core/Modeling/Keys/Keys.csproj index 17b83fd9c5..5416b6a0b2 100644 --- a/samples/core/Modeling/Keys/Keys.csproj +++ b/samples/core/Modeling/Keys/Keys.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.Keys EFModeling.Keys - + diff --git a/samples/core/Modeling/Misc/Misc.csproj b/samples/core/Modeling/Misc/Misc.csproj index 7273e1a408..dd32a12315 100644 --- a/samples/core/Modeling/Misc/Misc.csproj +++ b/samples/core/Modeling/Misc/Misc.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.ConcurrencyTokens EFModeling.ConcurrencyTokens - + diff --git a/samples/core/Modeling/OwnedEntities/OwnedEntities.csproj b/samples/core/Modeling/OwnedEntities/OwnedEntities.csproj index 8fb61d6049..7759631073 100644 --- a/samples/core/Modeling/OwnedEntities/OwnedEntities.csproj +++ b/samples/core/Modeling/OwnedEntities/OwnedEntities.csproj @@ -2,17 +2,17 @@ Exe - net6.0 + net8.0 EFModeling.OwnedEntities EFModeling.OwnedEntities - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/samples/core/Modeling/Relationships/Relationships.csproj b/samples/core/Modeling/Relationships/Relationships.csproj index 2a12205fee..ada81c2812 100644 --- a/samples/core/Modeling/Relationships/Relationships.csproj +++ b/samples/core/Modeling/Relationships/Relationships.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFModeling.Relationships EFModeling.Relationships - - + + diff --git a/samples/core/Modeling/Sequences/Sequences.csproj b/samples/core/Modeling/Sequences/Sequences.csproj index 3ff1c1cc10..8ba745275b 100644 --- a/samples/core/Modeling/Sequences/Sequences.csproj +++ b/samples/core/Modeling/Sequences/Sequences.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.Sequences EFModeling.Sequences - + diff --git a/samples/core/Modeling/ShadowAndIndexerProperties/ShadowAndIndexerProperties.csproj b/samples/core/Modeling/ShadowAndIndexerProperties/ShadowAndIndexerProperties.csproj index ba8f832d3e..3d813c5253 100644 --- a/samples/core/Modeling/ShadowAndIndexerProperties/ShadowAndIndexerProperties.csproj +++ b/samples/core/Modeling/ShadowAndIndexerProperties/ShadowAndIndexerProperties.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFModeling.ShadowAndIndexerProperties EFModeling.ShadowAndIndexerProperties - + diff --git a/samples/core/Modeling/TableSplitting/TableSplitting.csproj b/samples/core/Modeling/TableSplitting/TableSplitting.csproj index de5e286b47..9448abbbb3 100644 --- a/samples/core/Modeling/TableSplitting/TableSplitting.csproj +++ b/samples/core/Modeling/TableSplitting/TableSplitting.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFModeling.TableSplitting EFModeling.TableSplitting - - + + diff --git a/samples/core/Modeling/ValueConversions/ValueConversions.csproj b/samples/core/Modeling/ValueConversions/ValueConversions.csproj index a2f7d6ee7c..6a1278c7d3 100644 --- a/samples/core/Modeling/ValueConversions/ValueConversions.csproj +++ b/samples/core/Modeling/ValueConversions/ValueConversions.csproj @@ -2,15 +2,15 @@ Exe - net6.0 + net8.0 EFModeling.ValueConversions EFModeling.ValueConversions - - - + + + diff --git a/samples/core/Performance/AspNetContextPooling/AspNetContextPooling.csproj b/samples/core/Performance/AspNetContextPooling/AspNetContextPooling.csproj index 35e7a1f357..01ef07593d 100644 --- a/samples/core/Performance/AspNetContextPooling/AspNetContextPooling.csproj +++ b/samples/core/Performance/AspNetContextPooling/AspNetContextPooling.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable Performance.AspNetContextPooling Performance.AspNetContextPooling @@ -9,8 +9,8 @@ - - + + diff --git a/samples/core/Performance/AspNetContextPoolingWithState/AspNetContextPoolingWithState.csproj b/samples/core/Performance/AspNetContextPoolingWithState/AspNetContextPoolingWithState.csproj index d56d108d62..81aae69578 100644 --- a/samples/core/Performance/AspNetContextPoolingWithState/AspNetContextPoolingWithState.csproj +++ b/samples/core/Performance/AspNetContextPoolingWithState/AspNetContextPoolingWithState.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable Performance.AspNetContextPoolingWithState Performance.AspNetContextPoolingWithState @@ -9,8 +9,8 @@ - - + + diff --git a/samples/core/Performance/Other/Other.csproj b/samples/core/Performance/Other/Other.csproj index c7cd2b45cd..ebcba6ebd7 100644 --- a/samples/core/Performance/Other/Other.csproj +++ b/samples/core/Performance/Other/Other.csproj @@ -2,15 +2,15 @@ Exe - net6.0 + net8.0 Performance.Other Performance.Other - - - + + + diff --git a/samples/core/Querying/ClientEvaluation/ClientEvaluation.csproj b/samples/core/Querying/ClientEvaluation/ClientEvaluation.csproj index 24eb6a159f..10845fcc90 100644 --- a/samples/core/Querying/ClientEvaluation/ClientEvaluation.csproj +++ b/samples/core/Querying/ClientEvaluation/ClientEvaluation.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.ClientEvaluation EFQuerying.ClientEvaluation - - + + diff --git a/samples/core/Querying/ComplexQuery/ComplexQuery.csproj b/samples/core/Querying/ComplexQuery/ComplexQuery.csproj index 9ac8c64906..dcf1ea2ac1 100644 --- a/samples/core/Querying/ComplexQuery/ComplexQuery.csproj +++ b/samples/core/Querying/ComplexQuery/ComplexQuery.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.ComplexQuery EFQuerying.ComplexQuery - - + + diff --git a/samples/core/Querying/NullSemantics/NullSemantics.csproj b/samples/core/Querying/NullSemantics/NullSemantics.csproj index 65afa565f5..b1ba58ee1b 100644 --- a/samples/core/Querying/NullSemantics/NullSemantics.csproj +++ b/samples/core/Querying/NullSemantics/NullSemantics.csproj @@ -2,11 +2,11 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Querying/Overview/Overview.csproj b/samples/core/Querying/Overview/Overview.csproj index c38d87b5df..dd53647d81 100644 --- a/samples/core/Querying/Overview/Overview.csproj +++ b/samples/core/Querying/Overview/Overview.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.Overview EFQuerying.Overview - - + + diff --git a/samples/core/Querying/Pagination/Pagination.csproj b/samples/core/Querying/Pagination/Pagination.csproj index 8c56bcaa2e..f904e2b7d3 100644 --- a/samples/core/Querying/Pagination/Pagination.csproj +++ b/samples/core/Querying/Pagination/Pagination.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.Pagination EFQuerying.Pagination - - + + diff --git a/samples/core/Querying/QueryFilters/QueryFilters.csproj b/samples/core/Querying/QueryFilters/QueryFilters.csproj index f1b1c96b51..8955d9035d 100644 --- a/samples/core/Querying/QueryFilters/QueryFilters.csproj +++ b/samples/core/Querying/QueryFilters/QueryFilters.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.QueryFilters EFQuerying.QueryFilters - - + + diff --git a/samples/core/Querying/RelatedData/RelatedData.csproj b/samples/core/Querying/RelatedData/RelatedData.csproj index 19f0207d4f..0c14055ae3 100644 --- a/samples/core/Querying/RelatedData/RelatedData.csproj +++ b/samples/core/Querying/RelatedData/RelatedData.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.RelatedData EFQuerying.RelatedData - - + + diff --git a/samples/core/Querying/SqlQueries/SqlQueries.csproj b/samples/core/Querying/SqlQueries/SqlQueries.csproj index 02c4839768..24ce313273 100644 --- a/samples/core/Querying/SqlQueries/SqlQueries.csproj +++ b/samples/core/Querying/SqlQueries/SqlQueries.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.SqlQueries EFQuerying.SqlQueries - - + + diff --git a/samples/core/Querying/Tags/Tags.csproj b/samples/core/Querying/Tags/Tags.csproj index e8c38f2418..62bff9113a 100644 --- a/samples/core/Querying/Tags/Tags.csproj +++ b/samples/core/Querying/Tags/Tags.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFQuerying.Tags EFQuerying.Tags - + diff --git a/samples/core/Querying/Tracking/Tracking.csproj b/samples/core/Querying/Tracking/Tracking.csproj index d04c30d5b2..dc2dabd251 100644 --- a/samples/core/Querying/Tracking/Tracking.csproj +++ b/samples/core/Querying/Tracking/Tracking.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.Tracking EFQuerying.Tracking - - + + diff --git a/samples/core/Querying/UserDefinedFunctionMapping/UserDefinedFunctionMapping.csproj b/samples/core/Querying/UserDefinedFunctionMapping/UserDefinedFunctionMapping.csproj index 42c0779d98..677f1a0e1c 100644 --- a/samples/core/Querying/UserDefinedFunctionMapping/UserDefinedFunctionMapping.csproj +++ b/samples/core/Querying/UserDefinedFunctionMapping/UserDefinedFunctionMapping.csproj @@ -2,14 +2,14 @@ Exe - net6.0 + net8.0 EFQuerying.UserDefinedFunctionMapping EFQuerying.UserDefinedFunctionMapping - - + + diff --git a/samples/core/Saving/Saving.csproj b/samples/core/Saving/Saving.csproj index 811f190cf7..3d4c12e750 100644 --- a/samples/core/Saving/Saving.csproj +++ b/samples/core/Saving/Saving.csproj @@ -2,13 +2,13 @@ Exe - net6.0 + net8.0 EFSaving EFSaving - + diff --git a/samples/core/Schemas/Migrations/Migrations.csproj b/samples/core/Schemas/Migrations/Migrations.csproj index 753afc36e8..2bf284f24a 100644 --- a/samples/core/Schemas/Migrations/Migrations.csproj +++ b/samples/core/Schemas/Migrations/Migrations.csproj @@ -1,11 +1,11 @@ - net6.0 + net8.0 - + diff --git a/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Data/WebApplication1.Data.csproj b/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Data/WebApplication1.Data.csproj index 7da9a01fc7..88d883cf15 100644 --- a/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Data/WebApplication1.Data.csproj +++ b/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Data/WebApplication1.Data.csproj @@ -1,12 +1,12 @@ - net6.0 + net8.0 - - + + diff --git a/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Migrations/WebApplication1.Migrations.csproj b/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Migrations/WebApplication1.Migrations.csproj index 926d926efd..8aec49fe29 100644 --- a/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Migrations/WebApplication1.Migrations.csproj +++ b/samples/core/Schemas/ThreeProjectMigrations/WebApplication1.Migrations/WebApplication1.Migrations.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 diff --git a/samples/core/Schemas/ThreeProjectMigrations/WebApplication1/WebApplication1.csproj b/samples/core/Schemas/ThreeProjectMigrations/WebApplication1/WebApplication1.csproj index 16e5189ae7..cf6e3df687 100644 --- a/samples/core/Schemas/ThreeProjectMigrations/WebApplication1/WebApplication1.csproj +++ b/samples/core/Schemas/ThreeProjectMigrations/WebApplication1/WebApplication1.csproj @@ -1,14 +1,14 @@ - net6.0 + net8.0 aspnet-WebApplication1-1FCCDCDB-A580-40BC-B4DC-17498EB689D9 - - - + + + diff --git a/samples/core/Schemas/TwoProjectMigrations/SqlServerMigrations/SqlServerMigrations.csproj b/samples/core/Schemas/TwoProjectMigrations/SqlServerMigrations/SqlServerMigrations.csproj index d47507f153..1498640f3e 100644 --- a/samples/core/Schemas/TwoProjectMigrations/SqlServerMigrations/SqlServerMigrations.csproj +++ b/samples/core/Schemas/TwoProjectMigrations/SqlServerMigrations/SqlServerMigrations.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 ..\WorkerService1\bin\ diff --git a/samples/core/Schemas/TwoProjectMigrations/SqliteMigrations/SqliteMigrations.csproj b/samples/core/Schemas/TwoProjectMigrations/SqliteMigrations/SqliteMigrations.csproj index 7ac9936d8e..eb72464294 100644 --- a/samples/core/Schemas/TwoProjectMigrations/SqliteMigrations/SqliteMigrations.csproj +++ b/samples/core/Schemas/TwoProjectMigrations/SqliteMigrations/SqliteMigrations.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 ..\WorkerService1\bin\ diff --git a/samples/core/Schemas/TwoProjectMigrations/WorkerService1/WorkerService1.csproj b/samples/core/Schemas/TwoProjectMigrations/WorkerService1/WorkerService1.csproj index 1c1bf85202..f90f4da586 100644 --- a/samples/core/Schemas/TwoProjectMigrations/WorkerService1/WorkerService1.csproj +++ b/samples/core/Schemas/TwoProjectMigrations/WorkerService1/WorkerService1.csproj @@ -1,18 +1,18 @@ - net6.0 + net8.0 dotnet-WorkerService1-F7BD8083-51FA-4910-9F6D-FCDE8274AF8B - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/samples/core/Spatial/Projections/Projections.csproj b/samples/core/Spatial/Projections/Projections.csproj index 8004e3e8b1..c865f5b0e7 100644 --- a/samples/core/Spatial/Projections/Projections.csproj +++ b/samples/core/Spatial/Projections/Projections.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 diff --git a/samples/core/Spatial/SqlServer/SqlServer.csproj b/samples/core/Spatial/SqlServer/SqlServer.csproj index 2e19e315f4..bfbde05817 100644 --- a/samples/core/Spatial/SqlServer/SqlServer.csproj +++ b/samples/core/Spatial/SqlServer/SqlServer.csproj @@ -2,11 +2,11 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/SqlServer/SqlServer.csproj b/samples/core/SqlServer/SqlServer.csproj index 65afa565f5..b1ba58ee1b 100644 --- a/samples/core/SqlServer/SqlServer.csproj +++ b/samples/core/SqlServer/SqlServer.csproj @@ -2,11 +2,11 @@ Exe - net6.0 + net8.0 - + diff --git a/samples/core/Testing/BloggingWebApi/BloggingWebApi.csproj b/samples/core/Testing/BloggingWebApi/BloggingWebApi.csproj index d3cf58c2d5..acf48cb6ae 100644 --- a/samples/core/Testing/BloggingWebApi/BloggingWebApi.csproj +++ b/samples/core/Testing/BloggingWebApi/BloggingWebApi.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 Items EF.Testing.BloggingWebApi EF.Testing.BloggingWebApi diff --git a/samples/core/Testing/BusinessLogic/BusinessLogic.csproj b/samples/core/Testing/BusinessLogic/BusinessLogic.csproj index 7e943fa050..8ec39846e9 100644 --- a/samples/core/Testing/BusinessLogic/BusinessLogic.csproj +++ b/samples/core/Testing/BusinessLogic/BusinessLogic.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 EF.Testing.BusinessLogic EF.Testing.BusinessLogic - + diff --git a/samples/core/Testing/TestingWithTheDatabase/TestingWithTheDatabase.csproj b/samples/core/Testing/TestingWithTheDatabase/TestingWithTheDatabase.csproj index 96170fa2af..7a17738a81 100644 --- a/samples/core/Testing/TestingWithTheDatabase/TestingWithTheDatabase.csproj +++ b/samples/core/Testing/TestingWithTheDatabase/TestingWithTheDatabase.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 EF.Testing.IntegrationTests EF.Testing.IntegrationTests false diff --git a/samples/core/Testing/TestingWithoutTheDatabase/TestingWithoutTheDatabase.csproj b/samples/core/Testing/TestingWithoutTheDatabase/TestingWithoutTheDatabase.csproj index 7db6cc90e7..1523e8aab6 100644 --- a/samples/core/Testing/TestingWithoutTheDatabase/TestingWithoutTheDatabase.csproj +++ b/samples/core/Testing/TestingWithoutTheDatabase/TestingWithoutTheDatabase.csproj @@ -1,15 +1,15 @@ - net6.0 + net8.0 EF.Testing.UnitTests EF.Testing.UnitTests false - - + +