From 97da967f2f5cf9161dadfcde0d5b350d8aa22cca Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Fri, 8 Mar 2024 10:10:06 +0000 Subject: [PATCH] Updates to What's New --- .../core/what-is-new/ef-core-8.0/whatsnew.md | 2 + .../core/what-is-new/ef-core-9.0/whatsnew.md | 266 ++++++++++++++++-- .../NewInEFCore9/BlogsContext.cs | 2 +- .../NewInEFCore9/CustomConventionsSample.cs | 120 ++++++++ .../NewInEFCore9/ExecuteUpdateSample.cs | 2 +- .../NewInEFCore9/HierarchyIdSample.cs | 121 ++++++++ .../NewInEFCore9/LeastGreatestSample.cs | 2 +- .../NewInEFCore9/ModelBuildingSample.cs | 130 ++++----- .../NewInEFCore9/NewInEFCore9.csproj | 18 +- .../Miscellaneous/NewInEFCore9/Program.cs | 7 +- .../Miscellaneous/NewInEFCore9/QuerySample.cs | 30 ++ 11 files changed, 586 insertions(+), 114 deletions(-) create mode 100644 samples/core/Miscellaneous/NewInEFCore9/CustomConventionsSample.cs create mode 100644 samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs 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 a5f4f4ce8b..5297b86e3b 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 @@ -1290,6 +1290,8 @@ 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: 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 9c1a939f6b..a93f5a76c4 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,7 +2,7 @@ title: What's New in EF Core 9 description: Overview of new features in EF Core 9 author: ajcvickers -ms.date: 02/06/2024 +ms.date: 03/07/2024 uid: core/what-is-new/ef-core-9.0/whatsnew --- @@ -17,17 +17,16 @@ EF9 is available as [daily builds](https://github.com/dotnet/efcore/blob/main/do EF9 targets .NET 8, and can therefore be used with either [.NET 8 (LTS)](https://dotnet.microsoft.com/download/dotnet/8.0) or a .NET 9 preview. -## EF Core 9 Focus - -The team has been working primarily on EF internals, so there are no new big features in EF Core 9 Preview 1. However, this means we need to get people like you (If you're reading a preview 1 post, then you're a really engaged part of the community; thank you!) to run your code on these new internals. We have over 120,000 tests, but it's not enough! We need you, people running real code on our bits, in order to find issues and ship a solid release! +> [!TIP] +> The _What's New_ docs are updated for each preview. All the samples are set up to use the [EF9 daily builds](https://github.com/dotnet/efcore/blob/main/docs/DailyBuilds.md), which usually have several additional weeks of completed work compared to the latest preview. We strongly encourage use of the daily builds when testing new features so that you're not doing your testing against stale bits. -That being said, if you're not motivated by new internals, then there are several smaller enhancements, one of which might just be something you have been waiting for, so read on! +## LINQ and SQL translation -### Improved queries +The team is working on some significant architecture changes to the query pipeline in EF Core 9 as part of our continued improvements to JSON mapping and document databases. This means we need to get **people like you** to run your code on these new internals. (If you're reading a "What's New" doc at this point in the release, then you're a really engaged part of the community; thank you!) We have over 120,000 tests, but it's not enough! We need you, people running real code on our bits, in order to find issues and ship a solid release! -The team continually strives to generate better SQL from your LINQ queries. This has already begun in EF Core 9 with the following other query enhancements. + -#### Prune columns passed to OPENJSON's WITH clause +### Prune columns passed to OPENJSON's WITH clause > [!TIP] > The code shown here comes from [JsonColumnsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/JsonColumnsSample.cs). @@ -100,7 +99,9 @@ WHERE ( FROM OPENJSON([t].[Text]) AS [t0]) = 1 ``` -#### Translations involving GREATEST/LEAST + + +### Translations involving GREATEST/LEAST > [!TIP] > The code shown here comes from [LeastGreatestSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/LeastGreatestSample.cs). @@ -176,7 +177,9 @@ SELECT LEAST(( FROM [Pubs] AS [p] ``` -#### Force or prevent query parameterization + + +### Force or prevent query parameterization > [!TIP] > The code shown here comes from [QuerySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs). @@ -251,6 +254,83 @@ info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft. WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1 ``` + + +### Inlined uncorrelated subqueries + +> [!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, an IQueryable referenced in another query may be executed as a separate database roundtrip. For example, consider the following LINQ query: + + +[!code-csharp[InlinedSubquery](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=InlinedSubquery)] + +In EF8, the query for `dotnetPosts` is executed as one round trip, and then the final results are executed as second query. For example, on SQL Server: + +```sql +SELECT COUNT(*) +FROM [Posts] AS [p] +WHERE [p].[Title] LIKE N'%.NET%' + +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 [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2 +ORDER BY (SELECT 1) +OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY +``` + +In EF9, the `IQueryable` in the `dotnetPosts` is inlined, resulting in a single round trip: + +```sql +SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], ( + SELECT COUNT(*) + FROM [Posts] AS [p0] + WHERE [p0].[Title] LIKE N'%.NET%') +FROM [Posts] AS [p] +WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2 +ORDER BY (SELECT 1) +OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY +``` + + + +### New `ToHashSetAsync` methods + +> [!TIP] +> The code shown here comes from [QuerySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs). + +The methods have existed since .NET Core 2.0. In EF9, the equivalent async methods have been added. For example: + + +[!code-csharp[ToHashSetAsync](../../../../samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs?name=ToHashSetAsync)] + +This enhancement was contributed by [@wertzui](https://github.com/wertzui). Many thanks! + +## ExecuteUpdate and ExecuteDelete + + + ### Allow passing complex type instances to ExecuteUpdate > [!TIP] @@ -363,6 +443,10 @@ FROM [Customers] AS [c] WHERE [c].[Name] = @__name_0 ``` +## Migrations + + + ### Improved temporal table migrations The migration created when changing an existing table into a temporal table has been reduced in size for EF9. For example, in EF8 making a single existing table a temporal table results in the following migration: @@ -476,12 +560,109 @@ protected override void Up(MigrationBuilder migrationBuilder) } ``` -### Improved model building +## Model building + + + +### Specify caching for sequences + +> [!TIP] +> The code shown here comes from [ModelBuildingSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs). + +EF9 allows setting the [caching options for database sequences](/sql/t-sql/statements/create-sequence-transact-sql) for any relational database provider that supports this. For example, `UseCache` can be used to explicitly turn on caching and set the cache size: + + +[!code-csharp[UseCache](../../../../samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs?name=UseCache)] + +This results in the following sequence definition when using SQL Server: + +```sql +CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3; +``` + +Similarly, `UseNoCache` explicitly turns off caching: + + +[!code-csharp[UseNoCache](../../../../samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs?name=UseNoCache)] + +```sql +CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE; +``` + +If neither `UseCache` or `UseNoCache` are called, then caching is not specified and the database will use whatever its default is. This may be a different default for different databases. + +This enhancement was contributed by [@bikbov](https://github.com/bikbov). Many thanks! + + + +### Specify fill-factor for keys and indexes > [!TIP] > The code shown here comes from [ModelBuildingSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs). -#### Make existing model building conventions more extensible +EF9 supports specification of the [SQL Server fill-factor](/sql/relational-databases/indexes/specify-fill-factor-for-an-index) when using EF Core Migrations to create keys and indexes. From the SQL Server docs, "When an index is created or rebuilt, the fill-factor value determines the percentage of space on each leaf-level page to be filled with data, reserving the remainder on each page as free space for future growth." + +The fill-factor can be set on a single or composite primary and alternate keys and indexes. For example: + + +[!code-csharp[FillFactor](../../../../samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs?name=FillFactor)] + +When applied to existing tables, this will alter the tables to the fill-factor to the constraint: + +```sql +ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn]; +ALTER TABLE [User] DROP CONSTRAINT [PK_User]; +DROP INDEX [IX_User_Name] ON [User]; +DROP INDEX [IX_User_Region_Tag] ON [User]; + +ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80); +ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80); +CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80); +CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80); +``` + +> [!NOTE] +> There is currently a bug in preview 2 where the fill-factors are not included when the table is created for the first time. This is tracked by [Issue #33269](https://github.com/dotnet/efcore/issues/33269) + +This enhancement was contributed by [@deano-hunter](https://github.com/deano-hunter). Many thanks! + + + +### Make existing model building conventions more extensible + +> [!TIP] +> The code shown here comes from [CustomConventionsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/CustomConventionsSample.cs). Public model building conventions for applications were [introduced in EF7](xref:core/modeling/bulk-configuration#Conventions). In EF9, we have made it easier to extend some of the existing conventions. For example, [the code to map properties by attribute in EF7](xref:core/what-is-new/ef-core-7.0/whatsnew#model-building-conventions) is this: @@ -571,9 +752,11 @@ In EF9, this can be simplified down to the following: } } --> -[!code-csharp[AttributeBasedPropertyDiscoveryConvention](../../../../samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs?name=AttributeBasedPropertyDiscoveryConvention)] +[!code-csharp[AttributeBasedPropertyDiscoveryConvention](../../../../samples/core/Miscellaneous/NewInEFCore9/CustomConventionsSample.cs?name=AttributeBasedPropertyDiscoveryConvention)] -#### Update ApplyConfigurationsFromAssembly to call non-public constructors + + +### Update ApplyConfigurationsFromAssembly to call non-public constructors In previous versions of EF Core, the `ApplyConfigurationsFromAssembly` method only instantiated configuration types with a public, parameterless constructors. In EF9, we have both [improved the error messages generated when this fails](https://github.com/dotnet/efcore/pull/32577), and also enabled instantiation by non-public constructor. This is useful when co-locating configuration in a private nested class which should never be instantiated by application code. For example: @@ -599,13 +782,52 @@ public class Country As an aside, some people think this pattern is an abomination because it couples the entity type to the configuration. Other people think it is very useful because it co-locates configuration with the entity type. Let's not debate this here. :-) -### Everything else in Preview 1 +## SQL Server HierarchyId + +> [!TIP] +> The code shown here comes from [HierarchyIdSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs). + + + +### Sugar for HierarchyId path generation + +First class support for the SQL Server `HierarchyId` type was [added in EF8](xref:core/what-is-new/ef-core-8.0/whatsnew#hierarchyid). In EF9, a sugar method has been added to make it easier to create new child nodes in the tree structure. For example, the following code queries for an existing entity with a `HierarchyId` property: + + +[!code-csharp[HierarchyIdQuery](../../../../samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs?name=HierarchyIdQuery)] + +This `HierarchyId` property can then be used to create child nodes without any explicit string manipulation. For example: + + +[!code-csharp[HierarchyIdParse1](../../../../samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs?name=HierarchyIdParse1)] + +If `daisy` has a `HierarchyId` of `/4/1/3/1/`, then, `child1` will get the `HierarchyId` "/4/1/3/1/1/", and `child2` will get the `HierarchyId` "/4/1/3/1/2/". + +To create a node between these two children, an additional sub-level can be used. For example: + + +[!code-csharp[HierarchyIdParse2](../../../../samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs?name=HierarchyIdParse2)] + +This creates a node with a `HierarchyId` of `/4/1/3/1/1.5/`, putting it bteween `child1` and `child2`. + +This enhancement was contributed by [@Rezakazemi890](https://github.com/Rezakazemi890). Many thanks! + +## Tooling + + + +### Fewer rebuilds -The [dotnet/efcore](https://github.com/dotnet/efcore/) GitHub repo is the source-of-truth for all work completed in EF Core. Preview 1 contains: +The [`dotnet ef` command line tool](xref:core/cli/dotnet) by default builds your project before executing the tool. This is because not rebuilding before running the tool is a common source of confusion when things don't work. Experienced developers can use the `--no-build` option to avoid this build, which may be slow. However, even the `--no-build` option could cause the project to be re-built the next time it is built outside of the EF tooling. -- 90+ bug fixes since the 8.0.0 release. This includes: - - [51 bug fixes in EF9 Preview 1 only](https://github.com/dotnet/efcore/issues?q=is%3Aissue+milestone%3A9.0.0-preview1+is%3Aclosed+label%3Atype-bug). These are bug fixes that did not meet the bar for patching. - - [8 bug fixes also shipped in 8.0.1](https://github.com/dotnet/efcore/issues?q=is%3Aissue+milestone%3A8.0.1+is%3Aclosed) - - [25 bug fixes also shipped in 8.0.2](https://github.com/dotnet/efcore/issues?q=is%3Aissue+milestone%3A8.0.2+is%3Aclosed+) -- [30 enhancements](https://github.com/dotnet/efcore/issues?q=is%3Aissue+milestone%3A9.0.0-preview1+is%3Aclosed+label%3Atype-enhancement+), the most interesting of which are described above. -- [5 cleanup issues](https://github.com/dotnet/efcore/issues?q=is%3Aissue+milestone%3A9.0.0-preview1+is%3Aclosed+-label%3Atype-enhancement+-label%3Atype-bug) +We believe a [community contribution](https://github.com/dotnet/efcore/pull/32860) from [@Suchiman](https://github.com/Suchiman) has fixed this. However, we're also conscious that tweaks around MSBuild behaviors have a tendency to have unintended consequences, so we're asking people like you to try this out and report back on any negative experiences you have. diff --git a/samples/core/Miscellaneous/NewInEFCore9/BlogsContext.cs b/samples/core/Miscellaneous/NewInEFCore9/BlogsContext.cs index 3eea21154b..27f45592d4 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/BlogsContext.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/BlogsContext.cs @@ -210,7 +210,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => (UseSqlite ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") : optionsBuilder.UseSqlServer( - @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}", + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name};ConnectRetryCount=0", sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseNetTopologySuite())) .EnableSensitiveDataLogging() .LogTo( diff --git a/samples/core/Miscellaneous/NewInEFCore9/CustomConventionsSample.cs b/samples/core/Miscellaneous/NewInEFCore9/CustomConventionsSample.cs new file mode 100644 index 0000000000..5566bac029 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore9/CustomConventionsSample.cs @@ -0,0 +1,120 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace NewInEfCore9; + +public static class CustomConventionsSample +{ + public static async Task Conventions_enhancements_in_EF9() + { + PrintSampleName(); + + await using var context = new TestContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + Console.WriteLine(context.Model.ToDebugString()); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class TestContext : DbContext + { + public bool LoggingEnabled { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name};ConnectRetryCount=0") + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); + } + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.Conventions.Replace( + serviceProvider => new AttributeBasedPropertyDiscoveryConvention( + serviceProvider.GetRequiredService())); + } + + public async Task Seed() + { + await SaveChangesAsync(); + } + } + + #region AttributeBasedPropertyDiscoveryConvention + public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies) + : PropertyDiscoveryConvention(dependencies) + { + protected override bool IsCandidatePrimitiveProperty( + MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping) + { + if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping)) + { + if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true)) + { + return true; + } + + structuralType.Builder.Ignore(memberInfo.Name); + } + + mapping = null; + return false; + } + } + #endregion + + #region Country + public class Country + { + [Persist] + public int Code { get; set; } + + [Persist] + public required string Name { get; set; } + + public bool IsDirty { get; set; } // Will not be mapped by default. + + private class FooConfiguration : IEntityTypeConfiguration + { + private FooConfiguration() + { + } + + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.Code); + } + } + } + #endregion +} + +#region PersistAttribute +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +public sealed class PersistAttribute : Attribute +{ +} +#endregion diff --git a/samples/core/Miscellaneous/NewInEFCore9/ExecuteUpdateSample.cs b/samples/core/Miscellaneous/NewInEFCore9/ExecuteUpdateSample.cs index b0b251e2fb..35dc53eedc 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/ExecuteUpdateSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/ExecuteUpdateSample.cs @@ -216,7 +216,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => (UseSqlite ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") : optionsBuilder.UseSqlServer( - @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}", + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name};ConnectRetryCount=0", sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseNetTopologySuite())) .EnableSensitiveDataLogging() .LogTo(Console.WriteLine, LogLevel.Information); diff --git a/samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs b/samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs new file mode 100644 index 0000000000..07ce56f249 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore9/HierarchyIdSample.cs @@ -0,0 +1,121 @@ +public static class HierarchyIdSample +{ + public static async Task SQL_Server_HierarchyId() + { + PrintSampleName(); + + await using var context = new FamilyTreeContext(); + await context.Database.EnsureDeletedAsync(); + + context.LoggingEnabled = true; + + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.ChangeTracker.Clear(); + + #region HierarchyIdQuery + var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy"); + #endregion + + #region HierarchyIdParse1 + var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast"); + var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills"); + #endregion + + #region HierarchyIdParse2 + var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast"); + #endregion + + context.AddRange(child1, child2, child1b); + + await context.SaveChangesAsync(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + #region Halfling + public class Halfling + { + public Halfling(HierarchyId pathFromPatriarch, string name, int? yearOfBirth = null) + { + PathFromPatriarch = pathFromPatriarch; + Name = name; + YearOfBirth = yearOfBirth; + } + + public int Id { get; private set; } + public HierarchyId PathFromPatriarch { get; set; } + public string Name { get; set; } + public int? YearOfBirth { get; set; } + } + #endregion + + public class FamilyTreeContext : DbContext + { + public bool LoggingEnabled { get; set; } + + public DbSet Halflings => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlServer( + @$"Server=(localdb)\mssqllocaldb;Database={GetType().Name};ConnectRetryCount=0", + sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseHierarchyId()) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + } + + public async Task Seed() + { + #region AddRangeAsync + await AddRangeAsync( + new Halfling(HierarchyId.Parse("/"), "Balbo", 1167), + new Halfling(HierarchyId.Parse("/1/"), "Mungo", 1207), + new Halfling(HierarchyId.Parse("/2/"), "Pansy", 1212), + new Halfling(HierarchyId.Parse("/3/"), "Ponto", 1216), + new Halfling(HierarchyId.Parse("/4/"), "Largo", 1220), + new Halfling(HierarchyId.Parse("/5/"), "Lily", 1222), + new Halfling(HierarchyId.Parse("/1/1/"), "Bungo", 1246), + new Halfling(HierarchyId.Parse("/1/2/"), "Belba", 1256), + new Halfling(HierarchyId.Parse("/1/3/"), "Longo", 1260), + new Halfling(HierarchyId.Parse("/1/4/"), "Linda", 1262), + new Halfling(HierarchyId.Parse("/1/5/"), "Bingo", 1264), + new Halfling(HierarchyId.Parse("/3/1/"), "Rosa", 1256), + new Halfling(HierarchyId.Parse("/3/2/"), "Polo"), + new Halfling(HierarchyId.Parse("/4/1/"), "Fosco", 1264), + new Halfling(HierarchyId.Parse("/1/1/1/"), "Bilbo", 1290), + new Halfling(HierarchyId.Parse("/1/3/1/"), "Otho", 1310), + new Halfling(HierarchyId.Parse("/1/5/1/"), "Falco", 1303), + new Halfling(HierarchyId.Parse("/3/2/1/"), "Posco", 1302), + new Halfling(HierarchyId.Parse("/3/2/2/"), "Prisca", 1306), + new Halfling(HierarchyId.Parse("/4/1/1/"), "Dora", 1302), + new Halfling(HierarchyId.Parse("/4/1/2/"), "Drogo", 1308), + new Halfling(HierarchyId.Parse("/4/1/3/"), "Dudo", 1311), + new Halfling(HierarchyId.Parse("/1/3/1/1/"), "Lotho", 1310), + new Halfling(HierarchyId.Parse("/1/5/1/1/"), "Poppy", 1344), + new Halfling(HierarchyId.Parse("/3/2/1/1/"), "Ponto", 1346), + new Halfling(HierarchyId.Parse("/3/2/1/2/"), "Porto", 1348), + new Halfling(HierarchyId.Parse("/3/2/1/3/"), "Peony", 1350), + new Halfling(HierarchyId.Parse("/4/1/2/1/"), "Frodo", 1368), + new Halfling(HierarchyId.Parse("/4/1/3/1/"), "Daisy", 1350), + new Halfling(HierarchyId.Parse("/3/2/1/1/1/"), "Angelica", 1381)); + + await SaveChangesAsync(); + #endregion + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore9/LeastGreatestSample.cs b/samples/core/Miscellaneous/NewInEFCore9/LeastGreatestSample.cs index 9e7a5aafd2..8fde721208 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/LeastGreatestSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/LeastGreatestSample.cs @@ -207,7 +207,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => (UseSqlite ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}.db") // Note that SQL Server 2022 is required. - : optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}")) + : optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name};ConnectRetryCount=0")) .EnableSensitiveDataLogging() .LogTo( s => diff --git a/samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs b/samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs index f1e5532548..487cc5ea22 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/ModelBuildingSample.cs @@ -1,26 +1,17 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Metadata.Conventions; -using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage; - -namespace NewInEfCore9; +namespace NewInEfCore9; public static class ModelBuildingSample { - public static async Task Model_building_enhancements_in_EF9() + public static async Task Model_building_improvements_in_EF9() { PrintSampleName(); - await using var context = new TestContext(); + await using var context = new ModelBuildingContext(); await context.Database.EnsureDeletedAsync(); - await context.Database.EnsureCreatedAsync(); - await context.Seed(); context.LoggingEnabled = true; - context.ChangeTracker.Clear(); - Console.WriteLine(context.Model.ToDebugString()); + await context.Database.EnsureCreatedAsync(); } private static void PrintSampleName([CallerMemberName] string? methodName = null) @@ -29,12 +20,12 @@ private static void PrintSampleName([CallerMemberName] string? methodName = null Console.WriteLine(); } - public class TestContext : DbContext + public class ModelBuildingContext : DbContext { public bool LoggingEnabled { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}") + => optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name};ConnectRetryCount=0") .EnableSensitiveDataLogging() .LogTo( s => @@ -47,74 +38,55 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); - } - - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - { - configurationBuilder.Conventions.Replace( - serviceProvider => new AttributeBasedPropertyDiscoveryConvention( - serviceProvider.GetRequiredService())); - } - - public async Task Seed() - { - await SaveChangesAsync(); + #region FillFactor + modelBuilder.Entity() + .HasKey(e => e.Id) + .HasFillFactor(80); + + modelBuilder.Entity() + .HasAlternateKey(e => new { e.Region, e.Ssn }) + .HasFillFactor(80); + + modelBuilder.Entity() + .HasIndex(e => new { e.Name }) + .HasFillFactor(80); + + modelBuilder.Entity() + .HasIndex(e => new { e.Region, e.Tag }) + .HasFillFactor(80); + #endregion + + #region UseCache + modelBuilder.HasSequence("MyCachedSequence") + .HasMin(10).HasMax(255000) + .IsCyclic() + .StartsAt(11).IncrementsBy(2) + .UseCache(3); + #endregion + + #region UseNoCache + modelBuilder.HasSequence("MyUncachedSequence") + .HasMin(10).HasMax(255000) + .IsCyclic() + .StartsAt(11).IncrementsBy(2) + .UseNoCache(); + #endregion + + #region DefaultCache + modelBuilder.HasSequence("MySequence") + .HasMin(10).HasMax(255000) + .IsCyclic() + .StartsAt(11).IncrementsBy(2); + #endregion } } - #region AttributeBasedPropertyDiscoveryConvention - public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies) - : PropertyDiscoveryConvention(dependencies) + public class User { - protected override bool IsCandidatePrimitiveProperty( - MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping) - { - if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping)) - { - if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true)) - { - return true; - } - - structuralType.Builder.Ignore(memberInfo.Name); - } - - mapping = null; - return false; - } - } - #endregion - - #region Country - public class Country - { - [Persist] - public int Code { get; set; } - - [Persist] + public int Id { get; set; } + public required string Region { get; set; } public required string Name { get; set; } - - public bool IsDirty { get; set; } // Will not be mapped by default. - - private class FooConfiguration : IEntityTypeConfiguration - { - private FooConfiguration() - { - } - - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(e => e.Code); - } - } + public string? Ssn { get; set; } + public string? Tag { get; set; } } - #endregion -} - -#region PersistAttribute -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] -public sealed class PersistAttribute : Attribute -{ } -#endregion diff --git a/samples/core/Miscellaneous/NewInEFCore9/NewInEFCore9.csproj b/samples/core/Miscellaneous/NewInEFCore9/NewInEFCore9.csproj index 1a5d3f391c..29b8b89380 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 e9a36e0aef..8fa1f3c3da 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/Program.cs @@ -11,12 +11,17 @@ public static async Task Main() // await LeastGreatestSample.Queries_using_Least_and_Greatest(); await LeastGreatestSample.Queries_using_Least_and_Greatest_on_SQLite(); - await ModelBuildingSample.Model_building_enhancements_in_EF9(); + 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(); + } } diff --git a/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs b/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs index 2d536af8ac..8b855cffc2 100644 --- a/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs +++ b/samples/core/Miscellaneous/NewInEFCore9/QuerySample.cs @@ -66,6 +66,36 @@ async Task> GetPostsForceConstant(int id) e => e.Title == ".NET Blog" && e.Id == EF.Constant(id)) .ToListAsync(); #endregion + + Console.WriteLine(); + Console.WriteLine("Inline subquery:"); + Console.WriteLine(); + + #region InlinedSubquery + var dotnetPosts = context + .Posts + .Where(p => p.Title.Contains(".NET")); + + var results = dotnetPosts + .Where(p => p.Id > 2) + .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() }) + .Skip(2).Take(10) + .ToArray(); + #endregion + + Console.WriteLine(); + Console.WriteLine("ToHashSetAsync:"); + Console.WriteLine(); + + #region ToHashSetAsync + var set1 = await context.Posts + .Where(p => p.Tags.Count > 3) + .ToHashSetAsync(); + + var set2 = await context.Posts + .Where(p => p.Tags.Count > 3) + .ToHashSetAsync(ReferenceEqualityComparer.Instance); + #endregion } private static void PrintSampleName([CallerMemberName] string? methodName = null)