diff --git a/entity-framework/core/extensions/index.md b/entity-framework/core/extensions/index.md index e55b7fb9d7..6745b955bb 100644 --- a/entity-framework/core/extensions/index.md +++ b/entity-framework/core/extensions/index.md @@ -359,7 +359,7 @@ These packages are designed to integrate directly with EF Core to expose various Enhance the local development experience by simplifying the management of your cloud-native app's configuration and interconnections. For EF Core: 8. -[Website](https://learn.microsoft.com/dotnet/aspire/get-started/aspire-overview) | [GitHub repository](https://github.com/dotnet/aspire) | [NuGet](https://www.nuget.org/profiles/aspire) +[Website](/dotnet/aspire/get-started/aspire-overview) | [GitHub repository](https://github.com/dotnet/aspire) | [NuGet](https://www.nuget.org/profiles/aspire) ### HotChocolate diff --git a/entity-framework/core/learn-more/community-standups.md b/entity-framework/core/learn-more/community-standups.md index 4d0775ef2a..29c59cf79e 100644 --- a/entity-framework/core/learn-more/community-standups.md +++ b/entity-framework/core/learn-more/community-standups.md @@ -1591,7 +1591,6 @@ Featuring: Links: - Blog: [Oracle EF Core 3.1 Production Release](https://medium.com/oracledevs/oracle-ef-core-3-1-production-release-9e470eaf3d03) -- Blog: [Seeding data in EF Core using SQL scripts](https://dejanstojanovic.net/aspnet/2020/september/seeding-data-in-ef-core-using-sql-scripts/) - Blog: [Announcing Entity Framework Core (EFCore) 5.0 RC1](https://devblogs.microsoft.com/dotnet/announcing-entity-framework-core-efcore-5-0-rc1/) - Docs: [Migrations Overview](xref: core/managing-schemas/migrations/index) - Docs: [EF Core daily builds](https://aka.ms/ef-daily-builds) diff --git a/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md index e6926d5cec..a1d6852efc 100644 --- a/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-9.0/whatsnew.md @@ -2,13 +2,13 @@ title: What's New in EF Core 9 description: Overview of new features in EF Core 9 author: ajcvickers -ms.date: 05/02/2024 +ms.date: 06/09/2024 uid: core/what-is-new/ef-core-9.0/whatsnew --- # What's New in EF Core 9 -EF Core 9 (EF9) is the next release after EF Core 8 and is scheduled for release in November 2024. See [_Plan for Entity Framework Core 9_](xref:core/what-is-new/ef-core-9.0/plan) for details. +EF Core 9 (EF9) is the next release after EF Core 8 and is scheduled for release in November 2024. EF9 is available as [daily builds](https://github.com/dotnet/efcore/blob/main/docs/DailyBuilds.md) which contain all the latest EF9 features and API tweaks. The samples here make use of these daily builds. @@ -26,6 +26,261 @@ EF9 targets .NET 8, and can therefore be used with either [.NET 8 (LTS)](https:/ We are working on significant updates in EF9 to the EF Core database provider for Azure Cosmos DB for NoSQL. +### Hierarchical partition keys + +> [!TIP] +> The code shown here comes from [HierarchicalPartitionKeysSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs). + +Each document stored in the Cosmos database has a unique resource ID. In addition, each document can contain a "partition key" which determines the logical partitioning of data such that the database can be effectively scaled. More information on choosing partition keys can be found in [_Partitioning and horizontal scaling in Azure Cosmos DB_](/azure/cosmos-db/partitioning-overview). + +Recent releases of Azure Cosmos DB for NoSQL (Cosmos SDK version 3.33.0 or later) have expanded partitioning capabilities to support [subpartitioning through the specification of up to three levels of hierarchy in the partition key](/azure/cosmos-db/hierarchical-partition-keys). EF Core 9 supports specification of hierarchical partition keys in the model, automatic extraction of these values from queries, and manual specification of a hierarchical partition key for a given query. + +#### Configuring hierarchical partition keys + +Partition keys are specified using the model building API, typically in . There must be a mapped property in the entity type for each level of the partition key. For example, consider a `UserSession` entity type: + + +[!code-csharp[UserSession](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=UserSession)] + +The following code specifies a three-level partition key using the `TenantId`, `UserId`, and `SessionId` properties: + + +[!code-csharp[HasPartitionKey](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=HasPartitionKey)] + +> [!TIP] +> This partition key definition follows the example given in [_Choose your hierarchical partition keys_](/azure/cosmos-db/hierarchical-partition-keys#choose-your-hierarchical-partition-keys) from the Azure Cosmos DB documentation. + +Notice how, starting with EF Core 9, properties of any mapped type can be used in the partition key. For `bool` and numeric types, like the `int SessionId` property, the value is used directly in the partition key. Other types, like the `Guid UserId` property, are automatically converted to strings. + +#### Saving documents with hierarchical partition keys + +Saving a new document with a hierarchical partition key is the same as saving any new document with EF Core. The primary key and partition key properties must have non-default values, or EF Core value generation can be used to create values. For example, the following code inserts `UserSession` documents where the `Id` property is generated by EF Core, and all the partition key properties have been set explicitly: + + +[!code-csharp[Inserts](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=Inserts)] + +The logs from calling `SaveChangesAsync` show in the following `CreateItem` calls: + +```output +info: 6/10/2024 18:41:04.456 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) + Executed CreateItem (167 ms, 7.81 RU) ActivityId='23891b55-7375-40e5-aa4b-2c57ca6a376e', Container='UserSessionContext', Id='UserSession|d5e2614b-71f2-4e6b-d41a-08dc89748055', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' +info: 6/10/2024 18:41:04.478 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) + Executed CreateItem (14 ms, 7.81 RU) ActivityId='7fdcfb3e-455c-45dd-b444-02b66575a28f', Container='UserSessionContext', Id='UserSession|01cc0102-5212-4785-d41b-08dc89748055', Partition='["Microsoft","adae5dde-8a67-432d-9dec-fd7ec86fd9f6",7.0]' +info: 6/10/2024 18:41:04.491 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) + Executed CreateItem (13 ms, 7.81 RU) ActivityId='3f7e6026-8edf-4f2c-8918-09434dc039bf', Container='UserSessionContext', Id='UserSession|e5a467c0-bb1e-4ffe-d41c-08dc89748055', Partition='["Microsoft","61967254-aff8-493a-b7f8-e62da36d8367",7.0]' +info: 6/10/2024 18:41:04.507 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) + Executed CreateItem (15 ms, 7.81 RU) ActivityId='04c6f4b2-0ad0-4708-874e-dc8967726d18', Container='UserSessionContext', Id='UserSession|fd47726a-fb68-4c63-d41d-08dc89748055', Partition='["Microsoft","bc0150cf-5147-44b8-8823-865f4f2323e1",7.0]' +``` + +Notice that the partition key values have been extracted from the entity instance and included in the call to `CreateItem` to ensure maximum efficiency on the server. + +#### Point reads using hierarchical partition keys + +By convention, EF Core includes the partition key properties in the primary key definition for the entity type. For example, inspecting the [model debug view](xref:core/modeling/index#debug-view) shows the following mapping for the `UserSession` entity type: + +```output +EntityType: UserSession + Properties: + Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd + TenantId (string) Required PK AfterSave:Throw + UserId (Guid) Required PK AfterSave:Throw + SessionId (int) Required PK AfterSave:Throw + Discriminator (no field, string) Shadow Required AfterSave:Throw + Username (string) + __id (no field, string) Shadow Required AlternateKey AfterSave:Throw + __jObject (no field, JObject) Shadow BeforeSave:Ignore AfterSave:Ignore ValueGenerated.OnAddOrUpdate + Keys: + Id, TenantId, UserId, SessionId PK + __id, TenantId, UserId, SessionId +``` + +Notice that the primary key definition is `Id, TenantId, UserId, SessionId`. This means that can be used to lookup a document. For example: + + +[!code-csharp[FindAsync](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=FindAsync)] + +Logging from EF Core shows that a point-read (using `ReadItem`) is executed for maximum efficiency: + +```output +info: 6/10/2024 18:41:04.651 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command) + Reading resource 'UserSession|e5a467c0-bb1e-4ffe-d41c-08dc89748055' item from container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]'. +info: 6/10/2024 18:41:04.668 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command) + Executed ReadItem (8 ms, 1 RU) ActivityId='a016f26c-6bd0-4c66-953b-a8f1297df41a', Container='UserSessionContext', Id='UserSession|e5a467c0-bb1e-4ffe-d41c-08dc89748055', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' +``` + +#### Queries using hierarchical partition keys + +EF Core will extract the partition key values from queries and apply them to the Cosmos query API to ensure the queries are constrained appropriately to the fewest number of partitions possible. For example, consider a LINQ query that supplies values for all levels of the partition key: + + +[!code-csharp[FullPartitionKey](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=FullPartitionKey)] + +When executing this query, EF Core will extract the values of the `tenantId`, `userId`, and `sessionId` parameters, and pass them to the Cosmos query API as the partition key value. For example, see the logs from executing the query above: + +```output +info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) + Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]] + SELECT c + FROM root c + WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a")) +``` + +Notice that the partition key comparisons have been removed from the `WHERE` clause, and are instead passed directly to the Cosmos API as partition key `["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]`. + +> [!IMPORTANT] +> Because the query includes values for all parts of the partition key, this that the query us routed to the single partition that contains the data for the specified values of `TenantId`, `UserId`, and `SessionId`. This is more efficient than the queries below which only use none, or only some, of the partition key values. + +With hierarchical partitions, more efficient queries can still be generated when only the top partition key hierarchy is known. For example, the following LINQ query uses the top two parts of the partition key--that is, `TenantId` and `UserId`: + + +[!code-csharp[TopTwoPartitionKey](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=TopTwoPartitionKey)] + +EF Core still extracts the partition key values when executing this query: + +```output +info: 6/10/2024 19:24:46.581 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) + Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c"]' [Parameters=[]] + SELECT c + FROM root c + WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a")) +``` + +This query does not include the `SessionId`, so it cannot target a single partition. However, it will still be a targeted, cross-partition query returning data for all sessions of a single tenant and user ID. + +Likewise, if only the top value in the hierarchy is specified, then it will be used on its own. For example: + + +[!code-csharp[TopOnePartitionKey](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=TopOnePartitionKey)] + +Which results in the following logs: + +```output +info: 6/11/2024 09:30:42.532 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) + Executing SQL query for container 'UserSessionContext' in partition '["Microsoft"]' [Parameters=[]] + SELECT c + FROM root c + WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a")) +``` + +Since this query only contains the `TenantId` part of the partition key it cannot target a single partition. However, as with the previous example it will still be a targeted, cross-partition query returning data for all sessions and users in a single tenant. + +It is important to understand that using the second and/or third values of the hierarchical partition key, without including the first value, will result in a query that covers all partitions. For example, consider a query including both `SessionId` and `UserId`, but not including `TenantId`: + + +[!code-csharp[BottomTwoPartitionKey](../../../../samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs?name=BottomTwoPartitionKey)] + +The logs show that this is translated without a partition key, since the `TenantId` is missing: + +```output +info: 6/11/2024 09:30:42.553 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) + Executing SQL query for container 'UserSessionContext' in partition 'None' [Parameters=[]] + SELECT c + FROM root c + WHERE (c["Discriminator"] = "UserSession") +``` + +> [!NOTE] +> [Issue #33960](https://github.com/dotnet/efcore/issues/33960) is tracking a bug in this translation. + ### Role-based access Azure Cosmos DB for NoSQL includes a [built-in role-based access control (RBAC) system](/azure/cosmos-db/role-based-access-control). This is now supported by EF9 for both management and use of containers. No changes are required to application code. See [Issue #32197](https://github.com/dotnet/efcore/issues/32197) for more information. @@ -425,6 +680,56 @@ The meth This enhancement was contributed by [@wertzui](https://github.com/wertzui). Many thanks! +### Queries using Count != 0 are optimized + +> [!TIP] +> The code shown here comes from [QuerySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs). + +In EF8, the following LINQ query was translated to use the SQL `COUNT` function: + + +[!code-csharp[NormalizeCount](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=NormalizeCount)] + +EF9 now generates a more efficient translation using `EXISTS`: + +```sql +SELECT "b"."Id", "b"."Name", "b"."SiteUri" +FROM "Blogs" AS "b" +WHERE EXISTS ( + SELECT 1 + FROM "Posts" AS "p" + WHERE "b"."Id" = "p"."BlogId") +``` + +### More `TimeOnly` methods are translated for Azure SQL/SQL Server + +> [!TIP] +> The code shown here comes from [QuerySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs). + +Queries using and are now translated when using SQL Server or Azure SQL. For example, the following LINQ query uses `FromDateTime` to extract the time-of-day value from a column and compare it to a `TimeOnly` value passed in: + + +[!code-csharp[FromDateTime](../../../../samples/core/Miscellaneous/NewInEFCore9/DateOnlyTimeOnlySample.cs?name=FromDateTime)] + +This is translated to the following when using SQL Azure or SQL Server: + +```sql +SELECT [s].[Id], [s].[Founded], [s].[LastVisited], [s].[LegacyTime], [s].[Name], [s].[OpeningHours] +FROM [Schools] AS [s] +WHERE CAST([s].[LastVisited] AS time) >= @__visitedTime_0 +``` + +`FromTimeSpan` is translated in a similar manner. + ## ExecuteUpdate and ExecuteDelete diff --git a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosDiagnosticsSample.cs b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosDiagnosticsSample.cs index c237fe17e3..3942e95ee8 100644 --- a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosDiagnosticsSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosDiagnosticsSample.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; public static class CosmosDiagnosticsSample { - public static void Cosmos_diagnostics() + public static async Task Cosmos_diagnostics() { Console.WriteLine($">>>> Sample: {nameof(Cosmos_diagnostics)}"); Console.WriteLine(); - Helpers.RecreateCleanDatabase(); - Helpers.PopulateDatabase(); + await Helpers.RecreateCleanDatabase(); + await Helpers.PopulateDatabase(); using var context = new ShapesContext(); @@ -30,7 +30,7 @@ public static void Cosmos_diagnostics() InsertedOn = DateTime.UtcNow }; context.Add(triangle); - context.SaveChanges(); + await context.SaveChangesAsync(); #endregion Console.WriteLine(); @@ -38,7 +38,7 @@ public static void Cosmos_diagnostics() Console.WriteLine(); #region QueryEvents - var equilateral = context.Triangles.Single(e => e.Name == "Equilateral"); + var equilateral = await context.Triangles.SingleAsync(e => e.Name == "Equilateral"); #endregion Console.WriteLine(); @@ -46,7 +46,7 @@ public static void Cosmos_diagnostics() Console.WriteLine(); #region FindEvents - var isosceles = context.Triangles.Find("Isosceles", "TrianglesPartition"); + var isosceles = await context.Triangles.FindAsync("Isosceles", "TrianglesPartition"); #endregion Console.WriteLine(); @@ -55,7 +55,7 @@ public static void Cosmos_diagnostics() #region UpdateEvents triangle.Angle2 = 89; - context.SaveChanges(); + await context.SaveChangesAsync(); #endregion Console.WriteLine(); @@ -64,7 +64,7 @@ public static void Cosmos_diagnostics() #region DeleteEvents context.Remove(triangle); - context.SaveChanges(); + await context.SaveChangesAsync(); #endregion Console.WriteLine(); @@ -72,15 +72,15 @@ public static void Cosmos_diagnostics() public static class Helpers { - public static void RecreateCleanDatabase() + public static async Task RecreateCleanDatabase() { using var context = new ShapesContext(quiet: true); - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); } - public static void PopulateDatabase() + public static async Task PopulateDatabase() { using var context = new ShapesContext(quiet: true); @@ -94,7 +94,7 @@ public static void PopulateDatabase() }; context.AddRange(triangles); - context.SaveChanges(); + await context.SaveChangesAsync(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosImplicitOwnershipSample.cs b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosImplicitOwnershipSample.cs index 66e5ae80f1..54592e885c 100644 --- a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosImplicitOwnershipSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosImplicitOwnershipSample.cs @@ -2,20 +2,21 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; public static class CosmosImplicitOwnershipSample { - public static void Cosmos_models_use_implicit_ownership_by_default() + public static async Task Cosmos_models_use_implicit_ownership_by_default() { Console.WriteLine($">>>> Sample: {nameof(Cosmos_models_use_implicit_ownership_by_default)}"); Console.WriteLine(); using (var context = new FamilyContext()) { - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); context.AddRange( new Family @@ -44,7 +45,7 @@ public static void Cosmos_models_use_implicit_ownership_by_default() LastName = "Wakefield", Parents = { - new() { FamilyName = "Wakefield", FirstName = "Robin" }, + new() { FamilyName = "Wakefield", FirstName = "Robin" }, new() { FamilyName = "Miller", FirstName = "Ben" } }, Children = @@ -69,14 +70,14 @@ public static void Cosmos_models_use_implicit_ownership_by_default() IsRegistered = true }); - context.SaveChanges(); + await context.SaveChangesAsync(); } Console.WriteLine(); using (var context = new FamilyContext()) { - var families = context.Families.ToList(); + var families = await context.Families.ToListAsync(); Console.WriteLine(); @@ -96,10 +97,10 @@ public class Family { [JsonPropertyName("id")] public string Id { get; set; } - + public string LastName { get; set; } public bool IsRegistered { get; set; } - + public Address Address { get; set; } public IList Parents { get; } = new List(); @@ -153,7 +154,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasPartitionKey(e => e.LastName); #endregion } - + // Never called; just for documentation. private void OldOnModelCreating(ModelBuilder modelBuilder) { @@ -167,7 +168,7 @@ private void OldOnModelCreating(ModelBuilder modelBuilder) .OwnsMany(c => c.Pets); modelBuilder.Entity() - .OwnsOne(f => f.Address); + .OwnsOne(f => f.Address); #endregion } diff --git a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosPrimitiveTypesSample.cs b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosPrimitiveTypesSample.cs index 6934aaebeb..9613eeb2ee 100644 --- a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosPrimitiveTypesSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/CosmosPrimitiveTypesSample.cs @@ -1,17 +1,18 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; public static class CosmosPrimitiveTypesSample { - public static void Collections_and_dictionaries_of_primitive_types() + public static async Task Collections_and_dictionaries_of_primitive_types() { Console.WriteLine($">>>> Sample: {nameof(Collections_and_dictionaries_of_primitive_types)}"); Console.WriteLine(); - Helpers.RecreateCleanDatabase(); + await Helpers.RecreateCleanDatabase(); #region Insert using var context = new BooksContext(); @@ -35,14 +36,14 @@ public static void Collections_and_dictionaries_of_primitive_types() }; context.Add(book); - context.SaveChanges(); + await context.SaveChangesAsync(); #endregion #region Updates book.Quotes.Add("Pressing the emergency button lowered the rods again."); book.Notes["48"] = "Chiesa d'Oro"; - context.SaveChanges(); + await context.SaveChangesAsync(); #endregion Console.WriteLine(); @@ -50,12 +51,12 @@ public static void Collections_and_dictionaries_of_primitive_types() public static class Helpers { - public static void RecreateCleanDatabase() + public static async Task RecreateCleanDatabase() { using var context = new BooksContext(quiet: true); - context.Database.EnsureDeleted(); - context.Database.EnsureCreated(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/Program.cs b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/Program.cs index c914a3af48..a7fe86192e 100644 --- a/samples/core/Miscellaneous/NewInEFCore6.Cosmos/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore6.Cosmos/Program.cs @@ -5,12 +5,12 @@ public class Program public static async Task Main() { // Note: These samples requires the Cosmos DB emulator to be installed and running - CosmosPrimitiveTypesSample.Collections_and_dictionaries_of_primitive_types(); + await CosmosPrimitiveTypesSample.Collections_and_dictionaries_of_primitive_types(); await CosmosQueriesSample.Cosmos_queries(); - CosmosDiagnosticsSample.Cosmos_diagnostics(); + await CosmosDiagnosticsSample.Cosmos_diagnostics(); CosmosModelConfigurationSample.Cosmos_configure_time_to_live(); await CosmosModelConfigurationSample.Cosmos_configure_time_to_live_per_instance(); - CosmosImplicitOwnershipSample.Cosmos_models_use_implicit_ownership_by_default(); + await CosmosImplicitOwnershipSample.Cosmos_models_use_implicit_ownership_by_default(); CosmosMinimalApiSample.Add_a_DbContext_and_provider(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/App/App.csproj b/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/App/App.csproj index fe513d3fc4..8accd90c2f 100644 --- a/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/App/App.csproj +++ b/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/App/App.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/Model/Model.csproj b/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/Model/Model.csproj index 2af439d9d3..a877bb1332 100644 --- a/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/Model/Model.csproj +++ b/samples/core/Miscellaneous/NewInEFCore9.CompiledModels/Model/Model.csproj @@ -15,13 +15,13 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + diff --git a/samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs b/samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs new file mode 100644 index 0000000000..b99ba2b865 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore9.Cosmos/HierarchicalPartitionKeysSample.cs @@ -0,0 +1,248 @@ +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +public static class HierarchicalPartitionKeysSample +{ + public static async Task UseHierarchicalPartitionKeys() + { + Console.WriteLine($">>>> Sample: {nameof(UseHierarchicalPartitionKeys)}"); + Console.WriteLine(); + + await Helpers.RecreateCleanDatabase(); + + Guid userSessionId; + + using (var context = new UserSessionContext()) + { + #region Inserts + var tenantId = "Microsoft"; + var sessionId = 7; + + context.AddRange( + new UserSession + { + TenantId = tenantId, + UserId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C"), + SessionId = sessionId, + Username = "mac" + }, + new UserSession + { + TenantId = tenantId, + UserId = new Guid("ADAE5DDE-8A67-432D-9DEC-FD7EC86FD9F6"), + SessionId = sessionId, + Username = "toast" + }, + new UserSession + { + TenantId = tenantId, + UserId = new Guid("61967254-AFF8-493A-B7F8-E62DA36D8367"), + SessionId = sessionId, + Username = "willow" + }, + new UserSession + { + TenantId = tenantId, + UserId = new Guid("BC0150CF-5147-44B8-8823-865F4F2323E1"), + SessionId = sessionId, + Username = "alice" + }); + + await context.SaveChangesAsync(); + #endregion + + userSessionId = context.ChangeTracker.Entries().Single(e => e.Entity.Username == "willow").Entity.Id; + } + + Console.WriteLine(); + Console.WriteLine("Use Find to create a point-read:"); + Console.WriteLine(); + + using (var context = new UserSessionContext()) + { + #region FindAsync + var tenantId = "Microsoft"; + var sessionId = 7; + var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C"); + + var session = await context.Sessions.FindAsync( + userSessionId, tenantId, userId, sessionId); + #endregion + } + + Console.WriteLine(); + Console.WriteLine("Execute a query with full hierarchical partition key info:"); + Console.WriteLine(); + + using (var context = new UserSessionContext()) + { + #region FullPartitionKey + var tenantId = "Microsoft"; + var sessionId = 7; + var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C"); + + var sessions = await context.Sessions + .Where( + e => e.TenantId == tenantId + && e.UserId == userId + && e.SessionId == sessionId + && e.Username.Contains("a")) + .ToListAsync(); + #endregion + } + + Console.WriteLine(); + Console.WriteLine("Execute a query with only top two levels of hierarchical partition key:"); + Console.WriteLine(); + + using (var context = new UserSessionContext()) + { + #region TopTwoPartitionKey + var tenantId = "Microsoft"; + var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C"); + + var sessions = await context.Sessions + .Where( + e => e.TenantId == tenantId + && e.UserId == userId + && e.Username.Contains("a")) + .ToListAsync(); + #endregion + } + + Console.WriteLine(); + Console.WriteLine("Execute a query with only top level of hierarchical partition key:"); + Console.WriteLine(); + + using (var context = new UserSessionContext()) + { + #region TopOnePartitionKey + var tenantId = "Microsoft"; + + var sessions = await context.Sessions + .Where( + e => e.TenantId == tenantId + && e.Username.Contains("a")) + .ToListAsync(); + #endregion + } + + Console.WriteLine(); + Console.WriteLine("Execute a queries with incomplete subkey values:"); + Console.WriteLine(); + + using (var context = new UserSessionContext()) + { + var tenantId = "Microsoft"; + var sessionId = 7; + var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C"); + + #region BottomPartitionKey + var sessions1 = await context.Sessions + .Where( + e => e.SessionId == sessionId + && e.Username.Contains("a")) + .ToListAsync(); + #endregion + + #region MiddlePartitionKey + var sessions2 = await context.Sessions + .Where( + e => e.UserId == userId + && e.Username.Contains("a")) + .ToListAsync(); + #endregion + + #region BottomTwoPartitionKey + var sessions3 = await context.Sessions + .Where( + e => e.SessionId == sessionId + && e.UserId == userId + && e.Username.Contains("a")) + .ToListAsync(); + #endregion + } + } + + public static class Helpers + { + public static async Task RecreateCleanDatabase() + { + await using var context = new UserSessionContext(quiet: true); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + } + } + + #region UserSession + public class UserSession + { + // Item ID + public Guid Id { get; set; } + + // Partition Key + public string TenantId { get; set; } = null!; + public Guid UserId { get; set; } + public int SessionId { get; set; } + + // Other members + public string Username { get; set; } = null!; + } + #endregion + + public class UserSessionContext : DbContext + { + public DbSet Sessions { get; set; } + + private readonly bool _quiet; + + public UserSessionContext(bool quiet = false) + { + _quiet = quiet; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region HasPartitionKey + modelBuilder + .Entity() + .HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId }); + #endregion + + // See https://github.com/dotnet/efcore/issues/33961 + modelBuilder + .Entity() + .Property(e => e.Id).ValueGeneratedOnAdd(); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .EnableSensitiveDataLogging() + .UseCosmos( + "https://localhost:8081", + "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", + "HierarchicalPartitionKeys", + cosmosOptionsBuilder => + { + cosmosOptionsBuilder.HttpClientFactory( + () => new HttpClient( + new HttpClientHandler + { + ServerCertificateCustomValidationCallback = + HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + })); + }); + + if (!_quiet) + { + optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information); + } + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore9.Cosmos/NewInEFCore9.Cosmos.csproj b/samples/core/Miscellaneous/NewInEFCore9.Cosmos/NewInEFCore9.Cosmos.csproj index ba4fe7e8f0..4a5031a626 100644 --- a/samples/core/Miscellaneous/NewInEFCore9.Cosmos/NewInEFCore9.Cosmos.csproj +++ b/samples/core/Miscellaneous/NewInEFCore9.Cosmos/NewInEFCore9.Cosmos.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/core/Miscellaneous/NewInEFCore9.Cosmos/Program.cs b/samples/core/Miscellaneous/NewInEFCore9.Cosmos/Program.cs index dacbe2028a..95a17c33f8 100644 --- a/samples/core/Miscellaneous/NewInEFCore9.Cosmos/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore9.Cosmos/Program.cs @@ -7,5 +7,6 @@ public static async Task Main() // Note: These samples requires the Cosmos DB emulator to be installed and running CosmosSyncApisSample.Cosmos_provider_blocks_sync_APIs(); await CosmosPrimitiveTypesSample.Collections_and_dictionaries_of_primitive_types(); + await HierarchicalPartitionKeysSample.UseHierarchicalPartitionKeys(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore9/DateOnlyTimeOnlySample.cs b/samples/core/Miscellaneous/NewInEFCore9/DateOnlyTimeOnlySample.cs new file mode 100644 index 0000000000..6a792c8b3a --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore9/DateOnlyTimeOnlySample.cs @@ -0,0 +1,251 @@ +namespace NewInEfCore9; + +public static class DateOnlyTimeOnlySample +{ + public static Task Can_use_DateOnly_TimeOnly_on_SQL_Server() + { + PrintSampleName(); + return DateOnlyTimeOnlyTest(); + } + + public static Task Can_use_DateOnly_TimeOnly_on_SQL_Server_with_JSON() + { + PrintSampleName(); + return DateOnlyTimeOnlyTest(); + } + + public static Task Can_use_DateOnly_TimeOnly_on_SQLite() + { + PrintSampleName(); + return DateOnlyTimeOnlyTest(); + } + + private static async Task DateOnlyTimeOnlyTest() + where TContext : BritishSchoolsContextBase, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + Console.WriteLine(); + + // Issue https://github.com/dotnet/efcore/issues/25103 + if (!context.UseSqlite) + { + #region FromDateTime + var visitedTime = new TimeOnly(12, 0); + var visited = await context.Schools + .Where(p => TimeOnly.FromDateTime(p.LastVisited) >= visitedTime) + .ToListAsync(); + #endregion + } + + Console.WriteLine(); + + // Issue https://github.com/dotnet/efcore/issues/25103 + if (!context.UseSqlite) + { + #region FromTimeSpan + var visitedTime = new TimeOnly(12, 0); + var visited = await context.Schools + .Where(p => TimeOnly.FromTimeSpan(p.LegacyTime) >= visitedTime) + .ToListAsync(); + #endregion + } + + Console.WriteLine(); + + // Issue https://github.com/dotnet/efcore/issues/25103 + if (!context.UseSqlite) + { + var visitedAt = DateTime.UtcNow; + var visitedSchools = await context.Schools + .AsNoTracking() + .SelectMany(e => e.OpeningHours) + .Where(e => e.OpensAt <= TimeOnly.FromDateTime(visitedAt) && e.OpensAt > TimeOnly.FromDateTime(visitedAt)) + .ToListAsync(); + } + + // Issue https://github.com/dotnet/efcore/issues/33937 + // // Issue https://github.com/dotnet/efcore/issues/30223 + // if (!context.UsesJson + // && !context.UseSqlite) + // { + // await context.Schools + // .SelectMany(e => e.OpeningHours) + // .Where(e => e.DayOfWeek == DayOfWeek.Friday) + // .ExecuteUpdateAsync(s => s.SetProperty(t => t.OpensAt, t => t.OpensAt!.Value.AddHours(-1))); + // } + + Console.WriteLine(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class BritishSchoolsContextBase : DbContext + { + protected BritishSchoolsContextBase(bool useSqlite = false) + { + UseSqlite = useSqlite; + } + + public bool UseSqlite { get; } + public virtual bool UsesJson => false; + public bool LoggingEnabled { get; set; } + + public DbSet Schools => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") + : optionsBuilder.UseSqlServer( + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name};ConnectRetryCount=0", + sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseNetTopologySuite())) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + } + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + } + + public async Task Seed() + { + AddRange( + new School + { + Name = "Stowe School", + Founded = new(1923, 5, 11), + Terms = + { + new() { Name = "Michaelmas", FirstDay = new(2022, 9, 7), LastDay = new(2022, 12, 16) }, + new() { Name = "Lent", FirstDay = new(2023, 1, 8), LastDay = new(2023, 3, 24) }, + new() { Name = "Summer", FirstDay = new(2023, 4, 18), LastDay = new(2023, 7, 8) } + }, + OpeningHours = + { + new(DayOfWeek.Sunday, null, null), + new(DayOfWeek.Monday, new(8, 00), new(18, 00)), + new(DayOfWeek.Tuesday, new(8, 00), new(18, 00)), + new(DayOfWeek.Wednesday, new(8, 00), new(18, 00)), + new(DayOfWeek.Thursday, new(8, 00), new(18, 00)), + new(DayOfWeek.Friday, new(8, 00), new(18, 00)), + new(DayOfWeek.Saturday, new(8, 00), new(17, 00)) + } + }, + new School + { + Name = "Farr High School", + Founded = new(1964, 5, 1), + Terms = + { + new() { Name = "Autumn", FirstDay = new(2022, 8, 16), LastDay = new(2022, 12, 23) }, + new() { Name = "Winter", FirstDay = new(2023, 1, 9), LastDay = new(2023, 3, 31) }, + new() { Name = "Summer", FirstDay = new(2023, 4, 17), LastDay = new(2023, 6, 29) } + }, + OpeningHours = + { + new(DayOfWeek.Sunday, null, null), + new(DayOfWeek.Monday, new(8, 45), new(15, 35)), + new(DayOfWeek.Tuesday, new(8, 45), new(15, 35)), + new(DayOfWeek.Wednesday, new(8, 45), new(15, 35)), + new(DayOfWeek.Thursday, new(8, 45), new(15, 35)), + new(DayOfWeek.Friday, new(8, 45), new(12, 50)), + new(DayOfWeek.Saturday, null, null) + } + }); + + await SaveChangesAsync(); + } + } + + public class BritishSchoolsContext : BritishSchoolsContextBase + { + } + + public class BritishSchoolsContextJson : BritishSchoolsContextBase + { + + public override bool UsesJson => true; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().OwnsMany(e => e.OpeningHours).ToJson(); + } + } + + public class BritishSchoolsContextSqlite : BritishSchoolsContextBase + { + public BritishSchoolsContextSqlite() + : base(useSqlite: true) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity().OwnsMany( + e => e.OpeningHours, b => + { + b.Property("Id"); + b.HasKey("Id"); + }); + } + } + + public class School + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public DateOnly Founded { get; set; } + public DateTime LastVisited { get; set; } + public TimeSpan LegacyTime { get; set; } + public List Terms { get; } = new(); + public List OpeningHours { get; } = new(); + } + + public class Term + { + public int Id { get; set; } + public string Name { get; set; } = null!; + public DateOnly FirstDay { get; set; } + public DateOnly LastDay { get; set; } + public School School { get; set; } = null!; + } + + [Owned] + public class OpeningHours + { + public OpeningHours(DayOfWeek dayOfWeek, TimeOnly? opensAt, TimeOnly? closesAt) + { + DayOfWeek = dayOfWeek; + OpensAt = opensAt; + ClosesAt = closesAt; + } + + public DayOfWeek DayOfWeek { get; private set; } + public TimeOnly? OpensAt { get; set; } + public TimeOnly? ClosesAt { get; set; } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore9/NewInEFCore9.csproj b/samples/core/Miscellaneous/NewInEFCore9/NewInEFCore9.csproj index 1f4c24d2a1..c96a49f1f9 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/NewInEFCore9.csproj +++ b/samples/core/Miscellaneous/NewInEFCore9/NewInEFCore9.csproj @@ -10,15 +10,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore9/Program.cs b/samples/core/Miscellaneous/NewInEFCore9/Program.cs index 9f63b9e133..bda58bf996 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/Program.cs @@ -7,26 +7,30 @@ public static async Task Main() await PrimitiveCollectionsSample.Queries_using_readonly_primitive_collections(); await PrimitiveCollectionsSample.Queries_using_readonly_primitive_collections_SQLite(); - // await ComplexTypesSample.GropupBy_complex_type_instances(); - // await ComplexTypesSample.GropupBy_complex_type_instances_on_SQLite(); - // - // await QuerySample.Query_improvements_in_EF9(); - // await QuerySample.Query_improvements_in_EF9_on_SQLite(); - // - // // Note that SQL Server 2022 is required for Greater and Least queries. - // // await LeastGreatestSample.Queries_using_Least_and_Greatest(); - // await LeastGreatestSample.Queries_using_Least_and_Greatest_on_SQLite(); - // - // await CustomConventionsSample.Conventions_enhancements_in_EF9(); - // - // await JsonColumnsSample.Columns_from_JSON_are_pruned_when_needed(); - // await JsonColumnsSample.Columns_from_JSON_are_pruned_when_needed_on_SQLite(); - // - // await ExecuteUpdateSample.ExecuteUpdate_for_complex_type_instances(); - // await ExecuteUpdateSample.ExecuteUpdate_for_complex_type_instances_on_SQLite(); - // - // await HierarchyIdSample.SQL_Server_HierarchyId(); - // - // await ModelBuildingSample.Model_building_improvements_in_EF9(); + await ComplexTypesSample.GropupBy_complex_type_instances(); + await ComplexTypesSample.GropupBy_complex_type_instances_on_SQLite(); + + await QuerySample.Query_improvements_in_EF9(); + await QuerySample.Query_improvements_in_EF9_on_SQLite(); + + // Note that SQL Server 2022 is required for Greater and Least queries. + // await LeastGreatestSample.Queries_using_Least_and_Greatest(); + await LeastGreatestSample.Queries_using_Least_and_Greatest_on_SQLite(); + + await CustomConventionsSample.Conventions_enhancements_in_EF9(); + + await JsonColumnsSample.Columns_from_JSON_are_pruned_when_needed(); + await JsonColumnsSample.Columns_from_JSON_are_pruned_when_needed_on_SQLite(); + + await ExecuteUpdateSample.ExecuteUpdate_for_complex_type_instances(); + await ExecuteUpdateSample.ExecuteUpdate_for_complex_type_instances_on_SQLite(); + + await HierarchyIdSample.SQL_Server_HierarchyId(); + + await ModelBuildingSample.Model_building_improvements_in_EF9(); + + 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(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs b/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs index 8b855cffc2..a91b67aa55 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs @@ -96,6 +96,16 @@ async Task> GetPostsForceConstant(int id) .Where(p => p.Tags.Count > 3) .ToHashSetAsync(ReferenceEqualityComparer.Instance); #endregion + + Console.WriteLine(); + Console.WriteLine("Normalize Count != 0:"); + Console.WriteLine(); + + #region NormalizeCount + var blogsWithPost = await context.Blogs + .Where(b => b.Posts.Count > 0) + .ToListAsync(); + #endregion } private static void PrintSampleName([CallerMemberName] string? methodName = null)