From a0bbae1b498f547c783d82a2dcadfa8755640583 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 1 Oct 2022 22:16:45 +0100 Subject: [PATCH] What's new: SaveChanges --- .../core/what-is-new/ef-core-7.0/whatsnew.md | 240 ++++++++++++++++++ .../NewInEFCore7/GroupByEntityTypeSample.cs | 2 +- .../GroupByFinalOperatorSample.cs | 2 +- .../GroupJoinFinalOperatorSample.cs | 2 +- .../Miscellaneous/NewInEFCore7/Program.cs | 5 +- .../NewInEFCore7/ReadOnlySetQuerySample.cs | 2 +- .../SaveChangesPerformanceSample.cs | 192 ++++++++++++++ .../SpatialAggregateFunctionsSample.cs | 2 +- 8 files changed, 440 insertions(+), 7 deletions(-) create mode 100644 samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs 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 6e3a2447e2..8e5b9ab3e6 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 @@ -3076,3 +3076,243 @@ LEFT JOIN ( ) AS [t] ON [b].[Id] = [t].[BlogId] ORDER BY [b].[Id], [t].[Title] ``` + +## Faster SaveChanges + +In EF7, the performance of and has been significantly improved. In some scenarios, saving changes is now four times faster than with EF Core 6.0! + +Most of these improvements come from: + +- Generating faster SQL +- Performing fewer roundtrips to the database + +Some examples of these improvements are shown below. + +> [!NOTE] +> See [Announcing Entity Framework Core 7 Preview 6: Performance Edition](https://devblogs.microsoft.com/dotnet/announcing-ef-core-7-preview6-performance-optimizations/) on the .NET Blog for an in-depth discussion of these changes. + +> [!TIP] +> The code shown here comes from [SaveChangesPerformanceSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs). + +### Unneeded transactions are eliminated + +All modern relational databases guarantee transactionality for (most) single SQL statements. That is, the statement will never be only partially completed, even if an error occurs. EF7 avoids starting an explicit transaction in these cases. + +For example, looking at the logging for the following call to `SaveChanges`: + + +[!code-csharp[SimpleInsert](../../../../samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs?name=SimpleInsert)] + +Shows that in EF Core 6.0, the `INSERT` command is wrapped by commands to begin and and then commit a transaction: + +```output +dbug: 9/29/2022 11:43:09.196 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) + Began transaction with isolation level 'ReadCommitted'. +info: 9/29/2022 11:43:09.265 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (27ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30'] + SET NOCOUNT ON; + INSERT INTO [Blogs] ([Name]) + VALUES (@p0); + SELECT [Id] + FROM [Blogs] + WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); +dbug: 9/29/2022 11:43:09.297 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) + Committed transaction. +``` + +EF7 detects that the transaction is not needed here and so removes these calls: + +```output +info: 9/29/2022 11:42:34.776 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (25ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30'] + SET IMPLICIT_TRANSACTIONS OFF; + SET NOCOUNT ON; + INSERT INTO [Blogs] ([Name]) + OUTPUT INSERTED.[Id] + VALUES (@p0); +``` + +This removes two database roundtrips, which can make a huge difference to overall performance, especially when the latency of calls to the database is high. In typical production systems, the database is not co-located on the same machine as the application. This means latency is often relatively high, making this optimization particularly effective in real-world production systems. + +### Improved SQL for simple Identity insert + +The case above inserts a single row with an `IDENTITY` key column and no other database-generated values. EF7 simplifies the SQL in this case by using `OUTPUT INSERTED`. While this simplification is not valid for many other cases, it is still significant because this kind of single-row insert is very common in many applications. + +### Inserting multiple rows + +In EF Core 6.0, the default approach for inserting multiple rows was driven by limitations in SQL Server support for tables with triggers. We wanted to make sure that the default experience worked even for the minority of users with triggers in their tables. This meant that we could not use a simple `OUTPUT` clause, because, on SQL Server, this [doesn't work with triggers](/sql/t-sql/queries/output-clause-transact-sql#triggers). Instead, when inserting multiple entities, EF Core 6.0 generated some fairly convoluted SQL. For example, this call to `SaveChanges`: + + +[!code-csharp[MultipleInsert](../../../../samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs?name=MultipleInsert)] + +Results in the following actions when run against SQL Server with EF Core 6.0: + +```output +dbug: 9/30/2022 17:19:51.919 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) + Began transaction with isolation level 'ReadCommitted'. +info: 9/30/2022 17:19:51.993 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (27ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30'] + SET NOCOUNT ON; + DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]); + MERGE [Blogs] USING ( + VALUES (@p0, 0), + (@p1, 1), + (@p2, 2), + (@p3, 3)) AS i ([Name], _Position) ON 1=0 + WHEN NOT MATCHED THEN + INSERT ([Name]) + VALUES (i.[Name]) + OUTPUT INSERTED.[Id], i._Position + INTO @inserted0; + + SELECT [i].[Id] FROM @inserted0 i + ORDER BY [i].[_Position]; +dbug: 9/30/2022 17:19:52.023 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) + Committed transaction. +``` + +> [!IMPORTANT] +> Even though this is complicated, batching multiple inserts like this is still significantly faster than sending a single command for each insert. + +In EF7, you can still get this SQL if your tables contain triggers, but for the common case we now generate much more efficient commands: + +```output +info: 9/30/2022 17:40:37.612 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (4ms) [Parameters=[@p0='Foo0' (Nullable = false) (Size = 4000), @p1='Foo1' (Nullable = false) (Size = 4000), @p2='Foo2' (Nullable = false) (Size = 4000), @p3='Foo3' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30'] + SET IMPLICIT_TRANSACTIONS OFF; + SET NOCOUNT ON; + MERGE [Blogs] USING ( + VALUES (@p0, 0), + (@p1, 1), + (@p2, 2), + (@p3, 3)) AS i ([Name], _Position) ON 1=0 + WHEN NOT MATCHED THEN + INSERT ([Name]) + VALUES (i.[Name]) + OUTPUT INSERTED.[Id], i._Position; +``` + +The transaction is gone, as in the single insert case, because `MERGE` is a single statement protected by an implicit transaction. Also, the temporary table is gone and the OUTPUT clause now sends the generated IDs directly back to the client. This can be **four times faster than on EF Core 6.0**, depending on environmental factors such as latency between the application and database. + +### Triggers + +If the table has triggers, then the call to `SaveChanges` in the code above will throw an exception: + +> Unhandled exception. Microsoft.EntityFrameworkCore.DbUpdateException:
+> Could not save changes because the target table has database triggers. Please configure your entity type accordingly, see `https://aka.ms/efcore-docs-sqlserver-save-changes-and-triggers` for more information.
+> ---> Microsoft.Data.SqlClient.SqlException (0x80131904):
+> The target table 'BlogsWithTriggers' of the DML statement cannot have any enabled triggers if the statement contains an OUTPUT clause without INTO clause. + +The following code can be used to inform EF Core that the table has a trigger: + + +[!code-csharp[HasTrigger](../../../../samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs?name=HasTrigger)] + +EF7 will then revert to the EF Core 6.0 SQL when sending insert and update commands for this table. + +For more information, including a convention to automatically configure all mapped tables with triggers, see [SQL Server tables with triggers now require special EF Core configuration](xref:core/what-is-new/ef-core-7.0/breaking-changes#sqlserver-tables-with-triggers) in the EF7 breaking changes documentation. + +### Fewer roundtrips for inserting graphs + +Consider inserting a graph of entities containing a new principal entity and also new dependent entities with foreign keys that reference the new principal. For example: + + +[!code-csharp[InsertGraph](../../../../samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs?name=InsertGraph)] + +If the principal's primary key is generated by the database, then the value to set for the foreign key in the dependent is not known until the principal has been inserted. EF Core generates two roundtrips for this--one to insert the principal and get back the new primary key, and a second to insert the dependents with the foreign key value set. And since there are two statements for this, a transaction is needed, meaning there are in total four roundtrips: + +```output +dbug: 10/1/2022 13:12:02.517 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) + Began transaction with isolation level 'ReadCommitted'. +info: 10/1/2022 13:12:02.517 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (0ms) [Parameters=[@p0='MyBlog' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30'] + SET IMPLICIT_TRANSACTIONS OFF; + SET NOCOUNT ON; + INSERT INTO [Blogs] ([Name]) + OUTPUT INSERTED.[Id] + VALUES (@p0); +info: 10/1/2022 13:12:02.529 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (5ms) [Parameters=[@p1='6', @p2='My first post' (Nullable = false) (Size = 4000), @p3='6', @p4='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30'] + SET IMPLICIT_TRANSACTIONS OFF; + SET NOCOUNT ON; + MERGE [Post] USING ( + VALUES (@p1, @p2, 0), + (@p3, @p4, 1)) AS i ([BlogId], [Title], _Position) ON 1=0 + WHEN NOT MATCHED THEN + INSERT ([BlogId], [Title]) + VALUES (i.[BlogId], i.[Title]) + OUTPUT INSERTED.[Id], i._Position; +dbug: 10/1/2022 13:12:02.531 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) + Committed transaction. +``` + +However, in some cases the primary key value is known before the principal is inserted. This includes: + +- Key values that are not automatically generated +- Key values that are generated on the client, such as keys +- Key values that are generated on the server in batches, such as when using a hi-lo value generator + +In EF7, these cases are now optimized into a single round-trip. For example, in the case above on SQL Server, the `Blog.Id` primary key can be configured to use the hi-lo generation strategy: + + +[!code-csharp[UseHiLo](../../../../samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs?name=UseHiLo)] + +The `SaveChanges` call from above is now optimized to a single roundtrip for the inserts. + +```output +dbug: 10/1/2022 21:51:55.805 RelationalEventId.TransactionStarted[20200] (Microsoft.EntityFrameworkCore.Database.Transaction) + Began transaction with isolation level 'ReadCommitted'. +info: 10/1/2022 21:51:55.806 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (0ms) [Parameters=[@p0='9', @p1='MyBlog' (Nullable = false) (Size = 4000), @p2='10', @p3='9', @p4='My first post' (Nullable = false) (Size = 4000), @p5='11', @p6='9', @p7='My second post' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='30'] + SET NOCOUNT ON; + INSERT INTO [Blogs] ([Id], [Name]) + VALUES (@p0, @p1); + INSERT INTO [Posts] ([Id], [BlogId], [Title]) + VALUES (@p2, @p3, @p4), + (@p5, @p6, @p7); +dbug: 10/1/2022 21:51:55.807 RelationalEventId.TransactionCommitted[20202] (Microsoft.EntityFrameworkCore.Database.Transaction) + Committed transaction. +``` + +Notice that a transaction is still needed here. This is because inserts are being made into two separate tables. + +EF7 also uses a single batch in other cases where EF Core 6.0 would create more than one. For example, when deleting and inserting rows into the same table. + +### The value of SaveChanges + +As some of the examples here show, saving results to the database can be a complex business. This is where using something like EF Core really shows its value. EF Core: + +- Batches multiple insert, update, and delete commands together to reduce roundtrips +- Figures out if an explicit transaction is needed or not +- Determines what order to insert, update, and delete entities so that database constraints are not violated +- Ensures database generated values are returned efficiently and propagated back into entities +- Automatically sets foreign key values using the values generated for primary keys +- Detect concurrency conflicts + +In addition, different database systems require different SQL for many of these cases. The EF Core database provider works with EF Core to ensure correct and efficient commands are sent for each case. diff --git a/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs b/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs index cbd8fd9a43..26b94808db 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/GroupByEntityTypeSample.cs @@ -11,7 +11,7 @@ public static Task GroupBy_entity_type_SqlServer() public static Task GroupBy_entity_type_Sqlite() { PrintSampleName(); - return QueryTest(); + return QueryTest(); } public static Task GroupBy_entity_type_InMemory() diff --git a/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs b/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs index 3715088fab..9afd5c7016 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/GroupByFinalOperatorSample.cs @@ -11,7 +11,7 @@ public static Task GroupBy_final_operator_SqlServer() public static Task GroupBy_final_operator_Sqlite() { PrintSampleName(); - return QueryTest(); + return QueryTest(); } public static Task GroupBy_final_operator_InMemory() diff --git a/samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs b/samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs index 6b1068169b..e81526c6a9 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/GroupJoinFinalOperatorSample.cs @@ -11,7 +11,7 @@ public static Task GroupJoin_final_operator_SqlServer() public static Task GroupJoin_final_operator_Sqlite() { PrintSampleName(); - return QueryTest(); + return QueryTest(); } public static Task GroupJoin_final_operator_InMemory() diff --git a/samples/core/Miscellaneous/NewInEFCore7/Program.cs b/samples/core/Miscellaneous/NewInEFCore7/Program.cs index d99b37a514..164d63b76e 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/Program.cs @@ -53,7 +53,6 @@ public static async Task Main() 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(); @@ -63,11 +62,13 @@ public static async Task Main() 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(); + + await SaveChangesPerformanceSample.SaveChanges_SQL_generation_samples_SqlServer(); + await SaveChangesPerformanceSample.SaveChanges_SQL_generation_samples_Sqlite(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs b/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs index d4d57439fd..1fba3603c7 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/ReadOnlySetQuerySample.cs @@ -13,7 +13,7 @@ public static Task Use_Contains_with_IReadOnlySet_SqlServer() public static Task Use_Contains_with_IReadOnlySet_Sqlite() { PrintSampleName(); - return QueryTest(); + return QueryTest(); } public static Task Use_Contains_with_IReadOnlySet_InMemory() diff --git a/samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs b/samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs new file mode 100644 index 0000000000..e9c5a63991 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/SaveChangesPerformanceSample.cs @@ -0,0 +1,192 @@ +using Microsoft.EntityFrameworkCore.Diagnostics; + +namespace NewInEfCore7; + +public static class SaveChangesPerformanceSample +{ + public static Task SaveChanges_SQL_generation_samples_SqlServer() + { + PrintSampleName(); + return SaveChangesTest(); + } + + public static Task SaveChanges_SQL_generation_samples_Sqlite() + { + PrintSampleName(); + return SaveChangesTest(); + } + + private static async Task SaveChangesTest() + where TContext : PerfContext, new() + { + await using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + if (context.Database.IsSqlServer()) + { + context.Database.ExecuteSqlRaw( + @" +CREATE TRIGGER TRG_InsertUpdateBlog +ON BlogsWithTriggers +AFTER INSERT, UPDATE AS +BEGIN + IF @@ROWCOUNT = 0 + return + SET nocount on; + + UPDATE BlogsWithTriggers set StoreUpdated = StoreUpdated + 1 + WHERE Id IN(SELECT INSERTED.Id FROM INSERTED); +END"); + } + } + + await using (var context = new TContext()) + { + #region SimpleInsert + await context.AddAsync(new Blog { Name = "MyBlog" }); + await context.SaveChangesAsync(); + #endregion + } + + await using (var context = new TContext()) + { + #region MultipleInsert + for (var i = 0; i < 4; i++) + { + await context.AddAsync(new Blog { Name = "Foo" + i }); + } + await context.SaveChangesAsync(); + #endregion + } + + await using (var context = new TContext()) + { + await context.AddAsync(new BlogWithTrigger { Name = "MyBlog" }); + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + for (var i = 0; i < 4; i++) + { + await context.AddAsync(new BlogWithTrigger { Name = "Foo" + i }); + } + await context.SaveChangesAsync(); + } + + await using (var context = new TContext()) + { + #region InsertGraph + await context.AddAsync(new Blog + { + Name = "MyBlog", + Posts = + { + new() { Title = "My first post" }, + new() { Title = "My second post" } + } + }); + await context.SaveChangesAsync(); + #endregion + } + + await using (var context = new TContext()) + { + #region InsertGraph + await context.AddAsync(new Blog + { + Name = "MyBlog", + Posts = + { + new() { Title = "My first post" }, + new() { Title = "My second post" } + } + }); + await context.SaveChangesAsync(); + #endregion + } + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public abstract class PerfContext : DbContext + { + public DbSet Blogs => Set(); + public DbSet Posts => Set(); + public DbSet BlogsWithTriggers => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .Property(e => e.StoreUpdated) + .HasDefaultValue(0) + .ValueGeneratedOnAddOrUpdate(); + + if (Database.IsSqlServer()) + { + #region HasTrigger + modelBuilder + .Entity() + .ToTable(tb => tb.HasTrigger("TRG_InsertUpdateBlog")); + #endregion + + #region UseHiLo + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + modelBuilder.Entity().Property(e => e.Id).UseHiLo(); + #endregion + } + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .LogTo( + Console.WriteLine, + new[] + { + RelationalEventId.CommandExecuted, RelationalEventId.TransactionStarted, RelationalEventId.TransactionCommitted + }) + .EnableSensitiveDataLogging(); + } + + public class PerfContextSqlServer : PerfContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=SaveChangesPerf")); + } + + public class PerfContextSqlite : PerfContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => base.OnConfiguring( + optionsBuilder.UseSqlite("Data Source = savechangesperf.db")); + } + + public class Blog + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public List Posts { get; } = new(); + } + + public class Post + { + public int Id { get; set; } + public string Title { get; set; } = default!; + public Blog Blog { get; set; } = default!; + } + + public class BlogWithTrigger + { + public int Id { get; set; } + public string Name { get; set; } = default!; + public virtual int StoreUpdated { get; set; } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs b/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs index 9ae7b846c5..3cadd0f408 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/SpatialAggregateFunctionsSample.cs @@ -15,7 +15,7 @@ public static Task Translate_spatial_aggregate_functions_SqlServer() public static Task Translate_spatial_aggregate_functions_Sqlite() { PrintSampleName(); - return QueryTest(); + return QueryTest(); } public static Task Translate_spatial_aggregate_functions_InMemory()