diff --git a/entity-framework/core/querying/sql-queries.md b/entity-framework/core/querying/sql-queries.md index f35d5d7ba5..8c9bbb4b5e 100644 --- a/entity-framework/core/querying/sql-queries.md +++ b/entity-framework/core/querying/sql-queries.md @@ -43,7 +43,7 @@ The following example passes a single parameter to a stored procedure by includi [!code-csharp[Main](../../../samples/core/Querying/SqlQueries/Program.cs#FromSqlStoredProcedureParameter)] -While this syntax may look like regular C# [string interpolation](https://learn.microsoft.com/dotnet/csharp/language-reference/tokens/interpolated), the supplied value is wrapped in a `DbParameter` and the generated parameter name inserted where the `{0}` placeholder was specified. This makes > The safe from SQL injection attacks, and sends the value efficiently and correctly to the database. +While this syntax may look like regular C# [string interpolation](/dotnet/csharp/language-reference/tokens/interpolated), the supplied value is wrapped in a `DbParameter` and the generated parameter name inserted where the `{0}` placeholder was specified. This makes > The safe from SQL injection attacks, and sends the value efficiently and correctly to the database. When executing stored procedures, it can be useful to use named parameters in the SQL query string, especially when the stored procedure has optional parameters: 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 226684e608..60eb088178 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/27/2022 +ms.date: 10/10/2022 uid: core/what-is-new/ef-core-7 --- @@ -3314,6 +3314,28 @@ LEFT JOIN ( ORDER BY [b].[Id], [t].[Title] ``` +### Cosmos translation for `Regex.IsMatch` + +> [!TIP] +> The code shown here comes from [CosmosQueriesSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/CosmosQueriesSample.cs). + +EF7 supports using in LINQ queries against Azure Cosmos DB. For example: + + +[!code-csharp[RegexIsMatch](../../../../samples/core/Miscellaneous/NewInEFCore7/CosmosQueriesSample.cs?name=RegexIsMatch)] + +Translates to the following SQL: + +```sql +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Triangle") AND RegexMatch(c["Name"], "[a-z]t[a-z]", "i")) +``` + ## DbContext API and behavior enhancements EF7 contains a variety of small improvements to and related classes. @@ -3527,3 +3549,783 @@ Notice: - The iterator stops traversing from a given node when the callback delegate returns `false`. This example keeps track of visited entities and returns `false` when the entity has already been visited. This prevents infinite loops resulting from cycles in the graph. - The `EntityEntryGraphNode` object allows state to be passed around without capturing it into the delegate. - For every node visited other than the first, the node it was discovered from and the navigation is was discovered via are passed to the callback. + +## Model building enhancements + +EF7 contains a variety of small improvements in model building. + +> [!TIP] +> The code for samples in this section comes from [ModelBuildingSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs). + +### Indexes can be ascending or descending + +By default, EF Core creates ascending indexes. EF7 also supports creation of descending indexes. For example: + +```csharp + modelBuilder + .Entity() + .HasIndex(post => post.Title) + .IsDescending(); +``` + +Or, using the `Index` mapping attribute: + +```csharp + [Index(nameof(Title), AllDescending = true)] + public class Post + { + public int Id { get; set; } + + [MaxLength(64)] + public string? Title { get; set; } + } +``` + +Multiple indexes can be created on the same ordered set of columns by giving the indexes names. This allows creating both ascending and descending indexes on the same columns. For example: + + +[!code-csharp[TwoIndexes](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=TwoIndexes)] + +Or, using mapping attributes: + + +[!code-csharp[TwoIndexesByAttribute](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=TwoIndexesByAttribute)] + +This generates the following SQL on SQL Server: + +```sql +CREATE INDEX [AscendingIndex] ON [Posts] ([Title]); +CREATE INDEX [DescendingIndex] ON [Posts] ([Title] DESC); +``` + +Indexes over multiple columns can also have different ordering defined for each column. For example: + + +[!code-csharp[CompositeIndex](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=CompositeIndex)] + +Or, using a mapping attribute: + + +[!code-csharp[CompositeIndexByAttribute](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=CompositeIndexByAttribute)] + +This results in the following SQL when using SQL Server: + +```sql +CREATE INDEX [IX_Blogs_Name_Owner] ON [Blogs] ([Name], [Owner] DESC); +``` + +### Mapping attribute for composite keys + +EF7 introduces a new mapping attribute (aka "data annotation") for specifying the primary key property or properties of any entity type. Unlike , [PrimaryKeyAttribute](https://github.com/dotnet/efcore/blob/main/src/EFCore.Abstractions/PrimaryKeyAttribute.cs) is placed on the entity type class rather than on the key property. For example: + +```csharp + [PrimaryKey(nameof(PostKey))] + public class Post + { + public int PostKey { get; set; } + } +``` + +This makes it a natural fit for defining composite keys: + + +[!code-csharp[CompositePrimaryKey](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=CompositePrimaryKey)] + +Defining the index on the class also means it can be used to specify private properties or fields as keys, even though these would usually be ignored when building the EF model. For example: + +```csharp +[PrimaryKey(nameof(_id))] +public class Tag +{ + private readonly int _id; +} +``` + +### `DeleteBehavior` mapping attribute + +EF7 introduces a mapping attribute (aka "data annotation") to specify the for a relationship. For example, [required relationships](xref:core/modeling/relationships) are created with by default. This can be changed to by default using [DeleteBehaviorAttribute](https://github.com/dotnet/efcore/blob/main/src/EFCore.Abstractions/DeleteBehaviorAttribute.cs): + +```csharp + public class Post + { + public int Id { get; set; } + public string? Title { get; set; } + + [DeleteBehavior(DeleteBehavior.NoAction)] + public Blog Blog { get; set; } = null!; + } +``` + +This will disable cascade deletes for the Blog-Posts relationship. + +### Properties mapped to different column names + +Some mapping patterns result in the same CLR property being mapped to a column in each of multiple different tables. EF7 allows these columns to have different names. For example, consider a simple inheritance hierarchy: + + +[!code-csharp[Animals](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=Animals)] + +With the TPT [inheritance mapping strategy](xref:core/modeling/inheritance), these types will be mapped to three tables. However, the primary key column in each table may have a different name. For example: + +```sql +CREATE TABLE [Animals] ( + [Id] int NOT NULL IDENTITY, + [Breed] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Animals] PRIMARY KEY ([Id]) +); + +CREATE TABLE [Cats] ( + [CatId] int NOT NULL, + [EducationalLevel] nvarchar(max) NULL, + CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]), + CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE +); + +CREATE TABLE [Dogs] ( + [DogId] int NOT NULL, + [FavoriteToy] nvarchar(max) NULL, + CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]), + CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE +); +``` + +EF7 allows this mapping to be configured using a nested table builder: + + +[!code-csharp[AnimalsTpt](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=AnimalsTpt)] + +With the TPC inheritance mapping, the `Breed` property can also be mapped to different column names in different tables. For example, consider the following TPC tables: + +```sql +CREATE TABLE [Cats] ( + [CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]), + [CatBreed] nvarchar(max) NOT NULL, + [EducationalLevel] nvarchar(max) NULL, + CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]) +); + +CREATE TABLE [Dogs] ( + [DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]), + [DogBreed] nvarchar(max) NOT NULL, + [FavoriteToy] nvarchar(max) NULL, + CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]) +); +``` + +EF7 supports this table mapping: + + +[!code-csharp[AnimalsTpc](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=AnimalsTpc)] + +### Unidirectional many-to-many relationships + +EF7 supports [many-to-many relationships](xref:core/modeling/relationships#many-to-many) where one side or the other does not have a navigation property. For example, consider `Post` and `Tag` types: + +```csharp +public class Post +{ + public int Id { get; set; } + public string? Title { get; set; } + public Blog Blog { get; set; } = null!; + public List Tags { get; } = new(); +} +``` + + +[!code-csharp[Tag](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=Tag)] + +Notice that the `Post` type has a navigation property for a list of tags, but the `Tag` type does not have a navigation property for posts. In EF7, this can still be configured as a many-to-many relationship, allowing the same `Tag` object to be used for many different posts. For example: + + +[!code-csharp[ManyToMany](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=ManyToMany)] + +This results in mapping to the appropriate join table: + +```sql +CREATE TABLE [Tags] ( + [Id] int NOT NULL IDENTITY, + [TagName] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]) +); + +CREATE TABLE [Posts] ( + [Id] int NOT NULL IDENTITY, + [Title] nvarchar(64) NULL, + [BlogId] int NOT NULL, + CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]), + CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]) +); + +CREATE TABLE [PostTag] ( + [PostId] int NOT NULL, + [TagsId] int NOT NULL, + CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostId], [TagsId]), + CONSTRAINT [FK_PostTag_Posts_PostId] FOREIGN KEY ([PostId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE, + CONSTRAINT [FK_PostTag_Tags_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE +); +``` + +And the relationship can be used as a many-to-many in the normal way. For example, inserting some posts which share various tags from a common set: + + +[!code-csharp[InsertPostsAndTags](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=InsertPostsAndTags)] + +### Entity splitting + +Entity splitting maps a single entity type to multiple tables. For example, consider a database with three tables that hold customer data: + +- A `Customers` table for customer information +- A `PhoneNumbers` table for the customer's phone number +- A `Addresses` table for the customer's address + +Here are definitions for these tables in SQL Server: + +```sql +CREATE TABLE [Customers] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Customers] PRIMARY KEY ([Id]) +); + +CREATE TABLE [PhoneNumbers] ( + [CustomerId] int NOT NULL, + [PhoneNumber] nvarchar(max) NULL, + CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]), + CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE +); + +CREATE TABLE [Addresses] ( + [CustomerId] int NOT NULL, + [Street] nvarchar(max) NOT NULL, + [City] nvarchar(max) NOT NULL, + [PostCode] nvarchar(max) NULL, + [Country] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]), + CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE +); +``` + +Each of these tables would typically be mapped to their own entity type, with relationships between the types. However, if all three tables are always used together, then it can be more convenient to map them all to a single entity type. For example: + + +[!code-csharp[CombinedCustomer](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=CombinedCustomer)] + +This is achieved in EF7 by calling `SplitToTable` for each split in the entity type. For example, the following code splits the `Customer` entity type to the `Customers`, `PhoneNumbers`, and `Addresses` tables shown above: + + +[!code-csharp[TableSplitting](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=TableSplitting)] + +Notice also that, if necessary, different primary key column names can be specified for each of the tables. + +### SQL Server UTF-8 strings + +SQL Server Unicode strings as represented by the [`nchar` and `nvarchar` data types](/sql/t-sql/data-types/nchar-and-nvarchar-transact-sql) are stored as [UTF-16](https://en.wikipedia.org/wiki/UTF-16). In addition, the [`char` and `varchar` data types](/sql/t-sql/data-types/char-and-varchar-transact-sql) are used to store non-Unicode strings with support for various [character sets](https://en.wikipedia.org/wiki/Extended_ASCII). + +Starting with SQL Server 2019, the `char` and `varchar` data types can be used to instead store Unicode strings with [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding. The is achieved by setting one of the [UTF-8 collations](/sql/relational-databases/collations/collation-and-unicode-support). For example, the following code configures a variable length SQL Server UTF-8 string for the `CommentText` column: + + +[!code-csharp[Utf8](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=Utf8)] + +This configuration generates the following SQL Server column definition: + +```sql +CREATE TABLE [Comment] ( + [PostId] int NOT NULL, + [CommentId] int NOT NULL, + [CommentText] varchar(max) COLLATE LATIN1_GENERAL_100_CI_AS_SC_UTF8 NOT NULL, + CONSTRAINT [PK_Comment] PRIMARY KEY ([PostId], [CommentId]) +); + ``` + +### Temporal tables support owned entities + +EF Core [SQL Server temporal tables](xref:core/what-is-new/ef-core-6.0/whatsnew#sql-server-temporal-tables) mapping has been enhanced in EF7 to support [table sharing](xref:core/modeling/table-splitting). Most notably, the default mapping for [owned single entities](xref:core/modeling/owned-entities) uses table sharing. + +For example, consider an owner entity type `Employee` and its owned entity type `EmployeeInfo`: + + +[!code-csharp[EmployeeAndEmployeeInfo](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=EmployeeAndEmployeeInfo)] + +If these types are mapped to the same table, then in EF7 that table can be made a temporal table: + + + +[!code-csharp[OwnedTemporalTable](../../../../samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs?name=OwnedTemporalTable)] + +> [!NOTE] +> Making this configuration easier is tracked by [Issue #29303](https://github.com/dotnet/efcore/issues/29303). Vote for this issue if it's something you would like to see implemented. + +## Improved value generation + +EF7 includes two significant improvements to the automatic generation of values for key properties. + +> [!TIP] +> The code for samples in this section comes from [ValueGenerationSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs). + +### Value generation for DDD guarded types + +In domain-driven design (DDD), "guarded keys" can improve the type safety of key properties. This is achieved by wrapping the key type in another type which is specific to the use of the key. For example, the following code defines a `ProductId` type for product keys, and a `CategoryId` type for category keys. + + +[!code-csharp[GuardedKeys](../../../../samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs?name=GuardedKeys)] + +These are then used in `Product` and `Category` entity types: + + +[!code-csharp[ProductAndCategory](../../../../samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs?name=ProductAndCategory)] + +This makes it impossible to accidentally assign the ID for a category to a product, or vice versa. + +> [!WARNING] +> As with many DDD concepts, this improved type safety comes at the expense of additional code complexity. It is worth considering whether, for example, assigning a product ID to a category is something that is ever likely to happen. Keeping things simple may be overall more beneficial to the codebase. + +The guarded key types shown here both wrap `int` key values, which means integer values will be used in the mapped database tables. This is achieved by defining [value converters](xref:core/modeling/value-conversions) for the types: + + +[!code-csharp[KeyConverters](../../../../samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs?name=KeyConverters)] + +> [!NOTE] +> The code here uses `struct` types. This means they have appropriate value-type semantics for use as keys. If `class` types are used instead, then they will need to either override equality semantics or also specify a [value comparer](xref:core/modeling/value-comparers). + +In EF7, key types based on value converters can use automatically generated key values so long as the underlying type supports this. This is configured in the normal way using `ValueGeneratedOnAdd`: + + +[!code-csharp[ValueGeneratedOnAdd](../../../../samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs?name=ValueGeneratedOnAdd)] + +By default, this results in `IDENTITY` columns when used with SQL Server: + +```sql +CREATE TABLE [Categories] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Categories] PRIMARY KEY ([Id])); + +CREATE TABLE [Products] ( + [Id] int NOT NULL IDENTITY, + [Name] nvarchar(max) NOT NULL, + [CategoryId] int NOT NULL, + CONSTRAINT [PK_Products] PRIMARY KEY ([Id]), + CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE); +``` + +Which are used in the normal way to generate key values when inserting entities: + +```sql +MERGE [Categories] USING ( +VALUES (@p0, 0), +(@p1, 1)) AS i ([Name], _Position) ON 1=0 +WHEN NOT MATCHED THEN +INSERT ([Name]) +VALUES (i.[Name]) +OUTPUT INSERTED.[Id], i._Position; +``` + +### Sequence-based key generation for SQL Server + +EF Core supports key value generation using [SQL Server `IDENTITY` columns](/dotnet/api/microsoft.entityframeworkcore.sqlservermodelbuilderextensions.useidentitycolumns), or [a Hi-Lo pattern](/dotnet/api/microsoft.entityframeworkcore.sqlservermodelbuilderextensions.usehilo) based on blocks of keys generated by a database sequence. EF7 introduces support for a database sequence attached to the key's column default constraint. In its simplest form, this just requires telling EF Core to use a sequence for the key property: + +```csharp +modelBuilder.Entity().Property(product => product.Id).UseSequence(); +``` + +This results in a sequence being defined in the database: + +```sql +CREATE SEQUENCE [ProductSequence] START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE; +``` + +Which is then used in the key column default constraint: + +```sql +CREATE TABLE [Products] ( + [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [ProductSequence]), + [Name] nvarchar(max) NOT NULL, + [CategoryId] int NOT NULL, + CONSTRAINT [PK_Products] PRIMARY KEY ([Id]), + CONSTRAINT [FK_Products_Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [Categories] ([Id]) ON DELETE CASCADE); +``` + +> [!NOTE] +> This form of key generation is used by default for generated keys in entity type hierarchies using the [TPC mapping strategy](xref:core/modeling/inheritance). + +If desired, the sequence can be given a different name and schema. For example: + + +[!code-csharp[Sequence](../../../../samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs?name=Sequence)] + +Further configuration of the sequence is formed by configuring it explicitly in the model. For example: + + +[!code-csharp[ConfigureSequence](../../../../samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs?name=ConfigureSequence)] + +## Migrations tooling improvements + +EF7 includes two significant improvements when using the [EF Core Migrations command-line tools](xref:core/managing-schemas/migrations/index). + +### UseSqlServer etc. accept null + +It is very common to read a connection string from a configuration file and then pass that connection string to `UseSqlServer`, `UseSqlite`, or the equivalent method for another provider. For example: + +```csharp + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("BloggingDatabase"))); +``` + +It is also common to pass a connection string [applying migrations](xref:core/managing-schemas/migrations/applying). For example: + +```text +dotnet ef database update --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb" +``` + +Or when using a [Migrations bundle](xref:core/managing-schemas/migrations/applying#bundles). + +```dotnetcli +./bundle.exe --connection "Server=(localdb)\mssqllocaldb;Database=MyAppDb" +``` + +In this case, even though the connection string read from configuration will not be used, the application startup code will still attempt to read it from configuration and pass it to `UseSqlServer`. If the configuration is not available, then this results in passing null to `UseSqlServer`. In EF7, this is allowed, as long as the connection string is ultimately set later, such as by passing `--connection` to the command-line tool. + +> [!NOTE] +> This change has been made for `UseSqlServer` and `UseSqlite`. For other providers, contact the provider maintainer to make an equivalent change if it has not yet been done for that provider. + +### Detect when tools are running + +EF Core runs application code when the [`dotnet-ef`](xref:core/cli/dotnet) or [PowerShell](xref:core/cli/powershell) commands are being used. Sometimes it may be necessary to detect this situation to prevent inappropriate code being executed at design-time. For example, code that automatically applies migrations at startup should probably not do this at design-time. In EF7, this can be detected using the `EF.IsDesignTime` flag: + +```csharp +if (!EF.IsDesignTime) +{ + await context.Database.MigrateAsync(); +} +``` + +EF Core sets the `IsDesignTime` to `true` when application code is running on behalf of tools. + +## Performance enhancements for proxies + +EF Core supports dynamically generated proxies for [lazy-loading](xref:core/querying/related-data/lazy) and [change-tracking](xref:core/change-tracking/change-detection#change-tracking-proxies). EF7 contains two performance improvements when using these proxies: + +- The proxy types are now created lazily. This means that the initial model building time when using proxies can be massively faster with EF7 than it was with EF Core 6.0. +- Proxies can now be used with compiled models. + +Here are some performance results for model with 449 entity types, 6390 properties, and 720 relationships. + +| Scenario | Method | Mean | Error | StdDev | +|-------------------------------------------------------------|------------------|--------:|---------:|---------:| +| EF Core 6.0 without proxies | TimeToFirstQuery | 1.085 s | 0.0083 s | 0.0167 s | +| EF Core 6.0 with change-tracking proxies | TimeToFirstQuery | 13.01 s | 0.2040 s | 0.4110 s | +| EF Core 7.0 without proxies | TimeToFirstQuery | 1.442 s | 0.0134 s | 0.0272 s | +| EF Core 7.0 with change-tracking proxies | TimeToFirstQuery | 1.446 s | 0.0160 s | 0.0323 s | +| EF Core 7.0 with change-tracking proxies and compiled model | TimeToFirstQuery | 0.162 s | 0.0062 s | 0.0125 s | + +So, in this case, a model with change-tracking proxies can be ready to execute the first query 80 times faster in EF7 than was possible with EF Core 6.0. + +## First-class Windows Forms Data Binding + +The Windows Forms team have been making some [great improvements to the Visual Studio Designer experience](https://devblogs.microsoft.com/dotnet/state-of-the-windows-forms-designer-for-net-applications/). This includes [new experiences for data binding](https://devblogs.microsoft.com/dotnet/databinding-with-the-oop-windows-forms-designer/) that integrates well with EF Core. + +In brief, the new experience provides Visual Studio U.I. for creating an : + +![Choose Category data source type](../../get-started/_static/winforms-choose-category-type.png) + +This can then be bound to an EF Core `DBset` with some simple code: + +```csharp +public partial class MainForm : Form +{ + private ProductsContext? dbContext; + + public MainForm() + { + InitializeComponent(); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + this.dbContext = new ProductsContext(); + + this.dbContext.Categories.Load(); + this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList(); + } + + protected override void OnClosing(CancelEventArgs e) + { + base.OnClosing(e); + + this.dbContext?.Dispose(); + this.dbContext = null; + } +} +``` + +See [Getting Started with Windows Forms](xref:core/get-started/winforms) for a complete walkthrough and [downloadable WinForms sample application](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/WinForms). diff --git a/samples/core/Miscellaneous/NewInEFCore6/TemporalTablesSample.cs b/samples/core/Miscellaneous/NewInEFCore6/TemporalTablesSample.cs index 09daf4a983..cb531ffdfa 100644 --- a/samples/core/Miscellaneous/NewInEFCore6/TemporalTablesSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore6/TemporalTablesSample.cs @@ -205,7 +205,6 @@ public static void Use_SQL_Server_temporal_tables() } } - using (var context = new EmployeeContext(quiet: true)) { Console.WriteLine(); @@ -245,7 +244,6 @@ public static void Use_SQL_Server_temporal_tables() } } - Console.WriteLine(); } diff --git a/samples/core/Miscellaneous/NewInEFCore7/CosmosQueriesSample.cs b/samples/core/Miscellaneous/NewInEFCore7/CosmosQueriesSample.cs new file mode 100644 index 0000000000..3580bea3a0 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/CosmosQueriesSample.cs @@ -0,0 +1,82 @@ +using System.Text.RegularExpressions; + +namespace NewInEfCore7; + +public static class CosmosQueriesSample +{ + public static async Task Cosmos_translations_for_RegEx_Match() + { + PrintSampleName(); + + await using var context = new ShapesContext(); + + try + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + } + catch (HttpRequestException exception) + { + Console.WriteLine($"Cosmos emulator not found: '{exception.Message}'"); + return; + } + + await context.AddRangeAsync( + new Triangle { Name = "Acute", Angle1 = 75, Angle2 = 85, InsertedOn = DateTime.UtcNow - TimeSpan.FromDays(2) }, + new Triangle { Name = "Obtuse", Angle1 = 110, Angle2 = 35, InsertedOn = DateTime.UtcNow - TimeSpan.FromDays(1) }, + new Triangle { Name = "Right", Angle1 = 90, Angle2 = 45, InsertedOn = DateTime.UtcNow }, + new Triangle { Name = "Isosceles", Angle1 = 75, Angle2 = 75, InsertedOn = DateTime.UtcNow + TimeSpan.FromDays(1) }, + new Triangle { Name = "Equilateral", Angle1 = 60, Angle2 = 60, InsertedOn = DateTime.UtcNow + TimeSpan.FromDays(2) } + ); + + await context.SaveChangesAsync(); + context.ChangeTracker.Clear(); + + #region RegexIsMatch + var containsInnerT = await context.Triangles + .Where(o => Regex.IsMatch(o.Name, "[a-z]t[a-z]", RegexOptions.IgnoreCase)) + .ToListAsync(); + #endregion + + Console.WriteLine(); + foreach (var result in containsInnerT) + { + Console.WriteLine($" {result.Name}"); + } + Console.WriteLine(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class Triangle + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public double Angle1 { get; set; } + public double Angle2 { get; set; } + public DateTime InsertedOn { get; set; } + } + + public class ShapesContext : DbContext + { + public DbSet Triangles => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToContainer("Shapes"); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseCosmos( + "https://localhost:8081", + "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", + "Queries") + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs new file mode 100644 index 0000000000..8cdfc0f6c0 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/ModelBuildingSample.cs @@ -0,0 +1,588 @@ +namespace NewInEfCore7; + +public static class ModelBuildingSample +{ + public static async Task Indexes_can_be_ordered() + { + PrintSampleName(); + + await using var context = new BlogsContext(); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + } + + public static async Task Property_can_be_mapped_to_different_column_names_TPT() + { + PrintSampleName(); + + await using var context = new AnimalsTptContext(); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + } + + public static async Task Property_can_be_mapped_to_different_column_names_TPC() + { + PrintSampleName(); + + await using var context = new AnimalsTpcContext(); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + } + + public static async Task Entity_splitting() + { + PrintSampleName(); + + await using (var context = new EntitySplittingContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + await context.AddRangeAsync( + new Customer("Alice", "1 Main St", "Chigley", "CW1 5ZH", "UK") { PhoneNumber = "01632 12348" }, + new Customer("Mac", "2 Main St", "Chigley", "CW1 5ZH", "UK"), + new Customer("Toast", "3 Main St", "Chigley", null, "UK") { PhoneNumber = "01632 12348" }); + + await context.SaveChangesAsync(); + } + + await using (var context = new EntitySplittingContext()) + { + var customers = await context.Customers + .Where(customer => customer.PhoneNumber!.StartsWith("01632") && customer.City == "Chigley") + .OrderBy(customer => customer.PostCode) + .ThenBy(customer => customer.Name) + .ToListAsync(); + + foreach (var customer in customers) + { + Console.WriteLine($"{customer.Name} from {customer.City} with phone {customer.PhoneNumber}"); + } + } + } + + public static async Task Temporal_tables_with_owned_types() + { + PrintSampleName(); + + DateTime timeStamp1; + DateTime timeStamp2; + DateTime timeStamp3; + DateTime timeStamp4; + + await using (var context = new EntitySplittingContext()) + { + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + await context.AddRangeAsync( + new Employee + { + Name = "Pinky Pie", + Info = new() + { + Address = "Sugarcube Corner, Ponyville, Equestria", + Department = "DevDiv", + Position = "Party Organizer", + AnnualSalary = 100.0m + } + }, + new Employee + { + Name = "Rainbow Dash", + Info = new() + { + Address = "Cloudominium, Ponyville, Equestria", + Department = "DevDiv", + Position = "Ponyville weather patrol", + AnnualSalary = 900.0m + } + }, + new Employee + { + Name = "Fluttershy", + Info = new() + { + Address = "Everfree Forest, Equestria", + Department = "DevDiv", + Position = "Animal caretaker", + AnnualSalary = 30.0m + } + }); + + await context.SaveChangesAsync(); + } + + await using (var context = new EntitySplittingContext()) + { + Console.WriteLine(); + Console.WriteLine("Starting data:"); + + var employees = await context.Employees.ToListAsync(); + foreach (var employee in employees) + { + var employeeEntry = context.Entry(employee); + var periodStart = employeeEntry.Property("PeriodStart").CurrentValue; + var periodEnd = employeeEntry.Property("PeriodEnd").CurrentValue; + + Console.WriteLine($" Employee {employee.Name} valid from {periodStart} to {periodEnd}"); + } + } + + await using (var context = new EntitySplittingContext()) + { + // Change the sleep values to emphasize the temporal nature of the data. + const int millisecondsDelay = 100; + + Thread.Sleep(millisecondsDelay); + timeStamp1 = DateTime.UtcNow; + Thread.Sleep(millisecondsDelay); + + var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash"); + employee.Info.Position = "Wonderbolt Trainee"; + await context.SaveChangesAsync(); + + Thread.Sleep(millisecondsDelay); + timeStamp2 = DateTime.UtcNow; + Thread.Sleep(millisecondsDelay); + + employee.Info.Position = "Wonderbolt Reservist"; + await context.SaveChangesAsync(); + + Thread.Sleep(millisecondsDelay); + timeStamp3 = DateTime.UtcNow; + Thread.Sleep(millisecondsDelay); + + employee.Info.Position = "Wonderbolt"; + await context.SaveChangesAsync(); + + Thread.Sleep(millisecondsDelay); + timeStamp4 = DateTime.UtcNow; + Thread.Sleep(millisecondsDelay); + } + + await using (var context = new EntitySplittingContext()) + { + #region NormalQuery + var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash"); + context.Remove(employee); + await context.SaveChangesAsync(); + #endregion + } + + await using (var context = new EntitySplittingContext()) + { + Console.WriteLine(); + Console.WriteLine("After updates and delete:"); + + #region TrackingQuery + var employees = await context.Employees.ToListAsync(); + foreach (var employee in employees) + { + var employeeEntry = context.Entry(employee); + var periodStart = employeeEntry.Property("PeriodStart").CurrentValue; + var periodEnd = employeeEntry.Property("PeriodEnd").CurrentValue; + + Console.WriteLine($" Employee {employee.Name} valid from {periodStart} to {periodEnd}"); + } + #endregion + + Console.WriteLine(); + Console.WriteLine("Historical data for Rainbow Dash:"); + + // GitHub Issue https://github.com/dotnet/efcore/issues/29156 + // #region TemporalAll + // var history = await context + // .Employees + // .TemporalAll() + // .Where(e => e.Name == "Rainbow Dash") + // .OrderBy(e => EF.Property(e, "PeriodStart")) + // .Select( + // e => new + // { + // Employee = e, + // PeriodStart = EF.Property(e, "PeriodStart"), + // PeriodEnd = EF.Property(e, "PeriodEnd") + // }) + // .ToListAsync(); + // + // foreach (var pointInTime in history) + // { + // Console.WriteLine( + // $" Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Info.Position}' from {pointInTime.PeriodStart} to {pointInTime.PeriodEnd}"); + // } + // #endregion + } + + // GitHub Issue https://github.com/dotnet/efcore/issues/29156 + // await using (var context = new EntitySplittingContext()) + // { + // Console.WriteLine(); + // Console.WriteLine($"Historical data for Rainbow Dash between {timeStamp2} and {timeStamp3}:"); + // + // #region TemporalBetween + // var history = await context + // .Employees + // .TemporalBetween(timeStamp2, timeStamp3) + // .Where(e => e.Name == "Rainbow Dash") + // .OrderBy(e => EF.Property(e, "PeriodStart")) + // .Select( + // e => new + // { + // Employee = e, + // PeriodStart = EF.Property(e, "PeriodStart"), + // PeriodEnd = EF.Property(e, "PeriodEnd") + // }) + // .ToListAsync(); + // #endregion + // + // foreach (var pointInTime in history) + // { + // Console.WriteLine( + // $" Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Info.Position}' from {pointInTime.PeriodStart} to {pointInTime.PeriodEnd}"); + // } + // } + + await using (var context = new EntitySplittingContext()) + { + Console.WriteLine(); + Console.WriteLine($"Historical data for Rainbow Dash as of {timeStamp2}:"); + + var history = await context + .Employees + .TemporalAsOf(timeStamp2) + .Where(e => e.Name == "Rainbow Dash") + .OrderBy(e => EF.Property(e, "PeriodStart")) + .Select( + e => new + { + Employee = e, PeriodStart = EF.Property(e, "PeriodStart"), PeriodEnd = EF.Property(e, "PeriodEnd") + }) + .ToListAsync(); + + foreach (var pointInTime in history) + { + Console.WriteLine( + $" Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Info.Position}' from {pointInTime.PeriodStart} to {pointInTime.PeriodEnd}"); + } + } + } + + public static async Task Unidirectional_many_to_many() + { + PrintSampleName(); + + await using var context = new BlogsContext(); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + #region InsertPostsAndTags + var tags = new Tag[] { new() { TagName = "Tag1" }, new() { TagName = "Tag2" }, new() { TagName = "Tag2" }, }; + + await context.AddRangeAsync(new Blog { Posts = + { + new Post { Tags = { tags[0], tags[1] } }, + new Post { Tags = { tags[1], tags[0], tags[2] } }, + new Post() + } }); + + await context.SaveChangesAsync(); + #endregion + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class BlogsContext : DbContext + { + public DbSet Blogs => Set(); + public DbSet Posts => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database=Blogs") + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region CompositeIndex + modelBuilder + .Entity() + .HasIndex(blog => new { blog.Name, blog.Owner }) + .IsDescending(false, true); + #endregion + + #region TwoIndexes + modelBuilder + .Entity() + .HasIndex(post => post.Title, "AscendingIndex"); + + modelBuilder + .Entity() + .HasIndex(post => post.Title, "DescendingIndex") + .IsDescending(); + #endregion + + #region ManyToMany + modelBuilder + .Entity() + .HasMany(post => post.Tags) + .WithMany(); + #endregion + + #region Utf8 + modelBuilder + .Entity() + .Property(comment => comment.CommentText) + .HasColumnType("varchar(max)") + .UseCollation("LATIN1_GENERAL_100_CI_AS_SC_UTF8"); + #endregion + + base.OnModelCreating(modelBuilder); + } + } + + #region CompositeIndexByAttribute + [Index(nameof(Name), nameof(Owner), IsDescending = new[] { false, true })] + public class Blog + { + public int Id { get; set; } + + [MaxLength(64)] + public string? Name { get; set; } + + [MaxLength(64)] + public string? Owner { get; set; } + + public List Posts { get; } = new(); + } + #endregion + + #region TwoIndexesByAttribute + [Index(nameof(Title), Name = "AscendingIndex")] + [Index(nameof(Title), Name = "DescendingIndex", AllDescending = true)] + public class Post + { + public int Id { get; set; } + + [MaxLength(64)] + public string? Title { get; set; } + + [DeleteBehavior(DeleteBehavior.NoAction)] + public Blog Blog { get; set; } = null!; + + public List Tags { get; } = new(); + } + #endregion + + #region CompositePrimaryKey + [PrimaryKey(nameof(PostId), nameof(CommentId))] + public class Comment + { + public int PostId { get; set; } + public int CommentId { get; set; } + public string CommentText { get; set; } = null!; + } + #endregion + + #region Tag + public class Tag + { + public int Id { get; set; } + public string TagName { get; set; } = null!; + } + #endregion + + public class AnimalsTptContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database=AnimalsTpt") + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region AnimalsTpt + modelBuilder.Entity().ToTable("Animals"); + + modelBuilder.Entity() + .ToTable( + "Cats", + tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId")); + + modelBuilder.Entity() + .ToTable( + "Dogs", + tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId")); + #endregion + } + } + + public class AnimalsTpcContext : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database=AnimalsTpc") + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region AnimalsTpc + modelBuilder.Entity().UseTpcMappingStrategy(); + + modelBuilder.Entity() + .ToTable( + "Cats", + builder => + { + builder.Property(cat => cat.Id).HasColumnName("CatId"); + builder.Property(cat => cat.Breed).HasColumnName("CatBreed"); + }); + + modelBuilder.Entity() + .ToTable( + "Dogs", + builder => + { + builder.Property(dog => dog.Id).HasColumnName("DogId"); + builder.Property(dog => dog.Breed).HasColumnName("DogBreed"); + }); + #endregion + } + } + + #region Animals + public abstract class Animal + { + public int Id { get; set; } + public string Breed { get; set; } = null!; + } + + public class Cat : Animal + { + public string? EducationalLevel { get; set; } + } + + public class Dog : Animal + { + public string? FavoriteToy { get; set; } + } + #endregion + + public class EntitySplittingContext : DbContext + { + public DbSet Customers + => Set(); + + public DbSet Employees + => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database=Images") + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region TableSplitting + modelBuilder.Entity( + entityBuilder => + { + entityBuilder + .ToTable("Customers") + .SplitToTable( + "PhoneNumbers", + tableBuilder => + { + tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId"); + tableBuilder.Property(customer => customer.PhoneNumber); + }) + .SplitToTable( + "Addresses", + tableBuilder => + { + tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId"); + tableBuilder.Property(customer => customer.Street); + tableBuilder.Property(customer => customer.City); + tableBuilder.Property(customer => customer.PostCode); + tableBuilder.Property(customer => customer.Country); + }); + }); + #endregion + + #region OwnedTemporalTable + modelBuilder + .Entity() + .ToTable( + "Employees", + tableBuilder => + { + tableBuilder.IsTemporal(); + tableBuilder.Property("PeriodStart").HasColumnName("PeriodStart"); + tableBuilder.Property("PeriodEnd").HasColumnName("PeriodEnd"); + }) + .OwnsOne( + employee => employee.Info, + ownedBuilder => ownedBuilder.ToTable( + "Employees", + tableBuilder => + { + tableBuilder.IsTemporal(); + tableBuilder.Property("PeriodStart").HasColumnName("PeriodStart"); + tableBuilder.Property("PeriodEnd").HasColumnName("PeriodEnd"); + })); + #endregion + } + } + + #region CombinedCustomer + public class Customer + { + public Customer(string name, string street, string city, string? postCode, string country) + { + Name = name; + Street = street; + City = city; + PostCode = postCode; + Country = country; + } + + public int Id { get; set; } + public string Name { get; set; } + public string? PhoneNumber { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string? PostCode { get; set; } + public string Country { get; set; } + } + #endregion + + #region EmployeeAndEmployeeInfo + public class Employee + { + public Guid EmployeeId { get; set; } + public string Name { get; set; } = null!; + + public EmployeeInfo Info { get; set; } = null!; + } + + public class EmployeeInfo + { + public string Position { get; set; } = null!; + public string Department { get; set; } = null!; + public string? Address { get; set; } + public decimal? AnnualSalary { get; set; } + } + #endregion +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj index f63454813e..23d744e162 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj +++ b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj @@ -9,25 +9,26 @@ - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore7/Program.cs b/samples/core/Miscellaneous/NewInEFCore7/Program.cs index cce8d1121a..6eb8c2758a 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/Program.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/Program.cs @@ -34,10 +34,15 @@ public static async Task Main() await StoredProcedureMappingSample.Insert_Update_and_Delete_using_stored_procedures_with_TPC(); await SimpleMaterializationSample.Simple_actions_on_entity_creation(); + await QueryInterceptionSample.LINQ_expression_tree_interception(); + await OptimisticConcurrencyInterceptionSample.Optimistic_concurrency_interception(); + 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(); @@ -71,5 +76,17 @@ public static async Task Main() await DbContextApiSample.Find_siblings(); await DbContextApiSample.Get_entry_for_shared_type_entity_type(); await DbContextApiSample.Use_IEntityEntryGraphIterator(); + + await ModelBuildingSample.Indexes_can_be_ordered(); + await ModelBuildingSample.Property_can_be_mapped_to_different_column_names_TPT(); + await ModelBuildingSample.Property_can_be_mapped_to_different_column_names_TPC(); + await ModelBuildingSample.Unidirectional_many_to_many(); + await ModelBuildingSample.Entity_splitting(); + await ModelBuildingSample.Temporal_tables_with_owned_types(); + + await ValueGenerationSample.Can_use_value_generation_with_converted_types(); + + // Requires the Cosmos emulator + await CosmosQueriesSample.Cosmos_translations_for_RegEx_Match(); } } diff --git a/samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs new file mode 100644 index 0000000000..b8afb2905d --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/ValueGenerationSample.cs @@ -0,0 +1,133 @@ +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace NewInEfCore7; + +public static class ValueGenerationSample +{ + public static async Task Can_use_value_generation_with_converted_types() + { + PrintSampleName(); + + await using var context = new ProductsContext(); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + + await context.AddRangeAsync( + new Category("Foods") { Products = { new("Marmite"), new("Toast"), new("Butter") } }, + new Category("Beverages") { Products = { new("Tea"), new("Milk") } }); + + await context.SaveChangesAsync(); + context.ChangeTracker.Clear(); + + var categoriesAndProducts = await context.Categories.Include(category => category.Products).ToListAsync(); + + Console.WriteLine(); + foreach (var category in categoriesAndProducts) + { + Console.WriteLine($"Category {category.Id.Value} is '{category.Name}'"); + foreach (var product in category.Products) + { + Console.WriteLine($" Product {product.Id.Value} is '{product.Name}'"); + } + } + Console.WriteLine(); + } + + private static void PrintSampleName([CallerMemberName] string? methodName = null) + { + Console.WriteLine($">>>> Sample: {methodName}"); + Console.WriteLine(); + } + + public class ProductsContext : DbContext + { + public DbSet Products => Set(); + public DbSet Categories => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database=Products") + .LogTo(Console.WriteLine, LogLevel.Information) + .EnableSensitiveDataLogging(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region ValueGeneratedOnAdd + modelBuilder.Entity().Property(product => product.Id).ValueGeneratedOnAdd(); + modelBuilder.Entity().Property(category => category.Id).ValueGeneratedOnAdd(); + #endregion + + #region ConfigureSequence + modelBuilder + .HasSequence("ProductsSequence", "northwind") + .StartsAt(1000) + .IncrementsBy(2); + #endregion + + #region Sequence + modelBuilder + .Entity() + .Property(product => product.Id) + .UseSequence("ProductsSequence", "northwind"); + #endregion + } + + #region KeyConverters + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + configurationBuilder.Properties().HaveConversion(); + configurationBuilder.Properties().HaveConversion(); + } + + private class ProductIdConverter : ValueConverter + { + public ProductIdConverter() + : base(v => v.Value, v => new(v)) + { + } + } + + private class CategoryIdConverter : ValueConverter + { + public CategoryIdConverter() + : base(v => v.Value, v => new(v)) + { + } + } + #endregion + } + + #region GuardedKeys + public readonly struct ProductId + { + public ProductId(int value) => Value = value; + public int Value { get; } + } + + public readonly struct CategoryId + { + public CategoryId(int value) => Value = value; + public int Value { get; } + } + #endregion + + #region ProductAndCategory + public class Product + { + public Product(string name) => Name = name; + public ProductId Id { get; set; } + public string Name { get; set; } + public CategoryId CategoryId { get; set; } + public Category Category { get; set; } = null!; + } + + public class Category + { + public Category(string name) => Name = name; + public CategoryId Id { get; set; } + public string Name { get; set; } + public List Products { get; } = new(); + } + #endregion +}