diff --git a/.gitignore b/.gitignore index 6f2737cb40..ac30dc6617 100644 --- a/.gitignore +++ b/.gitignore @@ -363,4 +363,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Rider +.idea/ diff --git a/.openpublishing.redirection.json b/.openpublishing.redirection.json index 4d85091739..f560a75e23 100644 --- a/.openpublishing.redirection.json +++ b/.openpublishing.redirection.json @@ -459,6 +459,11 @@ "source_path": "entity-framework/core/testing/in-memory.md", "redirect_url": "/ef/core/testing/index", "redirect_document_id": false + }, + { + "source_path": "entity-framework/core/managing-schemas/scaffolding.md", + "redirect_url": "/ef/core/managing-schemas/scaffolding/index", + "redirect_document_id": false } ] } diff --git a/entity-framework/core/change-tracking/entity-entries.md b/entity-framework/core/change-tracking/entity-entries.md index ba03140e1a..1b3c33b44d 100644 --- a/entity-framework/core/change-tracking/entity-entries.md +++ b/entity-framework/core/change-tracking/entity-entries.md @@ -592,7 +592,7 @@ For example: --> [!code-csharp[Using_DbSet_Local_to_query_tracked_entities_4](../../../samples/core/ChangeTracking/AccessingTrackedEntities/Samples.cs?name=Using_DbSet_Local_to_query_tracked_entities_4)] -See [Get Started with WPF](xref:core/get-started/wpf) for more information on WPF data binding with EF Core. +See [Get Started with WPF](xref:core/get-started/wpf) for more information on WPF data binding with EF Core, and [Get Started with Windows Forms](xref:core/get-started/winforms) for more information on Windows Forms data binding with EF Core. > [!TIP] > The local view for a given DbSet instance is created lazily when first accessed and then cached. LocalView creation itself is fast and it does not use significant memory. However, it does call [DetectChanges](xref:core/change-tracking/change-detection), which can be slow for large numbers of entities. The collections created by `ToObservableCollection` and `ToBindingList` are also created lazily and then cached. Both of these methods create new collections, which can be slow and use a lot of memory when thousands of entities are involved. diff --git a/entity-framework/core/extensions/index.md b/entity-framework/core/extensions/index.md index 97c1d82e3b..ccb9e1ed01 100644 --- a/entity-framework/core/extensions/index.md +++ b/entity-framework/core/extensions/index.md @@ -2,13 +2,13 @@ title: Tools & Extensions - EF Core description: External tools and extensions for Entity Framework Core author: ErikEJ -ms.date: 02/21/2021 +ms.date: 07/22/2022 uid: core/extensions/index --- # EF Core Tools & Extensions -These tools and extensions provide additional functionality for Entity Framework Core 2.1 and later. +These tools and extensions provide additional functionality for Entity Framework Core 3 and later. > [!IMPORTANT] > Extensions are built by a variety of sources and aren't maintained as part of the Entity Framework Core project. When considering a third party extension, be sure to evaluate its quality, licensing, compatibility, support, etc. to ensure it meets your requirements. In particular, an extension built for an older version of EF Core may need updating before it will work with the latest versions. @@ -17,13 +17,13 @@ These tools and extensions provide additional functionality for Entity Framework ### LLBLGen Pro -LLBLGen Pro is an entity modeling solution with support for Entity Framework and Entity Framework Core. It lets you easily define your entity model and map it to your database, using database first or model first, so you can get started writing queries right away. For EF Core: 2, 3, 5. +LLBLGen Pro is an entity modeling solution with support for Entity Framework and Entity Framework Core. It lets you easily define your entity model and map it to your database, using database first or model first, so you can get started writing queries right away. For EF Core: 2, 3, 5, 6. [Website](https://www.llblgen.com/) ### Devart Entity Developer -Entity Developer is a powerful O/RM designer for ADO.NET Entity Framework, NHibernate, LinqConnect, Telerik Data Access, and LINQ to SQL. It supports designing EF Core models visually, using model first or database first approaches, and C# or Visual Basic code generation. For EF Core: 2, 3, 5. +Entity Developer is a powerful O/RM designer for ADO.NET Entity Framework, NHibernate, LinqConnect, Telerik Data Access, and LINQ to SQL. It supports designing EF Core models visually, using model first or database first approaches, and C# or Visual Basic code generation. For EF Core: 2, 3, 5, 6. [Website](https://www.devart.com/entitydeveloper/) @@ -41,27 +41,9 @@ EF Core Power Tools is a Visual Studio extension that exposes various EF Core de ### Entity Framework Visual Editor -Entity Framework Visual Editor is a Visual Studio extension that adds an O/RM designer for visual design of EF 6, and EF Core classes. Code is generated using T4 templates so can be customized to suit any needs. It supports inheritance, unidirectional and bidirectional associations, enumerations, and the ability to color-code your classes and add text blocks to explain potentially arcane parts of your design. For EF Core: 2, 3, 5. +Entity Framework Visual Editor is a Visual Studio extension that adds an O/RM designer for visual design of EF 6, and EF Core classes. Code is generated using T4 templates so can be customized to suit any needs. It supports inheritance, unidirectional and bidirectional associations, enumerations, and the ability to color-code your classes and add text blocks to explain potentially arcane parts of your design. For EF Core: 2, 3, 5, 6, 7. -[Marketplace](https://marketplace.visualstudio.com/items?itemName=michaelsawczyn.EFDesigner) - -### CatFactory - -CatFactory is a scaffolding engine for .NET Core that can automate the generation of DbContext classes, entities, mapping configurations, and repository classes from a SQL Server database. For EF Core: 2. - -[GitHub repository](https://github.com/hherzl/CatFactory.EntityFrameworkCore) - -### LoreSoft's Entity Framework Core Generator - -Entity Framework Core Generator (efg) is a .NET Core CLI tool that can generate EF Core models from an existing database, much like `dotnet ef dbcontext scaffold`, but it also supports safe code [regeneration](https://efg.loresoft.com/en/latest/regeneration/) via region replacement or by parsing mapping files. This tool supports generating view models, validation, and object mapper code. For EF Core: 2. - -[Tutorial](https://www.loresoft.com/Generate-ASP-NET-Web-API) | [Documentation](https://efg.loresoft.com/en/latest/) - -### Geco - -Geco (Generator Console) is a simple code generator based on a console project, that runs on .NET Core and uses C# interpolated strings for code generation. Geco includes a reverse model generator for EF Core with support for pluralization, singularization, and editable templates. It also provides a seed data script generator, a script runner, and a database cleaner. For EF Core: 2. - -[GitHub repository](https://github.com/iQuarc/Geco) +[Marketplace](https://marketplace.visualstudio.com/items?itemName=michaelsawczyn.EFDesigner2022) ### IWAPI @@ -131,12 +113,6 @@ Provides a wrapper around [SQL Server Express LocalDB](/sql/database-engine/conf [GitHub repository](https://github.com/SimonCropp/LocalDb) | [NuGet](https://www.nuget.org/packages/EfLocalDb) -### EFCore.TemporalSupport - -An implementation of temporal support. For EF Core: 2. - -[GitHub repository](https://github.com/cpoDesign/EFCore.TemporalSupport) | [NuGet](https://www.nuget.org/packages/EFCoreTemporalSupport) - ### EfCoreTemporalTable > [!NOTE] @@ -155,12 +131,6 @@ Extension library for Entity Framework Core which allows developers who use SQL [GitHub repository](https://github.com/findulov/EntityFrameworkCore.TemporalTables) | [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.TemporalTables) -### EntityFrameworkCore.Cacheable - -A high-performance second-level query cache. For EF Core: 2. - -[GitHub repository](https://github.com/SteffenMangold/EntityFrameworkCore.Cacheable) | [NuGet](https://www.nuget.org/packages/EntityFrameworkCore.Cacheable) - ### EntityFrameworkCore.NCache NCache Entity Framework Core Provider is a distributed second level cache provider for caching query results. The distributed architecture of NCache makes it more scalable and highly available. For EF Core: 2, 3. @@ -234,6 +204,9 @@ Adds native support to EntityFrameworkCore for SQL Server for the NodaTime types ### Dabble.EntityFrameworkCore.Temporal.Query +> [!NOTE] +> SQL Server temporal tables are supported directly within EF Core as of [EF Core 6](/ef/core/what-is-new/ef-core-6.0/whatsnew#sql-server-temporal-tables). + LINQ extensions to Entity Framework Core 3.1 to support Microsoft SQL Server Temporal Table Querying. For EF Core: 3. [GitHub repository](https://github.com/Adam-Langley/efcore-temporal-query) | [NuGet](https://www.nuget.org/packages/Dabble.EntityFrameworkCore.Temporal.Query) @@ -302,12 +275,6 @@ Supports SQL Server, Postgres, MySql, SQLite and Oracle. For EF Core: 3, 5. [GitHub Repository](https://github.com/Giorgi/EntityFramework.Exceptions) -### EFCoreAuditing - -A Library for Entity Framework Core to support automatically recording data changes history (audit logging), soft-delete, and snake_case naming convention functionality. For EF Core: 2. - -[GitHub Repository](https://github.com/OKTAYKIR/EFCoreAuditing) | [NuGet](https://www.nuget.org/packages/EFCore.Auditing) - ### EntityFrameworkCore.FSharp Adds F# design-time support to EF Core. For EF Core: 5. @@ -351,6 +318,17 @@ Provides window (analytics) functions and binary functions for EF Core. Provider [GitHub repository](https://github.com/zompinc/efcore-extensions) | [NuGet](https://www.nuget.org/packages/Zomp.EFCore.WindowFunctions.SqlServer) +### Ainoraz.EFCore.IncludeBuilder + +Extension for EF Core that provides alternative `Include` syntax in order to better support the following scenarios: + +- Loading multiple entities on the same level (siblings). +- Writing extension methods that are independent of nesting level. + +For EF Core: 6. + +[GitHub repository](https://github.com/AinoraZ/EFCore.IncludeBuilder) | [NuGet](https://www.nuget.org/packages/Ainoraz.EFCore.IncludeBuilder/) + ## API Integrations These packages are designed to integrate directly with EF Core to expose various APIs. diff --git a/entity-framework/core/get-started/_static/winforms-add-class.png b/entity-framework/core/get-started/_static/winforms-add-class.png new file mode 100644 index 0000000000..b22464beb1 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-add-class.png differ diff --git a/entity-framework/core/get-started/_static/winforms-add-new-object-data-source.png b/entity-framework/core/get-started/_static/winforms-add-new-object-data-source.png new file mode 100644 index 0000000000..8d01e68af2 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-add-new-object-data-source.png differ diff --git a/entity-framework/core/get-started/_static/winforms-after-save.png b/entity-framework/core/get-started/_static/winforms-after-save.png new file mode 100644 index 0000000000..1e78eac31d Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-after-save.png differ diff --git a/entity-framework/core/get-started/_static/winforms-before-save.png b/entity-framework/core/get-started/_static/winforms-before-save.png new file mode 100644 index 0000000000..56e1c08433 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-before-save.png differ diff --git a/entity-framework/core/get-started/_static/winforms-categoryid-read-only.png b/entity-framework/core/get-started/_static/winforms-categoryid-read-only.png new file mode 100644 index 0000000000..1677aa98bf Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-categoryid-read-only.png differ diff --git a/entity-framework/core/get-started/_static/winforms-choose-category-type.png b/entity-framework/core/get-started/_static/winforms-choose-category-type.png new file mode 100644 index 0000000000..918dabd809 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-choose-category-type.png differ diff --git a/entity-framework/core/get-started/_static/winforms-choose-category.png b/entity-framework/core/get-started/_static/winforms-choose-category.png new file mode 100644 index 0000000000..99156f5c82 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-choose-category.png differ diff --git a/entity-framework/core/get-started/_static/winforms-choose-products.png b/entity-framework/core/get-started/_static/winforms-choose-products.png new file mode 100644 index 0000000000..68d8b2215d Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-choose-products.png differ diff --git a/entity-framework/core/get-started/_static/winforms-datagrid-view.png b/entity-framework/core/get-started/_static/winforms-datagrid-view.png new file mode 100644 index 0000000000..43765f03af Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-datagrid-view.png differ diff --git a/entity-framework/core/get-started/_static/winforms-design-binding-picker.png b/entity-framework/core/get-started/_static/winforms-design-binding-picker.png new file mode 100644 index 0000000000..e0ca46a4ff Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-design-binding-picker.png differ diff --git a/entity-framework/core/get-started/_static/winforms-edit-columns.png b/entity-framework/core/get-started/_static/winforms-edit-columns.png new file mode 100644 index 0000000000..dacb4775f8 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-edit-columns.png differ diff --git a/entity-framework/core/get-started/_static/winforms-first-run.png b/entity-framework/core/get-started/_static/winforms-first-run.png new file mode 100644 index 0000000000..b0a3d231f4 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-first-run.png differ diff --git a/entity-framework/core/get-started/_static/winforms-form-layout.png b/entity-framework/core/get-started/_static/winforms-form-layout.png new file mode 100644 index 0000000000..c688d28666 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-form-layout.png differ diff --git a/entity-framework/core/get-started/_static/winforms-install-package.png b/entity-framework/core/get-started/_static/winforms-install-package.png new file mode 100644 index 0000000000..5518995976 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-install-package.png differ diff --git a/entity-framework/core/get-started/_static/winforms-mainform.png b/entity-framework/core/get-started/_static/winforms-mainform.png new file mode 100644 index 0000000000..17126d90cb Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-mainform.png differ diff --git a/entity-framework/core/get-started/_static/winforms-manage-nuget.png b/entity-framework/core/get-started/_static/winforms-manage-nuget.png new file mode 100644 index 0000000000..44c52f10b7 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-manage-nuget.png differ diff --git a/entity-framework/core/get-started/_static/winforms-new-project.png b/entity-framework/core/get-started/_static/winforms-new-project.png new file mode 100644 index 0000000000..ca542b473c Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-new-project.png differ diff --git a/entity-framework/core/get-started/_static/winforms-product-columns.png b/entity-framework/core/get-started/_static/winforms-product-columns.png new file mode 100644 index 0000000000..7139eda215 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-product-columns.png differ diff --git a/entity-framework/core/get-started/_static/winforms-products-and-categories.png b/entity-framework/core/get-started/_static/winforms-products-and-categories.png new file mode 100644 index 0000000000..c5dae116c1 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-products-and-categories.png differ diff --git a/entity-framework/core/get-started/_static/winforms-products-are-loaded.png b/entity-framework/core/get-started/_static/winforms-products-are-loaded.png new file mode 100644 index 0000000000..de5f4f5a87 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-products-are-loaded.png differ diff --git a/entity-framework/core/get-started/_static/winforms-save-click-event.png b/entity-framework/core/get-started/_static/winforms-save-click-event.png new file mode 100644 index 0000000000..93d6d1392f Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-save-click-event.png differ diff --git a/entity-framework/core/get-started/_static/winforms-selection-changed-event.png b/entity-framework/core/get-started/_static/winforms-selection-changed-event.png new file mode 100644 index 0000000000..851c39af27 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-selection-changed-event.png differ diff --git a/entity-framework/core/get-started/_static/winforms-view-code.png b/entity-framework/core/get-started/_static/winforms-view-code.png new file mode 100644 index 0000000000..d378f8ff37 Binary files /dev/null and b/entity-framework/core/get-started/_static/winforms-view-code.png differ diff --git a/entity-framework/core/get-started/winforms.md b/entity-framework/core/get-started/winforms.md new file mode 100644 index 0000000000..878ec0fa25 --- /dev/null +++ b/entity-framework/core/get-started/winforms.md @@ -0,0 +1,319 @@ +--- +title: Get Started with Windows Forms - EF Core +description: Getting started tutorial for using Windows Forms (WinForms) with Entity Framework Core +author: ajcvickers +ms.author: avickers +ms.date: 08/04/2021 +uid: core/get-started/winforms +--- + +# Getting Started with Windows Forms + +This step-by-step walkthrough shows how to build a simple Windows Forms (WinForms) application backed by a SQLite database. The application uses Entity Framework Core (EF Core) to load data from the database, track changes made to that data, and persist those changes back to the database. + +The screen shots and code listings in this walkthrough are taken from Visual Studio 2022 17.3.0. + +> [!TIP] +> You can view this article's [sample on GitHub](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/WinForms). + +## Prerequisites + +You need to have Visual Studio 2022 17.3 or later installed with the **.NET desktop workload** selected to complete this walkthrough. For more information about installing the latest version of Visual Studio, see [Install Visual Studio](/visualstudio/install/install-visual-studio). + +## Create the Application + +1. Open Visual Studio +2. On the start window, choose **Create new project**. +3. Choose **Windows Forms App** and then choose **Next**. + + ![Create a new Windows Forms project](_static/winforms-new-project.png) + +4. At the next screen, give the project a name, for example, **GetStartedWinForms**, and choose **Next**. +5. At the next screen, choose the .NET version to use. This walkthrough was created with .NET 7, but it should also work with later versions. +6. Choose **Create**. + +## Install the EF Core NuGet packages + +1. Right-click on the solution and choose **Manage NuGet Packages for Solution...** + + ![Manage NuGet Packages for Solution](_static/winforms-manage-nuget.png) + +2. Choose the **Browse** tab and search for "Microsoft.EntityFrameworkCore.Sqlite". +3. Select the **Microsoft.EntityFrameworkCore.Sqlite** package. +4. Check the project **GetStartedWinForms** in the right pane. +5. Choose the latest version. To use a pre-release version, make sure that the **Include prerelease** box is checked. +6. Click **Install** + + ![Install the Microsoft.EntityFrameworkCore.Sqlite package](_static/winforms-install-package.png) + +> [!NOTE] +> The **Microsoft.EntityFrameworkCore.Sqlite** is the "database provider" package for using EF Core with a SQLite database. Similar packages are available for other database systems. Installing a database provider package automatically brings in all the dependencies needed to use EF Core with that database system. This includes the **Microsoft.EntityFrameworkCore** base package. + +## Define a Model + +In this walkthrough we will implement a model using "Code First". This means that EF Core will create the database tables and schema based on the C# classes you define. See [Managing Database Schemas](xref:core/managing-schemas/index) to see how to use an existing database instead. + +1. Right-click on project and choose **Add**, then **Class...** to add a new class. + + ![Add new class](_static/winforms-add-class.png) + +2. Use the filename `Product.cs` and replace the code for the class with: + + [!code-csharp[](../../../samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Product.cs)] + +3. Repeat to create `Category.cs` with the following code: + + [!code-csharp[](../../../samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Category.cs)] + +The `Products` property on the `Category` class and the `Category` property on the `Product` class are called "navigations". In EF Core, navigations define a relationship between two entity types. In this case, the `Product.Category` navigation references the category to which a given product belongs. Likewise, the `Category.Products` collection navigation contains all the products for a given category. + +> [!TIP] +> When using Windows Forms, the `ObservableCollectionListSource`, which implements `IListSource`, can be used for collection navigations. This is not necessary, but does improve the two-way data binding experience. + +## Define the DbContext + +In EF Core, a class derived from `DbContext` is used to configure entity types in a model and act as a session for interacting with the database. In the simplest case, a `DbContext` class: + +- Contains `DbSet` properties for each entity type in the model. +- Overrides the `OnConfiguring` method to configure the database provider and connection string to use. See [Configuring a DbContext](xref:core/dbcontext-configuration/index) for more information. + +In this case, the DbContext class also overrides the `OnModelCreating` method to provide some sample data for the application. + +Add a new `ProductsContext.cs` class to the project with the following code: + +[!code-csharp[](../../../samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/ProductsContext.cs)] + +Make sure to **build the solution** at this point. + +## Adding controls to the form + +The application will show a list of categories and a list of products. When a category is selected in the first list, then the second list will change to show products for that category. These lists can be modified to add, remove, or edit products and categories, and these changes can be saved to the SQLite database by clicking a **Save** button. + +1. Change the name of the main form from `Form1` to `MainForm`. + + ![Rename Form1 to MainForm](_static/winforms-mainform.png) + +2. And change the title to "Products and Categories". + + ![Title MainForm as "Products and Categories"](_static/winforms-products-and-categories.png) + +3. Using the **Toolbox**, add two `DataGridView` controls, arranged next to each other. + + ![Add DataGridView](_static/winforms-datagrid-view.png) + +4. In the **Properties** for the first `DataGridView`, change the **Name** to `dataGridViewCategories`. +5. In the **Properties** for the second `DataGridView`, change the **Name** to `dataGridViewProducts`. +6. Also using the **Toolbox**, add a `Button` control. +7. Name the button `buttonSave` and give it the text "Save". The form should look something this this: + + ![Form layout](_static/winforms-form-layout.png) + +## Data binding + +The next step is to connect the `Product` and `Category` types from the model to the `DataGridView` controls. This will bind the data loaded by EF Core to the controls, such that the entities tracked by EF Core are kept in sync with those displayed in the controls. + +1. Click the **Designer Action Glyph** on the first `DataGridView`. This is the tiny button at the top-right corner of the control. + + ![The Designer Action Glyph](_static/winforms-design-binding-picker.png) + +2. This opens the _Action List_, from which the drop-down for **Chose Data Source** can be accessed. We haven't created a data source yet, so go to the bottom and choose **Add new Object Data Source...**. + + ![Add new Object Data Source](_static/winforms-add-new-object-data-source.png) + +3. Choose **Category** to create an object data source for categories, and click **OK**. + + ![Choose Category data source type](_static/winforms-choose-category-type.png) + + > [!TIP] + > If no data source types appear here, then make sure that `Product.cs`, `Category.cs` and `ProductsContext.cs` have been added to the project _**and the solution has been built**_. + +4. Now the **Choose Data Source** drop-down contains the object data source we just created. Expand **Other Data Sources**, then **Project Data Sources**, and choose **Category**. + + ![Choose Category data source](_static/winforms-choose-category.png) + + The second `DataGridView` will be bound to products. However, rather than binding to the top-level `Product` type, it will instead be bound to the `Products` navigation from the `Category` binding of the first `DataGridView`. This means that when a category is selected in the first view, the products for that category will automatically be used in the second view. + +5. Using the **Designer Action Glyph** on the second `DataGridView`, choose **Choose Data Source**, then expand the `categoryBindingSource` and choose `Products`. + + ![Choose Products data source](_static/winforms-choose-products.png) + +## Configuring what is displayed + +By default, a column is created in the `DataGridView` for every property of the bound types. Also, the values for each of these properties can be edited by the user. However, some values, such as the primary key values, are conceptually read-only, and so should not be edited. Also, some properties, such as the `CategoryId` foreign key property and the `Category` navigation are not useful to the user, and so should be hidden. + +> [!TIP] +> It is common to hide primary key properties in a real application. They are left visible here to make it easy to see what EF Core is doing behind the scenes. + +1. Right-click on the first `DataGridView` and choose **Edit Columns...**. + + ![Edit DataGridView columns](_static/winforms-edit-columns.png) + +2. Make the `CategoryId` column, which represents the primary key, read-only, and click **OK**. + + ![Make CategoryId column read-only](_static/winforms-categoryid-read-only.png) + +3. Right-click on the second `DataGridView` and choose **Edit Columns...**. Make the `ProductId` column read-only, and remove the `CategoryId` and `Category` columns, then click **OK**. + + ![Make ProductId column read-only and remove CategoryId and Category columns](_static/winforms-product-columns.png) + +## Connecting to EF Core + +The application now needs a small amount of code to connect EF Core to the data-bound controls. + +1. Open the `MainForm` code by right-clicking on the file and choosing **View Code**. + + ![View Code](_static/winforms-view-code.png) + +2. Add a private field to hold the `DbContext` for the session, and add overrides for the `OnLoad` and `OnClosing` methods. The code should look like this: + +```csharp +using Microsoft.EntityFrameworkCore; +using System.ComponentModel; + +namespace GetStartedWinForms +{ + public partial class MainForm : Form + { + private ProductsContext? dbContext; + + public MainForm() + { + InitializeComponent(); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + this.dbContext = new ProductsContext(); + + // Uncomment the line below to start fresh with a new database. + // this.dbContext.Database.EnsureDeleted(); + this.dbContext.Database.EnsureCreated(); + + 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; + } + } +} +``` + +The `OnLoad` method is called when the form is loaded. At this time + +- An instance of the `ProductsContext` is created that will be used to load and track changes to products and categories displayed by the application. +- `EnsureCreated` is called on the `DbContext` to create the SQLite database if it does not already exist. This is a quick way to create a database when prototyping or testing applications. However, if the model changes, then the database will need to be deleted so that it can be created again. (The `EnsureDeleted` line can be un-commented to easily delete and re-create the database when the application is run.) You may instead wish to use [EF Core Migrations](xref:core/managing-schemas/migrations/index) to modify and update the database schema without losing any data. +- `EnsureCreated` will also populate the new database with the data defined in the `ProductsContext.OnModelCreating` method. +- The `Load` extension method is used to load all the categories from database into the `DbContext`. These entities will now be tracked by the `DbContext`, which will detect any changes made when the categories are edited by the user. +- The `categoryBindingSource.DataSource` property is initialized to the categories that are being tracked by the `DbContext`. This is done by calling `Local.ToBindingList()` on the `Categories` `DbSet` property. `Local` provides access to a local view of the tracked categories, with events hooked up to ensure the local data stays in sync with the displayed data, and vice versa. `ToBindingList()` exposes this data as an `IBindingList`, which is understood by Windows Forms data binding. + +The `OnClosing` method is called when the form is closed. At this time, the `DbContext` is disposed, which ensures any database resources will be freed, and the `dbContext` field is set to null so that it cannot be used again. + +## Populating the Products view + +If the application is started at this point, then it should look something like this: + +![Fist run of the application](_static/winforms-first-run.png) + +Notice that the categories have been loaded from the database, but the products table remains empty. Also, the **Save** button does not work. + +To populate the products table, EF Core needs to load products from the database for the selected category. To achieve this: + +1. In the designer for the main form, select the `DataGridView` for categories. +2. In the **Properties** for the `DataGridView`, choose the events (the lightning button), and double click the **SelectionChanged** event. + + ![Add the SelectionChanged event](_static/winforms-selection-changed-event.png) + + This will create stub in the main form code for an event to be fired whenever the category selection changes. + +3. Fill in the code for the event: + +```csharp +private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e) +{ + if (this.dbContext != null) + { + var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem; + + if (category != null) + { + this.dbContext.Entry(category).Collection(e => e.Products).Load(); + } + } +} +``` + +In this code, if there is an active (non-null) `DbContext` session, then we obtain the `Category` instance bound to the currently selected row of the `DataViewGrid`. (This may be `null` if the final row in the view is selected, which is used to create new categories.) If there is a selected category, then the `DbContext` is instructed to load the products associated with that category. This is done by: + +- Getting an `EntityEntry` for the `Category` instance (`dbContext.Entry(category)`) +- Letting EF Core know that we want to operate on the `Products` collection navigation of that `Category` (`.Collection(e => e.Products)`) +- And finally telling EF Core that we want to load that collection of products from the database (`.Load();`) + +> [!TIP] +> When `Load` is called, EF Core will only access the database to load the products if they have not already been loaded. + +If the application is now run again, then it should load the appropriate products whenever a category is selected: + +![Products are loaded](_static/winforms-products-are-loaded.png) + +## Saving changes + +Finally, the **Save** button can be connected to EF Core so that any changes made to the products and categories are saved to the database. + +1. In the designer for the main form, select the **Save** button. +2. In the **Properties** for the `Button`, choose the events (the lightning button), and double click the **Click** event. + + ![Add the Click event for Save](_static/winforms-save-click-event.png) + +3. Fill in the code for the event: + +```csharp +private void buttonSave_Click(object sender, EventArgs e) +{ + this.dbContext!.SaveChanges(); + + this.dataGridViewCategories.Refresh(); + this.dataGridViewProducts.Refresh(); +} +``` + +This code calls `SaveChanges` on the `DbContext`, which saves any changes made to the SQLite database. If no changes were made, then this is a no-op, and no database call is made. After saving, the `DataGridView` controls are refreshed. This is because EF Core reads generated primary key values for any new products and categories from the database. Calling `Refresh` updates the display with these generated values. + +## The final application + +Here is the full code for the main form: + +[!code-csharp[](../../../samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.cs)] + +The application can now be run, and products and categories can be added, deleted, and edited. Notice that if the **Save** button is clicked before closing the application, then any changes made will be stored in the database and re-loaded when the application is re-started. If **Save** is not clicked, then any changes are lost when the application is re-started. + +> [!TIP] +> A new category or product can be added to a `DataViewControl` using the empty row at the bottom of the control. A row can be deleted by selecting it and pressing the **Del** key. + +**Before saving** + +![The running application before clicking Save](_static/winforms-before-save.png) + +**After saving** + +![The running application after clicking Save](_static/winforms-after-save.png) + +Notice that the primary key values for the added category and products are populated when **Save** is clicked. + +## Learn more + +- [Configuring a DbContext](xref:core/dbcontext-configuration/index) +- [Creating and configuring a model](xref:core/modeling/index) +- [Managing Database Schemas](xref:core/managing-schemas/index) +- [Querying Data](xref:core/querying/index) +- [Change Tracking](xref:core/change-tracking/index) +- [Saving Data](xref:core/saving/index) +- [Databinding with the OOP Windows Forms Designer](https://devblogs.microsoft.com/dotnet/databinding-with-the-oop-windows-forms-designer/) diff --git a/entity-framework/core/get-started/winforms.md.stub b/entity-framework/core/get-started/winforms.md.stub deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/entity-framework/core/managing-schemas/scaffolding.md b/entity-framework/core/managing-schemas/scaffolding/index.md similarity index 95% rename from entity-framework/core/managing-schemas/scaffolding.md rename to entity-framework/core/managing-schemas/scaffolding/index.md index 2a85351de7..52aa444171 100644 --- a/entity-framework/core/managing-schemas/scaffolding.md +++ b/entity-framework/core/managing-schemas/scaffolding/index.md @@ -37,11 +37,12 @@ Scaffold-DbContext 'Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook' ### Configuration and User Secrets -If you have an ASP.NET Core project, you can use the `Name=` syntax to read the connection string from configuration. +If you have a .NET application that uses the hosting model and configuration system, such as an ASP.NET Core project, then you can use the `Name=` syntax to read the connection string from configuration. This works well with the [Secret Manager tool](/aspnet/core/security/app-secrets#secret-manager) to keep your database password separate from your codebase. ```dotnetcli +dotnet user-secrets init dotnet user-secrets set ConnectionStrings:Chinook "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook" dotnet ef dbcontext scaffold Name=ConnectionStrings:Chinook Microsoft.EntityFrameworkCore.SqlServer ``` @@ -161,6 +162,8 @@ Customize the entity type classes and DbContext class to fit your needs. For exa You can also add additional constructors, methods, properties, etc. using another partial class in a separate file. This approach works even when you intend to reverse engineer the model again. +Starting in EF7, you can also use T4 text templates to customize the generated code. See [Custom Reverse Engineering Templates](xref:core/managing-schemas/scaffolding/templates) for more details. + ## Updating the model After making changes to the database, you may need to update your EF Core model to reflect those changes. If the database changes are simple, it may be easiest just to manually make the changes to your EF Core model. For example, renaming a table or column, removing a column, or updating a column's type are trivial changes to make in code. diff --git a/entity-framework/core/managing-schemas/scaffolding/templates.md b/entity-framework/core/managing-schemas/scaffolding/templates.md new file mode 100644 index 0000000000..f8e85092ad --- /dev/null +++ b/entity-framework/core/managing-schemas/scaffolding/templates.md @@ -0,0 +1,224 @@ +--- +title: Custom Reverse Engineering Templates - EF Core +description: Using T4 text templates to customize the scaffolded code when reverse engineering an Entity Framework Core model from a database +author: bricelam +ms.date: 08/16/2022 +uid: core/managing-schemas/scaffolding/templates +--- +# Custom Reverse Engineering Templates + +> [!NOTE] +> This feature was added in EF Core 7. + +While [reverse engineering](xref:core/managing-schemas/scaffolding), Entity Framework Core strives to scaffold good, general-purpose code that can be used in a variety of app types and uses [common coding conventions](/dotnet/csharp/fundamentals/coding-style/coding-conventions) for a consistent look and a familiar feel. Sometimes, however, more specialized code and alternative coding styles are desirable. This article shows how to customize the scaffolded code using [T4 text templates](/visualstudio/modeling/code-generation-and-t4-text-templates). + +## Prerequisites + +This article assumes you're familiar with [reverse engineering in EF Core](xref:core/managing-schemas/scaffolding). If not, please review that article before proceeding. + +## Adding the default templates + +The first step to customizing the scaffolded code is to add the default templates to your project. The default templates are the ones used internally by EF Core when reverse engineering. They provide a starting point for you to begin customizing the scaffolded code. + +Start by installing the EF Core template package for `dotnet new`: + +```dotnetcli +dotnet new install Microsoft.EntityFrameworkCore.Templates::7.0.0-* +``` + +> [!TIP] +> The version suffix `::7.0.0-*` installs the latest prerelease version of the template package. It can be omitted once the final version of EF Core 7 is released. + +Now you can add the default templates to your project. Do this by running the following command from your project directory. + +```dotnetcli +dotnet new ef-templates +``` + +This command adds the following files to your project. + +- CodeTemplates/ + - EFCore/ + - DbContext.t4 + - EntityType.t4 + +The `DbContext.t4` template is used to scaffold a DbContext class for the database, and the `EntityType.t4` template is used to scaffold entity type classes for each table and view in the database. + +> [!TIP] +> The .t4 extension is used (instead of .tt) to prevent Visual Studio from transforming the templates. The templates will be transformed by EF Core instead. + +## Introduction to T4 + +Let's open the `DbContext.t4` template and inspect its contents. This file is a [T4 text template](/visualstudio/modeling/writing-a-t4-text-template). T4 is a language for generating text using .NET. The following code is for illustrative purposes only; it does not represent the full contents of the file. + +> [!IMPORTANT] +> T4 text templates--especially ones that generate code--can be difficult to read without syntax highlighting. If necessary, search for an extension to your code editor that enables T4 syntax highlighting. + +```T4 +<#@ template hostSpecific="true" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #> +<#@ parameter name="NamespaceHint" type="System.String" #> +<#@ import namespace="Microsoft.EntityFrameworkCore" #> +<# + if (!string.IsNullOrEmpty(NamespaceHint)) + { +#> +namespace <#= NamespaceHint #>; +``` + +The first few lines that begin with `<#@` are called directives. They affect how the template is transformed. The following table briefly describes each kind of directive used. + +Directive | Description +----------- | ----------- +`template` | Specifies hostSpecific="true" which enables using the `Host` property inside the template to access EF Core services. +`assembly` | Adds assembly references required to compile the template. +`parameter` | Declares parameters that will be passed in by EF Core when transforming the template. +`import` | Like C# using directives, brings namespaces into scope for the template code. + +After the directives, the next section of `DbContext.t4` is called a control block. A standard control block begins with `<#` and ends with `#>`. The code inside of it will be executed when transforming the template. For a list of properties and methods available inside control blocks, see the [TextTransformation](/dotnet/api/microsoft.visualstudio.texttemplating.texttransformation) class. + +Anything outside of a control block will be copied directly to the template output. + +An expression control block begins with `<#=`. The code inside of it will be evaluated and the result will be added to the template output. These are similar to C# interpolated string arguments. + +For a more detailed and complete explanation of the T4 syntax, see [Writing a T4 Text Template](/visualstudio/modeling/writing-a-t4-text-template). + +## Customize the entity types + +Let's walk through what it's like to customize a template. By default, EF Core generates the following code for collection navigation properties. + +```C# +public virtual ICollection Albums { get; } = new List(); +``` + +Using `List` is a good default for most applications. However, if you're using a XAML-based framework like WPF, WinUI, or .NET MAUI, you often want to use `ObservableCollection` instead to enable data binding. + +Open the `EntityType.t4` template and find where it generates `List`. It looks like this: + +```T4 + if (navigation.IsCollection) + { +#> + public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new List<<#= targetType #>>(); +<# + } +``` + +Replace List with ObservableCollection. + +```T4 +public virtual ICollection<<#= targetType #>> <#= navigation.Name #> { get; } = new ObservableCollection<<#= targetType #>>(); +``` + +We also need to add a `using` directive to the scaffolded code. The usings are specified in a list near the top of the template. Add `System.Collections.ObjectModel` to the list. + +```C# +var usings = new List +{ + "System", + "System.Collections.Generic", + "System.Collections.ObjectModel" +}; +``` + +Test the changes by using the reverse engineering commands. The templates inside your project are used automatically by the commands. + +### [.NET Core CLI](#tab/dotnet-core-cli) + +```dotnetcli +dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook" Microsoft.EntityFrameworkCore.SqlServer +``` + +If you've ran the command previously, add the `--force` option to overwrite the existing files. + +### [Visual Studio](#tab/vs) + +```powershell +Scaffold-DbContext 'Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook' Microsoft.EntityFrameworkCore.SqlServer +``` + +If you've ran the command previously, add the `-Force` switch to overwrite the existing files. + +*** + +If you did everything correctly, the collection navigation properties should now use `ObservableCollection`. + +```C# +public virtual ICollection Albums { get; } = new ObservableCollection(); +``` + +## Updating templates + +When you add the default templates to your project, it creates a copy of them based on that version of EF Core. As bugs are fixed and features are added in subsequent versions of EF Core, your templates may become out of date. You should review the changes made in the EF Core templates and merge them into your customized templates. + +One way to review the changes made to the EF Core templates is to use git to compare them between versions. The following command will clone the EF Core repository and generate a diff of these files between versions 7.0.0 and 8.0.0. + +```console +git clone --no-checkout https://github.com/dotnet/efcore.git +cd efcore +git diff v7.0.0 v8.0.0 -- src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.tt src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.tt +``` + +Another way to review the changes is to download the two versions of [Microsoft.EntityFrameworkCore.Templates](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Templates) from NuGet, extract their contents (you can change the file extensions to .zip), and compare those files. + +Before adding the default templates to a new project, remember to update to the latest EF Core template package. + +```dotnetcli +dotnet new update +``` + +## Advanced usage + +### Ignoring the input model + +The `Model` and `EntityType` parameters represent one possible way of mapping to the database. You can choose to ignore or change parts of the model. For example, the navigation names we provide may not be ideal, and you can replace them with your own when scaffolding the code. Other things like constraint names and index filters are only used by Migrations and can safely be omitted from the model if you don't intend to use Migrations with the scaffolded code. Likewise, you may want to omit sequences or default constraints if they're not used by your app. + +When making advanced changes like this, just make sure the resulting model remains compatible with the database. Reviewing the SQL generated by `dbContext.Database.GenerateCreateScript()` is a good way to validate this. + +### Entity configuration classes + +For large models, the OnModelCreating method of the DbContext class can become unmanageably large. One way to address this is to use `IEntityTypeConfiguration` classes. See [Creating and configuring a model](xref:core/modeling/index#grouping-configuration) for more information about these classes. + +To scaffold these classes, you can use a third template called `EntityTypeConfiguration.t4`. Like the `EntityType.t4` template, it gets used for each entity type in the model and uses the `EntityType` template parameter. + +### Scaffolding other types of files + +The primary purpose of reverse engineering in EF Core is to scaffold a DbContext and entity types. However, there's nothing in the tools that require you to actually scaffold code. For example, you could instead scaffold an entity relationship diagram using [Mermaid](https://mermaid-js.github.io/). + +````T4 +<#@ output extension=".md" #> +<#@ assembly name="Microsoft.EntityFrameworkCore" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Relational" #> +<#@ assembly name="Microsoft.EntityFrameworkCore.Design" #> +<#@ parameter name="Model" type="Microsoft.EntityFrameworkCore.Metadata.IModel" #> +<#@ parameter name="Options" type="Microsoft.EntityFrameworkCore.Scaffolding.ModelCodeGenerationOptions" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="Microsoft.EntityFrameworkCore" #> +# <#= Options.ContextName #> + +```mermaid +erDiagram +<# + foreach (var entityType in Model.GetEntityTypes().Where(e => !e.IsSimpleManyToManyJoinEntityType())) + { +#> + <#= entityType.Name #> { + } +<# + foreach (var foreignKey in entityType.GetForeignKeys()) + { +#> + <#= entityType.Name #> <#= foreignKey.IsUnique ? "|" : "}" #>o--<#= foreignKey.IsRequired ? "|" : "o" #>| <#= foreignKey.PrincipalEntityType.Name #> : <#= foreignKey.GetConstraintName() #> +<# + } + + foreach (var skipNavigation in entityType.GetSkipNavigations().Where(n => n.IsLeftNavigation())) + { +#> + <#= entityType.Name #> }o--o{ <#= skipNavigation.TargetEntityType.Name #> : <#= skipNavigation.JoinEntityType.Name #> +<# + } + } +#> +``` +```` diff --git a/entity-framework/core/modeling/bulk-configuration.md b/entity-framework/core/modeling/bulk-configuration.md index 6d654f2ba4..cf4c4fca1e 100644 --- a/entity-framework/core/modeling/bulk-configuration.md +++ b/entity-framework/core/modeling/bulk-configuration.md @@ -52,6 +52,9 @@ And this example shows how to configure some facets on all properties of type `s > 4. Non-nullable value type > 5. Exact type +> [!NOTE] +> Data annotations do not override pre-convention configuration. + ### Ignoring types Pre-convention configuration also allows to ignore a type and prevent it from being discovered by conventions either as an entity type or as a property on an entity type: diff --git a/entity-framework/core/modeling/indexes.md b/entity-framework/core/modeling/indexes.md index 948d42f43f..7a75600654 100644 --- a/entity-framework/core/modeling/indexes.md +++ b/entity-framework/core/modeling/indexes.md @@ -61,6 +61,37 @@ By default, indexes aren't unique: multiple rows are allowed to have the same va Attempting to insert more than one entity with the same values for the index's column set will cause an exception to be thrown. +## Index sort order + +> [!NOTE] +> This feature is being introduced in EF Core 7.0. + +In most databases, each column covered by an index can be either ascending or descending. For indexes covering only one column, this typically does not matter: the database can traverse the index in reverse order asif needed. However, for composite indexes, the ordering can be crucial for good performance, and can mean the difference between an index getting used by a query or not. In general, the index columns' sort orders should correspond to those specified in the `ORDER BY` clause of your query. + +The index sort order is ascending by default. You can make all columns have descending order as follows: + +### [Data Annotations](#tab/data-annotations) + +[!code-csharp[Main](../../../samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescending.cs?name=IndexDescending&highlight=1)] + +### [Fluent API](#tab/fluent-api) + +[!code-csharp[Main](../../../samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescending.cs?name=IndexDescending&highlight=5)] + +*** + +You may also specify the sort order on a column-by-column basis as follows: + +### [Data Annotations](#tab/data-annotations) + +[!code-csharp[Main](../../../samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescendingAscending.cs?name=IndexDescendingAscending&highlight=1)] + +### [Fluent API](#tab/fluent-api) + +[!code-csharp[Main](../../../samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescendingAscending.cs?name=IndexDescendingAscending&highlight=5)] + +*** + ## Index name By convention, indexes created in a relational database are named `IX__`. For composite indexes, `` becomes an underscore separated list of property names. 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 159f71fd9f..a829258501 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,10 +2,1185 @@ title: What's New in EF Core 7.0 description: Overview of new features in EF Core 7.0 author: ajcvickers -ms.date: 12/17/2021 +ms.date: 08/12/2022 uid: core/what-is-new/ef-core-7 --- # What's New in EF Core 7.0 -New features introduced in EF Core 7.0 will be documented here. +EF Core 7.0 (EF7) is the next release after EF Core 6.0 and is scheduled for release in November 2022. See [_Plan for Entity Framework Core 7.0_](xref:core/what-is-new/ef-core-7.0/plan) for details and [_.NET Data Biweekly Updates (2022)_](https://github.com/dotnet/efcore/issues/27185) for progress on the plan. + +EF7 is currently in preview. The latest release on NuGet is [EF Core 7.0 Preview 7](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore/7.0.0-preview.7.22376.2). + +EF7 is also available as [daily builds](https://github.com/dotnet/efcore/blob/main/docs/DailyBuilds.md) which contain all the latest EF7 features and API tweaks. The samples here make use of these daily builds. + +> [!TIP] +> You can run and debug into the samples by [downloading the sample code from GitHub](https://github.com/dotnet/EntityFramework.Docs). Each section links to the source code specific to that section. + +EF7 targets .NET 6, and so can be used with either [.NET 6 (LTS)](https://dotnet.microsoft.com/download/dotnet/6.0) or [.NET 7](https://dotnet.microsoft.com/download/dotnet/7.0). + +## JSON Columns + +Most relational databases support columns that contain JSON documents. The JSON in these columns can be drilled into with queries. This allows, for example, filtering and sorting by the elements of the documents, as well as projection of elements out of the documents into results. JSON columns allow relational databases to take on some of the characteristics of document databases, creating a useful hybrid between the two. + +EF7 contains provider-agnostic support for JSON columns, with an implementation for SQL Server. This support allows mapping of aggregates built from .NET types to JSON documents. Normal LINQ queries can be used on the aggregates, and these will be translated to the appropriate query constructs needed to drill into the JSON. EF7 also supports updating and saving changes to the JSON documents. + +> [!NOTE] +> SQLite support for JSON is [planned for post EF7](https://github.com/dotnet/efcore/issues/28816). The PostgreSQL and Pomelo MySQL providers already contain some support for JSON columns. We will be working with the authors of those providers to align JSON support across all providers. + +### Mapping to JSON columns + +In EF Core, aggregate types are defined using `OwnsOne` and `OwnsMany`. For example, consider an aggregate type to store contact information: + + +[!code-csharp[ContactDetailsAggregate](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=ContactDetailsAggregate)] + +This can then be used in an "owner" entity type, for example, to store the contact details of an author: + +```csharp +public class Author +{ + public int Id { get; set; } + public string Name { get; set; } + public ContactDetails Contact { get; set; } +} +``` + +The aggregate type is configured in `OnModelCreating` using `OwnsOne`: + + +[!code-csharp[TableSharingAggregate](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=TableSharingAggregate)] + +By default, relational database providers map aggregate types like this to the same table as the owning entity type. That is, each property of the `ContactDetails` and `Address` classes are mapped to a column in the `Authors` table. + +Some saved authors with contact details will look like this: + +**Authors** + +| Id | Name | Contact\_Address\_Street | Contact\_Address\_City | Contact\_Address\_Postcode | Contact\_Address\_Country | Contact\_Phone | +|:----|:-----------------|:-------------------------|:-----------------------|:---------------------------|:--------------------------|:---------------| +| 1 | Maddy Montaquila | 1 Main St | Camberwick Green | CW1 5ZH | UK | 01632 12345 | +| 2 | Jeremy Likness | 2 Main St | Chigley | CW1 5ZH | UK | 01632 12346 | +| 3 | Daniel Roth | 3 Main St | Camberwick Green | CW1 5ZH | UK | 01632 12347 | +| 4 | Arthur Vickers | 15a Main St | Chigley | CW1 5ZH | United Kingdom | 01632 22345 | +| 5 | Brice Lambson | 4 Main St | Chigley | CW1 5ZH | UK | 01632 12349 | + +If desired, each entity type making up the aggregate can be mapped to its own table instead: + + +[!code-csharp[TableMappedAggregate](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=TableMappedAggregate)] + +The same data is then stored across three tables: + +**Authors** + +| Id | Name | +|:----|:-----------------| +| 1 | Maddy Montaquila | +| 2 | Jeremy Likness | +| 3 | Daniel Roth | +| 4 | Arthur Vickers | +| 5 | Brice Lambson | + +**Contacts** + +| AuthorId | Phone | +|:---------|:------------| +| 1 | 01632 12345 | +| 2 | 01632 12346 | +| 3 | 01632 12347 | +| 4 | 01632 22345 | +| 5 | 01632 12349 | + +**Addresses** + +| ContactDetailsAuthorId | Street | City | Postcode | Country | +|:-----------------------|:------------|:-----------------|:---------|:---------------| +| 1 | 1 Main St | Camberwick Green | CW1 5ZH | UK | +| 2 | 2 Main St | Chigley | CW1 5ZH | UK | +| 3 | 3 Main St | Camberwick Green | CW1 5ZH | UK | +| 4 | 15a Main St | Chigley | CW1 5ZH | United Kingdom | +| 5 | 4 Main St | Chigley | CW1 5ZH | UK | + +Now, for the interesting part. In EF7, the `ContactDetails` aggregate type can be mapped to a JSON column. This requires just one call to `ToJson()` when configuring the aggregate type: + + +[!code-csharp[JsonColumnAggregate](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=JsonColumnAggregate)] + +The `Authors` table will now contain a JSON column for `ContactDetails` populated with a JSON document for each author: + +**Authors** + +| Id | Name | Contact | +|:----|:-----------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1 | Maddy Montaquila | {
  "Phone":"01632 12345",
  "Address": {
    "City":"Camberwick Green",
    "Country":"UK",
    "Postcode":"CW1 5ZH",
    "Street":"1 Main St"
  }
} | +| 2 | Jeremy Likness | {
  "Phone":"01632 12346",
  "Address": {
    "City":"Chigley",
    "Country":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"2 Main St"
  }
} | +| 3 | Daniel Roth | {
  "Phone":"01632 12347",
  "Address": {
    "City":"Camberwick Green",
    "Country":"UK",
    "Postcode":"CW1 5ZH",
    "Street":"3 Main St"
  }
} | +| 4 | Arthur Vickers | {
  "Phone":"01632 12348",
  "Address": {
    "City":"Chigley",
    "Country":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"15a Main St"
  }
} | +| 5 | Brice Lambson | {
  "Phone":"01632 12349",
  "Address": {
    "City":"Chigley",
    "Country":"UK",
    "Postcode":"CH1 5ZH",
    "Street":"4 Main St"
  }
} | + +> [!TIP] +> This use of aggregates is very similar to the way JSON documents are mapped when using the EF Core provider for Azure Cosmos DB. JSON columns bring the capabilities of using EF Core against document databases to documents embedded in a relational database. + +The JSON documents shown above are very simple, but this mapping capability can also be used with more complex document structures. For example, consider an aggregate type used to represent metadata about a post: + + +[!code-csharp[PostMetadataAggregate](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=PostMetadataAggregate)] + +This aggregate type contains several nested types and collections. Calls to `OwnsOne` and `OwnsMany` are used to map this aggregate type: + + +[!code-csharp[PostMetadataConfig](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=PostMetadataConfig)] + +With this mapping, EF7 can create and query into a complex JSON document like this: + +```json +{ + "Views": 5085, + "TopGeographies": [ + { + "Browsers": "Firefox, Netscape", + "Count": 924, + "Latitude": 110.793, + "Longitude": 39.2431 + }, + { + "Browsers": "Firefox, Netscape", + "Count": 885, + "Latitude": 133.793, + "Longitude": 45.2431 + } + ], + "TopSearches": [ + { + "Count": 9359, + "Term": "Search #1" + } + ], + "Updates": [ + { + "PostedFrom": "127.0.0.1", + "UpdatedBy": "Admin", + "UpdatedOn": "1996-02-17T19:24:29.5429092Z", + "Commits": [] + }, + { + "PostedFrom": "127.0.0.1", + "UpdatedBy": "Admin", + "UpdatedOn": "2019-11-24T19:24:29.5429093Z", + "Commits": [ + { + "Comment": "Commit #1", + "CommittedOn": "2022-08-21T00:00:00+01:00" + } + ] + }, + { + "PostedFrom": "127.0.0.1", + "UpdatedBy": "Admin", + "UpdatedOn": "1997-05-28T19:24:29.5429097Z", + "Commits": [ + { + "Comment": "Commit #1", + "CommittedOn": "2022-08-21T00:00:00+01:00" + }, + { + "Comment": "Commit #2", + "CommittedOn": "2022-08-21T00:00:00+01:00" + } + ] + } + ] +} +``` + +> [!NOTE] +> Mapping spatial types directly to JSON is not yet supported. The document above uses `double` values as a workaround. Vote for [Support spatial types in JSON columns](https://github.com/dotnet/efcore/issues/28811) if this is something you are interested in. + +> [!NOTE] +> Mapping collections of primitive types to JSON is not yet supported. The document above uses a value converter to transform the collection into a comma-separated string. Vote for [Json: add support for collection of primitive types](https://github.com/dotnet/efcore/issues/28688) if this is something you are interested in. + +### Queries into JSON columns + +Queries into JSON columns work just the same as querying into any other aggregate type in EF Core. That is, just use LINQ! Here are some examples. + +A query for all authors that live in Chigley: + + +[!code-csharp[AuthorsInChigley](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=AuthorsInChigley)] + +This query generates the following SQL when using SQL Server: + +```sql +SELECT [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$') +FROM [Authors] AS [a] +WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' +``` + +Notice the use of `JSON_VALUE` to get the `City` from the `Address` inside the JSON document. + +`Select` can be used to extract and project elements from the JSON document: + + +[!code-csharp[PostcodesInChigley](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=PostcodesInChigley)] + +This query generates the following SQL: + +```sql +SELECT CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max)) +FROM [Authors] AS [a] +WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' +``` + +Here's an example that does a bit more in the filter and projection, and also orders by the phone number in the JSON document: + + +[!code-csharp[OrderedAddresses](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=OrderedAddresses)] + +This query generates the following SQL: + +```sql +SELECT (((((([a].[Name] + N' (') + CAST(JSON_VALUE([a].[Contact],'$.Address.Street') AS nvarchar(max))) + N', ') + CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max))) + N' ') + CAST(JSON_VALUE([a].[Contact],'$.Address.Postcode') AS nvarchar(max))) + N')' +FROM [Authors] AS [a] +WHERE (CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Chigley' AND CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max)) IS NOT NULL) OR ([a].[Name] LIKE N'D%') +ORDER BY CAST(JSON_VALUE([a].[Contact],'$.Phone') AS nvarchar(max)) +``` + +And when the JSON document contains collections, then these can be projected out in the results: + + +[!code-csharp[PostsWithViews](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=PostsWithViews)] + +This query generates the following SQL: + +```sql +SELECT [a].[Name], CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int), JSON_QUERY([p].[Metadata],'$.TopSearches'), [p].[Id], JSON_QUERY([p].[Metadata],'$.Updates') +FROM [Posts] AS [p] +LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id] +WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000 +``` + +> [!NOTE] +> More complex queries involving JSON collections require `jsonpath` support. Vote for [Support jsonpath querying](https://github.com/dotnet/efcore/issues/28616) if this is something you are interested in. + +## ExecuteUpdate and ExecuteDelete (Bulk updates) + +By default, EF Core [tracks changes to entities](xref:core/change-tracking/index), and then [sends updates to the database](xref:core/saving/index) when one of the `SaveChanges` methods is called. Changes are only sent for properties and relationships that have actually changed. Also, the tracked entities remain in sync with the changes sent to the database. This mechanism is an efficient and convenient way to send general-purpose inserts, updates, and deletes to the database. These changes are also batched to reduce the number of database round-trips. + +However, it is sometimes useful to execute update or delete commands on the database without involving the change tracker. EF7 enables this with the new `ExecuteUpdate` and `ExecuteDelete` methods. These methods are applied to a LINQ query and will update or delete entities in the database based on the results of that query. Many entities can be updated with a single command and the entities are not loaded into memory, which means this can result in more efficient updates and deletes. + +However, keep in mind that: + +- The specific changes to make must be specified explicitly; they are not automatically detected by EF Core. +- Any tracked entities will not be kept in sync. +- Additional commands may need to be sent in the correct order so as not to violate database constraints. For example deleting dependents before a principal can be deleted. + +All of this means that the `ExecuteUpdate` and `ExecuteDelete` methods complement, rather than replace, the existing `SaveChanges` mechanism. + +### Sample model + +The examples below use a simple model with blogs, posts, tags, and authors: + + +[!code-csharp[BlogsModel](../../../../samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs?name=BlogsModel)] + +> [!TIP] +> The sample model can be found in [BlogsContext.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs). + +### Basic `ExecuteDelete` examples + +> [!TIP] +> The code shown here comes from [ExecuteDeleteSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs). + +Calling `ExecuteDelete` or `ExecuteDeleteAsync` on a `DbSet` immediately deletes all entities of that `DbSet` from the database. For example, to delete all `Tag` entities: + + +[!code-csharp[DeleteAllTags](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllTags)] + +This executes the following SQL when using SQL Server: + +```sql +DELETE FROM [t] +FROM [Tags] AS [t] +``` + +More interestingly, the query can contain a filter. For example: + + +[!code-csharp[DeleteTagsContainingDotNet](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteTagsContainingDotNet)] + +This executes the following SQL: + +```sql +DELETE FROM [t] +FROM [Tags] AS [t] +WHERE [t].[Text] LIKE N'%.NET%' +``` + +The query can also use more complex filters, including navigations to other types. For example, to delete tags only from old blog posts: + + +[!code-csharp[DeleteTagsFromOldPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteTagsFromOldPosts)] + +Which executes: + +```sql +DELETE FROM [t] +FROM [Tags] AS [t] +WHERE NOT EXISTS ( + SELECT 1 + FROM [PostTag] AS [p] + INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id] + WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022)) +``` + +### Basic `ExecuteUpdate` examples + +> [!TIP] +> The code shown here comes from [ExecuteUpdateSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs). + +`ExecuteUpdate` and `ExecuteUpdateAsync` behave in a very similar way to the `ExecuteDelete` methods. The main difference is that an update requires knowing _which_ properties to update, and _how_ to update them. This is achieved using one or more calls to `SetProperty`. For example, to update the `Name` of every blog: + + +[!code-csharp[UpdateAllBlogs](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs?name=UpdateAllBlogs)] + +The first parameter of `SetProperty` specifies which property to update; in this case, `Blog.Name`. The second parameter specifies how the new value should be calculated; in this case, by taking the existing value and appending `"*Featured!*"`. The resulting SQL is: + +```sql +UPDATE [b] + SET [b].[Name] = [b].[Name] + N' *Featured!*' +FROM [Blogs] AS [b] +``` + +As with `ExecuteDelete`, the query can be used to filter which entities are updated. In addition, multiple calls to `SetProperty` can be used to update more than one property on the target entity. For example, to update the `Title` and `Content` of all posts published before 2022: + + +[!code-csharp[UpdateOldPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs?name=UpdateOldPosts)] + +In this case the generated SQL is a bit more complicated: + +```sql +UPDATE [p] + SET [p].[Content] = (([p].[Content] + N' ( This content was published in ') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')', + [p].[Title] = (([p].[Title] + N' (') + COALESCE(CAST(DATEPART(year, [p].[PublishedOn]) AS nvarchar(max)), N'')) + N')' +FROM [Posts] AS [p] +WHERE DATEPART(year, [p].[PublishedOn]) < 2022 +``` + +Finally, again as with `ExecuteDelete`, the filter can reference other tables. For example, to update all tags from old posts: + + +[!code-csharp[UpdateTagsOnOldPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs?name=UpdateTagsOnOldPosts)] + +Which generates: + +```sql +UPDATE [t] + SET [t].[Text] = [t].[Text] + N' (old)' +FROM [Tags] AS [t] +WHERE NOT EXISTS ( + SELECT 1 + FROM [PostTag] AS [p] + INNER JOIN [Posts] AS [p0] ON [p].[PostsId] = [p0].[Id] + WHERE [t].[Id] = [p].[TagsId] AND NOT (DATEPART(year, [p0].[PublishedOn]) < 2022)) +``` + +### Inheritance and multiple tables + +`ExecuteUpdate` and `ExecuteDelete` can only act on a single table. This has implications when working with different [inheritance mapping strategies](xref:core/modeling/inheritance). Generally, there are no problems when using the TPH mapping strategy, since there is only one table to modify. For example, deleting all `FeaturedPost` entities: + + +[!code-csharp[DeleteFeaturedPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteFeaturedPosts)] + +Generates the following SQL when using TPH mapping: + +```sql +DELETE FROM [p] +FROM [Posts] AS [p] +WHERE [p].[Discriminator] = N'FeaturedPost' +``` + +There are also no issues for this case when using the TPC mapping strategy, since again only changes to a single table are needed: + +```sql +DELETE FROM [f] +FROM [FeaturedPosts] AS [f] +``` + +However, attempting this when using the TPT mapping strategy will fail since it would require deleting rows from two different tables. + +Adding a filter to the query often means the operation will fail with both the TPC and TPT strategies. This is again because the rows may need to be deleted from multiple tables. For example, this query: + + +[!code-csharp[DeletePostsForGivenAuthor](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeletePostsForGivenAuthor)] + +Generates the following SQL when using TPH: + +```sql +DELETE FROM [p] +FROM [Posts] AS [p] +LEFT JOIN [Authors] AS [a] ON [p].[AuthorId] = [a].[Id] +WHERE [a].[Name] IS NOT NULL AND ([a].[Name] LIKE N'Arthur%') +``` + +But fails when using TPC or TPT. + +> [!TIP] +> [Issue #10879](https://github.com/dotnet/efcore/issues/28520) tracks adding support for automatically sending multiple commands in these scenarios. Vote for this issue if it's something you would like to see implemented. + +### `ExecuteDelete` and relationships + +As mentioned above, it may be necessary to delete or update dependent entities before the principal of a relationship can be deleted. For example, each `Post` is a dependent of its associated `Author`. This means that an author cannot be deleted if a post still references it; doing so will violate the foreign key constraint in the database. For example, attempting this: + +```csharp +await context.Authors.ExecuteDeleteAsync(); +``` + +Will result in the following exception on SQL Server: + +> Microsoft.Data.SqlClient.SqlException (0x80131904): The DELETE statement conflicted with the REFERENCE constraint "FK_Posts_Authors_AuthorId". The conflict occurred in database "TphBlogsContext", table "dbo.Posts", column 'AuthorId'. +The statement has been terminated. + +To fix this, we must first either delete the posts, or sever the relationship between each post and its author by setting `AuthorId` foreign key property to null. For example, using the delete option: + + +[!code-csharp[DeleteAllAuthors](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllAuthors)] + +This results in two separate commands; the first to delete the dependents: + +```sql +DELETE FROM [p] +FROM [Posts] AS [p] +``` + +And the second to delete the principals: + +```sql +DELETE FROM [a] +FROM [Authors] AS [a] +``` + +> [!IMPORTANT] +> Multiple `ExecuteDelete` and `ExecuteUpdate` commands will not be contained in a single transaction by default. However, the [DbContext transaction APIs](xref:core/saving/transactions) can be used in the normal way to wrap these commands in a transaction. + +> [!TIP] +> Sending these commands in a single round-trip depends on [Issue #10879](https://github.com/dotnet/efcore/issues/10879). Vote for this issue if it's something you would like to see implemented. + +Configuring [cascade deletes](xref:core/saving/cascade-delete) in the database can be very useful here. In our model, the relationship between `Blog` and `Post` is required, which causes EF Core to configure a cascade delete by convention. This means when a blog is deleted in the database, then all its dependent posts will also be deleted. It then follows that to delete all blogs and posts we need only delete the blogs: + + +[!code-csharp[DeleteAllBlogsAndPosts](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllBlogsAndPosts)] + +This results in the following SQL: + +```sql +DELETE FROM [b] +FROM [Blogs] AS [b] +``` + +Which, as it is deleting a blog, will also cause all related posts to be deleted by the configured cascade delete. + +## Table-per-concrete-type (TPC) inheritance mapping + +By default, EF Core maps an inheritance hierarchy of .NET types to a single database table. This is known as the [table-per-hierarchy (TPH)](xref:core/modeling/inheritance#table-per-hierarchy-and-discriminator-configuration) mapping strategy. EF Core 5.0 introduced the [table-per-type (TPT)](xref:core/modeling/inheritance#table-per-type-configuration) strategy, which supports mapping each .NET type to a different database table. EF7 introduces the table-per-concrete-type (TPC) strategy. TPC also maps .NET types to different tables, but in a way that addresses some common performance issues with the TPT strategy. + +> [!TIP] +> The code shown here comes from [TpcInheritanceSample.cs](https://github.com/dotnet/EntityFramework.Docs/tree/main/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs). + +> [!TIP] +> The EF Team demonstrated and talked in depth about TPC mapping in an episode of the .NET Data Community Standup. As with [all Community Standup episodes](https://aka.ms/efstandups), you can [watch the TPC episode now on YouTube](https://youtu.be/HaL6DKW1mrg). + +### TPC database schema + +The TPC strategy is similar to the TPT strategy except that a different table is created for every _concrete_ type in the hierarchy, but tables are **not** created for _abstract_ types--hence the name “table-per-concrete-type”. As with TPT, the table itself indicates the type of the object saved. However, unlike TPT mapping, each table contains columns for every property in the concrete type and its base types. TPC database schemas are denormalized. + +For example, consider mapping this hierarchy: + + +[!code-csharp[AnimalsHierarchy](../../../../samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs?name=AnimalsHierarchy)] + +When using SQL Server, the tables created for this hierarchy are: + +```sql +CREATE TABLE [Cats] ( + [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]), + [Name] nvarchar(max) NOT NULL, + [FoodId] uniqueidentifier NULL, + [Vet] nvarchar(max) NULL, + [EducationLevel] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Cats] PRIMARY KEY ([Id])); + +CREATE TABLE [Dogs] ( + [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]), + [Name] nvarchar(max) NOT NULL, + [FoodId] uniqueidentifier NULL, + [Vet] nvarchar(max) NULL, + [FavoriteToy] nvarchar(max) NOT NULL, + CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id])); + +CREATE TABLE [FarmAnimals] ( + [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]), + [Name] nvarchar(max) NOT NULL, + [FoodId] uniqueidentifier NULL, + [Value] decimal(18,2) NOT NULL, + [Species] nvarchar(max) NOT NULL, + CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id])); + +CREATE TABLE [Humans] ( + [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]), + [Name] nvarchar(max) NOT NULL, + [FoodId] uniqueidentifier NULL, + [FavoriteAnimalId] int NULL, + CONSTRAINT [PK_Humans] PRIMARY KEY ([Id])); +``` + +Notice that: + +- There are no tables for the `Animal` or `Pet` types, since these are `abstract` in the object model. Remember that C# does not allow instances of abstract types, and there is therefore no situation where an abstract type instance will be saved to the database. +- The mapping of properties in base types is repeated for each concrete type. For example, every table has a `Name` column, and both Cats and Dogs have a `Vet` column. + +- Saving some data into this database results in the following: + +**Cats table** + +| Id | Name | FoodId | Vet | EducationLevel | +|:----|:-------|:-------------------------------------|:---------------------|:---------------| +| 1 | Alice | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | MBA | +| 2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Preschool | +| 8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Bothell Pet Hospital | BSc | + +**Dogs table** + +| Id | Name | FoodId | Vet | FavoriteToy | +|:----|:------|:-------------------------------------|:---------|:-------------| +| 3 | Toast | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | Mr. Squirrel | + +**FarmAnimals table** + +| Id | Name | FoodId | Value | Species | +|:----|:------|:-------------------------------------|:-------|:-----------------------| +| 4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100.00 | Equus africanus asinus | + +**Humans table** + +| Id | Name | FoodId | FavoriteAnimalId | +|:----|:-------|:-------------------------------------|:---------------------------------------------------------------------------| +| 5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | -2147482646 // See [#28654](https://github.com/dotnet/efcore/issues/28654) | +| 6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | -2147482644 // See [#28654](https://github.com/dotnet/efcore/issues/28654) | +| 9 | Katie | null | -2147482640 // See [#28654](https://github.com/dotnet/efcore/issues/28654) | + +Notice that, unlike with TPT mapping, all the information for a single object is contained in a single table. And, unlike with TPH mapping, there is no combination of column and row in any table where that is never used by the model. We'll see below how these characteristics can be important for queries and storage. + +### Configuring TPC inheritance + +All types in an inheritance hierarchy must be explicitly included in the model when mapping the hierarchy with EF Core. This can be done by creating `DbSet` properties on your `DbContext` for every type: + + +[!code-csharp[AnimalSets](../../../../samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs?name=AnimalSets)] + +Or by using the `Entity` method in `OnModelCreating`: + + +[!code-csharp[AnimalsInModelBuilder](../../../../samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs?name=AnimalsInModelBuilder)] + +> [!IMPORTANT] +> This is different from the legacy EF6 behavior, where derived types of mapped base types would be automatically discovered if they were contained in the same assembly. + +Nothing else needs to be done to map the hierarchy as TPH, since it is the default strategy. However, starting with EF7, TPH can made explicit by calling `UseTphMappingStrategy` on the base type of the hierarchy: + + +[!code-csharp[UseTphMappingStrategy](../../../../samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs?name=UseTphMappingStrategy)] + +To use TPT instead, change this to `UseTptMappingStrategy`: + + +[!code-csharp[UseTptMappingStrategy](../../../../samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs?name=UseTptMappingStrategy)] + +Likewise, `UseTpcMappingStrategy` is used to configure TPC: + + +[!code-csharp[UseTpcMappingStrategy](../../../../samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs?name=UseTpcMappingStrategy)] + +In each case, the table name used for each type is taken from the `DbSet` property name on your `DbContext`, or [can be configured](xref:core/modeling/entity-types#table-name) using the `ToTable` builder method, or the `[Table]` attribute. + +### TPC query performance + +For queries, the TPC strategy is an improvement over TPT because it ensures that the information for a given entity instance is always stored in a single table. This means the TPC strategy can be useful when the mapped hierarchy is large and has many concrete (usually leaf) types, each with a large number of properties, and where only a small subset of types are used in most queries. + +The SQL generated for three simple LINQ queries can be used to observe where TPC does well when compared to TPH and TPT. These queries are: + +1. A query that returns entities of all types in the hierarchy: + + ```csharp + context.Animals.ToList(); + ``` + +2. A query that returns entities from a subset of types in the hierarchy: + + ```csharp + context.Pets.ToList(); + ``` + +3. A query that returns only entities from a single leaf type in the hierarchy: + + ```csharp + context.Cats.ToList(); + ``` + +#### TPH queries + +When using TPH, all three queries only query a single table, but with different filters on the discriminator column: + +1. TPH SQL returning entities of all types in the hierarchy: + + ```sql + SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Species], [a].[Value], [a].[FavoriteAnimalId], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy] + FROM [Animals] AS [a] + ``` + +2. TPH SQL returning entities from a subset of types in the hierarchy: + + ```sql + SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel], [a].[FavoriteToy] + FROM [Animals] AS [a] + WHERE [a].[Discriminator] IN (N'Cat', N'Dog') + ``` + +3. TPH SQL returning only entities from a single leaf type in the hierarchy: + + ```sql + SELECT [a].[Id], [a].[Discriminator], [a].[FoodId], [a].[Name], [a].[Vet], [a].[EducationLevel] + FROM [Animals] AS [a] + WHERE [a].[Discriminator] = N'Cat' + ``` + +All these queries should perform well, especially with an appropriate database index on the discriminator column. + +#### TPT queries + +When using TPT, all of these queries require joining multiple tables, since the data for any given concrete type is split across many tables: + +1. TPT SQL returning entities of all types in the hierarchy: + + ```sql + SELECT [a].[Id], [a].[FoodId], [a].[Name], [f].[Species], [f].[Value], [h].[FavoriteAnimalId], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE + WHEN [d].[Id] IS NOT NULL THEN N'Dog' + WHEN [c].[Id] IS NOT NULL THEN N'Cat' + WHEN [h].[Id] IS NOT NULL THEN N'Human' + WHEN [f].[Id] IS NOT NULL THEN N'FarmAnimal' + END AS [Discriminator] + FROM [Animals] AS [a] + LEFT JOIN [FarmAnimals] AS [f] ON [a].[Id] = [f].[Id] + LEFT JOIN [Humans] AS [h] ON [a].[Id] = [h].[Id] + LEFT JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id] + LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id] + LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id] + ``` + +2. TPT SQL returning entities from a subset of types in the hierarchy: + + ```sql + SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel], [d].[FavoriteToy], CASE + WHEN [d].[Id] IS NOT NULL THEN N'Dog' + WHEN [c].[Id] IS NOT NULL THEN N'Cat' + END AS [Discriminator] + FROM [Animals] AS [a] + INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id] + LEFT JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id] + LEFT JOIN [Dogs] AS [d] ON [a].[Id] = [d].[Id] + ``` + +3. TPT SQL returning only entities from a single leaf type in the hierarchy: + + ```sql + SELECT [a].[Id], [a].[FoodId], [a].[Name], [p].[Vet], [c].[EducationLevel] + FROM [Animals] AS [a] + INNER JOIN [Pets] AS [p] ON [a].[Id] = [p].[Id] + INNER JOIN [Cats] AS [c] ON [a].[Id] = [c].[Id] + ``` + +> [!NOTE] +> EF Core uses “discriminator synthesis” to determine which table the data comes from, and hence the correct type to use. This works because the LEFT JOIN returns nulls for the dependent ID column (the “sub-tables”) which aren’t the correct type. So for a dog, `[d].[Id]` will be non-null, and all the other (concrete) IDs will be null. + +All of these queries can suffer from performance issues due to the table joins. This is why TPT is never a good choice for query performance. + +#### TPC queries + +TPC improves over TPT for all of these queries because the number of tables that need to be queried is reduced. In addition, the results from each table are combined using `UNION ALL`, which can be considerably faster than a table join, since it does not need to perform any matching between rows or de-duplication of rows. + +1. TPC SQL returning entities of all types in the hierarchy: + + ```sql + SELECT [f].[Id], [f].[FoodId], [f].[Name], [f].[Species], [f].[Value], NULL AS [FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'FarmAnimal' AS [Discriminator] + FROM [FarmAnimals] AS [f] + UNION ALL + SELECT [h].[Id], [h].[FoodId], [h].[Name], NULL AS [Species], NULL AS [Value], [h].[FavoriteAnimalId], NULL AS [Vet], NULL AS [EducationLevel], NULL AS [FavoriteToy], N'Human' AS [Discriminator] + FROM [Humans] AS [h] + UNION ALL + SELECT [c].[Id], [c].[FoodId], [c].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator] + FROM [Cats] AS [c] + UNION ALL + SELECT [d].[Id], [d].[FoodId], [d].[Name], NULL AS [Species], NULL AS [Value], NULL AS [FavoriteAnimalId], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator] + FROM [Dogs] AS [d] + ``` + +2. TPC SQL returning entities from a subset of types in the hierarchy: + + ```sql + SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel], NULL AS [FavoriteToy], N'Cat' AS [Discriminator] + FROM [Cats] AS [c] + UNION ALL + SELECT [d].[Id], [d].[FoodId], [d].[Name], [d].[Vet], NULL AS [EducationLevel], [d].[FavoriteToy], N'Dog' AS [Discriminator] + FROM [Dogs] AS [d] + ``` + +3. TPC SQL returning only entities from a single leaf type in the hierarchy: + + ```sql + SELECT [c].[Id], [c].[FoodId], [c].[Name], [c].[Vet], [c].[EducationLevel] + FROM [Cats] AS [c] + ``` + +Even though TPC is better than TPT for all of these queries, the TPH queries are still better when returning instances of multiple types. This is one of the reasons that TPH is the default strategy used by EF Core. + +As the SQL for query #3 shows, TPC really excels when querying for entities of a single leaf type. The query only uses a single table and needs no filtering. + +### TPC inserts and updates + +TPC also performs well when saving a new entity, since this requires inserting only a single row into a single table. This is also true for TPH. With TPT, rows must be inserted into many tables, which will perform less well. + +The same is often true for updates, although in this case if all columns being updated are in the same table, even for TPT, then the difference may not be significant. + +### Space considerations + +Both TPT and TPC can use less storage than TPH when there are many subtypes with many properties that are often not used. This is because every row in the TPH table must store a `NULL` for each of these unused properties. In practice, this is rarely an issue, but it could be worth considering when storing large amounts of data with these characteristics. + +> [!TIP] +> If your database system supports it (e.g. SQL Server), then consider using "sparse columns" for TPH columns that will be rarely populated. + +### Key generation + +The inheritance mapping strategy chosen has consequences for how primary key values are generated and managed. Keys in TPH are easy, since each entity instance is represented by a single row in a single table. Any kind of key value generation can be used, and no additional constraints are needed. + +For the TPT strategy, there is always a row in the table mapped to the base type of the hierarchy. Any kind of key generation can be used on this row, and the keys for other tables are linked to this table using foreign key constraints. + +Things get a bit more complicated for TPC. First, it’s important to understand that EF Core requires that all entities in a hierarchy must have a unique key value, even if the entities have different types. So, using our example model, a Dog cannot have the same Id key value as a Cat. Second, unlike TPT, there is no common table that can act as the single place where key values live and can be generated. This means a simple `Identity` column cannot be used. + +For databases that support sequences, key values can be generated by using a single sequence referenced in the default constraint for each table. This is the strategy used in the TPC tables shown above, where each table has the following: + +```sql +[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]) +``` + +`AnimalSequence` is a database sequence created by EF Core. This strategy is used by default for TPC hierarchies when using the EF Core database provider for SQL Server. Database providers for other databases that support sequences should have a similar default. Other key generation strategies that use sequences, such as Hi-Lo patterns, may also be used with TPC. + +While standard Identity columns will not work with TPC, it is possible to use Identity columns if each table is configured with an appropriate seed and increment such that the values generated for each table will never conflict. For example: + + +[!code-csharp[UsingIdentity](../../../../samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs?name=UsingIdentity)] + +SQLite does not support sequences or Identity seed/increment, and hence integer key value generation is not supported when using SQLite with the TPC strategy. However, client-side generation or globally unique keys--for example, GUID keys--are supported on any database, including SQLite. + +### Foreign key constraints + +The TPC mapping strategy creates a denormalized SQL schema--this is one reason why some database purists are against it. For example, consider the foreign key column `FavoriteAnimalId`. The value in this column must match the primary key value of some animal. This can be enforced in the database with a simple FK constraint when using TPH or TPT. For example: + +```sql +CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id]) +``` + +But when using TPC, the primary key for an animal is stored in the table for the concrete type of that animal. For example, a cat's primary key is stored in the `Cats.Id` column, while a dog's primary key is stored in the `Dogs.Id` column, and so on. This means an FK constraint cannot be created for this relationship. + +In practice, this is not a problem as long as the application does not attempt to insert invalid data. For example, if all the data is inserted by EF Core and uses navigations to relate entities, then it is guaranteed that the FK column will contain valid PK value at all times. + +### Summary and guidance + +In summary, TPC is a good mapping strategy to use when your code will mostly query for entities of a single leaf type. This is because the storage requirements are smaller, and there is no discriminator column that may need an index. Inserts and updates are also efficient. + +That being said, TPH is usually fine for most applications, and is a good default for a wide range of scenarios, so don't add the complexity of TPC if you don't need it. Specifically, if your code will mostly query for entities of many types, such as writing queries against the base type, then lean towards TPH over TPC. + +Use TPT only if constrained to do so by external factors. + +## Custom Reverse Engineering Templates + +You can now customize the scaffolded code when reverse engineering an EF model from a database. Get started by adding the default templates to your project: + +```dotnetcli +dotnet new install Microsoft.EntityFrameworkCore.Templates::7.0.0-* +dotnet new ef-templates +``` + +The templates can then be customize and will automatically be used by `dotnet ef dbcontext scaffold` and `Scaffold-DbContext`. + +For more details, see [Custom Reverse Engineering Templates](xref:core/managing-schemas/scaffolding/templates). diff --git a/entity-framework/dotnet-data/index.yml b/entity-framework/dotnet-data/index.yml index 561b8ce04f..2f181aea9f 100644 --- a/entity-framework/dotnet-data/index.yml +++ b/entity-framework/dotnet-data/index.yml @@ -1,11 +1,11 @@ ### YamlMime:Hub -title: .NET Data documentation -summary: .NET is the open source and cross-platform for integrating data, data services and data backends with your applications. +title: .NET data documentation +summary: .NET is the open source and cross-platform solution for integrating data, data services, and data backends with your applications. brand: dotnet metadata: - title: .NET Data documentation + title: .NET data documentation description: Welcome to the .NET data hub! What data-related task would you like to perform? This page is your gateway to documentation and tutorials for the various ways you interact with data from .NET. ms.product: entity-framework ms.topic: hub-page @@ -17,17 +17,7 @@ metadata: # Maximum of 8 items highlightedContent: # itemType: architecture | concept | deploy | download | get-started | how-to-guide | learn | overview | quickstart | reference | tutorial | video | whats-new - items: - # Card - - title: "LINQ overview" - itemType: overview - url: /dotnet/standard/linq - - # Card - - title: "SQLite overview" - itemType: overview - url: /dotnet/standard/data/sqlite - + items: # Card - title: "Microsoft SQL documentation" itemType: reference @@ -38,6 +28,11 @@ highlightedContent: itemType: reference url: /azure/cosmos-db/ + # Card + - title: "LINQ overview" + itemType: overview + url: /dotnet/standard/linq + # Card - title: "Entity Framework Core (Overview)" itemType: overview @@ -47,6 +42,11 @@ highlightedContent: - title: "Entity Framework Core (Tutorial)" itemType: learn url: /learn/modules/persist-data-ef-core/ + + # Card + - title: "SQLite overview" + itemType: overview + url: /dotnet/standard/data/sqlite # Card - title: "What is ML.NET?" @@ -132,9 +132,9 @@ additionalContent: - url: /ef/core/ text: "Overview" - url: /ef/core/get-started/overview/first-app - text: "Create you first EF Core app" + text: "Create your first EF Core app" - url: /learn/modules/persist-data-ef-core/ - text: "Learn: persist and retrieve relational data with EF Core" + text: "Learn: Persist and retrieve relational data with EF Core" - url: /ef/core/providers/ text: "Supported databases" @@ -152,7 +152,7 @@ additionalContent: links: - url: /dotnet/spark/what-is-apache-spark-dotnet text: "What is .NET for Apache Spark?" - - url: /learn/data/spark-tutorial/intro + - url: https://dotnet.microsoft.com/learn/data/spark-tutorial/intro text: "10-minute .NET for Apache Spark(tm) Tutorial" - url: /azure/synapse-analytics/overview-what-is text: "What is Azure Synapse?" @@ -167,6 +167,16 @@ additionalContent: - url: /dotnet/machine-learning/tutorials/ text: "ML.NET tutorials" + # Card + - title: Mobile apps + links: + - url: /azure/developer/mobile-apps/azure-mobile-apps/overview + text: "Microsoft Datasync Framework (Azure mobile apps)" + - url: /azure/developer/mobile-apps/azure-mobile-apps/quickstarts/maui/ + text: "Build a .NET MAUI mobile app with Azure Mobile Apps" + - url: /azure/developer/mobile-apps/azure-mobile-apps/howto/data-sync + text: "Offline data sync" + # Card - title: ETL and analytics links: @@ -188,6 +198,7 @@ additionalContent: text: "Send and receive events from .NET using Azure Event Hubs" - url: /dotnet/orleans/overview text: "Distributed .NET apps with Orleans" + # Section - title: "APIs" diff --git a/entity-framework/index.yml b/entity-framework/index.yml index 9fbadc29db..24d6758494 100644 --- a/entity-framework/index.yml +++ b/entity-framework/index.yml @@ -41,7 +41,11 @@ highlightedContent: # Card - title: "API reference browser" itemType: reference - url: /dotnet/api/?view=efcore-3.1 + url: /dotnet/api/?view=efcore-6.0&preserve-view=true + # Card + - title: "Explore other options for .NET Data" + itemType: reference + url: dotnet-data/index.yml # Card with summary style additionalContent: @@ -60,6 +64,8 @@ additionalContent: text: "ASP.NET Core MVC web app accessing SQL Server with EF Core" - url: core/get-started/wpf.md text: "WPF .NET Core app accessing SQLite with EF Core" + - url: core/get-started/winforms.md + text: "Windows Forms .NET Core app accessing SQLite with EF Core" - url: core/get-started/xamarin.md text: "Xamarin mobile app accessing SQLite with EF Core" # Card @@ -92,7 +98,7 @@ additionalContent: text: "Blazor Server" - url: core/get-started/wpf.md text: "Windows Presentation Foundation (WPF)" - - url: ef6/fundamentals/databinding/winforms.md + - url: core/get-started/winforms.md text: "Windows Forms (WinForms)" - url: core/get-started/xamarin.md text: "Xamarin" @@ -176,7 +182,7 @@ additionalContent: text: "Migration - custom history table" - url: core/managing-schemas/ensure-created.md text: "Create and drop APIs" - - url: core/managing-schemas/scaffolding.md + - url: core/managing-schemas/scaffolding/index.md text: "Reverse engineering (scaffolding)" # Card - title: Save data diff --git a/entity-framework/toc.yml b/entity-framework/toc.yml index 925e42051e..be1fc5773d 100644 --- a/entity-framework/toc.yml +++ b/entity-framework/toc.yml @@ -53,7 +53,8 @@ href: /aspnet/core/blazor/blazor-server-ef-core - name: WPF .NET Core tutorial href: core/get-started/wpf.md - #- name: Windows Forms tutorial + - name: Windows Forms tutorial + href: core/get-started/winforms.md - name: Xamarin tutorial href: core/get-started/xamarin.md @@ -192,7 +193,11 @@ - name: Create and drop APIs href: core/managing-schemas/ensure-created.md - name: Reverse engineering (scaffolding) - href: core/managing-schemas/scaffolding.md + items: + - name: Overview + href: core/managing-schemas/scaffolding/index.md + - name: Custom templates + href: core/managing-schemas/scaffolding/templates.md - name: Query data items: @@ -410,7 +415,7 @@ href: core/cli/services.md - name: EF Core API reference >> - href: /dotnet/api/?view=efcore-5.0 + href: /dotnet/api/?view=efcore-6.0&preserve-view=true - name: Entity Framework 6 items: @@ -719,4 +724,4 @@ - name: Russian href: ef6/resources/licenses/ef6/rus.md - name: EF6 API reference >> - href: /dotnet/api/?view=entity-framework-6.2.0 + href: /dotnet/api/?view=entity-framework-6.2.0&preserve-view=true diff --git a/samples/NuGet.Config b/samples/NuGet.Config index 6548586147..92c61da23f 100644 --- a/samples/NuGet.Config +++ b/samples/NuGet.Config @@ -3,6 +3,7 @@ + diff --git a/samples/core/Miscellaneous/Multitenancy/Common/Contact.cs b/samples/core/Miscellaneous/Multitenancy/Common/Contact.cs index de3932c24c..7262834591 100644 --- a/samples/core/Miscellaneous/Multitenancy/Common/Contact.cs +++ b/samples/core/Miscellaneous/Multitenancy/Common/Contact.cs @@ -2,18 +2,18 @@ public class Contact { public Guid Id { get; set; } - public string Name {get; set;} + public string Name { get; set; } = null!; public bool IsUnicorn {get; set; } public static Contact[] GeneratedContacts => - new [] + new [] { new Contact { Name = "Magic Unicorns", IsUnicorn = true }, - new Contact + new Contact { Name = "Unicorns Running", IsUnicorn = true diff --git a/samples/core/Miscellaneous/NewInEFCore7/.config/dotnet-tools.json b/samples/core/Miscellaneous/NewInEFCore7/.config/dotnet-tools.json new file mode 100644 index 0000000000..492c27696d --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.0-rc.1.22408.1", + "commands": [ + "dotnet-ef" + ] + } + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs new file mode 100644 index 0000000000..c6e03f4bf7 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs @@ -0,0 +1,615 @@ +using System.Net; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; + +namespace NewInEfCore7; + +#region BlogsModel +public class Blog +{ + public Blog(string name) + { + Name = name; + } + + public int Id { get; private set; } + public string Name { get; set; } + public List Posts { get; } = new(); +} + +public class Post +{ + public Post(string title, string content, DateTime publishedOn) + { + Title = title; + Content = content; + PublishedOn = publishedOn; + } + + public int Id { get; private set; } + public string Title { get; set; } + public string Content { get; set; } + public DateTime PublishedOn { get; set; } + public Blog Blog { get; set; } = null!; + public List Tags { get; } = new(); + public Author? Author { get; set; } + public PostMetadata? Metadata { get; set; } +} + +public class FeaturedPost : Post +{ + public FeaturedPost(string title, string content, DateTime publishedOn, string promoText) + : base(title, content, publishedOn) + { + PromoText = promoText; + } + + public string PromoText { get; set; } +} + +public class Tag +{ + public Tag(string text) + { + Text = text; + } + + public int Id { get; private set; } + public string Text { get; set; } + public List Posts { get; } = new(); +} + +public class Author +{ + public Author(string name) + { + Name = name; + } + + public int Id { get; private set; } + public string Name { get; set; } + public ContactDetails Contact { get; set; } = null!; + public List Posts { get; } = new(); +} +#endregion + +#region ContactDetailsAggregate +public class ContactDetails +{ + public Address Address { get; init; } = null!; + public string? Phone { get; set; } +} + +public class Address +{ + public Address(string street, string city, string postcode, string country) + { + Street = street; + City = city; + Postcode = postcode; + Country = country; + } + + public string Street { get; set; } + public string City { get; set; } + public string Postcode { get; set; } + public string Country { get; set; } +} +#endregion + +#region PostMetadataAggregate +public class PostMetadata +{ + public PostMetadata(int views) + { + Views = views; + } + + public int Views { get; set; } + public List TopSearches { get; } = new(); + public List TopGeographies { get; } = new(); + public List Updates { get; } = new(); +} + +public class SearchTerm +{ + public SearchTerm(string term, int count) + { + Term = term; + Count = count; + } + + public string Term { get; private set; } + public int Count { get; private set; } +} + +public class Visits +{ + public Visits(double latitude, double longitude, int count) + { + Latitude = latitude; + Longitude = longitude; + Count = count; + } + + public double Latitude { get; private set; } + public double Longitude { get; private set; } + public int Count { get; private set; } + public List? Browsers { get; set; } +} + +public class PostUpdate +{ + public PostUpdate(IPAddress postedFrom, DateTime updatedOn) + { + PostedFrom = postedFrom; + UpdatedOn = updatedOn; + } + + public IPAddress PostedFrom { get; private set; } + public string? UpdatedBy { get; init; } + public DateTime UpdatedOn { get; private set; } + public List Commits { get; } = new(); +} + +public class Commit +{ + public Commit(DateTime committedOn, string comment) + { + CommittedOn = committedOn; + Comment = comment; + } + + public DateTime CommittedOn { get; private set; } + public string Comment { get; set; } +} +#endregion + +public abstract class BlogsContext : DbContext +{ + protected BlogsContext(bool useSqlite) + { + UseSqlite = useSqlite; + } + + public bool UseSqlite { get; } + public bool LoggingEnabled { get; set; } + public abstract MappingStrategy MappingStrategy { get; } + + public DbSet Blogs => Set(); + public DbSet Tags => Set(); + public DbSet Posts => Set(); + public DbSet Authors => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => (UseSqlite + ? optionsBuilder.UseSqlite(@$"DataSource={GetType().Name}") + : optionsBuilder.UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}", + sqlServerOptionsBuilder => sqlServerOptionsBuilder.UseNetTopologySuite())) + .EnableSensitiveDataLogging() + .LogTo( + s => + { + if (LoggingEnabled) + { + Console.WriteLine(s); + } + }, LogLevel.Information); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + } + + public async Task Seed() + { + var tagEntityFramework = new Tag("Entity Framework"); + var tagDotNet = new Tag(".NET"); + var tagDotNetMaui = new Tag(".NET MAUI"); + var tagAspDotNet = new Tag("ASP.NET"); + var tagAspDotNetCore = new Tag("ASP.NET Core"); + var tagDotNetCore = new Tag(".NET Core"); + var tagHacking = new Tag("Hacking"); + var tagLinux = new Tag("Linux"); + var tagSqlite = new Tag("SQLite"); + var tagVisualStudio = new Tag("Visual Studio"); + var tagGraphQl = new Tag("GraphQL"); + var tagCosmosDb = new Tag("CosmosDB"); + var tagBlazor = new Tag("Blazor"); + + var maddy = new Author("Maddy Montaquila") + { + Contact = new() { Address = new("1 Main St", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12345" } + }; + var jeremy = new Author("Jeremy Likness") + { + Contact = new() { Address = new("2 Main St", "Chigley", "CW1 5ZH", "UK"), Phone = "01632 12346" } + }; + var dan = new Author("Daniel Roth") + { + Contact = new() { Address = new("3 Main St", "Camberwick Green", "CW1 5ZH", "UK"), Phone = "01632 12347" } + }; + var arthur = new Author("Arthur Vickers") + { + Contact = new() { Address = new("15a Main St", "Chigley", "CW1 5ZH", "UK"), Phone = "01632 12348" } + }; + var brice = new Author("Brice Lambson") + { + Contact = new() { Address = new("4 Main St", "Chigley", "CW1 5ZH", "UK"), Phone = "01632 12349" } + }; + + var blogs = new List + { + new(".NET Blog") + { + Posts = + { + new Post( + "Productivity comes to .NET MAUI in Visual Studio 2022", + "Visual Studio 2022 17.3 is now available and...", + new DateTime(2022, 8, 9)) { Tags = { tagDotNetMaui, tagDotNet }, Author = maddy, Metadata = BuildPostMetadata() }, + new Post( + "Announcing .NET 7 Preview 7", ".NET 7 Preview 7 is now available with improvements to System.LINQ, Unix...", + new DateTime(2022, 8, 9)) { Tags = { tagDotNet }, Author = jeremy, Metadata = BuildPostMetadata() }, + new Post( + "ASP.NET Core updates in .NET 7 Preview 7", ".NET 7 Preview 7 is now available! Check out what's new in...", + new DateTime(2022, 8, 9)) + { + Tags = { tagDotNet, tagAspDotNet, tagAspDotNetCore }, Author = dan, Metadata = BuildPostMetadata() + }, + new FeaturedPost( + "Announcing Entity Framework 7 Preview 7: Interceptors!", + "Announcing EF7 Preview 7 with new and improved interceptors, and...", + new DateTime(2022, 8, 9), + "Loads of runnable code!") + { + Tags = { tagEntityFramework, tagDotNet, tagDotNetCore }, Author = arthur, Metadata = BuildPostMetadata() + } + }, + }, + new("1unicorn2") + { + Posts = + { + new Post( + "Hacking my Sixth Form College network in 1991", + "Back in 1991 I was a student at Franklin Sixth Form College...", + new DateTime(2020, 4, 10)) { Tags = { tagHacking }, Author = arthur, Metadata = BuildPostMetadata() }, + new FeaturedPost( + "All your versions are belong to us", + "Totally made up conversations about choosing Entity Framework version numbers...", + new DateTime(2020, 3, 26), + "Way funny!") { Tags = { tagEntityFramework }, Author = arthur, Metadata = BuildPostMetadata() }, + new Post( + "Moving to Linux", "A few weeks ago, I decided to move from Windows to Linux as...", + new DateTime(2020, 3, 7)) { Tags = { tagLinux }, Author = arthur, Metadata = BuildPostMetadata() }, + new Post( + "Welcome to One Unicorn 2.0!", "I created my first blog back in 2011..", + new DateTime(2020, 2, 29)) { Tags = { tagEntityFramework }, Author = arthur, Metadata = BuildPostMetadata() } + } + }, + new("Brice's Blog") + { + Posts = + { + new FeaturedPost( + "SQLite in Visual Studio 2022", "A couple of years ago, I was thinking of ways...", + new DateTime(2022, 7, 26), "Love for VS!") + { + Tags = { tagSqlite, tagVisualStudio }, Author = brice, Metadata = BuildPostMetadata() + }, + new Post( + "On .NET - Entity Framework Migrations Explained", + "This week, @JamesMontemagno invited me onto the On .NET show...", + new DateTime(2022, 5, 4)) + { + Tags = { tagEntityFramework, tagDotNet }, Author = brice, Metadata = BuildPostMetadata() + }, + new Post( + "Dear DBA: A silly idea", "We have fun on the Entity Framework team...", + new DateTime(2022, 3, 31)) { Tags = { tagEntityFramework }, Author = brice, Metadata = BuildPostMetadata() }, + new Post( + "Microsoft.Data.Sqlite 6", "It’s that time of year again. Microsoft.Data.Sqlite version...", + new DateTime(2021, 11, 8)) { Tags = { tagSqlite, tagDotNet }, Author = brice, Metadata = BuildPostMetadata() } + } + }, + new("Developer for Life") + { + Posts = + { + new Post( + "GraphQL for .NET Developers", "A comprehensive overview of GraphQL as...", + new DateTime(2021, 7, 1)) + { + Tags = { tagDotNet, tagGraphQl, tagAspDotNetCore }, Author = jeremy, Metadata = BuildPostMetadata() + }, + new FeaturedPost( + "Azure Cosmos DB With EF Core on Blazor Server", + "Learn how to build Azure Cosmos DB apps using Entity Framework Core...", + new DateTime(2021, 5, 16), + "Blazor FTW!") + { + Tags = + { + tagDotNet, + tagEntityFramework, + tagAspDotNetCore, + tagCosmosDb, + tagBlazor + }, + Author = jeremy, + Metadata = BuildPostMetadata() + }, + new Post( + "Multi-tenancy with EF Core in Blazor Server Apps", + "Learn several ways to implement multi-tenant databases in Blazor Server apps...", + new DateTime(2021, 4, 29)) + { + Tags = { tagDotNet, tagEntityFramework, tagAspDotNetCore, tagBlazor }, + Author = jeremy, + Metadata = BuildPostMetadata() + }, + new Post( + "An Easier Blazor Debounce", "Where I propose a simple method to debounce input without...", + new DateTime(2021, 4, 12)) + { + Tags = { tagDotNet, tagAspDotNetCore, tagBlazor }, Author = jeremy, Metadata = BuildPostMetadata() + } + } + } + }; + + await AddRangeAsync(blogs); + await SaveChangesAsync(); + + PostMetadata BuildPostMetadata() + { + var random = new Random(Guid.NewGuid().GetHashCode()); + + var metadata = new PostMetadata(random.Next(10000)); + + for (var i = 0; i < random.Next(5); i++) + { + var update = new PostUpdate(IPAddress.Loopback, DateTime.UtcNow - TimeSpan.FromDays(random.Next(1, 10000))) + { + UpdatedBy = "Admin" + }; + + for (var j = 0; j < random.Next(3); j++) + { + update.Commits.Add(new(DateTime.Today, $"Commit #{j + 1}")); + } + + metadata.Updates.Add(update); + } + + for (var i = 0; i < random.Next(5); i++) + { + metadata.TopSearches.Add(new($"Search #{i + 1}", 10000 - random.Next(i * 1000, i * 1000 + 900))); + } + + for (var i = 0; i < random.Next(5); i++) + { + metadata.TopGeographies.Add( + new( + // Issue https://github.com/dotnet/efcore/issues/28811 (Support spatial types in JSON columns) + // new Point(115.7930 + 20 - random.Next(40), 37.2431 + 10 - random.Next(20)) { SRID = 4326 }, + 115.7930 + 20 - random.Next(40), + 37.2431 + 10 - random.Next(20), + 1000 - random.Next(i * 100, i * 100 + 90)) { Browsers = new() { "Firefox", "Netscape" } }); + } + + return metadata; + } + } +} + +public enum MappingStrategy +{ + Tph, + Tpt, + Tpc, +} + +public class TphBlogsContext : BlogsContext +{ + public TphBlogsContext() + : base(useSqlite: false) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Ignore(e => e.Metadata); + + // https://github.com/dotnet/efcore/issues/28671 + // modelBuilder.Entity().OwnsOne(e => e.Contact).OwnsOne(e => e.Address); + modelBuilder.Entity().Ignore(e => e.Contact); + + base.OnModelCreating(modelBuilder); + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tph; +} + +public class TphSqliteBlogsContext : BlogsContext +{ + public TphSqliteBlogsContext() + : base(useSqlite: true) + { + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tph; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Ignore(e => e.Metadata); + + // https://github.com/dotnet/efcore/issues/28671 + // modelBuilder.Entity().OwnsOne(e => e.Contact).OwnsOne(e => e.Address); + modelBuilder.Entity().Ignore(e => e.Contact); + + base.OnModelCreating(modelBuilder); + } +} + +public class TptBlogsContext : BlogsContext +{ + public TptBlogsContext() + : base(useSqlite: false) + { + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tpt; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("FeaturedPosts"); + modelBuilder.Entity().Ignore(e => e.Metadata); + + // https://github.com/dotnet/efcore/issues/28671 + // modelBuilder.Entity().OwnsOne(e => e.Contact).OwnsOne(e => e.Address); + modelBuilder.Entity().Ignore(e => e.Contact); + + base.OnModelCreating(modelBuilder); + } +} + +public class TpcBlogsContext : BlogsContext +{ + public TpcBlogsContext() + : base(useSqlite: false) + { + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tpc; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().ToTable("FeaturedPosts"); + modelBuilder.Entity().Ignore(e => e.Metadata); + + // https://github.com/dotnet/efcore/issues/28671 + // modelBuilder.Entity().OwnsOne(e => e.Contact).OwnsOne(e => e.Address); + modelBuilder.Entity().Ignore(e => e.Contact); + + base.OnModelCreating(modelBuilder); + } +} + +public abstract class JsonBlogsContextBase : BlogsContext +{ + protected JsonBlogsContextBase(bool useSqlite) + : base(useSqlite) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne( + author => author.Contact, ownedNavigationBuilder => + { + ownedNavigationBuilder.ToJson(); + ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address); + }); + + #region PostMetadataConfig + modelBuilder.Entity().OwnsOne( + post => post.Metadata, ownedNavigationBuilder => + { + ownedNavigationBuilder.ToJson(); + ownedNavigationBuilder.OwnsMany(metadata => metadata.TopSearches); + ownedNavigationBuilder.OwnsMany(metadata => metadata.TopGeographies); + ownedNavigationBuilder.OwnsMany( + metadata => metadata.Updates, + ownedOwnedNavigationBuilder => ownedOwnedNavigationBuilder.OwnsMany(update => update.Commits)); + }); + #endregion + + base.OnModelCreating(modelBuilder); + } + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + // Issue https://github.com/dotnet/efcore/issues/28688 (Json: add support for collection of primitive types) + configurationBuilder.Properties>().HaveConversion(); + + base.ConfigureConventions(configurationBuilder); + } + + private class StringListConverter : ValueConverter, string> + { + public StringListConverter() + : base(v => string.Join(", ", v!), v => v.Split(',', StringSplitOptions.TrimEntries).ToList()) + { + } + } + + public override MappingStrategy MappingStrategy => MappingStrategy.Tph; +} + +public class JsonBlogsContext : JsonBlogsContextBase +{ + public JsonBlogsContext() + : base(useSqlite: false) + { + } +} + +public class JsonBlogsContextSqlite : JsonBlogsContextBase +{ + public JsonBlogsContextSqlite() + : base(useSqlite: true) + { + } +} + +// Used only for code snippets: + +public abstract class TableSharingAggregateContext : TphBlogsContext +{ + #region TableSharingAggregate + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne( + author => author.Contact, ownedNavigationBuilder => + { + ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address); + }); + } + #endregion +} + +public abstract class TableMappedAggregateContext : TphBlogsContext +{ + #region TableMappedAggregate + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne( + author => author.Contact, ownedNavigationBuilder => + { + ownedNavigationBuilder.ToTable("Contacts"); + ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address, ownedOwnedNavigationBuilder => + { + ownedOwnedNavigationBuilder.ToTable("Addresses"); + }); + }); + } + #endregion +} + +public abstract class JsonColumnAggregateContext : TphBlogsContext +{ + #region JsonColumnAggregate + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne( + author => author.Contact, ownedNavigationBuilder => + { + ownedNavigationBuilder.ToJson(); + ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address); + }); + } + #endregion +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs new file mode 100644 index 0000000000..59303041f3 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs @@ -0,0 +1,277 @@ +namespace NewInEfCore7; + +public static class ExecuteDeleteSample +{ + public static Task ExecuteDelete() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + public static Task ExecuteDeleteTpt() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + public static Task ExecuteDeleteTpc() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + public static Task ExecuteDeleteSqlite() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteDelete)}"); + Console.WriteLine(); + + return ExecuteDeleteTest(); + } + + private static async Task ExecuteDeleteTest() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + await DeleteAllTags(); + await DeleteTagsContainingDotNet(); + await DeleteTagsFromOldPosts(); + + if (context.MappingStrategy == MappingStrategy.Tph) + { + await DeleteAllAuthors(); + await DeleteAuthorsWithOnePost(); + } + + if (context.MappingStrategy != MappingStrategy.Tpt) + { + await DeleteFeaturedPosts(); + } + + if (context.MappingStrategy == MappingStrategy.Tph + && !context.UseSqlite) + { + await DeletePostsForGivenAuthor(); + } + + if (context.MappingStrategy != MappingStrategy.Tpt) + { + // https://github.com/dotnet/efcore/issues/28532 + await DeleteAllBlogsAndPosts(); + } + + Console.WriteLine(); + } + + private static async Task DeleteAllTags() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete all tags..."); + Console.WriteLine( + $"Tags before delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteAllTags + await context.Tags.ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteTagsContainingDotNet() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete tags containing '.NET'..."); + Console.WriteLine( + $"Tags before delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteTagsContainingDotNet + await context.Tags.Where(t => t.Text.Contains(".NET")).ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteTagsFromOldPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete tags from old posts..."); + Console.WriteLine( + $"Tags before delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteTagsFromOldPosts + await context.Tags.Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)).ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after delete: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteAllAuthors() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete all authors..."); + Console.WriteLine( + $"Authors before delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteAllAuthors + await context.Posts.ExecuteDeleteAsync(); + await context.Authors.ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Authors after delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteFeaturedPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete featured posts..."); + Console.WriteLine( + $"Posts before delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteFeaturedPosts + await context.Set().ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeletePostsForGivenAuthor() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete posts for given author..."); + Console.WriteLine( + $"Posts before delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeletePostsForGivenAuthor + await context.Posts.Where(p => p.Author!.Name.StartsWith("Arthur")).ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteAllBlogsAndPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete all blogs and posts..."); + Console.WriteLine( + $"Blogs before delete: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine( + $"Posts before delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region DeleteAllBlogsAndPosts + await context.Blogs.ExecuteDeleteAsync(); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Blogs after delete: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine( + $"Posts after delete: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'" + e.Id + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task DeleteAuthorsWithOnePost() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Delete authors with only one post..."); + Console.WriteLine( + $"Authors before delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + await context.Posts.Where(p => p.Author!.Posts.Count <= 1) + .ExecuteUpdateAsync(s => s.SetProperty(p => EF.Property(p, "AuthorId"), p => null)); + await context.Authors.Where(a => a.Posts.Count <= 1).ExecuteDeleteAsync(); + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Authors after delete: {string.Join(", ", await context.Authors.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs new file mode 100644 index 0000000000..c223428cc6 --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs @@ -0,0 +1,167 @@ +namespace NewInEfCore7; + +public static class ExecuteUpdateSample +{ + public static Task ExecuteUpdate() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + public static Task ExecuteUpdateTpt() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + public static Task ExecuteUpdateTpc() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + public static Task ExecuteUpdateSqlite() + { + Console.WriteLine($">>>> Sample: {nameof(ExecuteUpdate)}"); + Console.WriteLine(); + + return ExecuteUpdateTest(); + } + + private static async Task ExecuteUpdateTest() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + await UpdateAllBlogs(); + + if (context.MappingStrategy == MappingStrategy.Tph) + { + await UpdateOldPosts(); + } + + if (context.MappingStrategy != MappingStrategy.Tpt) + { + await UpdateFeaturedPosts(); + } + + await UpdateTagsOnOldPosts(); + + Console.WriteLine(); + } + + private static async Task UpdateAllBlogs() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update names for all blogs..."); + Console.WriteLine( + $"Blogs before update: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region UpdateAllBlogs + await context.Blogs.ExecuteUpdateAsync( + s => s.SetProperty(b => b.Name, b => b.Name + " *Featured!*")); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Blogs after update: {string.Join(", ", await context.Blogs.AsNoTracking().Select(e => "'" + e.Name + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task UpdateOldPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update title and content for old posts..."); + Console.WriteLine( + $"Posts before update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region UpdateOldPosts + await context.Posts + .Where(p => p.PublishedOn.Year < 2022) + .ExecuteUpdateAsync(s => s + .SetProperty(b => b.Title, b => b.Title + " (" + b.PublishedOn.Year + ")") + .SetProperty(b => b.Content, b => b.Content + " ( This content was published in " + b.PublishedOn.Year + ")")); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task UpdateFeaturedPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update title and content for featured posts..."); + Console.WriteLine( + $"Posts before update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + await context.Set() + .ExecuteUpdateAsync( + s => s.SetProperty(b => b.Title, b => b.Title + " *Featured!*") + .SetProperty(b => b.Content, b => "Featured: " + b.Content)); + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "'").ToListAsync())}"); + Console.WriteLine(); + } + + private static async Task UpdateTagsOnOldPosts() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Update tags on old posts"); + Console.WriteLine( + $"Tags before update: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + + #region UpdateTagsOnOldPosts + await context.Tags + .Where(t => t.Posts.All(e => e.PublishedOn.Year < 2022)) + .ExecuteUpdateAsync(s => s.SetProperty(t => t.Text, t => t.Text + " (old)")); + #endregion + + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Tags after update: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); + Console.WriteLine(); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs b/samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs new file mode 100644 index 0000000000..8eaeab5add --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs @@ -0,0 +1,140 @@ +using System.Net; + +namespace NewInEfCore7; + +public static class JsonColumnsSample +{ + public static Task Json_columns_with_TPH() + { + Console.WriteLine($">>>> Sample: {nameof(Json_columns_with_TPH)}"); + Console.WriteLine(); + + return JsonColumnsTest(); + } + + public static Task Json_columns_with_TPH_on_SQLite() + { + Console.WriteLine($">>>> Sample: {nameof(Json_columns_with_TPH_on_SQLite)}"); + Console.WriteLine(); + + return JsonColumnsTest(); + } + + private static async Task JsonColumnsTest() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + await context.Seed(); + + context.LoggingEnabled = true; + context.ChangeTracker.Clear(); + + #region AuthorsInChigley + var authorsInChigley = await context.Authors + .Where(author => author.Contact.Address.City == "Chigley") + .ToListAsync(); + #endregion + + Console.WriteLine(); + foreach (var author in authorsInChigley) + { + Console.WriteLine($"{author.Name} lives at '{author.Contact.Address.Street}' in Chigley."); + } + + Console.WriteLine(); + + #region PostcodesInChigley + var postcodesInChigley = await context.Authors + .Where(author => author.Contact.Address.City == "Chigley") + .Select(author => author.Contact.Address.Postcode) + .ToListAsync(); + #endregion + + Console.WriteLine(); + Console.WriteLine($"Postcodes in Chigley are '{string.Join("', '", postcodesInChigley)}'"); + Console.WriteLine(); + + #region OrderedAddresses + var orderedAddresses = await context.Authors + .Where( + author => (author.Contact.Address.City == "Chigley" + && author.Contact.Phone != null) + || author.Name.StartsWith("D")) + .OrderBy(author => author.Contact.Phone) + .Select( + author => author.Name + " (" + author.Contact.Address.Street + + ", " + author.Contact.Address.City + + " " + author.Contact.Address.Postcode + ")") + .ToListAsync(); + #endregion + + Console.WriteLine(); + foreach (var address in orderedAddresses) + { + Console.WriteLine(address); + } + + Console.WriteLine(); + + // Since query below cannot use Include + // Issue: https://github.com/dotnet/efcore/issues/28808 + await context.Posts.LoadAsync(); + + var authorsInChigleyWithPosts = await context.Authors + .Where( + author => author.Contact.Address.City == "Chigley" + && author.Posts.Count > 1) + //.Include(author => author.Posts) + .ToListAsync(); + + Console.WriteLine(); + foreach (var author in authorsInChigleyWithPosts) + { + Console.WriteLine($"{author.Name} has {author.Posts.Count} posts"); + } + + Console.WriteLine(); + + #region PostsWithViews + var postsWithViews = await context.Posts.Where(post => post.Metadata!.Views > 3000) + .AsNoTracking() + .Select( + post => new + { + post.Author!.Name, + post.Metadata!.Views, + Searches = post.Metadata.TopSearches, + Commits = post.Metadata.Updates + }) + .ToListAsync(); + #endregion + + Console.WriteLine(); + foreach (var postWithViews in postsWithViews) + { + Console.WriteLine($"Post by {postWithViews.Name} with {postWithViews.Views} views had {postWithViews.Commits.Count} commits with {postWithViews.Searches.Sum(term => term.Count)} searches"); + } + + Console.WriteLine(); + + context.ChangeTracker.Clear(); + + var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur")); + + arthur.Contact.Phone = "01632 22345"; + arthur.Contact.Address.Country = "United Kingdom"; + + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + + var post = await context.Posts.SingleAsync(post => post.Title.StartsWith("Hacking")); + + post.Metadata!.Updates.Add(new PostUpdate(IPAddress.Broadcast, DateTime.UtcNow) { UpdatedBy = "User" }); + post.Metadata!.TopGeographies.Clear(); + + await context.SaveChangesAsync(); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj new file mode 100644 index 0000000000..88e1a0862a --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj @@ -0,0 +1,29 @@ + + + + Exe + net6.0 + enable + enable + NewInEfCore7 + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/core/Miscellaneous/NewInEFCore7/Program.cs b/samples/core/Miscellaneous/NewInEFCore7/Program.cs new file mode 100644 index 0000000000..432f35230a --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/Program.cs @@ -0,0 +1,28 @@ +using NewInEfCore7; + +public class Program +{ + public static async Task Main() + { + // await TpcInheritanceSample.Inheritance_with_TPH(); + // await TpcInheritanceSample.Inheritance_with_TPT(); + // await TpcInheritanceSample.Inheritance_with_TPC(); + // await TpcInheritanceSample.Inheritance_with_TPC_using_HiLo(); + // await TpcInheritanceSample.Inheritance_with_TPC_using_Identity(); + // + // await ExecuteDeleteSample.ExecuteDelete(); + // await ExecuteDeleteSample.ExecuteDeleteTpt(); + // await ExecuteDeleteSample.ExecuteDeleteTpc(); + // await ExecuteDeleteSample.ExecuteDeleteSqlite(); + // + // await ExecuteUpdateSample.ExecuteUpdate(); + // await ExecuteUpdateSample.ExecuteUpdateTpt(); + // await ExecuteUpdateSample.ExecuteUpdateTpc(); + // await ExecuteUpdateSample.ExecuteUpdateSqlite(); + + await JsonColumnsSample.Json_columns_with_TPH(); + + // Issue https://github.com/dotnet/efcore/issues/28816 (Json: add support for Sqlite provider) + // await JsonColumnsSample.Json_columns_with_TPH_on_SQLite(); + } +} diff --git a/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs b/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs new file mode 100644 index 0000000000..0f9649307b --- /dev/null +++ b/samples/core/Miscellaneous/NewInEFCore7/TpcInheritanceSample.cs @@ -0,0 +1,437 @@ +namespace NewInEfCore7; + +public static class TpcInheritanceSample +{ + public static Task Inheritance_with_TPH() + { + Console.WriteLine($">>>> Sample: {nameof(Inheritance_with_TPH)}"); + Console.WriteLine(); + + return ManyToManyTest(); + } + + public static Task Inheritance_with_TPT() + { + Console.WriteLine($">>>> Sample: {nameof(Inheritance_with_TPT)}"); + Console.WriteLine(); + + return ManyToManyTest(); + } + + public static Task Inheritance_with_TPC() + { + Console.WriteLine($">>>> Sample: {nameof(Inheritance_with_TPC)}"); + Console.WriteLine(); + + return ManyToManyTest(); + } + + public static Task Inheritance_with_TPC_using_HiLo() + { + Console.WriteLine($">>>> Sample: {nameof(Inheritance_with_TPC_using_HiLo)}"); + Console.WriteLine(); + + return ManyToManyTest(); + } + + public static Task Inheritance_with_TPC_using_Identity() + { + Console.WriteLine($">>>> Sample: {nameof(Inheritance_with_TPC_using_Identity)}"); + Console.WriteLine(); + + return ManyToManyTest(); + } + + public static async Task ManyToManyTest() + where TContext : AnimalsContext, new() + { + using (var context = new TContext()) + { + await context.Database.EnsureDeletedAsync(); + + Console.WriteLine(context.Model.ToDebugString()); + Console.WriteLine(); + + await context.Database.EnsureCreatedAsync(); + + var catFood = new PetFood("Lily's Kitchen", LifeStage.Adult); + var dogFood = new PetFood("Canagan", LifeStage.Adult); + var hay = new FarmFood("Hay"); + var sushi = new HumanFood("Sushi", 670); + + var arthur = new Human("Arthur") { Food = sushi }; + var wendy = new Human("Wendy"); + var christi = new Human("Christi"); + + var alice = new Cat("Alice", "MBA") { Vet = "Pengelly", Food = catFood, Humans = { arthur, wendy } }; + + var mac = new Cat("Mac", "Preschool") { Vet = "Pengelly", Food = catFood, Humans = { arthur, wendy } }; + + var toast = new Dog("Toast", "Mr. Squirrel") { Vet = "Pengelly", Food = dogFood, Humans = { arthur, wendy } }; + + var clyde = new FarmAnimal("Clyde", "Equus africanus asinus") { Value = 100.0m, Food = hay }; + + wendy.FavoriteAnimal = toast; + arthur.FavoriteAnimal = alice; + christi.FavoriteAnimal = clyde; + + await context.AddRangeAsync(wendy, arthur, christi, alice, mac, toast, clyde); + await context.SaveChangesAsync(); + } + + Console.WriteLine(); + + using (var context = new TContext()) + { + Console.WriteLine("All foods:"); + foreach (var food in await context.Foods.ToListAsync()) + { + Console.WriteLine($" >> {food}"); + } + + Console.WriteLine(); + + Console.WriteLine("All animals:"); + foreach (var animal in await context.Animals.ToListAsync()) + { + Console.WriteLine($" >> {animal}"); + } + + Console.WriteLine(); + + Console.WriteLine("Only pets:"); + foreach (var pet in await context.Pets.ToListAsync()) + { + Console.WriteLine($" >> {pet}"); + } + + Console.WriteLine(); + + Console.WriteLine("Only cats:"); + foreach (var cat in await context.Cats.ToListAsync()) + { + Console.WriteLine($" >> {cat}"); + } + + Console.WriteLine(); + + Console.WriteLine("Make some changes and save to the database."); + + var baxter = context.Add( + new Cat("Baxter", "BSc") { Vet = "Bothell Pet Hospital", Food = new HumanFood("Blueberry scones", 900) }).Entity; + + context.Add(new Human("Katie") { Pets = { baxter }, FavoriteAnimal = baxter }); + + context.Remove(context.Animals.Local.Single(e => e.Name == "Christi")); + context.Humans.Local.Single(e => e.Name == "Wendy").Food = new HumanFood("White pizza", 400); + + var sushi = context.Foods.OfType().Single(e => e.Name == "Sushi"); + sushi.Calories -= 100; + + await context.SaveChangesAsync(); + + Console.WriteLine(); + } + + using (var context = new TContext()) + { + Console.WriteLine("All animals including foods, pets, humans, and favorite animals:"); + foreach (var animal in await context.Animals + .AsNoTracking() + .Include(e => e.Food) + .Include(e => ((Human)e).Pets).ThenInclude(e => e.Food) + .Include(e => ((Pet)e).Humans).ThenInclude(e => e.Food) + .Include(e => ((Human)e).FavoriteAnimal).ThenInclude(e => e!.Food) + .ToListAsync()) + { + Console.Write($" >> {animal}"); + + if (animal is Pet pet + && pet.Humans.Any()) + { + Console.WriteLine($" has humans {string.Join(", ", pet.Humans.Select(e => e.Name))}"); + } + else if (animal is Human human + && human.Pets.Any()) + { + Console.WriteLine($" has pets {string.Join(", ", human.Pets.Select(e => e.Name))}"); + } + else + { + Console.WriteLine(); + } + } + + Console.WriteLine(); + } + + Console.WriteLine(); + } + + #region AnimalsHierarchy + public abstract class Animal + { + protected Animal(string name) + { + Name = name; + } + + public int Id { get; set; } + public string Name { get; set; } + public abstract string Species { get; } + + public Food? Food { get; set; } + } + + public abstract class Pet : Animal + { + protected Pet(string name) + : base(name) + { + } + + public string? Vet { get; set; } + + public ICollection Humans { get; } = new List(); + } + + public class FarmAnimal : Animal + { + public FarmAnimal(string name, string species) + : base(name) + { + Species = species; + } + + public override string Species { get; } + + [Precision(18, 2)] + public decimal Value { get; set; } + + public override string ToString() + => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? ""}"; + } + + public class Cat : Pet + { + public Cat(string name, string educationLevel) + : base(name) + { + EducationLevel = educationLevel; + } + + public string EducationLevel { get; set; } + public override string Species => "Felis catus"; + + public override string ToString() + => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? ""}"; + } + + public class Dog : Pet + { + public Dog(string name, string favoriteToy) + : base(name) + { + FavoriteToy = favoriteToy; + } + + public string FavoriteToy { get; set; } + public override string Species => "Canis familiaris"; + + public override string ToString() + => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? ""}"; + } + + public class Human : Animal + { + public Human(string name) + : base(name) + { + } + + public override string Species => "Homo sapiens"; + + public Animal? FavoriteAnimal { get; set; } + public ICollection Pets { get; } = new List(); + + public override string ToString() + => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? ""}'" + + $" eats {Food?.ToString() ?? ""}"; + } + #endregion + + public abstract class Food + { + public Guid Id { get; set; } + } + + public class PetFood : Food + { + public PetFood(string brand, LifeStage lifeStage) + { + Brand = brand; + LifeStage = lifeStage; + } + + public string Brand { get; set; } + public LifeStage LifeStage { get; set; } + + public override string ToString() + => $"Pet food by '{Brand}' ({Id}) for life stage {LifeStage}"; + } + + public enum LifeStage + { + Juvenile, + Adult, + Senior + } + + public class HumanFood : Food + { + public HumanFood(string name, int calories) + { + Name = name; + Calories = calories; + } + + [Column("Name")] + public string Name { get; set; } + + public int Calories { get; set; } + + public override string ToString() + => $"{Name} ({Id}) with calories {Calories}"; + } + + public class FarmFood : Food + { + public FarmFood(string name) + { + Name = name; + } + + [Column("Name")] + public string Name { get; set; } + + public override string ToString() + => $"{Name} ({Id})"; + } + + public class TphAnimalsContext : AnimalsContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region UseTphMappingStrategy + modelBuilder.Entity().UseTphMappingStrategy(); + #endregion + + modelBuilder.Entity().UseTpcMappingStrategy(); + base.OnModelCreating(modelBuilder); + } + } + + public class TptAnimalsContext : AnimalsContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region UseTptMappingStrategy + modelBuilder.Entity().UseTptMappingStrategy(); + #endregion + + modelBuilder.Entity().UseTpcMappingStrategy(); + base.OnModelCreating(modelBuilder); + } + } + + public class TpcAnimalsContext : AnimalsContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region UseTpcMappingStrategy + modelBuilder.Entity().UseTpcMappingStrategy(); + #endregion + + modelBuilder.Entity().UseTpcMappingStrategy(); + base.OnModelCreating(modelBuilder); + } + } + + public class TpcHiLoAnimalsContext : AnimalsContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.UseHiLo(); + modelBuilder.Entity().UseTpcMappingStrategy(); + modelBuilder.Entity().UseTpcMappingStrategy(); + base.OnModelCreating(modelBuilder); + } + } + + public class TpcIdentityAnimalsContext : AnimalsContext + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().UseTpcMappingStrategy(); + + #region UsingIdentity + modelBuilder.Entity().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4)); + modelBuilder.Entity().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4)); + modelBuilder.Entity().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4)); + modelBuilder.Entity().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4)); + #endregion + + modelBuilder.Entity().UseTpcMappingStrategy(); + + base.OnModelCreating(modelBuilder); + } + } + + public abstract class AnimalsContext : DbContext + { + #region AnimalSets + public DbSet Animals => Set(); + public DbSet Pets => Set(); + public DbSet FarmAnimals => Set(); + public DbSet Cats => Set(); + public DbSet Dogs => Set(); + public DbSet Humans => Set(); + #endregion + + public DbSet Foods => Set(); + public DbSet PetFoods => Set(); + public DbSet FarmFoods => Set(); + public DbSet HumanFoods => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .EnableSensitiveDataLogging() + .UseSqlServer(@$"Server=(localdb)\mssqllocaldb;Database={GetType().Name}"); + + optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region AnimalsInModelBuilder + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + #endregion + + modelBuilder.Entity().Property(e => e.Species); + + modelBuilder.Entity() + .HasMany(e => e.Pets) + .WithMany(e => e.Humans) + .UsingEntity>( + "PetsHumans", + r => r.HasOne().WithMany().OnDelete(DeleteBehavior.Cascade), + l => l.HasOne().WithMany().OnDelete(DeleteBehavior.ClientCascade)); + } + } +} diff --git a/samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescending.cs b/samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescending.cs new file mode 100644 index 0000000000..63d62f340d --- /dev/null +++ b/samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescending.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFModeling.IndexesAndConstraints.DataAnnotations.IndexDescending; + +internal class MyContext : DbContext +{ + public DbSet Blogs { get; set; } +} + +#region IndexDescending +[Index(nameof(Url), nameof(Rating), AllDescending = true)] +public class Blog +{ + public int BlogId { get; set; } + public string Url { get; set; } + public int Rating { get; set; } +} +#endregion diff --git a/samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescendingAscending.cs b/samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescendingAscending.cs new file mode 100644 index 0000000000..9f73ca995e --- /dev/null +++ b/samples/core/Modeling/IndexesAndConstraints/DataAnnotations/IndexDescendingAscending.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFModeling.IndexesAndConstraints.DataAnnotations.IndexDescendingAscending; + +internal class MyContext : DbContext +{ + public DbSet Blogs { get; set; } +} + +#region IndexDescendingAscending +[Index(nameof(Url), nameof(Rating), IsDescending = new[] { false, true })] +public class Blog +{ + public int BlogId { get; set; } + public string Url { get; set; } + public int Rating { get; set; } +} +#endregion diff --git a/samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescending.cs b/samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescending.cs new file mode 100644 index 0000000000..b3e841bcc5 --- /dev/null +++ b/samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescending.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFModeling.IndexesAndConstraints.FluentAPI.IndexDescending; + +internal class MyContext : DbContext +{ + public DbSet Blogs { get; set; } + + #region IndexDescending + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasIndex(b => new { b.Url, b.Rating }) + .IsDescending(); + } + #endregion +} + +public class Blog +{ + public int BlogId { get; set; } + public string Url { get; set; } + public int Rating { get; set; } +} diff --git a/samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescendingAscending.cs b/samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescendingAscending.cs new file mode 100644 index 0000000000..14fbf75d96 --- /dev/null +++ b/samples/core/Modeling/IndexesAndConstraints/FluentAPI/IndexDescendingAscending.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; + +namespace EFModeling.IndexesAndConstraints.FluentAPI.IndexDescendingAscending; + +internal class MyContext : DbContext +{ + public DbSet Blogs { get; set; } + + #region IndexDescendingAscending + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasIndex(b => new { b.Url, b.Rating }) + .IsDescending(false, true); + } + #endregion +} + +public class Blog +{ + public int BlogId { get; set; } + public string Url { get; set; } + public int Rating { get; set; } +} diff --git a/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj b/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj index f1fe04ef38..b7f7a4c31b 100644 --- a/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj +++ b/samples/core/Modeling/IndexesAndConstraints/IndexesAndConstraints.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/core/Samples.sln b/samples/core/Samples.sln index 3622a6e19d..42beacd427 100644 --- a/samples/core/Samples.sln +++ b/samples/core/Samples.sln @@ -191,6 +191,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetContextPooling", "Per EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetContextPoolingWithState", "Performance\AspNetContextPoolingWithState\AspNetContextPoolingWithState.csproj", "{EA3B9FAA-AAF2-4EB6-A64D-8C06FEAD33E5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NewInEFCore7", "Miscellaneous\NewInEFCore7\NewInEFCore7.csproj", "{72C964FF-07C3-4234-B277-D91C3D83BAEC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -529,6 +531,10 @@ Global {EA3B9FAA-AAF2-4EB6-A64D-8C06FEAD33E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA3B9FAA-AAF2-4EB6-A64D-8C06FEAD33E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3B9FAA-AAF2-4EB6-A64D-8C06FEAD33E5}.Release|Any CPU.Build.0 = Release|Any CPU + {72C964FF-07C3-4234-B277-D91C3D83BAEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72C964FF-07C3-4234-B277-D91C3D83BAEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72C964FF-07C3-4234-B277-D91C3D83BAEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72C964FF-07C3-4234-B277-D91C3D83BAEC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -612,6 +618,7 @@ Global {E6A055CF-9DFF-4E1D-8B04-3B33DD327AB8} = {BA37B31C-DEAC-46C5-976C-F3FDECA236DF} {FD79D5B5-55CE-4564-A031-FB500D1CDBCC} = {BA37B31C-DEAC-46C5-976C-F3FDECA236DF} {EA3B9FAA-AAF2-4EB6-A64D-8C06FEAD33E5} = {BA37B31C-DEAC-46C5-976C-F3FDECA236DF} + {72C964FF-07C3-4234-B277-D91C3D83BAEC} = {85AFD7F1-6943-40FE-B8EC-AA9DBB42CCA6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {20C98D35-54EF-46A6-8F3B-1855C1AE4F70} diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms.sln b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms.sln new file mode 100644 index 0000000000..eb04c6784f --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32728.150 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GetStartedWinForms", "GetStartedWinForms\GetStartedWinForms.csproj", "{4F4ED10B-6E60-4A22-88D2-553089460ADB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4F4ED10B-6E60-4A22-88D2-553089460ADB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F4ED10B-6E60-4A22-88D2-553089460ADB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F4ED10B-6E60-4A22-88D2-553089460ADB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F4ED10B-6E60-4A22-88D2-553089460ADB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {871AF531-D09B-42A7-8D8A-4293B23EF80B} + EndGlobalSection +EndGlobal diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Category.cs b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Category.cs new file mode 100644 index 0000000000..13d5b781dd --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Category.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace GetStartedWinForms; + +public class Category +{ + public int CategoryId { get; set; } + + public string? Name { get; set; } + + public virtual ObservableCollectionListSource Products { get; } = new(); +} diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.Designer.cs b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.Designer.cs new file mode 100644 index 0000000000..a316d10264 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.Designer.cs @@ -0,0 +1,151 @@ +namespace GetStartedWinForms +{ + partial class MainForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.dataGridViewCategories = new System.Windows.Forms.DataGridView(); + this.categoryIdDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.nameDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.categoryBindingSource = new System.Windows.Forms.BindingSource(this.components); + this.dataGridViewProducts = new System.Windows.Forms.DataGridView(); + this.productIdDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.nameDataGridViewTextBoxColumn1 = new System.Windows.Forms.DataGridViewTextBoxColumn(); + this.productsBindingSource = new System.Windows.Forms.BindingSource(this.components); + this.buttonSave = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.dataGridViewCategories)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.categoryBindingSource)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.dataGridViewProducts)).BeginInit(); + ((System.ComponentModel.ISupportInitialize)(this.productsBindingSource)).BeginInit(); + this.SuspendLayout(); + // + // dataGridViewCategories + // + this.dataGridViewCategories.AutoGenerateColumns = false; + this.dataGridViewCategories.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dataGridViewCategories.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.categoryIdDataGridViewTextBoxColumn, + this.nameDataGridViewTextBoxColumn}); + this.dataGridViewCategories.DataSource = this.categoryBindingSource; + this.dataGridViewCategories.Location = new System.Drawing.Point(12, 12); + this.dataGridViewCategories.Name = "dataGridViewCategories"; + this.dataGridViewCategories.RowTemplate.Height = 25; + this.dataGridViewCategories.Size = new System.Drawing.Size(240, 200); + this.dataGridViewCategories.TabIndex = 0; + this.dataGridViewCategories.SelectionChanged += new System.EventHandler(this.dataGridViewCategories_SelectionChanged); + // + // categoryIdDataGridViewTextBoxColumn + // + this.categoryIdDataGridViewTextBoxColumn.DataPropertyName = "CategoryId"; + this.categoryIdDataGridViewTextBoxColumn.HeaderText = "CategoryId"; + this.categoryIdDataGridViewTextBoxColumn.Name = "categoryIdDataGridViewTextBoxColumn"; + this.categoryIdDataGridViewTextBoxColumn.ReadOnly = true; + // + // nameDataGridViewTextBoxColumn + // + this.nameDataGridViewTextBoxColumn.DataPropertyName = "Name"; + this.nameDataGridViewTextBoxColumn.HeaderText = "Name"; + this.nameDataGridViewTextBoxColumn.Name = "nameDataGridViewTextBoxColumn"; + // + // categoryBindingSource + // + this.categoryBindingSource.DataSource = typeof(GetStartedWinForms.Category); + // + // dataGridViewProducts + // + this.dataGridViewProducts.AutoGenerateColumns = false; + this.dataGridViewProducts.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; + this.dataGridViewProducts.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] { + this.productIdDataGridViewTextBoxColumn, + this.nameDataGridViewTextBoxColumn1}); + this.dataGridViewProducts.DataSource = this.productsBindingSource; + this.dataGridViewProducts.Location = new System.Drawing.Point(258, 12); + this.dataGridViewProducts.Name = "dataGridViewProducts"; + this.dataGridViewProducts.RowTemplate.Height = 25; + this.dataGridViewProducts.Size = new System.Drawing.Size(240, 200); + this.dataGridViewProducts.TabIndex = 1; + // + // productIdDataGridViewTextBoxColumn + // + this.productIdDataGridViewTextBoxColumn.DataPropertyName = "ProductId"; + this.productIdDataGridViewTextBoxColumn.HeaderText = "ProductId"; + this.productIdDataGridViewTextBoxColumn.Name = "productIdDataGridViewTextBoxColumn"; + this.productIdDataGridViewTextBoxColumn.ReadOnly = true; + // + // nameDataGridViewTextBoxColumn1 + // + this.nameDataGridViewTextBoxColumn1.DataPropertyName = "Name"; + this.nameDataGridViewTextBoxColumn1.HeaderText = "Name"; + this.nameDataGridViewTextBoxColumn1.Name = "nameDataGridViewTextBoxColumn1"; + // + // productsBindingSource + // + this.productsBindingSource.DataMember = "Products"; + this.productsBindingSource.DataSource = this.categoryBindingSource; + // + // buttonSave + // + this.buttonSave.Location = new System.Drawing.Point(423, 218); + this.buttonSave.Name = "buttonSave"; + this.buttonSave.Size = new System.Drawing.Size(75, 23); + this.buttonSave.TabIndex = 2; + this.buttonSave.Text = "Save"; + this.buttonSave.UseVisualStyleBackColor = true; + this.buttonSave.Click += new System.EventHandler(this.buttonSave_Click); + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(512, 248); + this.Controls.Add(this.buttonSave); + this.Controls.Add(this.dataGridViewProducts); + this.Controls.Add(this.dataGridViewCategories); + this.Name = "MainForm"; + this.Text = "Products and Categories"; + ((System.ComponentModel.ISupportInitialize)(this.dataGridViewCategories)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.categoryBindingSource)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.dataGridViewProducts)).EndInit(); + ((System.ComponentModel.ISupportInitialize)(this.productsBindingSource)).EndInit(); + this.ResumeLayout(false); + + } + + #endregion + + private DataGridView dataGridViewCategories; + private DataGridViewTextBoxColumn categoryIdDataGridViewTextBoxColumn; + private DataGridViewTextBoxColumn nameDataGridViewTextBoxColumn; + private BindingSource categoryBindingSource; + private DataGridView dataGridViewProducts; + private DataGridViewTextBoxColumn productIdDataGridViewTextBoxColumn; + private DataGridViewTextBoxColumn nameDataGridViewTextBoxColumn1; + private BindingSource productsBindingSource; + private Button buttonSave; + } +} \ No newline at end of file diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.cs b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.cs new file mode 100644 index 0000000000..84cc348a10 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel; + +namespace GetStartedWinForms +{ + public partial class MainForm : Form + { + private ProductsContext? dbContext; + + public MainForm() + { + InitializeComponent(); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + this.dbContext = new ProductsContext(); + + // Uncomment the line below to start fresh with a new database. + // this.dbContext.Database.EnsureDeleted(); + this.dbContext.Database.EnsureCreated(); + + 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; + } + + private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e) + { + if (this.dbContext != null) + { + var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem; + + if (category != null) + { + this.dbContext.Entry(category).Collection(e => e.Products).Load(); + } + } + } + + private void buttonSave_Click(object sender, EventArgs e) + { + this.dbContext!.SaveChanges(); + + this.dataGridViewCategories.Refresh(); + this.dataGridViewProducts.Refresh(); + } + } +} diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.resx b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.resx new file mode 100644 index 0000000000..a9e3edcb02 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Form1.resx @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 195, 17 + + \ No newline at end of file diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/GetStartedWinForms.csproj b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/GetStartedWinForms.csproj new file mode 100644 index 0000000000..bb0cdaf829 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/GetStartedWinForms.csproj @@ -0,0 +1,15 @@ + + + + WinExe + net7.0-windows + enable + true + enable + + + + + + + \ No newline at end of file diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Product.cs b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Product.cs new file mode 100644 index 0000000000..580a5c35f3 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Product.cs @@ -0,0 +1,13 @@ +using System.ComponentModel; + +namespace GetStartedWinForms; + +public class Product +{ + public int ProductId { get; set; } + + public string? Name { get; set; } + + public int CategoryId { get; set; } + public virtual Category Category { get; set; } = null!; +} diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/ProductsContext.cs b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/ProductsContext.cs new file mode 100644 index 0000000000..a1ce0998c8 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/ProductsContext.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore; + +namespace GetStartedWinForms; + +public class ProductsContext : DbContext +{ + public DbSet Products { get; set; } + public DbSet Categories { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseSqlite("Data Source=products.db"); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData( + new Category { CategoryId = 1, Name = "Cheese" }, + new Category { CategoryId = 2, Name = "Meat" }, + new Category { CategoryId = 3, Name = "Fish" }, + new Category { CategoryId = 4, Name = "Bread" }); + + modelBuilder.Entity().HasData( + new Product { ProductId = 1, CategoryId = 1, Name = "Cheddar" }, + new Product { ProductId = 2, CategoryId = 1, Name = "Brie" }, + new Product { ProductId = 3, CategoryId = 1, Name = "Stilton" }, + new Product { ProductId = 4, CategoryId = 1, Name = "Cheshire" }, + new Product { ProductId = 5, CategoryId = 1, Name = "Swiss" }, + new Product { ProductId = 6, CategoryId = 1, Name = "Gruyere" }, + new Product { ProductId = 7, CategoryId = 1, Name = "Colby" }, + new Product { ProductId = 8, CategoryId = 1, Name = "Mozzela" }, + new Product { ProductId = 9, CategoryId = 1, Name = "Ricotta" }, + new Product { ProductId = 10, CategoryId = 1, Name = "Parmesan" }, + new Product { ProductId = 11, CategoryId = 2, Name = "Ham" }, + new Product { ProductId = 12, CategoryId = 2, Name = "Beef" }, + new Product { ProductId = 13, CategoryId = 2, Name = "Chicken" }, + new Product { ProductId = 14, CategoryId = 2, Name = "Turkey" }, + new Product { ProductId = 15, CategoryId = 2, Name = "Prosciutto" }, + new Product { ProductId = 16, CategoryId = 2, Name = "Bacon" }, + new Product { ProductId = 17, CategoryId = 2, Name = "Mutton" }, + new Product { ProductId = 18, CategoryId = 2, Name = "Pastrami" }, + new Product { ProductId = 19, CategoryId = 2, Name = "Hazlet" }, + new Product { ProductId = 20, CategoryId = 2, Name = "Salami" }, + new Product { ProductId = 21, CategoryId = 3, Name = "Salmon" }, + new Product { ProductId = 22, CategoryId = 3, Name = "Tuna" }, + new Product { ProductId = 23, CategoryId = 3, Name = "Mackerel" }, + new Product { ProductId = 24, CategoryId = 4, Name = "Rye" }, + new Product { ProductId = 25, CategoryId = 4, Name = "Wheat" }, + new Product { ProductId = 26, CategoryId = 4, Name = "Brioche" }, + new Product { ProductId = 27, CategoryId = 4, Name = "Naan" }, + new Product { ProductId = 28, CategoryId = 4, Name = "Focaccia" }, + new Product { ProductId = 29, CategoryId = 4, Name = "Malted" }, + new Product { ProductId = 30, CategoryId = 4, Name = "Sourdough" }, + new Product { ProductId = 31, CategoryId = 4, Name = "Corn" }, + new Product { ProductId = 32, CategoryId = 4, Name = "White" }, + new Product { ProductId = 33, CategoryId = 4, Name = "Soda" }); + } +} diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Program.cs b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Program.cs new file mode 100644 index 0000000000..85aabdf145 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Program.cs @@ -0,0 +1,17 @@ +namespace GetStartedWinForms +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new MainForm()); + } + } +} \ No newline at end of file diff --git a/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Properties/DataSources/Category.datasource b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Properties/DataSources/Category.datasource new file mode 100644 index 0000000000..07cd122652 --- /dev/null +++ b/samples/core/WinForms/GetStartedWinForms/GetStartedWinForms/Properties/DataSources/Category.datasource @@ -0,0 +1,10 @@ + + + + GetStartedWinForms.Category, GetStartedWinForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null + \ No newline at end of file