diff --git a/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md index 070023e56c..49d37288fb 100644 --- a/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md @@ -2,7 +2,7 @@ title: What's New in EF Core 7.0 description: Overview of new features in EF Core 7.0 author: ajcvickers -ms.date: 09/23/2022 +ms.date: 09/25/2022 uid: core/what-is-new/ef-core-7 --- @@ -2627,3 +2627,449 @@ info: Microsoft.EntityFrameworkCore.Database.Command[20101] info: InfoMessageLogger[1] Table 'Customers'. Scan count 1, logical reads 2, physical reads 0, page server reads 0, read-ahead reads 0, page server read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob page server reads 0, lob read-ahead reads 0, lob page server read-ahead reads 0. ``` + +## Query enhancements + +EF7 contains many improvements in the translation of LINQ queries. + +### GroupBy as final operator + +> [!TIP] +> The code shown here comes from [GroupByFinalOperatorSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs). + +EF7 supports using `GroupBy` as the final operator in a query. For example, this LINQ query: + + +[!code-csharp[GroupByFinalOperator](../../../../samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs?name=GroupByFinalOperator)] + +Translates to the following SQL when using SQL Server: + +```sql +SELECT [b].[Price], [b].[Id], [b].[AuthorId] +FROM [Books] AS [b] +ORDER BY [b].[Price] +``` + +> [!NOTE] +> This type of `GroupBy` does not translate directly to SQL. EF Core does the grouping on the returned results, which does not result in any additional data being transferred from the server. + +### GroupJoin as final operator + +> [!TIP] +> The code shown here comes from [GroupJoinFinalOperatorSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs). + +EF7 supports using `GroupJoin` as the final operator in a query. For example, this LINQ query: + + +[!code-csharp[GroupJoinFinalOperator](../../../../samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs?name=GroupJoinFinalOperator)] + +Translates to the following SQL when using SQL Server: + +```sql +SELECT [c].[Id], [c].[Name], [t].[Id], [t].[Amount], [t].[CustomerId] +FROM [Customers] AS [c] +OUTER APPLY ( + SELECT [o].[Id], [o].[Amount], [o].[CustomerId] + FROM [Orders] AS [o] + WHERE [c].[Id] = [o].[CustomerId] +) AS [t] +ORDER BY [c].[Id] +``` + +### GroupBy entity type + +> [!TIP] +> The code shown here comes from [GroupByEntityTypeSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs). + +EF7 supports grouping by an entity type. For example, this LINQ query: + + +[!code-csharp[GroupByEntityType](../../../../samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs?name=GroupByEntityType)] + +Translates to the following SQL when using SQLite: + +```sql +SELECT [a].[Id], [a].[Name], MAX([b].[Price]) AS [MaxPrice] +FROM [Books] AS [b] +INNER JOIN [Author] AS [a] ON [b].[AuthorId] = [a].[Id] +GROUP BY [a].[Id], [a].[Name] +``` + +Keep in mind that grouping by a unique property, such as the primary key, will always be more efficient than grouping by an entity type. However, grouping by entity types can be used for both keyed and keyless entity types. + +Also, grouping by an entity type with a primary key will always result in one group per entity instance, since every entity must have a unique key value. Consider instead switching the source of the query so that grouping in not required. For example, the following query returns the same results as the previous query: + +[!code-csharp[GroupByEntityType](../../../../samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs?name=GroupByEntityType)] + +This query translates to the following SQL when using SQLite: + +```sql +SELECT [a].[Id], [a].[Name], ( + SELECT MAX([b].[Price]) + FROM [Books] AS [b] + WHERE [a].[Id] = [b].[AuthorId]) AS [MaxPrice] +FROM [Authors] AS [a] +``` + +### Subqueries don't reference ungrouped columns from outer query + +> [!TIP] +> The code shown here comes from [UngroupedColumnsQuerySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/UngroupedColumnsQuerySample.cs). + +In EF Core 6.0, a `GROUP BY` clause would reference columns in the outer query, which fails with some databases and is inefficient in others. For example, the following query: + + +[!code-csharp[UngroupedColumns](../../../../samples/core/Miscellaneous/NewInEFCore7/UngroupedColumnsQuerySample.cs?name=UngroupedColumns)] + +In EF Core 6.0 on SQL Server, this was translated to: + +```sql +SELECT DATEPART(month, [i].[History]) AS [Month], COALESCE(SUM([i].[Amount]), 0.0) AS [Total], ( + SELECT COALESCE(SUM([p].[Amount]), 0.0) + FROM [Payments] AS [p] + WHERE DATEPART(month, [p].[History]) = DATEPART(month, [i].[History])) AS [Payment] +FROM [Invoices] AS [i] +GROUP BY DATEPART(month, [i].[History]) +``` + +On EF7, the translation is: + +```sql +SELECT [t].[Key] AS [Month], COALESCE(SUM([t].[Amount]), 0.0) AS [Total], ( + SELECT COALESCE(SUM([p].[Amount]), 0.0) + FROM [Payments] AS [p] + WHERE DATEPART(month, [p].[History]) = [t].[Key]) AS [Payment] +FROM ( + SELECT [i].[Amount], DATEPART(month, [i].[History]) AS [Key] + FROM [Invoices] AS [i] +) AS [t] +GROUP BY [t].[Key] +``` + +### Read-only collections can be used for `Contains` + +> [!TIP] +> The code shown here comes from [ReadOnlySetQuerySample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs). + +EF7 supports using `Contains` when the items to search for are contained in an `IReadOnlySet` or `IReadOnlyCollection`, or `IReadOnlyList`. For example, this LINQ query: + + +[!code-csharp[ReadOnlySetQuery](../../../../samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs?name=ReadOnlySetQuery)] + +Translates to the following SQL when using SQL Server: + +```sql +SELECT [c].[Id], [c].[Name] +FROM [Customers] AS [c] +WHERE EXISTS ( + SELECT 1 + FROM [Orders] AS [o] + WHERE [c].[Id] = [o].[Customer1Id] AND [o].[Id] IN (1, 3, 5)) +``` + +### Translations for aggregate functions + +EF7 introduces better extensibility for providers to translate aggregate functions. This and other work in this area has resulted in several new translations across providers, including: + +- [Translation of `String.Join` and `String.Concat`](https://github.com/dotnet/efcore/issues/2981) +- [Translation of spatial aggregate functions](https://github.com/dotnet/efcore/issues/13278) +- [Translation of statistics aggregate functions](https://github.com/dotnet/efcore/issues/28104) + +#### String aggregate functions + +> [!TIP] +> The code shown here comes from [StringAggregateFunctionsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/StringAggregateFunctionsSample.cs). + +Queries using and are now translated when appropriate. For example: + + +[!code-csharp[Join](../../../../samples/core/Miscellaneous/NewInEFCore7/StringAggregateFunctionsSample.cs?name=Join)] + +This query translates to following when using SQL Server: + +```sql +SELECT [a].[Id], [a].[Name], COALESCE(STRING_AGG([p].[Title], N'|'), N'') AS [Books] +FROM [Posts] AS [p] +LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id] +GROUP BY [a].[Id], [a].[Name] +``` + +When combined with other string function, these translations allow for some complex string manipulation on the server. For example: + + +[!code-csharp[ConcatAndJoin](../../../../samples/core/Miscellaneous/NewInEFCore7/StringAggregateFunctionsSample.cs?name=ConcatAndJoin)] + +This query translates to following when using SQL Server: + +```sql +SELECT [t].[Name], (N'''' + [t0].[Name]) + N''' ', [t0].[Name], [t].[c] +FROM ( + SELECT [a].[Name], COALESCE(STRING_AGG(CASE + WHEN CAST(LEN([p].[Content]) AS int) >= 10 THEN COALESCE((N'''' + COALESCE(SUBSTRING([p].[Content], 0 + 1, 10), N'')) + N''' ', N'') + END, N' | '), N'') AS [c] + FROM [Posts] AS [p] + LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id] + GROUP BY [a].[Name] +) AS [t] +OUTER APPLY ( + SELECT DISTINCT [b].[Name] + FROM [Posts] AS [p0] + LEFT JOIN [Authors] AS [a0] ON [p0].[AuthorId] = [a0].[Id] + INNER JOIN [Blogs] AS [b] ON [p0].[BlogId] = [b].[Id] + WHERE [t].[Name] = [a0].[Name] OR ([t].[Name] IS NULL AND [a0].[Name] IS NULL) +) AS [t0] +ORDER BY [t].[Name] +``` + +#### Spatial aggregate functions + +> [!TIP] +> The code shown here comes from [SpatialAggregateFunctionsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs). + +It is now possible for [database providers that support for NetTopologySuite](xref:core/modeling/spatial) to translate the following spatial aggregate functions: + +- [GeometryCombiner.Combine()](https://nettopologysuite.github.io/NetTopologySuite/api/NetTopologySuite.Geometries.Utilities.GeometryCombiner.html) +- [UnaryUnionOp.Union()](https://nettopologysuite.github.io/NetTopologySuite/api/NetTopologySuite.Operation.Union.UnaryUnionOp.html) +- [ConvexHull.Create()](http://nettopologysuite.github.io/NetTopologySuite/api/NetTopologySuite.Algorithm.ConvexHull.html) +- [EnvelopeCombiner.CombineAsGeometry()](https://nettopologysuite.github.io/NetTopologySuite/api/NetTopologySuite.Geometries.Utilities.EnvelopeCombiner.html) + +> [!TIP] +> These translations have been implemented by the team for SQL Server and SQLite. For other providers, contact the provider maintainer if support has not yet been implemented. + +For example: + + +[!code-csharp[SpatialAggregateFunctionsSample](../../../../samples/core/Miscellaneous/NewInEFCore7/QueryStatisticsLoggerSample.cs?name=SpatialAggregateFunctionsSample)] + +This query is translated to the following SQL when using SQL Server: + +```sql +SELECT [c].[Owner] AS [Id], geography::CollectionAggregate([c].[Location]) AS [Combined] +FROM [Caches] AS [c] +WHERE [c].[Location].Long < -90.0E0 +GROUP BY [c].[Owner] +``` + +#### Statistical aggregate functions + +> [!TIP] +> The code shown here comes from [StatisticalAggregateFunctionsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/StatisticalAggregateFunctionsSample.cs). + +SQL Server translations have been implemented for the following statistical functions: + +- [EF.Functions.StandardDeviationSample()](https://github.com/dotnet/efcore/blob/2cfc7c3b9020daf9d2e28d404a78814e69941421/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs#L1547) +- [EF.Functions.StandardDeviationPopulation()](https://github.com/dotnet/efcore/blob/2cfc7c3b9020daf9d2e28d404a78814e69941421/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs#L1621) +- [EF.Functions.VarianceSample()](https://github.com/dotnet/efcore/blob/2cfc7c3b9020daf9d2e28d404a78814e69941421/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs#L1695) +- [EF.Functions. VariancePopulation()](https://github.com/dotnet/efcore/blob/2cfc7c3b9020daf9d2e28d404a78814e69941421/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs#L1749) + +> [!TIP] +> These translations have been implemented by the team for SQL Server. For other providers, contact the provider maintainer if support has not yet been implemented. + +For example: + + +[!code-csharp[StatsForAll](../../../../samples/core/Miscellaneous/NewInEFCore7/StatisticalAggregateFunctionsSample.cs?name=StatsForAll)] + +This query is translated to the following SQL when using SQL Server: + +```sql +SELECT [u].[Id] AS [Author], COALESCE(SUM([d].[DownloadCount]), 0) AS [TotalCost], AVG(CAST([d].[DownloadCount] AS float)) AS [AverageViews], VARP([d].[DownloadCount]) AS [VariancePopulation], VAR([d].[DownloadCount]) AS [VarianceSample], STDEVP([d].[DownloadCount]) AS [StandardDeviationPopulation], STDEV([d].[DownloadCount]) AS [StandardDeviationSample] +FROM [Downloads] AS [d] +INNER JOIN [Uploader] AS [u] ON [d].[UploaderId] = [u].[Id] +GROUP BY [u].[Id] +``` + +### Translation of `string.IndexOf` + +> [!TIP] +> The code shown here comes from [MiscellaneousTranslationsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs). + +EF7 now translates in LINQ queries. For example: + + +[!code-csharp[StringIndexOf](../../../../samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs?name=StringIndexOf)] + +This query translates to the following SQL when using SQL Server: + +```sql +SELECT [p].[Title], CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1 AS [IndexOfEntity] +FROM [Posts] AS [p] +WHERE (CAST(CHARINDEX(N'Entity', [p].[Content]) AS int) - 1) > 0 +``` + +### Translation of `GetType` for entity types + +> [!TIP] +> The code shown here comes from [MiscellaneousTranslationsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs). + +EF7 now translates in LINQ queries. For example: + + +[!code-csharp[GetType](../../../../samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs?name=GetType)] + +This query translates to the following SQL when using SQL Server with TPH inheritance: + +```sql +SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText] +FROM [Posts] AS [p] +WHERE [p].[Discriminator] = N'Post' +``` + +Notice that this query returns only `Post` instances that are actually of type `Post`, and not those of any derived types. This is different from a query that uses `is` or `OfType`, which will also return instances of any derived types. For example, consider the query: + + +[!code-csharp[OfType](../../../../samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs?name=OfType)] + +Which translates to different SQL: + +```sql + SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText] + FROM [Posts] AS [p] +``` + +And will return both `Post` and `FeaturedPost` entities. + +### Support for `AT TIME ZONE` + +> [!TIP] +> The code shown here comes from [MiscellaneousTranslationsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs). + +EF7 introduces new [EF.Functions.AtTimeZone](https://github.com/dotnet/efcore/blob/2cfc7c3b9020daf9d2e28d404a78814e69941421/src/EFCore.SqlServer/Extensions/SqlServerDbFunctionsExtensions.cs#L1462) functions for and . These functions translate to `AT TIME ZONE` clauses in the generated SQL. For example: + + +[!code-csharp[AtTimeZone](../../../../samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs?name=AtTimeZone)] + +This query translates to the following SQL when using SQL Server: + +```sql +SELECT [p].[Title], [p].[PublishedOn] AT TIME ZONE 'Pacific Standard Time' AS [PacificTime], [p].[PublishedOn] AT TIME ZONE 'GMT Standard Time' AS [UkTime] +FROM [Posts] AS [p] +``` + +> [!TIP] +> These translations have been implemented by the team for SQL Server. For other providers, contact the provider maintainer if support has not yet been implemented. + +### Filtered Include on hidden navigations + +> [!TIP] +> The code shown here comes from [MiscellaneousTranslationsSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs). + +The [Include methods](xref:core/querying/related-data/eager) can now be used with . This allows [filtering and ordering](xref:core/querying/related-data/eager#filtered-include) even for private navigation properties, or private navigations represented by fields. For example: + + +[!code-csharp[FilteredInclude](../../../../samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs?name=FilteredInclude)] + +This is equivalent to: + +```csharp +var query = context.Blogs.Include( + blog => Posts + .Where(post => post.Content.Contains(".NET")) + .OrderBy(post => post.Title)); +``` + +But does not require `Blog.Posts` to be publicly accessible. + +When using SQL Server, the both queries above translate to: + +```sql +SELECT [b].[Id], [b].[Name], [t].[Id], [t].[AuthorId], [t].[BlogId], [t].[Content], [t].[Discriminator], [t].[PublishedOn], [t].[Title], [t].[PromoText] +FROM [Blogs] AS [b] +LEFT JOIN ( + SELECT [p].[Id], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText] + FROM [Posts] AS [p] + WHERE [p].[Content] LIKE N'%.NET%' +) AS [t] ON [b].[Id] = [t].[BlogId] +ORDER BY [b].[Id], [t].[Title] +``` diff --git a/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs b/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs new file mode 100644 index 0000000000..cbd8fd9a43 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs @@ -0,0 +1,125 @@ +namespace NewInEfCore7; + +public static class GroupByEntityTypeSample +{ + public static Task GroupBy_entity_type_SqlServer() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task GroupBy_entity_type_Sqlite() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task GroupBy_entity_type_InMemory() + { + PrintSampleName(); + return QueryTest(); + } + + private static async Task QueryTest() + where TContext : BookContext, new() + { + await using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + var toast = new Author { Name = "Toast" }; + var alice = new Author { Name = "Alice" }; + + await context.AddRangeAsync( + new Book { Author = alice, Price = 10 }, + new Book { Author = alice, Price = 11 }, + new Book { Author = toast, Price = 12 }, + new Book { Author = toast, Price = 13 }, + new Book { Author = toast, Price = 14 }); + + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + #region GroupByEntityType + + var query = context.Books + .GroupBy(s => s.Author) + .Select(s => new { Author = s.Key, MaxPrice = s.Max(p => p.Price) }); + + #endregion + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Author: {group.Author.Name}; MaxPrice = {group.MaxPrice}"); + } + } + + await using (var context = new TContext()) + { + #region GroupByEntityTypeReversed + var query = context.Authors + .Select(a => new { Author = a, MaxPrice = a.Books.Max(b => b.Price) }); + #endregion + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Author: {group.Author.Name}; MaxPrice = {group.MaxPrice}"); + } + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class BookContext : DbContext + { + public DbSet Books => Set(); + public DbSet Authors => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } + + public class BookContextSqlServer : BookContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Books")); + } + + public class BookContextSqlite : BookContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlite("Data Source = books.db")); + } + + public class BookContextInMemory : BookContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseInMemoryDatabase(nameof(BookContextInMemory))); + } + + public class Author + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public ICollection Books { get; } = new List(); + } + + public class Book + { + public int Id { get; set; } + public Author Author { get; set; } = default!; + public int Price { get; set; } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs b/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs new file mode 100644 index 0000000000..3715088fab --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs @@ -0,0 +1,107 @@ +namespace NewInEfCore7; + +public static class GroupByFinalOperatorSample +{ + public static Task GroupBy_final_operator_SqlServer() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task GroupBy_final_operator_Sqlite() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task GroupBy_final_operator_InMemory() + { + PrintSampleName(); + return QueryTest(); + } + + private static async Task QueryTest() + where TContext : BookContext, new() + { + await using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + var toast = new Author { Name = "Toast" }; + var alice = new Author { Name = "Alice" }; + + await context.AddRangeAsync( + new Book { Author = alice, Price = 10 }, + new Book { Author = alice, Price = 10 }, + new Book { Author = toast, Price = 12 }, + new Book { Author = toast, Price = 12 }, + new Book { Author = toast, Price = 14 }); + + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + #region GroupByFinalOperator + var query = context.Books.GroupBy(s => s.Price); + #endregion + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Price: {group.Key}; Count = {group.Count()}"); + } + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class BookContext : DbContext + { + public DbSet Books => Set(); + public DbSet Authors => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } + + public class BookContextSqlServer : BookContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Books")); + } + + public class BookContextSqlite : BookContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlite("Data Source = books.db")); + } + + public class BookContextInMemory : BookContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseInMemoryDatabase(nameof(BookContextInMemory))); + } + + public class Author + { + public int Id { get; set; } + public string Name { get; set; } = default!; + } + + public class Book + { + public int Id { get; set; } + public Author Author { get; set; } = default!; + public int? Price { get; set; } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs b/samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs new file mode 100644 index 0000000000..6b1068169b --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs @@ -0,0 +1,147 @@ +namespace NewInEfCore7; + +public static class GroupJoinFinalOperatorSample +{ + public static Task GroupJoin_final_operator_SqlServer() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task GroupJoin_final_operator_Sqlite() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task GroupJoin_final_operator_InMemory() + { + PrintSampleName(); + return QueryTest(); + } + + private static async Task QueryTest() + where TContext : GroupJoinContext, new() + { + await using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + var toast = new Customer { Name = "Toast" }; + var alice = new Customer { Name = "Alice" }; + + await context.AddRangeAsync( + new Order { Customer = alice, Amount = 10 }, + new Order { Customer = alice, Amount = 10 }, + new Order { Customer = toast, Amount = 12 }, + new Order { Customer = toast, Amount = 12 }, + new Order { Customer = toast, Amount = 14 }); + + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + #region GroupJoinFinalOperator + var query = context.Customers.GroupJoin( + context.Orders, c => c.Id, o => o.CustomerId, (c, os) => new { Customer = c, Orders = os }); + #endregion + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Customer: {group.Customer.Name}; Count = {group.Orders.Count()}"); + } + } + + await using (var context = new TContext()) + { + var query = context.Customers + .GroupJoin( + context.Orders, + o => o.Id, + bt => bt.CustomerId, + (o, bt) => new { Customer = o, BotTasks = bt, }); + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Customer: {group.Customer.Name}; Count = {group.BotTasks.Count()}"); + } + } + + await using (var context = new TContext()) + { + var query = + from customer in context.Customers + join order in context.Orders on customer.Id equals order.CustomerId into orderDetails + select new CustomerWithNavigationProperties { Customer = customer, Orders = orderDetails.ToList() }; + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Customer: {group.Customer.Name}; Count = {group.Orders.Count()}"); + } + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class GroupJoinContext : DbContext + { + public DbSet Customers => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } + + public class GroupJoinContextSqlServer : GroupJoinContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Customers")); + } + + public class GroupJoinContextSqlite : GroupJoinContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlite("Data Source = customers.db")); + } + + public class GroupJoinContextInMemory : GroupJoinContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseInMemoryDatabase(nameof(GroupJoinContextInMemory))); + } + + public class Customer + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public List Orders { get; } = new(); + } + + public class Order + { + public int Id { get; set; } + + public int? CustomerId { get; set; } + public Customer? Customer { get; set; } + + [Precision(18, 2)] + public decimal Amount { get; set; } + } + + public class CustomerWithNavigationProperties + { + public Customer Customer { get; set; } = null!; + public List Orders { get; set; } = null!; + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs b/samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs new file mode 100644 index 0000000000..dc73652641 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/MiscellaneousTranslationsSample.cs @@ -0,0 +1,118 @@ +namespace NewInEfCore7; + +public static class MiscellaneousTranslationsSample +{ + public static async Task Translate_string_IndexOf() + { + PrintSampleName(); + + await using (var context = new BlogsContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + } + + await using (var context = new BlogsContext { LoggingEnabled = true }) + { + #region StringIndexOf + var query = context.Posts + .Select(post => new { post.Title, IndexOfEntity = post.Content.IndexOf("Entity") }) + .Where(post => post.IndexOfEntity > 0); + #endregion + + await foreach (var post in query.AsAsyncEnumerable()) + { + Console.WriteLine(post); + } + + Console.WriteLine(); + } + + await using (var context = new BlogsContext { LoggingEnabled = true }) + { + #region GetType + var query = context.Posts.Where(post => post.GetType() == typeof(Post)); + #endregion + + await foreach (var post in query.AsAsyncEnumerable()) + { + Console.WriteLine($"{post.GetType().Name} : {post.Title}"); + } + + Console.WriteLine(); + } + + await using (var context = new BlogsContext { LoggingEnabled = true }) + { + #region OfType + var query = context.Posts.OfType(); + #endregion + + await foreach (var post in query.AsAsyncEnumerable()) + { + Console.WriteLine($"{post.GetType().Name} : {post.Title}"); + } + + Console.WriteLine(); + } + + await using (var context = new BlogsContext { LoggingEnabled = true }) + { + #region AtTimeZone + var query = context.Posts + .Select(post => new + { + post.Title, + PacificTime = EF.Functions.AtTimeZone(post.PublishedOn, "Pacific Standard Time"), + UkTime = EF.Functions.AtTimeZone(post.PublishedOn, "GMT Standard Time"), + }); + #endregion + + await foreach (var post in query.AsAsyncEnumerable()) + { + Console.WriteLine(post); + } + + Console.WriteLine(); + } + + await using (var context = new BlogsContext { LoggingEnabled = true }) + { + #region FilteredInclude + var query = context.Blogs.Include( + blog => EF.Property>(blog, "Posts") + .Where(post => post.Content.Contains(".NET")) + .OrderBy(post => post.Title)); + #endregion + + await foreach (var blog in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Blog {blog.Name}:"); + + foreach (var post in blog.Posts) + { + Console.WriteLine($" Post {post.Title}"); + } + } + + Console.WriteLine(); + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class BlogsContext : NewInEfCore7.BlogsContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Ignore(author => author.Contact); + modelBuilder.Entity().Ignore(post => post.Metadata); + base.OnModelCreating(modelBuilder); + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj index dcf498f65f..f5c0341e90 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj +++ b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj @@ -13,6 +13,7 @@ + diff --git a/samples/core/Miscellaneous/NewInEFCore7/Program.cs b/samples/core/Miscellaneous/NewInEFCore7/Program.cs index 2cb341a278..d99b37a514 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/Program.cs @@ -42,5 +42,32 @@ public static async Task Main() await InjectLoggerSample.Injecting_services_into_entities(); await LazyConnectionStringSample.Lazy_initialization_of_a_connection_string(); await QueryStatisticsLoggerSample.Executing_commands_after_consuming_a_result_set(); + + await UngroupedColumnsQuerySample.Subqueries_dont_reference_ungrouped_columns_from_outer_query_SqlServer(); + + await GroupByEntityTypeSample.GroupBy_entity_type_Sqlite(); + await GroupByEntityTypeSample.GroupBy_entity_type_SqlServer(); + await GroupByEntityTypeSample.GroupBy_entity_type_InMemory(); + + await GroupByFinalOperatorSample.GroupBy_final_operator_SqlServer(); + await GroupByFinalOperatorSample.GroupBy_final_operator_Sqlite(); + + await GroupJoinFinalOperatorSample.GroupJoin_final_operator_SqlServer(); + await GroupJoinFinalOperatorSample.GroupJoin_final_operator_Sqlite(); + await GroupJoinFinalOperatorSample.GroupJoin_final_operator_InMemory(); + + await ReadOnlySetQuerySample.Use_Contains_with_IReadOnlySet_SqlServer(); + await ReadOnlySetQuerySample.Use_Contains_with_IReadOnlySet_Sqlite(); + await ReadOnlySetQuerySample.Use_Contains_with_IReadOnlySet_InMemory(); + + await StringAggregateFunctionsSample.Translate_string_Concat_and_string_Join(); + + await SpatialAggregateFunctionsSample.Translate_spatial_aggregate_functions_SqlServer(); + await SpatialAggregateFunctionsSample.Translate_spatial_aggregate_functions_Sqlite(); + await SpatialAggregateFunctionsSample.Translate_spatial_aggregate_functions_InMemory(); + + await StatisticalAggregateFunctionsSample.Translate_statistical_aggregate_functions(); + + await MiscellaneousTranslationsSample.Translate_string_IndexOf(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs b/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs new file mode 100644 index 0000000000..d4d57439fd --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs @@ -0,0 +1,171 @@ +using System.Collections.ObjectModel; + +namespace NewInEfCore7; + +public static class ReadOnlySetQuerySample +{ + public static Task Use_Contains_with_IReadOnlySet_SqlServer() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task Use_Contains_with_IReadOnlySet_Sqlite() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task Use_Contains_with_IReadOnlySet_InMemory() + { + PrintSampleName(); + return QueryTest(); + } + + private static async Task QueryTest() + where TContext : ReadOnlySetContext, new() + { + await using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + var toast = new Customer1 { Name = "Toast" }; + var alice = new Customer2 { Name = "Alice" }; + var mac = new Customer3 { Name = "Mac" }; + + await context.AddRangeAsync( + new Order { Customer1 = toast, Customer2 = alice, Customer3 = mac, Amount = 10 }, + new Order { Customer1 = toast, Customer2 = alice, Customer3 = mac, Amount = 10 }, + new Order { Customer1 = toast, Customer2 = alice, Customer3 = mac, Amount = 12 }, + new Order { Customer1 = toast, Customer2 = alice, Customer3 = mac, Amount = 12 }, + new Order { Customer1 = toast, Customer2 = alice, Customer3 = mac, Amount = 14 }); + + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + #region ReadOnlySetQuery + IReadOnlySet searchIds = new HashSet { 1, 3, 5 }; + var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id))); + #endregion + + await foreach (var customer in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Customer: {customer.Name}"); + } + } + + await using (var context = new TContext()) + { + IReadOnlyCollection searchIds = new ReadOnlyCollection(new Collection { 1, 3, 5 }); + var query = context.Customers.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id))); + + await foreach (var customer in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Customer: {customer.Name}"); + } + } + + await using (var context = new TContext()) + { + var searchIds = new ReadOnlyCollection(new Collection { 1, 3, 5 }); + var query = context.Customer2s.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id))); + + await foreach (var customer in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Customer: {customer.Name}"); + } + } + + await using (var context = new TContext()) + { + IReadOnlyList searchIds = new List { 1, 3, 5 }; + var query = context.Customer3s.Where(p => p.Orders.Any(l => searchIds.Contains(l.Id))); + + await foreach (var customer in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Customer: {customer.Name}"); + } + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class ReadOnlySetContext : DbContext + { + public DbSet Customers => Set(); + public DbSet Customer2s => Set(); + public DbSet Customer3s => Set(); + public DbSet Orders => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } + + public class ReadOnlySetContextSqlServer : ReadOnlySetContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Customers")); + } + + public class ReadOnlySetContextSqlite : ReadOnlySetContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlite("Data Source = customers.db")); + } + + public class ReadOnlySetContextInMemory : ReadOnlySetContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseInMemoryDatabase(nameof(ReadOnlySetContextInMemory))); + } + + public class Customer1 + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public IReadOnlySet Orders { get; } = new HashSet(); + } + + public class Customer2 + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public IReadOnlyCollection Orders { get; } = new HashSet(); + } + + public class Customer3 + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public IReadOnlyList Orders { get; } = new List(); + } + + public class Order + { + public int Id { get; set; } + + public int? Customer1Id { get; set; } + public Customer1? Customer1 { get; set; } + + public int Customer2Id { get; set; } + public Customer2 Customer2 { get; set; } = null!; + + public int Customer3Id { get; set; } + public Customer3 Customer3 { get; set; } = null!; + + [Precision(18, 2)] + public decimal Amount { get; set; } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs b/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs new file mode 100644 index 0000000000..9ae7b846c5 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs @@ -0,0 +1,178 @@ +using NetTopologySuite.Geometries; +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Operation.Union; + +namespace NewInEfCore7; + +public static class SpatialAggregateFunctionsSample +{ + public static Task Translate_spatial_aggregate_functions_SqlServer() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task Translate_spatial_aggregate_functions_Sqlite() + { + PrintSampleName(); + return QueryTest(); + } + + public static Task Translate_spatial_aggregate_functions_InMemory() + { + PrintSampleName(); + return QueryTest(); + } + + private static async Task QueryTest() + where TContext : GeoCacheContext, new() + { + await using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + await context.AddRangeAsync( + new GeoCache { Name = "Sandpiper Cache", Owner = "Nilwob Inc.", Location = new Point(-93.71855, 41.760783) { SRID = 4326 } }, + new GeoCache { Name = "Paddy's Pot-O-Gold", Owner = "Nilwob Inc.", Location = new Point(-93.733633, 41.775633) { SRID = 4326 } }, + new GeoCache { Name = "Blazing Beacon", Owner = "The Spokes", Location = new Point(-1.606483, 55.392433) { SRID = 4326 } }, + new GeoCache { Name = "133 Steps to Relaxation", Owner = "Team isuforester", Location = new Point(-93.854733, 41.9389) { SRID = 4326 } }, + new EuclideanPoint { Name = "A", Point = new Point(1.0, 1.0) }, + new EuclideanPoint { Name = "A", Point = new Point(1.0, 2.0) }, + new EuclideanPoint { Name = "B", Point = new Point(2.0, 1.0) }, + new EuclideanPoint { Name = "B", Point = new Point(2.0, 2.0) }); + + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + #region GeometryCombinerCombine + var query = context.Caches + .Where(cache => cache.Location.X < -90) + .GroupBy(cache => cache.Owner) + .Select(grouping => new + { + Id = grouping.Key, + Combined = GeometryCombiner.Combine(grouping.Select(cache => cache.Location)) + }); + #endregion + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine(group); + } + } + + await using (var context = new TContext()) + { + var query = context.Caches + .Where(cache => cache.Location.X < -90) + .GroupBy(cache => cache.Owner) + .Select(grouping => new + { + Id = grouping.Key, + ConvexHull = NetTopologySuite.Algorithm.ConvexHull.Create(grouping.Select(cache => cache.Location)) + }); + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine(group); + } + } + + await using (var context = new TContext()) + { + var query = context.Caches + .Where(cache => cache.Location.X < -90) + .GroupBy(cache => cache.Owner) + .Select(grouping => new + { + Id = grouping.Key, + Union = UnaryUnionOp.Union(grouping.Select(cache => cache.Location)) + }); + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine(group); + } + } + + await using (var context = new TContext()) + { + var query = context.Points + .GroupBy(point => point.Name) + .Select(grouping => new + { + Id = grouping.Key, + Combined = EnvelopeCombiner.CombineAsGeometry(grouping.Select(point => point.Point)) + }); + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine(group); + } + } + + Console.WriteLine(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class GeoCacheContext : DbContext + { + public DbSet Caches => Set(); + public DbSet Points => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } + + public class GeoCacheContextSqlServer : GeoCacheContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlServer( + @"Server=(localdb)\mssqllocaldb;Database=GeoCaches", + b => b.UseNetTopologySuite())); + } + + public class GeoCacheContextSqlite : GeoCacheContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlite( + "Data Source = geocaches.db", + b => b.UseNetTopologySuite())); + } + + public class GeoCacheContextInMemory : GeoCacheContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseInMemoryDatabase(nameof(GeoCacheContextInMemory))); + } + + public class GeoCache + { + public int Id { get; set; } + public string? Owner { get; set; } + public string Name { get; set; } = null!; + public Point Location { get; set; } = null!; + } + + public class EuclideanPoint + { + public int Id { get; set; } + public string Name { get; set; } = null!; + + [Column(TypeName = "geometry")] + public Point Point { get; set; } = null!; + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/StatisticalAggregateFunctionsSample.cs b/samples/core/Miscellaneous/NewInEFCore7/StatisticalAggregateFunctionsSample.cs new file mode 100644 index 0000000000..6d8c8d0899 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/StatisticalAggregateFunctionsSample.cs @@ -0,0 +1,84 @@ +using NetTopologySuite.Geometries.Utilities; +using NetTopologySuite.Operation.Union; + +namespace NewInEfCore7; + +public static class StatisticalAggregateFunctionsSample +{ + public static async Task Translate_statistical_aggregate_functions() + { + PrintSampleName(); + + await using (var context = new StatisticsContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + var toast = new Uploader { Name = "Toast" }; + var alice = new Uploader { Name = "Alice" }; + + await context.AddRangeAsync( + new Download { Uploader = alice, DownloadCount = 1024 }, + new Download { Uploader = alice, DownloadCount = 2048 }, + new Download { Uploader = toast, DownloadCount = 4096 }, + new Download { Uploader = toast, DownloadCount = 8192 }, + new Download { Uploader = toast, DownloadCount = 16384 }); + + await context.SaveChangesAsync(); + + #region StatsForAll + var query = context.Downloads + .GroupBy(download => download.Uploader.Id) + .Select( + grouping => new + { + Author = grouping.Key, + TotalCost = grouping.Sum(d => d.DownloadCount), + AverageViews = grouping.Average(d => d.DownloadCount), + VariancePopulation = EF.Functions.VariancePopulation(grouping.Select(d => d.DownloadCount)), + VarianceSample = EF.Functions.VarianceSample(grouping.Select(d => d.DownloadCount)), + StandardDeviationPopulation = EF.Functions.StandardDeviationPopulation(grouping.Select(d => d.DownloadCount)), + StandardDeviationSample = EF.Functions.StandardDeviationSample(grouping.Select(d => d.DownloadCount)) + }); + #endregion + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine(group); + } + } + + Console.WriteLine(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class StatisticsContext : DbContext + { + public DbSet Downloads => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Downloads") + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } + + public class Uploader + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public ICollection Downloads { get; } = new List(); + } + + public class Download + { + public int Id { get; set; } + public Uploader Uploader { get; set; } = default!; + public int DownloadCount { get; set; } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/StringAggregateFunctionsSample.cs b/samples/core/Miscellaneous/NewInEFCore7/StringAggregateFunctionsSample.cs new file mode 100644 index 0000000000..6e5012a981 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/StringAggregateFunctionsSample.cs @@ -0,0 +1,76 @@ +namespace NewInEfCore7; + +public static class StringAggregateFunctionsSample +{ + public static async Task Translate_string_Concat_and_string_Join() + { + PrintSampleName(); + + await using (var context = new BlogsContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + context.ChangeTracker.Clear(); + context.LoggingEnabled = true; + + #region Join + + var query = context.Posts + .GroupBy(post => post.Author) + .Select(grouping => new { Author = grouping.Key, Books = string.Join("|", grouping.Select(post => post.Title)) }); + + #endregion + + await foreach (var author in query.AsAsyncEnumerable()) + { + Console.WriteLine($"{author}"); + } + + Console.WriteLine(); + } + + await using (var context = new BlogsContext { LoggingEnabled = true }) + { + #region ConcatAndJoin + + var query = context.Posts + .GroupBy(post => post.Author!.Name) + .Select( + grouping => + new + { + PostAuthor = grouping.Key, + Blogs = string.Concat( + grouping + .Select(post => post.Blog.Name) + .Distinct() + .Select(postName => "'" + postName + "' ")), + ContentSummaries = string.Join( + " | ", + grouping + .Where(post => post.Content.Length >= 10) + .Select(post => "'" + post.Content.Substring(0, 10) + "' ")) + }); + + #endregion + + await foreach (var author in query.AsAsyncEnumerable()) + { + Console.WriteLine($"{author}"); + } + + Console.WriteLine(); + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class BlogsContext : ModelBuildingBlogsContextBase + { + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/UngroupedColumnsQuerySample.cs b/samples/core/Miscellaneous/NewInEFCore7/UngroupedColumnsQuerySample.cs new file mode 100644 index 0000000000..f5996fdb23 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/UngroupedColumnsQuerySample.cs @@ -0,0 +1,102 @@ +namespace NewInEfCore7; + +public static class UngroupedColumnsQuerySample +{ + public static Task Subqueries_dont_reference_ungrouped_columns_from_outer_query_SqlServer() + { + PrintSampleName(); + return QueryTest(); + } + + private static async Task QueryTest() + where TContext : InvoiceContext, new() + { + await using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + await context.AddRangeAsync( + new Invoice { Amount = 10.20m, History = new DateTime(1973, 9, 3) }, + new Invoice { Amount = 11.20m, History = new DateTime(1973, 9, 13) }, + new Invoice { Amount = 12.20m, History = new DateTime(1973, 10, 3) }, + new Invoice { Amount = 13.20m, History = new DateTime(1973, 11, 3) }); + + await context.AddRangeAsync( + new Payment { Amount = 0.20m, History = new DateTime(1973, 9, 5) }, + new Payment { Amount = 1.20m, History = new DateTime(1973, 10, 13) }, + new Payment { Amount = 2.20m, History = new DateTime(1973, 10, 5) }, + new Payment { Amount = 3.20m, History = new DateTime(1973, 11, 7) }); + + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + #region UngroupedColumns + var query = from s in (from i in context.Invoices + group i by i.History.Month + into g + select new { Month = g.Key, Total = g.Sum(p => p.Amount), }) + select new + { + s.Month, s.Total, Payment = context.Payments.Where(p => p.History.Month == s.Month).Sum(p => p.Amount) + }; + #endregion + + await foreach (var group in query.AsAsyncEnumerable()) + { + Console.WriteLine($"Month: {group.Month}; Total = {group.Total}; Payment = {group.Payment}"); + } + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class InvoiceContext : DbContext + { + public DbSet Invoices => Set(); + public DbSet Payments => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } + + public class InvoiceContextSqlServer : InvoiceContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Invoices")); + } + + public class InvoiceContextSqlite : InvoiceContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlite("Data Source = invoices.db")); + } + + public class Invoice + { + public int Id { get; set; } + public DateTime History { get; set; } + + [Precision(18, 2)] + public decimal Amount { get; set; } + } + + public class Payment + { + public int Id { get; set; } + public DateTime History { get; set; } + + [Precision(18, 2)] + public decimal Amount { get; set; } + } +}