From c96fda95819bd94fe43a3a90a3d15109c57504a5 Mon Sep 17 00:00:00 2001
From: "Beth-Anne Harvey (AQUENT LLC)"
<28070425+GitHubber17@users.noreply.github.com>
Date: Thu, 16 Apr 2026 11:19:42 -0700
Subject: [PATCH 1/6] refresh content
---
aspnetcore/data/ef-mvc/crud.md | 217 ++++++++++++++++-----------------
1 file changed, 105 insertions(+), 112 deletions(-)
diff --git a/aspnetcore/data/ef-mvc/crud.md b/aspnetcore/data/ef-mvc/crud.md
index 7708a3fd9dc6..16918d5d1229 100644
--- a/aspnetcore/data/ef-mvc/crud.md
+++ b/aspnetcore/data/ef-mvc/crud.md
@@ -1,28 +1,30 @@
---
-title: "Tutorial: Implement CRUD Functionality - ASP.NET MVC with EF Core"
-description: "In this tutorial, you'll review and customize the CRUD (create, read, update, delete) code that the MVC scaffolding automatically creates for you in controllers and views."
+title: Implement CRUD - ASP.NET MVC with EF Core
+description: Review and customize the autogenerated CRUD (create, read, update, delete) code produced by the MVC scaffolding in your controllers and views.
author: tdykstra
ms.author: tdykstra
ms.custom: mvc
-ms.date: 02/04/2019
+ms.date: 04/16/2026
ms.topic: tutorial
uid: data/ef-mvc/crud
+
+# customer intent: As an ASP.NET developer, I want to make changes to the auto-generated CRUD code, so I can customize my controllers and views.
---
-# Tutorial: Implement CRUD Functionality - ASP.NET MVC with EF Core
+# Tutorial: Implement basic CRUD functionality - ASP.NET MVC with EF Core
-In the previous tutorial, you created an MVC application that stores and displays data using the Entity Framework and SQL Server LocalDB. In this tutorial, you'll review and customize the CRUD (create, read, update, delete) code that the MVC scaffolding automatically creates for you in controllers and views.
+In the previous tutorial, you created an MVC application that stores and displays data by using the [Entity Framework (EF) Core](/ef/core/) and a SQL Server local database. In this tutorial, you review and customize the CRUD (create, read, update, delete) code that the MVC scaffolding automatically creates for you in controllers and views.
> [!NOTE]
-> It's a common practice to implement the repository pattern in order to create an abstraction layer between your controller and the data access layer. To keep these tutorials simple and focused on teaching how to use the Entity Framework itself, they don't use repositories. For information about repositories with EF, see [the last tutorial in this series](advanced.md).
+> It's a common practice to implement the repository pattern in order to create an abstraction layer between your controller and the data access layer. To keep the examples simple and focused on demonstrating how to use the Entity Framework itself, the tutorials don't use repositories. For information about repositories with Entity Framework, see [the last tutorial in this series](advanced.md).
In this tutorial, you:
> [!div class="checklist"]
-> * Customize the Details page
-> * Update the Create page
-> * Update the Edit page
-> * Update the Delete page
+> * Customize pages: Details, Create, Edit, and Delete
+> * Explore how to protect against overposting
+> * Use different approaches for HttpPost Edit code and Delete
+> * Work with no-tracking queries
> * Close database connections
## Prerequisites
@@ -31,97 +33,99 @@ In this tutorial, you:
## Customize the Details page
-The scaffolded code for the Students Index page left out the `Enrollments` property, because that property holds a collection. In the **Details** page, you'll display the contents of the collection in an HTML table.
+The scaffolded code for the **Students Index** page left out the `Enrollments` property because the property holds a collection. The **Details** page displays the contents of the collection as an HTML table.
-In `Controllers/StudentsController.cs`, the action method for the Details view uses the `FirstOrDefaultAsync` method to retrieve a single `Student` entity. Add code that calls `Include`. `ThenInclude`, and `AsNoTracking` methods, as shown in the following highlighted code.
+In the _Controllers/StudentsController.cs_ file, the action method for the Details view uses the `FirstOrDefaultAsync` method to retrieve a single `Student` entity. You need to add code that calls the `Include`, `ThenInclude`, and `AsNoTracking` methods, as shown in the following highlighted code.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_Details&highlight=8-12)]
-The `Include` and `ThenInclude` methods cause the context to load the `Student.Enrollments` navigation property, and within each enrollment the `Enrollment.Course` navigation property. You'll learn more about these methods in the [read related data](read-related-data.md) tutorial.
+The `Include` and `ThenInclude` methods cause the context to load the `Student.Enrollments` navigation property and the `Enrollment.Course` navigation property within each enrollment. You learn more about these methods in the [read related data](read-related-data.md) tutorial.
-The `AsNoTracking` method improves performance in scenarios where the entities returned won't be updated in the current context's lifetime. You'll learn more about `AsNoTracking` at the end of this tutorial.
+The `AsNoTracking` method improves performance in scenarios where the returned entities aren't updated in the current context's lifetime. You learn more about the `AsNoTracking` method at the end of this tutorial.
-### Route data
+### Configure the route data
-The key value that's passed to the `Details` method comes from *route data*. Route data is data that the model binder found in a segment of the URL. For example, the default route specifies controller, action, and id segments:
+The key value passed to the `Details` method comes from the *route data*. Route data is data that the model binder finds in a segment of the URL. For example, the default route specifies `controller`, `action`, and `id` (identifier, ID) segments:
[!code-csharp[](intro/samples/5cu/Startup.cs?name=snippet2)]
-In the following URL, the default route maps Instructor as the controller, Index as the action, and 1 as the id; these are route data values.
+In the following URL, the default route maps _Instructor_ as the `controller`, _Index_ as the `action`, and 1 as the `id`. These values compose the route data.
-```
+```url
http://localhost:1230/Instructor/Index/1?courseID=2021
```
-The last part of the URL ("?courseID=2021") is a query string value. The model binder will also pass the ID value to the `Index` method `id` parameter if you pass it as a query string value:
+The last part of the URL (`?courseID=2021`) is a query string value. The model binder also passes the ID value to the `Index` method `id` parameter, if you pass it as a query string value:
-```
+```url
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
```
-In the Index page, hyperlink URLs are created by tag helper statements in the Razor view. In the following Razor code, the `id` parameter matches the default route, so `id` is added to the route data.
+In the **Index** page, hyperlink URLs are created by tag helper statements in the Razor view. In the following Razor code, the `id` parameter matches the default route, so the `id` value is added to the route data.
```html
Edit
```
-This generates the following HTML when `item.ID` is 6:
+This change generates the following HTML when the `item.ID` value is 6:
```html
Edit
```
-In the following Razor code, `studentID` doesn't match a parameter in the default route, so it's added as a query string.
+In the following Razor code, the `studentID` text doesn't match a parameter in the default route, so the text is added as a query string.
```html
Edit
```
-This generates the following HTML when `item.ID` is 6:
+This change generates the following HTML when the `item.ID` value is 6:
```html
Edit
```
-For more information about tag helpers, see .
+For more information about tag helpers, see [Tag Helpers in ASP.NET Core](../../mvc/views/tag-helpers/intro.md).
### Add enrollments to the Details view
-Open `Views/Students/Details.cshtml`. Each field is displayed using `DisplayNameFor` and `DisplayFor` helpers, as shown in the following example:
+Open the `Views/Students/Details.cshtml` file. Each field is displayed by using the `DisplayNameFor` and `DisplayFor` helpers, as shown in the following example.
[!code-cshtml[](intro/samples/cu/Views/Students/Details.cshtml?range=13-18&highlight=2,5)]
-After the last field and immediately before the closing `` tag, add the following code to display a list of enrollments:
+After the last field, and immediately before the closing `` tag, add the following code to display a list of enrollments:
[!code-cshtml[](intro/samples/cu/Views/Students/Details.cshtml?range=31-52)]
-If code indentation is wrong after you paste the code, press CTRL-K-D to correct it.
+If the code indentation is misaligned after you paste the snippet, use the **CTRL**+**K**+**D** keyboard shortcut to correct it.
-This code loops through the entities in the `Enrollments` navigation property. For each enrollment, it displays the course title and the grade. The course title is retrieved from the Course entity that's stored in the `Course` navigation property of the Enrollments entity.
+The new code loops through the entities in the `Enrollments` navigation property. For each enrollment, the code displays the course title and the grade. The course title is retrieved from the `Course` entity stored in the `Course` navigation property of the `Enrollments` entity.
-Run the app, select the **Students** tab, and click the **Details** link for a student. You see the list of courses and grades for the selected student:
+Run the app, select the **Students** tab, and select the **Details** link for a student. You see the list of courses and grades for the selected student:
-
+:::image type="content" source="crud/_static/student-details.png" border="false" alt-text="Screenshot of the Student Details page.":::
## Update the Create page
-In `StudentsController.cs`, modify the HttpPost `Create` method by adding a try-catch block and removing ID from the `Bind` attribute.
+In the `StudentsController.cs` file, modify the HttpPost `Create` method by adding a `try-catch` block and removing the `ID` property from the `Bind` attribute.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_Create&highlight=4,6-7,14-21)]
-This code adds the Student entity created by the ASP.NET Core MVC model binder to the Students entity set and then saves the changes to the database. (Model binder refers to the ASP.NET Core MVC functionality that makes it easier for you to work with data submitted by a form; a model binder converts posted form values to CLR types and passes them to the action method in parameters. In this case, the model binder instantiates a Student entity for you using property values from the Form collection.)
+This code adds the `Student` entity created by the ASP.NET Core MVC model binder to the `Students` entity set, and then saves the changes to the database.
-You removed `ID` from the `Bind` attribute because ID is the primary key value which SQL Server will set automatically when the row is inserted. Input from the user doesn't set the ID value.
+_Model binder_ refers to the ASP.NET Core MVC functionality that makes it easier for you to work with data submitted by a form. A model binder converts posted form values to CLR types and passes them to the action method in parameters. In this case, the model binder instantiates a `Student` entity for you by using property values from the `Form` collection.
-Other than the `Bind` attribute, the try-catch block is the only change you've made to the scaffolded code. If an exception that derives from `DbUpdateException` is caught while the changes are being saved, a generic error message is displayed. `DbUpdateException` exceptions are sometimes caused by something external to the application rather than a programming error, so the user is advised to try again. Although not implemented in this sample, a production quality application would log the exception. For more information, see the **Log for insight** section in [Monitoring and Telemetry (Building Real-World Cloud Apps with Azure)](/aspnet/aspnet/overview/developing-apps-with-windows-azure/building-real-world-cloud-apps-with-windows-azure/monitoring-and-telemetry).
+You removed the `ID` property from the `Bind` attribute because the ID is the primary key value that SQL Server sets automatically when it inserts the row. Input from the user doesn't set the ID value.
-The `ValidateAntiForgeryToken` attribute helps prevent cross-site request forgery (CSRF) attacks. The token is automatically injected into the view by the [FormTagHelper](xref:mvc/views/working-with-forms#the-form-tag-helper) and is included when the form is submitted by the user. The token is validated by the `ValidateAntiForgeryToken` attribute. For more information, see .
+Other than the `Bind` attribute, the `try-catch` block is the only change made to the scaffolded code. If an exception that derives from the `DbUpdateException` handler is caught while the changes are being saved, a generic error message is displayed. The cause for a `DbUpdateException` exception can be external to the application rather than a programming error, so the user is advised to try again. A production quality application logs such exception, but that functionality isn't implemented in this sample. For more information, see [Logging in .NET and ASP.NET Core](../../fundamentals/logging/index.md).
+
+The `ValidateAntiForgeryToken` attribute helps prevent cross-site request forgery (CSRF) attacks. The [Form Tag Helper](../../mvc/views/working-with-forms.md#the-form-tag-helper) method automatically injects the token into the view. The token is also included when the user submits the form. The `ValidateAntiForgeryToken` attribute checks the token. For more information, see t[Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core](../../security/anti-request-forgery.md) and [FormTagHelper Class](/dotnet/api/microsoft.aspnetcore.mvc.taghelpers.formtaghelper) reference.
-### Security note about overposting
+### Protect against overposting
-The `Bind` attribute that the scaffolded code includes on the `Create` method is one way to protect against overposting in create scenarios. For example, suppose the Student entity includes a `Secret` property that you don't want this web page to set.
+The `Bind` attribute that the scaffolded code includes on the `Create` method is one way to protect against overposting in create scenarios. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to set.
```csharp
public class Student
@@ -134,174 +138,163 @@ public class Student
}
```
-Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. Without the `Bind` attribute limiting the fields that the model binder uses when it creates a Student instance, the model binder would pick up that `Secret` form value and use it to create the Student entity instance. Then whatever value the hacker specified for the `Secret` form field would be updated in your database. The following image shows the Fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
+Even if you don't have a `Secret` field on the web page, a hacker might use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. Without the `Bind` attribute limiting the fields that the model binder uses when it creates a `Student` instance, the model binder can pick up that `Secret` form value and use it to create the `Student` entity instance. Then, whatever value the hacker specified for the `Secret` form field is updated in your database. The following image shows the Fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
-
+:::image type="content" source="crud/_static/fiddler.png" border="false" alt-text="Screenshot of the Composer view in Fiddler showing the Secret field added in the request body.":::
-The value "OverPost" would then be successfully added to the `Secret` property of the inserted row, although you never intended that the web page be able to set that property.
+The value "OverPost" is then successfully added to the `Secret` property of the inserted row, although you never intended that the web page set the property.
-You can prevent overposting in edit scenarios by reading the entity from the database first and then calling `TryUpdateModel`, passing in an explicit allowed properties list. That's the method used in these tutorials.
+You can prevent overposting in edit scenarios by reading the entity from the database first and then calling the `TryUpdateModel` method. Pass in an explicit allowed properties list, which is how the method is used in these tutorials.
-An alternative way to prevent overposting that's preferred by many developers is to use view models rather than entity classes with model binding. Include only the properties you want to update in the view model. Once the MVC model binder has finished, copy the view model properties to the entity instance, optionally using a tool such as AutoMapper. Use `_context.Entry` on the entity instance to set its state to `Unchanged`, and then set `Property("PropertyName").IsModified` to true on each entity property that's included in the view model. This method works in both edit and create scenarios.
+An alternate way to prevent overposting preferred by many developers is to use view models rather than entity classes with model binding. Include only the properties you want to update in the view model. After the MVC model binder completes, copy the view model properties to the entity instance, optionally by using a tool such as AutoMapper. Use the `_context.Entry` setting on the entity instance to set its state to `Unchanged`, and then set the `Property("PropertyName").IsModified` setting to true on each entity property included in the view model. This method works in both edit and create scenarios.
### Test the Create page
-The code in `Views/Students/Create.cshtml` uses `label`, `input`, and `span` (for validation messages) tag helpers for each field.
+The code in the `Views/Students/Create.cshtml` file uses the `label`, `input`, and `span` (for validation messages) tag helpers for each field.
-Run the app, select the **Students** tab, and click **Create New**.
+Run the app, select the **Students** tab, and then select **Create New**.
-Enter names and a date. Try entering an invalid date if your browser lets you do that. (Some browsers force you to use a date picker.) Then click **Create** to see the error message.
+Enter names and a date. Try entering an invalid date, as your browser allows. (Some browsers force you to use a date picker.) Select **Create** to see the error message.
-
+:::image type="content" source="crud/_static/date-error.png" border="false" alt-text="Screenshot of the Create page showing the validation error message for the invalid Date field value.":::
-This is server-side validation that you get by default; in a later tutorial you'll see how to add attributes that will generate code for client-side validation also. The following highlighted code shows the model validation check in the `Create` method.
+This scenario demonstrates the default server-side validation. In a later tutorial, you see how to add attributes that generate code for client-side validation. The following highlighted code shows the model validation check in the `Create` method.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_Create&highlight=8)]
-Change the date to a valid value and click **Create** to see the new student appear in the **Index** page.
+Change the date to a valid value, and select **Create** to see the new student appear in the **Index** page.
## Update the Edit page
-In `StudentController.cs`, the HttpGet `Edit` method (the one without the `HttpPost` attribute) uses the `FirstOrDefaultAsync` method to retrieve the selected Student entity, as you saw in the `Details` method. You don't need to change this method.
+In the `StudentController.cs` file, the HttpGet `Edit` method (the one without the `HttpPost` attribute) uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you reviewed earlier in the `Details` method. You don't need to change this method.
-### Recommended HttpPost Edit code: Read and update
+### Use recommended HttpPost Edit code: Read and update
-Replace the HttpPost Edit action method with the following code.
+Replace the HttpPost `Edit` method with the following code.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_ReadFirst)]
-These changes implement a security best practice to prevent overposting. The scaffolder generated a `Bind` attribute and added the entity created by the model binder to the entity set with a `Modified` flag. That code isn't recommended for many scenarios because the `Bind` attribute clears out any pre-existing data in fields not listed in the `Include` parameter.
+These changes implement a security best practice to prevent overposting. The scaffolder generated a `Bind` attribute and added the entity created by the model binder to the entity set with a `Modified` flag. That code isn't recommended for many scenarios because the `Bind` attribute clears out any preexisting data in fields not listed in the `Include` parameter.
-The new code reads the existing entity and calls `TryUpdateModel` to update fields in the retrieved entity [based on user input in the posted form data](xref:mvc/models/model-binding). The Entity Framework's automatic change tracking sets the `Modified` flag on the fields that are changed by form input. When the `SaveChanges` method is called, the Entity Framework creates SQL statements to update the database row. Concurrency conflicts are ignored, and only the table columns that were updated by the user are updated in the database. (A later tutorial shows how to handle concurrency conflicts.)
+The new code reads the existing entity and calls the `TryUpdateModel` method to update fields in the retrieved entity based on user input in the posted form data. (For more information, see [Model Binding in ASP.NET Core](../../mvc/models/model-binding.md).) The Entity Framework's automatic change tracking sets the `Modified` flag on fields that the form input updates. When the `SaveChanges` method is called, the Entity Framework creates SQL statements to update the database row. Concurrency conflicts are ignored, and only the table columns updated by the user are also updated in the database. (A later tutorial shows how to handle concurrency conflicts.)
-As a best practice to prevent overposting, the fields that you want to be updateable by the **Edit** page are declared in the `TryUpdateModel` parameters. (The empty string preceding the list of fields in the parameter list is for a prefix to use with the form fields names.) Currently there are no extra fields that you're protecting, but listing the fields that you want the model binder to bind ensures that if you add fields to the data model in the future, they're automatically protected until you explicitly add them here.
+As a best practice to prevent overposting, the fields that you want to be updateable by the **Edit** page are declared in the `TryUpdateModel` parameters. (The empty string preceding the list of fields in the parameter list is for a prefix to use with the form fields names.) Currently, there are no extra fields that you're protecting. If you list the fields you want the model binder to bind, you can prevent overposting for fields added in the future. The fields in the list are automatically protected until you explicitly add them.
-As a result of these changes, the method signature of the HttpPost `Edit` method is the same as the HttpGet `Edit` method; therefore you've renamed the method `EditPost`.
+As a result of these changes, the method signature of the HttpPost `Edit` method is the same as the HttpGet `Edit` method. The changes basically rename the method to `EditPost`.
-### Alternative HttpPost Edit code: Create and attach
+### Use alternate HttpPost Edit code: Create and attach
-The recommended HttpPost edit code ensures that only changed columns get updated and preserves data in properties that you don't want included for model binding. However, the read-first approach requires an extra database read, and can result in more complex code for handling concurrency conflicts. An alternative is to attach an entity created by the model binder to the EF context and mark it as modified. (Don't update your project with this code, it's only shown to illustrate an optional approach.)
+The recommended HttpPost `Edit` code ensures that only changed columns are updated and preserves data in properties that you don't want included for model binding. However, the read-first approach requires an extra database read, and can result in more complex code for handling concurrency conflicts. An alternative is to attach an entity created by the model binder to the Entity Framework context and mark it as modified. (Don't update your project with the following code. The code illustrates an optional approach.)
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_CreateAndAttach)]
You can use this approach when the web page UI includes all of the fields in the entity and can update any of them.
-The scaffolded code uses the create-and-attach approach but only catches `DbUpdateConcurrencyException` exceptions and returns 404 error codes. The example shown catches any database update exception and displays an error message.
+The scaffolded code uses the _create-and-attach_ approach but only catches `DbUpdateConcurrencyException` exceptions and returns 404 error codes. The example shown catches any database update exception and displays an error message.
-### Entity States
+### Understand entity states
-The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database, and this information determines what happens when you call the `SaveChanges` method. For example, when you pass a new entity to the `Add` method, that entity's state is set to `Added`. Then when you call the `SaveChanges` method, the database context issues a SQL INSERT command.
+The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database. This information determines what happens when you call the `SaveChanges` method. For example, when you pass a new entity to the `Add` method, that entity's state is set to `Added`. When you call the `SaveChanges` method, the database context issues a SQL INSERT command.
-An entity may be in one of the following states:
+An entity can be in one of the following states:
-* `Added`. The entity doesn't yet exist in the database. The `SaveChanges` method issues an INSERT statement.
+* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an INSERT statement.
-* `Unchanged`. Nothing needs to be done with this entity by the `SaveChanges` method. When you read an entity from the database, the entity starts out with this status.
+* `Unchanged`: Nothing needs to be done with this entity by the `SaveChanges` method. When you read an entity from the database, the entity starts out with this status.
-* `Modified`. Some or all of the entity's property values have been modified. The `SaveChanges` method issues an UPDATE statement.
+* `Modified`: Some or all of the entity's property values are modified. The `SaveChanges` method issues an UPDATE statement.
-* `Deleted`. The entity has been marked for deletion. The `SaveChanges` method issues a DELETE statement.
+* `Deleted`: The entity is marked for deletion. The `SaveChanges` method issues a DELETE statement.
-* `Detached`. The entity isn't being tracked by the database context.
+* `Detached`: The database context doesn't track the entity.
-In a desktop application, state changes are typically set automatically. You read an entity and make changes to some of its property values. This causes its entity state to automatically be changed to `Modified`. Then when you call `SaveChanges`, the Entity Framework generates a SQL UPDATE statement that updates only the actual properties that you changed.
+In a desktop application, state changes are typically set automatically. You read an entity and make changes to some of its property values. This behavior causes its entity state to automatically change to `Modified`. When you call the `SaveChanges` method, the Entity Framework generates a SQL UPDATE statement that updates only the actual properties that you changed.
-In a web app, the `DbContext` that initially reads an entity and displays its data to be edited is disposed after a page is rendered. When the HttpPost `Edit` action method is called, a new web request is made and you have a new instance of the `DbContext`. If you re-read the entity in that new context, you simulate desktop processing.
+In a web app, the `DbContext` method that initially reads an entity and displays its data to be edited is disposed after a page is rendered. When the HttpPost `Edit` action method is called, a new web request is made and you have a new instance of the `DbContext` method. If you re-read the entity in that new context, you simulate desktop processing.
-But if you don't want to do the extra read operation, you have to use the entity object created by the model binder. The simplest way to do this is to set the entity state to Modified as is done in the alternative HttpPost Edit code shown earlier. Then when you call `SaveChanges`, the Entity Framework updates all columns of the database row, because the context has no way to know which properties you changed.
+If you don't want to do the extra read operation, you have to use the entity object created by the model binder. The easiest approach is to set the entity state to `Modified` as is done in the alternative HttpPost `Edit` code shown earlier. When you call the `SaveChanges` method, the Entity Framework updates all columns of the database row because the context has no way to know which properties you changed.
-If you want to avoid the read-first approach, but you also want the SQL UPDATE statement to update only the fields that the user actually changed, the code is more complex. You have to save the original values in some way (such as by using hidden fields) so that they're available when the HttpPost `Edit` method is called. Then you can create a Student entity using the original values, call the `Attach` method with that original version of the entity, update the entity's values to the new values, and then call `SaveChanges`.
+If you want to avoid the _read-first_ approach, but you also want the SQL UPDATE statement to update only the fields the user changes, the code is more complex. You have to save the original values in some way, such as by using hidden fields. The values must be available when the HttpPost `Edit` method is called. You can create a `Student` entity by using the original values, call the `Attach` method with the original version of the entity, update the entity's values to the new values, and then call the `SaveChanges` method.
### Test the Edit page
-Run the app, select the **Students** tab, then click an **Edit** hyperlink.
+Run the app, select the **Students** tab, then select an **Edit** hyperlink.
-
+:::image type="content" source="crud/_static/student-edit.png" border="false" alt-text="Screenshot of the Edit page for a Student.":::
-Change some of the data and click **Save**. The **Index** page opens and you see the changed data.
+Change some of the data and select **Save**. The **Index** page opens and you see the changed data.
## Update the Delete page
-In `StudentController.cs`, the template code for the HttpGet `Delete` method uses the `FirstOrDefaultAsync` method to retrieve the selected Student entity, as you saw in the Details and Edit methods. However, to implement a custom error message when the call to `SaveChanges` fails, you'll add some functionality to this method and its corresponding view.
+In the `StudentController.cs` file, the template code for the HttpGet `Delete` method uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you saw in the `Details` and `Edit` methods. However, to implement a custom error message when the call to the `SaveChanges` method fails, you need to add some functionality to this method and its corresponding view.
-As you saw for update and create operations, delete operations require two action methods. The method that's called in response to a GET request displays a view that gives the user a chance to approve or cancel the delete operation. If the user approves it, a POST request is created. When that happens, the HttpPost `Delete` method is called and then that method actually performs the delete operation.
+As you saw for the update and create operations, delete operations require two action methods. The method called in response to a GET request displays a view that gives the user a chance to approve or cancel the delete operation. If the user approves, a POST request is created. The HttpPost `Delete` method is then called and this method actually performs the delete operation.
-You'll add a try-catch block to the HttpPost `Delete` method to handle any errors that might occur when the database is updated. If an error occurs, the HttpPost Delete method calls the HttpGet Delete method, passing it a parameter that indicates that an error has occurred. The HttpGet Delete method then redisplays the confirmation page along with the error message, giving the user an opportunity to cancel or try again.
+You need to add a try-catch block to the HttpPost `Delete` method to handle any errors that might occur when the database is updated. If an error occurs, the HttpPost `Delete` method calls the HttpGet `Delete` method, passing a parameter that indicates an error. The HttpGet `Delete` method redisplays the confirmation page along with the error message, giving the user an opportunity to cancel or try again.
Replace the HttpGet `Delete` action method with the following code, which manages error reporting.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_DeleteGet&highlight=1,9,16-21)]
-This code accepts an optional parameter that indicates whether the method was called after a failure to save changes. This parameter is false when the HttpGet `Delete` method is called without a previous failure. When it's called by the HttpPost `Delete` method in response to a database update error, the parameter is true and an error message is passed to the view.
+This code accepts an optional parameter that indicates whether the method call is after a failure to save changes. This parameter is false when the HttpGet `Delete` method is called without a previous failure. When the method is called by the HttpPost `Delete` method in response to a database update error, the parameter is true and an error message is passed to the view.
-### The read-first approach to HttpPost Delete
+### Use read-first approach to HttpPost Delete
-Replace the HttpPost `Delete` action method (named `DeleteConfirmed`) with the following code, which performs the actual delete operation and catches any database update errors.
+Replace the HttpPost `Delete` action method (named `DeleteConfirmed`) with the following code. This code performs the actual delete operation and catches any database update errors.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_DeleteWithReadFirst&highlight=6-9,11-12,16-21)]
-This code retrieves the selected entity, then calls the `Remove` method to set the entity's status to `Deleted`. When `SaveChanges` is called, a SQL DELETE command is generated.
+The code retrieves the selected entity, and then calls the `Remove` method to set the entity's status to `Deleted`. When the `SaveChanges` method is called, a SQL DELETE command is generated.
-### The create-and-attach approach to HttpPost Delete
+### Use create-and-attach approach to HttpPost Delete
-If improving performance in a high-volume application is a priority, you could avoid an unnecessary SQL query by instantiating a Student entity using only the primary key value and then setting the entity state to `Deleted`. That's all that the Entity Framework needs in order to delete the entity. (Don't put this code in your project; it's here just to illustrate an alternative.)
+If improving performance in a high-volume application is a priority, you might avoid an unnecessary SQL query by instantiating a `Student` entity by using only the primary key value and setting the entity state to `Deleted`. This information is all the Entity Framework needs to delete the entity. (Don't update your project with the following code. The code illustrates an alternate approach.)
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_DeleteWithoutReadFirst&highlight=7-8)]
-If the entity has related data that should also be deleted, make sure that cascade delete is configured in the database. With this approach to entity deletion, EF might not realize there are related entities to be deleted.
+If the entity also contains related data to delete, make sure that cascade delete is configured in the database. With this approach to entity deletion, Entity Framework might not realize there are related entities to delete.
### Update the Delete view
-In `Views/Student/Delete.cshtml`, add an error message between the h2 heading and the h3 heading, as shown in the following example:
+In the `Views/Student/Delete.cshtml` file, add an error message between the h2 heading and the h3 heading, as shown in the following example:
[!code-cshtml[](intro/samples/cu/Views/Students/Delete.cshtml?range=7-9&highlight=2)]
-Run the app, select the **Students** tab, and click a **Delete** hyperlink:
+Run the app, select the **Students** tab, and select a **Delete** hyperlink:
-
+:::image type="content" source="crud/_static/student-delete.png" border="false" alt-text="Screenshot of the Confirmation page for the Delete action.":::
-Click **Delete**. The Index page is displayed without the deleted student. (You'll see an example of the error handling code in action in the concurrency tutorial.)
+Select **Delete**. The **Index** page displays without the deleted student. (You see an example of the error handling code in action in the concurrency tutorial.)
## Close database connections
-To free up the resources that a database connection holds, the context instance must be disposed as soon as possible when you are done with it. The ASP.NET Core built-in [dependency injection](../../fundamentals/dependency-injection.md) takes care of that task for you.
+To free up the resources that a database connection holds, the context instance must be disposed as soon as possible when you're finished. The ASP.NET Core built-in [dependency injection](../../fundamentals/dependency-injection.md) takes care of the clean-up task.
-In `Startup.cs`, you call the [AddDbContext extension method](https://github.com/aspnet/EntityFrameworkCore/blob/03bcb5122e3f577a84498545fcf130ba79a3d987/src/Microsoft.EntityFrameworkCore/EntityFrameworkServiceCollectionExtensions.cs) to provision the `DbContext` class in the ASP.NET Core DI container. That method sets the service lifetime to `Scoped` by default. `Scoped` means the context object lifetime coincides with the web request life time, and the `Dispose` method will be called automatically at the end of the web request.
+In the `Startup.cs` file, you call the [AddDbContext extension method](https://github.com/dotnet/efcore/blob/03bcb5122e3f577a84498545fcf130ba79a3d987/src/Microsoft.EntityFrameworkCore/EntityFrameworkServiceCollectionExtensions.cs) to create the `DbContext` class in the ASP.NET Core DI container. The method sets the service lifetime to `Scoped` by default. `Scoped` means the context object lifetime coincides with the web request life time, and the `Dispose` method is called automatically at the end of the web request.
## Handle transactions
-By default the Entity Framework implicitly implements transactions. In scenarios where you make changes to multiple rows or tables and then call `SaveChanges`, the Entity Framework automatically makes sure that either all of your changes succeed or they all fail. If some changes are done first and then an error happens, those changes are automatically rolled back. For scenarios where you need more control -- for example, if you want to include operations done outside of Entity Framework in a transaction -- see [Transactions](/ef/core/saving/transactions).
+By default, the Entity Framework implicitly implements transactions. In scenarios where you make changes to multiple rows or tables and then call the `SaveChanges` method, the Entity Framework automatically makes sure that either all of your changes succeed or they all fail. If some changes are finished first and then an error occurs, the completed changes are automatically rolled back. In scenarios where you need more control, see [Transactions](/ef/core/saving/transactions). The examples cover how to include operations completed outside of Entity Framework in a transaction.
-## No-tracking queries
+## Disable tracking of entity objects (no-tracking queries)
-When a database context retrieves table rows and creates entity objects that represent them, by default it keeps track of whether the entities in memory are in sync with what's in the database. The data in memory acts as a cache and is used when you update an entity. This caching is often unnecessary in a web application because context instances are typically short-lived (a new one is created and disposed for each request) and the context that reads an entity is typically disposed before that entity is used again.
+When a database context retrieves table rows and creates entity objects that represent them, it tracks whether the entities in memory are in sync with the database, by default. The data in memory acts as a cache and is used when you update an entity. This caching is often unnecessary in a web application because context instances are typically short-lived (a new one is created and disposed for each request). The context that reads an entity is typically disposed before that entity is used again.
-You can disable tracking of entity objects in memory by calling the `AsNoTracking` method. Typical scenarios in which you might want to do that include the following:
+You can disable tracking of entity objects in memory by calling the `AsNoTracking` method. Here are some of the common scenarios for this action:
-* During the context lifetime you don't need to update any entities, and you don't need EF to [automatically load navigation properties with entities retrieved by separate queries](read-related-data.md). Frequently these conditions are met in a controller's HttpGet action methods.
+* During the context lifetime, you don't need to update any entities, and you don't need Entity Framework to [automatically load navigation properties with entities retrieved by separate queries](read-related-data.md). These conditions are frequently met in a controller's HttpGet `Action` methods.
-* You are running a query that retrieves a large volume of data, and only a small portion of the returned data will be updated. It may be more efficient to turn off tracking for the large query, and run a query later for the few entities that need to be updated.
+* You're running a query that retrieves a large volume of data, and only a small portion of the returned data gets updated. It can be more efficient to turn off tracking for the large query, and run a query later for the few entities that need updates.
-* You want to attach an entity in order to update it, but earlier you retrieved the same entity for a different purpose. Because the entity is already being tracked by the database context, you can't attach the entity that you want to change. One way to handle this situation is to call `AsNoTracking` on the earlier query.
+* You want to attach an entity to make updates, but earlier you retrieved the same entity for a different purpose. Because the database context already tracks the entity, you can't attach the entity you want to change. One way to handle this situation is to call the `AsNoTracking` method on the earlier query.
-For more information, see [Tracking vs. No-Tracking](/ef/core/querying/tracking).
+For more information, see [Tracking vs. No-Tracking queries](/ef/core/querying/tracking).
## Get the code
-[Download or view the completed application.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final)
-
-## Next steps
-
-In this tutorial, you:
-
-> [!div class="checklist"]
-> * Customized the Details page
-> * Updated the Create page
-> * Updated the Edit page
-> * Updated the Delete page
-> * Closed database connections
+[Download or view the completed application](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final),
-Advance to the next tutorial to learn how to expand the functionality of the **Index** page by adding sorting, filtering, and paging.
+## Next step
> [!div class="nextstepaction"]
-> [Next: Sorting, filtering, and paging](sort-filter-page.md)
+> [Sorting, filtering, and paging](sort-filter-page.md)
From a1768d18d1cfc2db61c1a74b02ff2212b533adbe Mon Sep 17 00:00:00 2001
From: "Beth-Anne Harvey (AQUENT LLC)"
<28070425+GitHubber17@users.noreply.github.com>
Date: Thu, 16 Apr 2026 12:09:17 -0700
Subject: [PATCH 2/6] adjust bookmark refs
---
aspnetcore/data/ef-mvc/advanced.md | 2 +-
aspnetcore/data/ef-mvc/crud.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/aspnetcore/data/ef-mvc/advanced.md b/aspnetcore/data/ef-mvc/advanced.md
index 5af9aede68d7..c305e216ac59 100644
--- a/aspnetcore/data/ef-mvc/advanced.md
+++ b/aspnetcore/data/ef-mvc/advanced.md
@@ -34,7 +34,7 @@ In this tutorial, you:
One of the advantages of using the Entity Framework is that it avoids tying your code too closely to a particular method of storing data. It does this by generating SQL queries and commands for you, which also frees you from having to write them yourself. But there are exceptional scenarios when you need to run specific SQL queries that you have manually created. For these scenarios, the Entity Framework Code First API includes methods that enable you to pass SQL commands directly to the database. You have the following options in EF Core 1.0:
-* Use the `DbSet.FromSql` method for queries that return entity types. The returned objects must be of the type expected by the `DbSet` object, and they're automatically tracked by the database context unless you [turn tracking off](crud.md#no-tracking-queries).
+* Use the `DbSet.FromSql` method for queries that return entity types. The returned objects must be of the type expected by the `DbSet` object, and they're automatically tracked by the database context unless you [turn tracking off](crud.md##disable-tracking-of-entity-objects-no-tracking-queries).
* Use the `Database.ExecuteSqlCommand` for non-query commands.
diff --git a/aspnetcore/data/ef-mvc/crud.md b/aspnetcore/data/ef-mvc/crud.md
index 16918d5d1229..ef896074b25c 100644
--- a/aspnetcore/data/ef-mvc/crud.md
+++ b/aspnetcore/data/ef-mvc/crud.md
@@ -288,7 +288,7 @@ You can disable tracking of entity objects in memory by calling the `AsNoTrackin
* You want to attach an entity to make updates, but earlier you retrieved the same entity for a different purpose. Because the database context already tracks the entity, you can't attach the entity you want to change. One way to handle this situation is to call the `AsNoTracking` method on the earlier query.
-For more information, see [Tracking vs. No-Tracking queries](/ef/core/querying/tracking).
+For more information, see [Tracking versus no-tracking queries](/ef/core/querying/tracking).
## Get the code
From 4d8cacf5053415f5c4ceb24a0eb71c78682652a4 Mon Sep 17 00:00:00 2001
From: "Beth-Anne Harvey (AQUENT LLC)"
<28070425+GitHubber17@users.noreply.github.com>
Date: Fri, 17 Apr 2026 09:02:22 -0700
Subject: [PATCH 3/6] edits to refresh content
---
aspnetcore/data/ef-mvc/advanced.md | 2 +-
aspnetcore/data/ef-mvc/crud.md | 32 +-
aspnetcore/data/ef-rp/crud.md | 483 ++++++-----------------------
aspnetcore/includes/RP-EF/intro.md | 4 +-
4 files changed, 120 insertions(+), 401 deletions(-)
diff --git a/aspnetcore/data/ef-mvc/advanced.md b/aspnetcore/data/ef-mvc/advanced.md
index c305e216ac59..8944e9651b90 100644
--- a/aspnetcore/data/ef-mvc/advanced.md
+++ b/aspnetcore/data/ef-mvc/advanced.md
@@ -34,7 +34,7 @@ In this tutorial, you:
One of the advantages of using the Entity Framework is that it avoids tying your code too closely to a particular method of storing data. It does this by generating SQL queries and commands for you, which also frees you from having to write them yourself. But there are exceptional scenarios when you need to run specific SQL queries that you have manually created. For these scenarios, the Entity Framework Code First API includes methods that enable you to pass SQL commands directly to the database. You have the following options in EF Core 1.0:
-* Use the `DbSet.FromSql` method for queries that return entity types. The returned objects must be of the type expected by the `DbSet` object, and they're automatically tracked by the database context unless you [turn tracking off](crud.md##disable-tracking-of-entity-objects-no-tracking-queries).
+* Use the `DbSet.FromSql` method for queries that return entity types. The returned objects must be of the type expected by the `DbSet` object, and they're automatically tracked by the database context unless you [turn tracking off](crud.md#disable-tracking-of-entity-objects-no-tracking-queries).
* Use the `Database.ExecuteSqlCommand` for non-query commands.
diff --git a/aspnetcore/data/ef-mvc/crud.md b/aspnetcore/data/ef-mvc/crud.md
index ef896074b25c..0a85c8a39ab9 100644
--- a/aspnetcore/data/ef-mvc/crud.md
+++ b/aspnetcore/data/ef-mvc/crud.md
@@ -21,15 +21,15 @@ In the previous tutorial, you created an MVC application that stores and display
In this tutorial, you:
> [!div class="checklist"]
-> * Customize pages: Details, Create, Edit, and Delete
+> * Customize pages: **Details**, **Create**, **Edit**, and **Delete**
> * Explore how to protect against overposting
-> * Use different approaches for HttpPost Edit code and Delete
+> * Use different approaches for HttpPost `Edit` code and `Delete`
> * Work with no-tracking queries
> * Close database connections
## Prerequisites
-* [Get started with EF Core and ASP.NET Core MVC](intro.md)
+* Complete the previous tutorial, [Get started with EF Core in an ASP.NET MVC web app](intro.md).
## Customize the Details page
@@ -89,7 +89,7 @@ For more information about tag helpers, see [Tag Helpers in ASP.NET Core](../../
### Add enrollments to the Details view
-Open the `Views/Students/Details.cshtml` file. Each field is displayed by using the `DisplayNameFor` and `DisplayFor` helpers, as shown in the following example.
+Open the _Views/Students/Details.cshtml_ file. Each field is displayed by using the `DisplayNameFor` and `DisplayFor` helpers, as shown in the following example.
[!code-cshtml[](intro/samples/cu/Views/Students/Details.cshtml?range=13-18&highlight=2,5)]
@@ -107,7 +107,7 @@ Run the app, select the **Students** tab, and select the **Details** link for a
## Update the Create page
-In the `StudentsController.cs` file, modify the HttpPost `Create` method by adding a `try-catch` block and removing the `ID` property from the `Bind` attribute.
+In the _StudentsController.cs_ file, modify the HttpPost `Create` method by adding a `try-catch` block and removing the `ID` property from the `Bind` attribute.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_Create&highlight=4,6-7,14-21)]
@@ -150,7 +150,7 @@ An alternate way to prevent overposting preferred by many developers is to use v
### Test the Create page
-The code in the `Views/Students/Create.cshtml` file uses the `label`, `input`, and `span` (for validation messages) tag helpers for each field.
+The code in the _Views/Students/Create.cshtml_ file uses the `label`, `input`, and `span` (for validation messages) tag helpers for each field.
Run the app, select the **Students** tab, and then select **Create New**.
@@ -166,7 +166,7 @@ Change the date to a valid value, and select **Create** to see the new student a
## Update the Edit page
-In the `StudentController.cs` file, the HttpGet `Edit` method (the one without the `HttpPost` attribute) uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you reviewed earlier in the `Details` method. You don't need to change this method.
+In the _StudentController.cs_ file, the HttpGet `Edit` method (the one without the `HttpPost` attribute) uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you reviewed earlier in the `Details` method. You don't need to change this method.
### Use recommended HttpPost Edit code: Read and update
@@ -198,23 +198,23 @@ The database context keeps track of whether entities in memory are in sync with
An entity can be in one of the following states:
-* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an INSERT statement.
+* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an `INSERT` statement.
* `Unchanged`: Nothing needs to be done with this entity by the `SaveChanges` method. When you read an entity from the database, the entity starts out with this status.
-* `Modified`: Some or all of the entity's property values are modified. The `SaveChanges` method issues an UPDATE statement.
+* `Modified`: Some or all of the entity's property values are modified. The `SaveChanges` method issues an `UPDATE` statement.
-* `Deleted`: The entity is marked for deletion. The `SaveChanges` method issues a DELETE statement.
+* `Deleted`: The entity is marked for deletion. The `SaveChanges` method issues a `DELETE` statement.
* `Detached`: The database context doesn't track the entity.
-In a desktop application, state changes are typically set automatically. You read an entity and make changes to some of its property values. This behavior causes its entity state to automatically change to `Modified`. When you call the `SaveChanges` method, the Entity Framework generates a SQL UPDATE statement that updates only the actual properties that you changed.
+In a desktop application, state changes are typically set automatically. You read an entity and make changes to some of its property values. This behavior causes its entity state to automatically change to `Modified`. When you call the `SaveChanges` method, the Entity Framework generates a SQL `UPDATE` statement that updates only the actual properties that you changed.
In a web app, the `DbContext` method that initially reads an entity and displays its data to be edited is disposed after a page is rendered. When the HttpPost `Edit` action method is called, a new web request is made and you have a new instance of the `DbContext` method. If you re-read the entity in that new context, you simulate desktop processing.
If you don't want to do the extra read operation, you have to use the entity object created by the model binder. The easiest approach is to set the entity state to `Modified` as is done in the alternative HttpPost `Edit` code shown earlier. When you call the `SaveChanges` method, the Entity Framework updates all columns of the database row because the context has no way to know which properties you changed.
-If you want to avoid the _read-first_ approach, but you also want the SQL UPDATE statement to update only the fields the user changes, the code is more complex. You have to save the original values in some way, such as by using hidden fields. The values must be available when the HttpPost `Edit` method is called. You can create a `Student` entity by using the original values, call the `Attach` method with the original version of the entity, update the entity's values to the new values, and then call the `SaveChanges` method.
+If you want to avoid the _read-first_ approach, but you also want the SQL `UPDATE` statement to update only the fields the user changes, the code is more complex. You have to save the original values in some way, such as by using hidden fields. The values must be available when the HttpPost `Edit` method is called. You can create a `Student` entity by using the original values, call the `Attach` method with the original version of the entity, update the entity's values to the new values, and then call the `SaveChanges` method.
### Test the Edit page
@@ -226,7 +226,7 @@ Change some of the data and select **Save**. The **Index** page opens and you se
## Update the Delete page
-In the `StudentController.cs` file, the template code for the HttpGet `Delete` method uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you saw in the `Details` and `Edit` methods. However, to implement a custom error message when the call to the `SaveChanges` method fails, you need to add some functionality to this method and its corresponding view.
+In the _StudentController.cs_ file, the template code for the HttpGet `Delete` method uses the `FirstOrDefaultAsync` method to retrieve the selected `Student` entity, as you saw in the `Details` and `Edit` methods. However, to implement a custom error message when the call to the `SaveChanges` method fails, you need to add some functionality to this method and its corresponding view.
As you saw for the update and create operations, delete operations require two action methods. The method called in response to a GET request displays a view that gives the user a chance to approve or cancel the delete operation. If the user approves, a POST request is created. The HttpPost `Delete` method is then called and this method actually performs the delete operation.
@@ -256,7 +256,7 @@ If the entity also contains related data to delete, make sure that cascade delet
### Update the Delete view
-In the `Views/Student/Delete.cshtml` file, add an error message between the h2 heading and the h3 heading, as shown in the following example:
+In the _Views/Student/Delete.cshtml_ file, add an error message between the h2 heading and the h3 heading, as shown in the following example:
[!code-cshtml[](intro/samples/cu/Views/Students/Delete.cshtml?range=7-9&highlight=2)]
@@ -270,7 +270,7 @@ Select **Delete**. The **Index** page displays without the deleted student. (You
To free up the resources that a database connection holds, the context instance must be disposed as soon as possible when you're finished. The ASP.NET Core built-in [dependency injection](../../fundamentals/dependency-injection.md) takes care of the clean-up task.
-In the `Startup.cs` file, you call the [AddDbContext extension method](https://github.com/dotnet/efcore/blob/03bcb5122e3f577a84498545fcf130ba79a3d987/src/Microsoft.EntityFrameworkCore/EntityFrameworkServiceCollectionExtensions.cs) to create the `DbContext` class in the ASP.NET Core DI container. The method sets the service lifetime to `Scoped` by default. `Scoped` means the context object lifetime coincides with the web request life time, and the `Dispose` method is called automatically at the end of the web request.
+In the _Startup.cs_ file, you call the [AddDbContext extension method](https://github.com/dotnet/efcore/blob/03bcb5122e3f577a84498545fcf130ba79a3d987/src/Microsoft.EntityFrameworkCore/EntityFrameworkServiceCollectionExtensions.cs) to create the `DbContext` class in the ASP.NET Core DI container. The method sets the service lifetime to `Scoped` by default. `Scoped` means the context object lifetime coincides with the web request life time, and the `Dispose` method is called automatically at the end of the web request.
## Handle transactions
@@ -297,4 +297,4 @@ For more information, see [Tracking versus no-tracking queries](/ef/core/queryin
## Next step
> [!div class="nextstepaction"]
-> [Sorting, filtering, and paging](sort-filter-page.md)
+> [Tutorial: Add sorting, filtering, and paging - ASP.NET MVC with EF Core](sort-filter-page.md)
\ No newline at end of file
diff --git a/aspnetcore/data/ef-rp/crud.md b/aspnetcore/data/ef-rp/crud.md
index f588a401b1b1..f60e66d9b36d 100644
--- a/aspnetcore/data/ef-rp/crud.md
+++ b/aspnetcore/data/ef-rp/crud.md
@@ -1,14 +1,17 @@
---
-title: Part 2, Razor Pages with EF Core in ASP.NET Core - CRUD
+title: "Part 2: Razor Pages - EF Core in ASP.NET, CRUD"
author: tdykstra
-description: Part 2 of Razor Pages and Entity Framework tutorial series.
+description: Review and customize the autogenerated CRUD (create, read, update, delete) code produced by the scaffolding in your Razor pages. Tutorial 2 of 8 for the Razor Pages and Entity Framework Core in ASP.NET series.
ms.author: tdykstra
monikerRange: '>= aspnetcore-3.1'
ms.custom: "mvc"
-ms.date: 11/11/2021
+ms.date: 04/17/2026
uid: data/ef-rp/crud
+
+# customer intent: As an ASP.NET developer, I want to make changes to the auto-generated CRUD code, so I can customize my Razor pages.
---
-# Part 2, Razor Pages with EF Core in ASP.NET Core - CRUD
+
+# Tutorial 2: Razor Pages with EF Core in ASP.NET Core - CRUD
[!INCLUDE[](~/includes/not-latest-version.md)]
@@ -16,93 +19,106 @@ By [Tom Dykstra](https://github.com/tdykstra), [Jeremy Likness](https://twitter.
[!INCLUDE [about the series](~/includes/RP-EF/intro.md)]
-:::moniker range=">= aspnetcore-6.0"
+This article presents the second tutorial in the eight-part series. In this tutorial, you review and customize the CRUD (create, read, update, delete) code that the scaffolding automatically creates for your Razor pages.
+
+In this tutorial, you:
-In this tutorial, the scaffolded CRUD (create, read, update, delete) code is reviewed and customized.
+> [!div class="checklist"]
+> * Customize pages: **Details**, **Create**, **Edit**, and **Delete**
+> * Explore how to protect against overposting
-## No repository
+## Prerequisites
-Some developers use a service layer or repository pattern to create an abstraction layer between the UI (Razor Pages) and the data access layer. This tutorial doesn't do that. To minimize complexity and keep the tutorial focused on EF Core, EF Core code is added directly to the page model classes.
+* Review the previous tutorial, [Razor Pages with Entity Framework Core in ASP.NET Core (Tutorial 1 of 8)](intro.md).
+
+## Review the tutorial approach (no repository)
+
+Some developers use a service layer or repository pattern to create an abstraction layer between the UI (the Razor Pages) and the data access layer. Rather than that approach, this tutorial adds the EF Core code directly to the page model classes. This method helps to minimize complexity and keep the tutorial focused on EF Core.
## Update the Details page
-The scaffolded code for the Students pages doesn't include enrollment data. In this section, enrollments are added to the `Details` page.
+The scaffolded code for the Students pages doesn't include enrollment data. In this section, enrollments are added to the **Details** page.
### Read enrollments
-To display a student's enrollment data on the page, the enrollment data must be read. The scaffolded code in `Pages/Students/Details.cshtml.cs` reads only the `Student` data, without the `Enrollment` data:
+To display a student's enrollment data on the page, the enrollment data must be read. The scaffolded code in the `Pages/Students/Details.cshtml.cs` file reads only the `Student` data without the `Enrollment` data:
[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Pages/Students/Details1.cshtml.cs?name=snippet_OnGetAsync&highlight=8)]
-Replace the `OnGetAsync` method with the following code to read enrollment data for the selected student. The changes are highlighted.
+Replace the `OnGetAsync` method with the following code that reads enrollment data for the selected student. The changes are highlighted.
[!code-csharp[Main](intro/samples/cu30/Pages/Students/Details.cshtml.cs?name=snippet_OnGetAsync&highlight=8-12)]
-The and [ThenInclude](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ThenInclude%60%603(Microsoft.EntityFrameworkCore.Query.IIncludableQueryable{%60%600,%60%601},System.Linq.Expressions.Expression{System.Func{%60%601,%60%602}})) methods cause the context to load the `Student.Enrollments` navigation property, and within each enrollment the `Enrollment.Course` navigation property. These methods are examined in detail in the [Read related data](xref:data/ef-rp/read-related-data) tutorial.
+The [EntityFrameworkQueryableExtensions.Include](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.include) and [ThenInclude](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.theninclude#microsoft-entityframeworkcore-entityframeworkqueryableextensions-theninclude-3(microsoft-entityframeworkcore-query-iincludablequeryable((-0-1))-system-linq-expressions-expression((system-func((-1-2))))))
+methods cause the context to load the `Student.Enrollments` navigation property, and within each enrollment, the `Enrollment.Course` navigation property. These methods are examined in detail in [Read related data (Tutorial 6 of 8)](read-related-data.md).
-The method improves performance in scenarios where the entities returned are not updated in the current context. `AsNoTracking` is discussed later in this tutorial.
+The [EntityFrameworkQueryableExtensions.AsNoTracking](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking) method improves performance in scenarios where the entities returned aren't updated in the current context. The `AsNoTracking` setting is discussed later in this tutorial.
### Display enrollments
-Replace the code in `Pages/Students/Details.cshtml` with the following code to display a list of enrollments. The changes are highlighted.
+To display a list of enrollments, replace the code in the _Pages/Students/Details.cshtml_ file with the following code. The changes are highlighted.
[!code-cshtml[Main](intro/samples/cu30/Pages/Students/Details.cshtml?highlight=32-53)]
-The preceding code loops through the entities in the `Enrollments` navigation property. For each enrollment, it displays the course title and the grade. The course title is retrieved from the `Course` entity that's stored in the `Course` navigation property of the Enrollments entity.
+This code loops through the entities in the `Enrollments` navigation property. For each enrollment, the code displays the course title and the grade. The course title is retrieved from the `Course` entity stored in the `Course` navigation property of the `Enrollments` entity.
+
+Run the app, select the **Students** tab, and select the **Details** link for a student. The list of courses and grades for the selected student is displayed.
-Run the app, select the **Students** tab, and click the **Details** link for a student. The list of courses and grades for the selected student is displayed.
+### Use different methods to read one entity
-### Ways to read one entity
+The generated code uses the [FirstOrDefaultAsync](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.firstordefaultasync#microsoft-entityframeworkcore-entityframeworkqueryableextensions-firstordefaultasync-1(system-linq-iqueryable((-0))-system-threading-cancellationtoken)) method to read one entity. This method returns null if nothing is found. Otherwise, it returns the first row found that satisfies the query filter criteria. The `FirstOrDefaultAsync` method is generally a better choice than the following alternatives:
-The generated code uses [FirstOrDefaultAsync](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync%60%601(System.Linq.IQueryable{%60%600},System.Threading.CancellationToken)) to read one entity. This method returns null if nothing is found; otherwise, it returns the first row found that satisfies the query filter criteria. `FirstOrDefaultAsync` is generally a better choice than the following alternatives:
+* The [SingleOrDefaultAsync](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.singleordefaultasync#microsoft-entityframeworkcore-entityframeworkqueryableextensions-singleordefaultasync-1(system-linq-iqueryable((-0))-system-linq-expressions-expression((system-func((-0-system-boolean))))-system-threading-cancellationtoken)) method throws an exception if there's more than one entity that satisfies the query filter. To determine if the query can return more than one row, the `SingleOrDefaultAsync` method tries to fetch multiple rows. The extra work is unnecessary if the query can only return one entity, as when it searches on a unique key.
-* [SingleOrDefaultAsync](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.SingleOrDefaultAsync%60%601(System.Linq.IQueryable{%60%600},System.Linq.Expressions.Expression{System.Func{%60%600,System.Boolean}},System.Threading.CancellationToken)) - Throws an exception if there's more than one entity that satisfies the query filter. To determine if more than one row could be returned by the query, `SingleOrDefaultAsync` tries to fetch multiple rows. This extra work is unnecessary if the query can only return one entity, as when it searches on a unique key.
-* [FindAsync](xref:Microsoft.EntityFrameworkCore.DbContext.FindAsync(System.Type,System.Object[])) - Finds an entity with the primary key (PK). If an entity with the PK is being tracked by the context, it's returned without a request to the database. This method is optimized to look up a single entity, but you can't call `Include` with `FindAsync`. So if related data is needed, `FirstOrDefaultAsync` is the better choice.
+* The [FindAsync](/dotnet/api/microsoft.entityframeworkcore.dbcontext.findasync#microsoft-entityframeworkcore-dbcontext-findasync(system-type-system-object())) method locates an entity with the primary key. If the context tracks an entity with the primary key, the key is returned without a request to the database. This method is optimized to look up a single entity, but you can't call the `Include` method with the `FindAsync` method. If related data is needed, the `FirstOrDefaultAsync` method is the better choice.
### Route data vs. query string
-The URL for the Details page is `https://localhost:/Students/Details?id=1`. The entity's primary key value is in the query string. Some developers prefer to pass the key value in route data: `https://localhost:/Students/Details/1`. For more information, see [Update the generated code](xref:tutorials/razor-pages/da1#update-the-generated-code).
+The URL for the **Details** page is `https://localhost:/Students/Details?id=1`. The entity's primary key value is in the query string. Some developers prefer to pass the key value in route data: `https://localhost:/Students/Details/1`. For more information, see [Update the generated pages (Tutorial 5 of 8)](../../tutorials/razor-pages/da1.md#update-the-generated-code).
## Update the Create page
-The scaffolded `OnPostAsync` code for the Create page is vulnerable to [overposting](#overposting). Replace the `OnPostAsync` method in `Pages/Students/Create.cshtml.cs` with the following code.
+The scaffolded `OnPostAsync` code for the **Create** page is vulnerable to [overposting](#prevent-overposting). Replace the `OnPostAsync` method in the _Pages/Students/Create.cshtml.cs_ file with the following code.
[!code-csharp[Main](intro/samples/cu30/Pages/Students/Create.cshtml.cs?name=snippet_OnPostAsync)]
-
+### Use the TryUpdateModelAsync method
-### TryUpdateModelAsync
+The code in the last section creates a `Student` object and then uses posted form fields to update the `Student` object properties. The [TryUpdateModelAsync](/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.tryupdatemodelasync#microsoft-aspnetcore-mvc-controllerbase-tryupdatemodelasync(system-object-system-type-system-string)) method:
-The preceding code creates a Student object and then uses posted form fields to update the Student object's properties. The [TryUpdateModelAsync](xref:Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(System.Object,System.Type,System.String)) method:
+* Uses the posted form values from the [PageModel.PageContext](/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel.pagecontext) property in the [PageModel](/dotnet/api/microsoft.aspnetcore.mvc.razorpages.pagemodel) class.
-* Uses the posted form values from the property in the .
* Updates only the properties listed (`s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate`).
+
* Looks for form fields with a "student" prefix. For example, `Student.FirstMidName`. It's not case sensitive.
-* Uses the [model binding](xref:mvc/models/model-binding) system to convert form values from strings to the types in the `Student` model. For example, `EnrollmentDate` is converted to `DateTime`.
-Run the app, and create a student entity to test the Create page.
+* Uses the [model binding](../../mvc/models/model-binding.md) system to convert form values from strings to the types in the `Student` model. For example, the `EnrollmentDate` value is converted to a `DateTime` type.
+
+Run the app, and create a student entity to test the **Create** page.
-## Overposting
+## Prevent overposting
-Using `TryUpdateModel` to update fields with posted values is a security best practice because it prevents overposting. For example, suppose the Student entity includes a `Secret` property that this web page shouldn't update or add:
+Using the `TryUpdateModel` method to update fields with posted values is a security best practice because it prevents overposting. For example, suppose the `Student` entity includes a `Secret` property that this web page shouldn't update or add:
[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Models/StudentZsecret.cs?name=snippet_Intro&highlight=7)]
-Even if the app doesn't have a `Secret` field on the create or update Razor Page, a hacker could set the `Secret` value by overposting. A hacker could use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. The original code doesn't limit the fields that the model binder uses when it creates a Student instance.
+Even if the app doesn't have a `Secret` field on the **Create** or **Update** page, a hacker could set the `Secret` value by overposting. A hacker might use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. The original code doesn't limit the fields that the model binder uses when it creates a `Student` instance.
-Whatever value the hacker specified for the `Secret` form field is updated in the database. The following image shows the Fiddler tool adding the `Secret` field, with the value "OverPost", to the posted form values.
+Whatever value the hacker specified for the `Secret` form field is updated in the database. The following image shows the Fiddler tool adding the `Secret` field with the value "OverPost" to the posted form values.
-
+:::image type="content" source="../ef-mvc/crud/_static/fiddler.png" border="false" alt-text="Screenshot of the Composer view in Fiddler showing the Secret field added in the request body.":::
-The value "OverPost" is successfully added to the `Secret` property of the inserted row. That happens even though the app designer never intended the `Secret` property to be set with the Create page.
+The value "OverPost" is successfully added to the `Secret` property of the inserted row. This result occurs even though the app designer never intended to set the `Secret` property with the **Create** page.
-### View model
+### Work with the View model
View models provide an alternative way to prevent overposting.
-The application model is often called the domain model. The domain model typically contains all the properties required by the corresponding entity in the database. The view model contains only the properties needed for the UI page, for example, the Create page.
+The application model is often called the _domain model_. The domain model typically contains all the properties required by the corresponding entity in the database. The view model contains only the properties needed for the UI page, for example, the **Create** page.
-In addition to the view model, some apps use a binding model or input model to pass data between the Razor Pages page model class and the browser.
+In addition to the view model, some apps use a binding model or input model to pass data between the Razor Pages page model class and the browser.
+
+:::moniker range=">= aspnetcore-5.0"
Consider the following `StudentVM` view model:
@@ -112,415 +128,118 @@ The following code uses the `StudentVM` view model to create a new student:
[!code-csharp[Main](intro/samples/cu50/Pages/Students/CreateVM.cshtml.cs?name=snippet)]
-The [SetValues](xref:Microsoft.EntityFrameworkCore.ChangeTracking.PropertyValues.SetValues(System.Object)) method sets the values of this object by reading values from another object. `SetValues` uses property name matching. The view model type:
+The [SetValues](/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues.setvalues#microsoft-entityframeworkcore-changetracking-propertyvalues-setvalues(system-object)) method sets the values of this object by reading values from another [PropertyValues](/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues) object. The `SetValues` method uses property name matching. The view model type:
* Doesn't need to be related to the model type.
-* Needs to have properties that match.
+* Requires properties that match.
-Using `StudentVM` requires the Create page use `StudentVM` rather than `Student`:
+Using the `StudentVM` view model requires that the **Create** page use the `StudentVM` entity rather than a `Student` entity:
[!code-cshtml[Main](intro/samples/cu50/Pages/Students/CreateVM.cshtml)]
-## Update the Edit page
-
-In `Pages/Students/Edit.cshtml.cs`, replace the `OnGetAsync` and `OnPostAsync` methods with the following code.
-
-[!code-csharp[Main](intro/samples/cu30/Pages/Students/Edit.cshtml.cs?name=snippet_OnGetPost)]
-
-The code changes are similar to the Create page with a few exceptions:
-
-* `FirstOrDefaultAsync` has been replaced with . When you don't have to include related data, `FindAsync` is more efficient.
-* `OnPostAsync` has an `id` parameter.
-* The current student is fetched from the database, rather than creating an empty student.
-
-Run the app, and test it by creating and editing a student.
-
-## Entity States
-
-The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database. This tracking information determines what happens when [SaveChangesAsync](xref:Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(System.Threading.CancellationToken)) is called. For example, when a new entity is passed to the method, that entity's state is set to . When `SaveChangesAsync` is called, the database context issues a SQL `INSERT` command.
-
-An entity may be in one of the [following states](xref:Microsoft.EntityFrameworkCore.EntityState):
-
-* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an `INSERT` statement.
-
-* `Unchanged`: No changes need to be saved with this entity. An entity has this status when it's read from the database.
-
-* `Modified`: Some or all of the entity's property values have been modified. The `SaveChanges` method issues an `UPDATE` statement.
-
-* `Deleted`: The entity has been marked for deletion. The `SaveChanges` method issues a `DELETE` statement.
-
-* `Detached`: The entity isn't being tracked by the database context.
-
-In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity state is automatically changed to `Modified`. Calling `SaveChanges` generates a SQL `UPDATE` statement that updates only the changed properties.
-
-In a web app, the `DbContext` that reads an entity and displays the data is disposed after a page is rendered. When a page's `OnPostAsync` method is called, a new web request is made and with a new instance of the `DbContext`. Rereading the entity in that new context simulates desktop processing.
-
-## Update the Delete page
-
-In this section, a custom error message is implemented when the call to `SaveChanges` fails.
-
-Replace the code in `Pages/Students/Delete.cshtml.cs` with the following code:
-
-[!code-csharp[Main](intro/samples/cu50/Pages/Students/Delete.cshtml.cs)]
-
-The preceding code:
-
-* Adds [Logging](xref:fundamentals/logging/index).
-* Adds the optional parameter `saveChangesError` to the `OnGetAsync` method signature. `saveChangesError` indicates whether the method was called after a failure to delete the student object.
-
-The delete operation might fail because of transient network problems. Transient network errors are more likely when the database is in the cloud. The `saveChangesError` parameter is `false` when the Delete page `OnGetAsync` is called from the UI. When `OnGetAsync` is called by `OnPostAsync` because the delete operation failed, the `saveChangesError` parameter is `true`.
-
-The `OnPostAsync` method retrieves the selected entity, then calls the [Remove](xref:Microsoft.EntityFrameworkCore.DbContext.Remove(System.Object)) method to set the entity's status to `Deleted`. When `SaveChanges` is called, a SQL `DELETE` command is generated. If `Remove` fails:
-
-* The database exception is caught.
-* The Delete pages `OnGetAsync` method is called with `saveChangesError=true`.
-
-Add an error message to `Pages/Students/Delete.cshtml`:
-
-[!code-cshtml[Main](intro/samples/cu50/Pages/Students/Delete.cshtml?highlight=10)]
-
-Run the app and delete a student to test the Delete page.
-
-## Next steps
-
-> [!div class="step-by-step"]
-> [Previous tutorial](xref:data/ef-rp/intro)
-> [Next tutorial](xref:data/ef-rp/sort-filter-page)
-
:::moniker-end
+:::moniker range="< aspnetcore-5.0"
-:::moniker range=">= aspnetcore-5.0 < aspnetcore-6.0"
-
-In this tutorial, the scaffolded CRUD (create, read, update, delete) code is reviewed and customized.
-
-## No repository
-
-Some developers use a service layer or repository pattern to create an abstraction layer between the UI (Razor Pages) and the data access layer. This tutorial doesn't do that. To minimize complexity and keep the tutorial focused on EF Core, EF Core code is added directly to the page model classes.
-
-## Update the Details page
-
-The scaffolded code for the Students pages doesn't include enrollment data. In this section, enrollments are added to the `Details` page.
-
-### Read enrollments
-
-To display a student's enrollment data on the page, the enrollment data must be read. The scaffolded code in `Pages/Students/Details.cshtml.cs` reads only the `Student` data, without the `Enrollment` data:
-
-[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Pages/Students/Details1.cshtml.cs?name=snippet_OnGetAsync&highlight=8)]
-
-Replace the `OnGetAsync` method with the following code to read enrollment data for the selected student. The changes are highlighted.
-
-[!code-csharp[Main](intro/samples/cu30/Pages/Students/Details.cshtml.cs?name=snippet_OnGetAsync&highlight=8-12)]
-
-The and [ThenInclude](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ThenInclude%60%603(Microsoft.EntityFrameworkCore.Query.IIncludableQueryable{%60%600,%60%601},System.Linq.Expressions.Expression{System.Func{%60%601,%60%602}})) methods cause the context to load the `Student.Enrollments` navigation property, and within each enrollment the `Enrollment.Course` navigation property. These methods are examined in detail in the [Read related data](xref:data/ef-rp/read-related-data) tutorial.
-
-The method improves performance in scenarios where the entities returned are not updated in the current context. `AsNoTracking` is discussed later in this tutorial.
-
-### Display enrollments
-
-Replace the code in `Pages/Students/Details.cshtml` with the following code to display a list of enrollments. The changes are highlighted.
-
-[!code-cshtml[Main](intro/samples/cu30/Pages/Students/Details.cshtml?highlight=32-53)]
-
-The preceding code loops through the entities in the `Enrollments` navigation property. For each enrollment, it displays the course title and the grade. The course title is retrieved from the `Course` entity that's stored in the `Course` navigation property of the Enrollments entity.
-
-Run the app, select the **Students** tab, and click the **Details** link for a student. The list of courses and grades for the selected student is displayed.
-
-### Ways to read one entity
-
-The generated code uses [FirstOrDefaultAsync](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync%60%601(System.Linq.IQueryable{%60%600},System.Threading.CancellationToken)) to read one entity. This method returns null if nothing is found; otherwise, it returns the first row found that satisfies the query filter criteria. `FirstOrDefaultAsync` is generally a better choice than the following alternatives:
-
-* [SingleOrDefaultAsync](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.SingleOrDefaultAsync%60%601(System.Linq.IQueryable{%60%600},System.Linq.Expressions.Expression{System.Func{%60%600,System.Boolean}},System.Threading.CancellationToken)) - Throws an exception if there's more than one entity that satisfies the query filter. To determine if more than one row could be returned by the query, `SingleOrDefaultAsync` tries to fetch multiple rows. This extra work is unnecessary if the query can only return one entity, as when it searches on a unique key.
-* [FindAsync](xref:Microsoft.EntityFrameworkCore.DbContext.FindAsync(System.Type,System.Object[])) - Finds an entity with the primary key (PK). If an entity with the PK is being tracked by the context, it's returned without a request to the database. This method is optimized to look up a single entity, but you can't call `Include` with `FindAsync`. So if related data is needed, `FirstOrDefaultAsync` is the better choice.
-
-### Route data vs. query string
-
-The URL for the Details page is `https://localhost:/Students/Details?id=1`. The entity's primary key value is in the query string. Some developers prefer to pass the key value in route data: `https://localhost:/Students/Details/1`. For more information, see [Update the generated code](xref:tutorials/razor-pages/da1#update-the-generated-code).
-
-## Update the Create page
-
-The scaffolded `OnPostAsync` code for the Create page is vulnerable to [overposting](#overposting). Replace the `OnPostAsync` method in `Pages/Students/Create.cshtml.cs` with the following code.
-
-[!code-csharp[Main](intro/samples/cu30/Pages/Students/Create.cshtml.cs?name=snippet_OnPostAsync)]
-
-
-
-### TryUpdateModelAsync
-
-The preceding code creates a Student object and then uses posted form fields to update the Student object's properties. The [TryUpdateModelAsync](xref:Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(System.Object,System.Type,System.String)) method:
-
-* Uses the posted form values from the property in the .
-* Updates only the properties listed (`s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate`).
-* Looks for form fields with a "student" prefix. For example, `Student.FirstMidName`. It's not case sensitive.
-* Uses the [model binding](xref:mvc/models/model-binding) system to convert form values from strings to the types in the `Student` model. For example, `EnrollmentDate` is converted to `DateTime`.
-
-Run the app, and create a student entity to test the Create page.
-
-## Overposting
-
-Using `TryUpdateModel` to update fields with posted values is a security best practice because it prevents overposting. For example, suppose the Student entity includes a `Secret` property that this web page shouldn't update or add:
-
-[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Models/StudentZsecret.cs?name=snippet_Intro&highlight=7)]
-
-Even if the app doesn't have a `Secret` field on the create or update Razor Page, a hacker could set the `Secret` value by overposting. A hacker could use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. The original code doesn't limit the fields that the model binder uses when it creates a Student instance.
-
-Whatever value the hacker specified for the `Secret` form field is updated in the database. The following image shows the Fiddler tool adding the `Secret` field, with the value "OverPost", to the posted form values.
-
-
-
-The value "OverPost" is successfully added to the `Secret` property of the inserted row. That happens even though the app designer never intended the `Secret` property to be set with the Create page.
-
-### View model
-
-View models provide an alternative way to prevent overposting.
-
-The application model is often called the domain model. The domain model typically contains all the properties required by the corresponding entity in the database. The view model contains only the properties needed for the UI page, for example, the Create page.
-
-In addition to the view model, some apps use a binding model or input model to pass data between the Razor Pages page model class and the browser.
-
-Consider the following `StudentVM` view model:
+Consider the following `Student` view model:
-[!code-csharp[Main](intro/samples/cu50/ViewModels/StudentVM.cs?name=snippet)]
+[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Models/StudentVM.cs)]
The following code uses the `StudentVM` view model to create a new student:
-[!code-csharp[Main](intro/samples/cu50/Pages/Students/CreateVM.cshtml.cs?name=snippet)]
+[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Pages/Students/CreateVM.cshtml.cs?name=snippet_OnPostAsync)]
-The [SetValues](xref:Microsoft.EntityFrameworkCore.ChangeTracking.PropertyValues.SetValues(System.Object)) method sets the values of this object by reading values from another object. `SetValues` uses property name matching. The view model type:
+The [SetValues](/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues.setvalues#microsoft-entityframeworkcore-changetracking-propertyvalues-setvalues(system-object)) method sets the values of this object by reading values from another [PropertyValues](/dotnet/api/microsoft.entityframeworkcore.changetracking.propertyvalues) object. The `SetValues` method uses property name matching. The view model type:
* Doesn't need to be related to the model type.
-* Needs to have properties that match.
+* Requires properties that match.
-Using `StudentVM` requires the Create page use `StudentVM` rather than `Student`:
+Using the `StudentVM` view model requires that the [Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu30snapshots/2-crud/Pages/Students/CreateVM.cshtml) file is updated to use a `StudentVM` entity rather than a `Student` entity.
-[!code-cshtml[Main](intro/samples/cu50/Pages/Students/CreateVM.cshtml)]
+:::moniker-end
## Update the Edit page
-In `Pages/Students/Edit.cshtml.cs`, replace the `OnGetAsync` and `OnPostAsync` methods with the following code.
+In the _Pages/Students/Edit.cshtml.cs_ file, replace the `OnGetAsync` and `OnPostAsync` methods with the following code.
[!code-csharp[Main](intro/samples/cu30/Pages/Students/Edit.cshtml.cs?name=snippet_OnGetPost)]
-The code changes are similar to the Create page with a few exceptions:
+The code changes are similar to the **Create** page with a few exceptions:
-* `FirstOrDefaultAsync` has been replaced with . When you don't have to include related data, `FindAsync` is more efficient.
-* `OnPostAsync` has an `id` parameter.
+* The `FirstOrDefaultAsync` method is replaced with the [FindAsync](/dotnet/api/microsoft.entityframeworkcore.dbset-1.findasync) method. When you don't have to include related data, the `FindAsync` method is more efficient.
+* The `OnPostAsync` method has an `id` parameter.
* The current student is fetched from the database, rather than creating an empty student.
Run the app, and test it by creating and editing a student.
-## Entity States
+## Use Entity States
-The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database. This tracking information determines what happens when [SaveChangesAsync](xref:Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(System.Threading.CancellationToken)) is called. For example, when a new entity is passed to the method, that entity's state is set to . When `SaveChangesAsync` is called, the database context issues a SQL `INSERT` command.
+The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database. The tracking information determines what happens when the [SaveChangesAsync](/dotnet/api/microsoft.entityframeworkcore.dbcontext.savechangesasync#microsoft-entityframeworkcore-dbcontext-savechangesasync(system-threading-cancellationtoken)) method is called. For example, when a new entity is passed to the [AddAsync](/dotnet/api/microsoft.entityframeworkcore.dbcontext.addasync) method, that entity's state is set to [Added](/dotnet/api/microsoft.entityframeworkcore.entitystate#microsoft-entityframeworkcore-entitystate-added). When the `SaveChangesAsync` method is called, the database context issues a SQL `INSERT` command.
-An entity may be in one of the [following states](xref:Microsoft.EntityFrameworkCore.EntityState):
+An entity can be in one of the [following states](/dotnet/api/microsoft.entityframeworkcore.entitystate):
* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an `INSERT` statement.
-* `Unchanged`: No changes need to be saved with this entity. An entity has this status when it's read from the database.
+* `Unchanged`: No changes need to be saved with this entity. An entity has this status when read from the database.
-* `Modified`: Some or all of the entity's property values have been modified. The `SaveChanges` method issues an `UPDATE` statement.
+* `Modified`: Some or all of the entity's property values are modified. The `SaveChanges` method issues an `UPDATE` statement.
-* `Deleted`: The entity has been marked for deletion. The `SaveChanges` method issues a `DELETE` statement.
+* `Deleted`: The entity is marked for deletion. The `SaveChanges` method issues a `DELETE` statement.
-* `Detached`: The entity isn't being tracked by the database context.
+* `Detached`: The database context doesn't track the entity.
-In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity state is automatically changed to `Modified`. Calling `SaveChanges` generates a SQL `UPDATE` statement that updates only the changed properties.
+In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity state is automatically changed to `Modified`. Calling the `SaveChanges` method generates a SQL `UPDATE` statement that updates only the changed properties.
-In a web app, the `DbContext` that reads an entity and displays the data is disposed after a page is rendered. When a page's `OnPostAsync` method is called, a new web request is made and with a new instance of the `DbContext`. Rereading the entity in that new context simulates desktop processing.
+In a web app, the `DbContext` object that reads an entity and displays the data is disposed after a page is rendered. When a page's `OnPostAsync` method is called, a new web request is made with a new instance of the `DbContext` object. Rereading the entity in the new context simulates desktop processing.
## Update the Delete page
In this section, a custom error message is implemented when the call to `SaveChanges` fails.
-Replace the code in `Pages/Students/Delete.cshtml.cs` with the following code:
-
-[!code-csharp[Main](intro/samples/cu50/Pages/Students/Delete.cshtml.cs)]
-
-The preceding code:
-
-* Adds [Logging](xref:fundamentals/logging/index).
-* Adds the optional parameter `saveChangesError` to the `OnGetAsync` method signature. `saveChangesError` indicates whether the method was called after a failure to delete the student object.
-
-The delete operation might fail because of transient network problems. Transient network errors are more likely when the database is in the cloud. The `saveChangesError` parameter is `false` when the Delete page `OnGetAsync` is called from the UI. When `OnGetAsync` is called by `OnPostAsync` because the delete operation failed, the `saveChangesError` parameter is `true`.
-
-The `OnPostAsync` method retrieves the selected entity, then calls the [Remove](xref:Microsoft.EntityFrameworkCore.DbContext.Remove(System.Object)) method to set the entity's status to `Deleted`. When `SaveChanges` is called, a SQL `DELETE` command is generated. If `Remove` fails:
-
-* The database exception is caught.
-* The Delete pages `OnGetAsync` method is called with `saveChangesError=true`.
-
-Add an error message to `Pages/Students/Delete.cshtml`:
+:::moniker range=">= aspnetcore-6.0"
-[!code-cshtml[Main](intro/samples/cu50/Pages/Students/Delete.cshtml?highlight=10)]
+Replace the code in the _Pages/Students/Delete.cshtml.cs_ file with the following code.
-Run the app and delete a student to test the Delete page.
+[!code-csharp[Main](intro/samples/cu50/Pages/Students/Delete.cshtml.cs)]
-## Next steps
+This code adds the following functionality:
-> [!div class="step-by-step"]
-> [Previous tutorial](xref:data/ef-rp/intro)
-> [Next tutorial](xref:data/ef-rp/sort-filter-page)
+* [Logging](../../fundamentals/logging/index.md) for .NET and ASP.NET Core.
+* The optional parameter `saveChangesError` to the `OnGetAsync` method signature. The `saveChangesError` parameter indicates whether the method call occurs after a failure to delete the `Student` object.
:::moniker-end
-
:::moniker range="< aspnetcore-5.0"
-In this tutorial, the scaffolded CRUD (create, read, update, delete) code is reviewed and customized.
-
-## No repository
-
-Some developers use a service layer or repository pattern to create an abstraction layer between the UI (Razor Pages) and the data access layer. This tutorial doesn't do that. To minimize complexity and keep the tutorial focused on EF Core, EF Core code is added directly to the page model classes.
-
-## Update the Details page
-
-The scaffolded code for the Students pages doesn't include enrollment data. In this section, enrollments are added to the Details page.
-
-### Read enrollments
-
-To display a student's enrollment data on the page, the enrollment data needs to be read. The scaffolded code in `Pages/Students/Details.cshtml.cs` reads only the Student data, without the Enrollment data:
-
-[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Pages/Students/Details1.cshtml.cs?name=snippet_OnGetAsync&highlight=8)]
-
-Replace the `OnGetAsync` method with the following code to read enrollment data for the selected student. The changes are highlighted.
-
-[!code-csharp[Main](intro/samples/cu30/Pages/Students/Details.cshtml.cs?name=snippet_OnGetAsync&highlight=8-12)]
-
-The and [ThenInclude](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ThenInclude%60%603(Microsoft.EntityFrameworkCore.Query.IIncludableQueryable{%60%600,%60%601},System.Linq.Expressions.Expression{System.Func{%60%601,%60%602}})) methods cause the context to load the `Student.Enrollments` navigation property, and within each enrollment the `Enrollment.Course` navigation property. These methods are examined in detail in the [Reading related data](xref:data/ef-rp/read-related-data) tutorial.
-
-The method improves performance in scenarios where the entities returned are not updated in the current context. `AsNoTracking` is discussed later in this tutorial.
-
-### Display enrollments
-
-Replace the code in `Pages/Students/Details.cshtml` with the following code to display a list of enrollments. The changes are highlighted.
-
-[!code-cshtml[Main](intro/samples/cu30/Pages/Students/Details.cshtml?highlight=32-53)]
-
-The preceding code loops through the entities in the `Enrollments` navigation property. For each enrollment, it displays the course title and the grade. The course title is retrieved from the Course entity that's stored in the `Course` navigation property of the Enrollments entity.
-
-Run the app, select the **Students** tab, and click the **Details** link for a student. The list of courses and grades for the selected student is displayed.
-
-### Ways to read one entity
-
-The generated code uses [FirstOrDefaultAsync](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync%60%601(System.Linq.IQueryable{%60%600},System.Threading.CancellationToken)) to read one entity. This method returns null if nothing is found; otherwise, it returns the first row found that satisfies the query filter criteria. `FirstOrDefaultAsync` is generally a better choice than the following alternatives:
-
-* [SingleOrDefaultAsync](xref:Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.SingleOrDefaultAsync%60%601(System.Linq.IQueryable{%60%600},System.Linq.Expressions.Expression{System.Func{%60%600,System.Boolean}},System.Threading.CancellationToken)) - Throws an exception if there's more than one entity that satisfies the query filter. To determine if more than one row could be returned by the query, `SingleOrDefaultAsync` tries to fetch multiple rows. This extra work is unnecessary if the query can only return one entity, as when it searches on a unique key.
-* [FindAsync](xref:Microsoft.EntityFrameworkCore.DbContext.FindAsync(System.Type,System.Object[])) - Finds an entity with the primary key (PK). If an entity with the PK is being tracked by the context, it's returned without a request to the database. This method is optimized to look up a single entity, but you can't call `Include` with `FindAsync`. So if related data is needed, `FirstOrDefaultAsync` is the better choice.
-
-### Route data vs. query string
-
-The URL for the Details page is `https://localhost:/Students/Details?id=1`. The entity's primary key value is in the query string. Some developers prefer to pass the key value in route data: `https://localhost:/Students/Details/1`. For more information, see [Update the generated code](xref:tutorials/razor-pages/da1#update-the-generated-code).
-
-## Update the Create page
-
-The scaffolded `OnPostAsync` code for the Create page is vulnerable to [overposting](#overposting). Replace the `OnPostAsync` method in `Pages/Students/Create.cshtml.cs` with the following code.
-
-[!code-csharp[Main](intro/samples/cu30/Pages/Students/Create.cshtml.cs?name=snippet_OnPostAsync)]
-
-
-
-### TryUpdateModelAsync
-
-The preceding code creates a Student object and then uses posted form fields to update the Student object's properties. The [TryUpdateModelAsync](xref:Microsoft.AspNetCore.Mvc.ControllerBase.TryUpdateModelAsync(System.Object,System.Type,System.String)) method:
-
-* Uses the posted form values from the property in the .
-* Updates only the properties listed (`s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate`).
-* Looks for form fields with a "student" prefix. For example, `Student.FirstMidName`. It's not case sensitive.
-* Uses the [model binding](xref:mvc/models/model-binding) system to convert form values from strings to the types in the `Student` model. For example, `EnrollmentDate` has to be converted to DateTime.
-
-Run the app, and create a student entity to test the Create page.
-
-## Overposting
-
-Using `TryUpdateModel` to update fields with posted values is a security best practice because it prevents overposting. For example, suppose the Student entity includes a `Secret` property that this web page shouldn't update or add:
-
-[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Models/StudentZsecret.cs?name=snippet_Intro&highlight=7)]
-
-Even if the app doesn't have a `Secret` field on the create or update Razor Page, a hacker could set the `Secret` value by overposting. A hacker could use a tool such as Fiddler, or write some JavaScript, to post a `Secret` form value. The original code doesn't limit the fields that the model binder uses when it creates a Student instance.
-
-Whatever value the hacker specified for the `Secret` form field is updated in the database. The following image shows the Fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
-
-
-
-The value "OverPost" is successfully added to the `Secret` property of the inserted row. That happens even though the app designer never intended the `Secret` property to be set with the Create page.
-
-### View model
-
-View models provide an alternative way to prevent overposting.
-
-The application model is often called the domain model. The domain model typically contains all the properties required by the corresponding entity in the database. The view model contains only the properties needed for the UI that it is used for (for example, the Create page).
-
-In addition to the view model, some apps use a binding model or input model to pass data between the Razor Pages page model class and the browser.
-
-Consider the following `Student` view model:
-
-[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Models/StudentVM.cs)]
-
-The following code uses the `StudentVM` view model to create a new student:
-
-[!code-csharp[Main](intro/samples/cu30snapshots/2-crud/Pages/Students/CreateVM.cshtml.cs?name=snippet_OnPostAsync)]
-
-The [SetValues](xref:Microsoft.EntityFrameworkCore.ChangeTracking.PropertyValues.SetValues(System.Object)) method sets the values of this object by reading values from another object. `SetValues` uses property name matching. The view model type doesn't need to be related to the model type, it just needs to have properties that match.
-
-Using `StudentVM` requires [Create.cshtml](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples/cu30snapshots/2-crud/Pages/Students/CreateVM.cshtml) be updated to use `StudentVM` rather than `Student`.
-
-## Update the Edit page
-
-In `Pages/Students/Edit.cshtml.cs`, replace the `OnGetAsync` and `OnPostAsync` methods with the following code.
+Replace the code in the _Pages/Students/Delete.cshtml.cs_ file with the following code. The changes are highlighted (other than cleanup of `using` statements).
-[!code-csharp[Main](intro/samples/cu30/Pages/Students/Edit.cshtml.cs?name=snippet_OnGetPost)]
-
-The code changes are similar to the Create page with a few exceptions:
-
-* `FirstOrDefaultAsync` has been replaced with . When included related data is not needed, `FindAsync` is more efficient.
-* `OnPostAsync` has an `id` parameter.
-* The current student is fetched from the database, rather than creating an empty student.
-
-Run the app, and test it by creating and editing a student.
-
-## Entity States
-
-The database context keeps track of whether entities in memory are in sync with their corresponding rows in the database. This tracking information determines what happens when [SaveChangesAsync](xref:Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(System.Threading.CancellationToken)) is called. For example, when a new entity is passed to the method, that entity's state is set to . When `SaveChangesAsync` is called, the database context issues a SQL INSERT command.
-
-An entity may be in one of the [following states](xref:Microsoft.EntityFrameworkCore.EntityState):
-
-* `Added`: The entity doesn't yet exist in the database. The `SaveChanges` method issues an INSERT statement.
-
-* `Unchanged`: No changes need to be saved with this entity. An entity has this status when it's read from the database.
-
-* `Modified`: Some or all of the entity's property values have been modified. The `SaveChanges` method issues an UPDATE statement.
-
-* `Deleted`: The entity has been marked for deletion. The `SaveChanges` method issues a DELETE statement.
-
-* `Detached`: The entity isn't being tracked by the database context.
-
-In a desktop app, state changes are typically set automatically. An entity is read, changes are made, and the entity state is automatically changed to `Modified`. Calling `SaveChanges` generates a SQL UPDATE statement that updates only the changed properties.
+[!code-csharp[Main](intro/samples/cu30/Pages/Students/Delete.cshtml.cs?name=snippet_All&highlight=20,22,30,38-41,53-71)]
-In a web app, the `DbContext` that reads an entity and displays the data is disposed after a page is rendered. When a page's `OnPostAsync` method is called, a new web request is made and with a new instance of the `DbContext`. Rereading the entity in that new context simulates desktop processing.
+This code adds the optional parameter `saveChangesError` to the `OnGetAsync` method signature. The `saveChangesError` parameter indicates whether the method call occurs after a failure to delete the `Student` object.
-## Update the Delete page
+:::moniker-end
-In this section, you implement a custom error message when the call to `SaveChanges` fails.
+The delete operation might fail because of transient network problems. Transient network errors are more likely when the database is in the cloud. The `saveChangesError` parameter is `false` when the **Delete** page `OnGetAsync` method is called from the UI. When the `OnGetAsync` method is called by the `OnPostAsync` method because the delete operation failed, the `saveChangesError` parameter is true.
-Replace the code in `Pages/Students/Delete.cshtml.cs` with the following code. The changes are highlighted (other than cleanup of `using` statements).
+The `OnPostAsync` method retrieves the selected entity, then calls the [Remove](/dotnet/api/microsoft.entityframeworkcore.dbcontext.remove#microsoft-entityframeworkcore-dbcontext-remove(system-object)) method to set the entity status to `Deleted`. When `SaveChanges` is called, a SQL `DELETE` command is generated. If call to the `Remove` method fails:
-[!code-csharp[Main](intro/samples/cu30/Pages/Students/Delete.cshtml.cs?name=snippet_All&highlight=20,22,30,38-41,53-71)]
+* The database exception is caught.
+* The **Delete** page `OnGetAsync` method is called with the `saveChangesError=true` parameter.
-The preceding code adds the optional parameter `saveChangesError` to the `OnGetAsync` method signature. `saveChangesError` indicates whether the method was called after a failure to delete the student object. The delete operation might fail because of transient network problems. Transient network errors are more likely when the database is in the cloud. The `saveChangesError` parameter is false when the Delete page `OnGetAsync` is called from the UI. When `OnGetAsync` is called by `OnPostAsync` (because the delete operation failed), the `saveChangesError` parameter is true.
+Add an error message to the **Delete** page (_Pages/Students/Delete.cshtml_).
-The `OnPostAsync` method retrieves the selected entity, then calls the [Remove](xref:Microsoft.EntityFrameworkCore.DbContext.Remove(System.Object)) method to set the entity's status to `Deleted`. When `SaveChanges` is called, a SQL DELETE command is generated. If `Remove` fails:
+:::moniker range=">= aspnetcore-6.0"
-* The database exception is caught.
-* The Delete page's `OnGetAsync` method is called with `saveChangesError=true`.
+[!code-cshtml[Main](intro/samples/cu50/Pages/Students/Delete.cshtml?highlight=10)]
-Add an error message to the Delete Razor Page (`Pages/Students/Delete.cshtml`):
+:::moniker-end
+:::moniker range="< aspnetcore-5.0"
[!code-cshtml[Main](intro/samples/cu30/Pages/Students/Delete.cshtml?highlight=10)]
-Run the app and delete a student to test the Delete page.
+:::moniker-end
-## Next steps
+Run the app and delete a student to test the **Delete** page.
-> [!div class="step-by-step"]
-> [Previous tutorial](xref:data/ef-rp/intro)
-> [Next tutorial](xref:data/ef-rp/sort-filter-page)
+## Next step
-:::moniker-end
+> [!div class="nextstepaction"]
+> [Tutorial 3: Razor Pages with EF Core in ASP.NET Code - Sort, Filter, Paging](sort-filter-page.md)
\ No newline at end of file
diff --git a/aspnetcore/includes/RP-EF/intro.md b/aspnetcore/includes/RP-EF/intro.md
index ed7eef927958..26b4d17dd387 100644
--- a/aspnetcore/includes/RP-EF/intro.md
+++ b/aspnetcore/includes/RP-EF/intro.md
@@ -1,3 +1,3 @@
-The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. For information about the tutorial series, see [the first tutorial](xref:data/ef-rp/intro).
+The Contoso University web app demonstrates how to create Razor Pages web apps by using Entity Framework Core and Visual Studio. For information about the tutorial series, see [the first tutorial](xref:data/ef-rp/intro).
-If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare that code to what you created by following the tutorial.
+If you run into problems you can't solve, download the [completed app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-rp/intro/samples) and compare the code to what you created by following the tutorial.
From a1bea885582c941711787580370cd1dc0436b9c0 Mon Sep 17 00:00:00 2001
From: "Beth-Anne Harvey (AQUENT LLC)"
<28070425+GitHubber17@users.noreply.github.com>
Date: Fri, 17 Apr 2026 09:18:01 -0700
Subject: [PATCH 4/6] edits
---
aspnetcore/data/ef-rp/crud.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/aspnetcore/data/ef-rp/crud.md b/aspnetcore/data/ef-rp/crud.md
index f60e66d9b36d..8b448f64c24f 100644
--- a/aspnetcore/data/ef-rp/crud.md
+++ b/aspnetcore/data/ef-rp/crud.md
@@ -26,6 +26,8 @@ In this tutorial, you:
> [!div class="checklist"]
> * Customize pages: **Details**, **Create**, **Edit**, and **Delete**
> * Explore how to protect against overposting
+> * Review different methods to read a single entity
+> * Explore the View model
## Prerequisites
@@ -52,7 +54,7 @@ Replace the `OnGetAsync` method with the following code that reads enrollment da
The [EntityFrameworkQueryableExtensions.Include](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.include) and [ThenInclude](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.theninclude#microsoft-entityframeworkcore-entityframeworkqueryableextensions-theninclude-3(microsoft-entityframeworkcore-query-iincludablequeryable((-0-1))-system-linq-expressions-expression((system-func((-1-2))))))
methods cause the context to load the `Student.Enrollments` navigation property, and within each enrollment, the `Enrollment.Course` navigation property. These methods are examined in detail in [Read related data (Tutorial 6 of 8)](read-related-data.md).
-The [EntityFrameworkQueryableExtensions.AsNoTracking](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking) method improves performance in scenarios where the entities returned aren't updated in the current context. The `AsNoTracking` setting is discussed later in this tutorial.
+The [EntityFrameworkQueryableExtensions.AsNoTracking\](/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.asnotracking) method improves performance in scenarios where the entities returned aren't updated in the current context. The `AsNoTracking` setting is discussed later in this tutorial.
### Display enrollments
From b8d85b6fe750569c0e06df50a90eb91d46f2d110 Mon Sep 17 00:00:00 2001
From: "Beth-Anne Harvey (AQUENT LLC)"
<28070425+GitHubber17@users.noreply.github.com>
Date: Fri, 17 Apr 2026 11:37:15 -0700
Subject: [PATCH 5/6] edits to refresh content
---
aspnetcore/data/ef-mvc/crud.md | 4 +-
aspnetcore/data/ef-mvc/sort-filter-page.md | 167 ++++++++++-----------
2 files changed, 81 insertions(+), 90 deletions(-)
diff --git a/aspnetcore/data/ef-mvc/crud.md b/aspnetcore/data/ef-mvc/crud.md
index 0a85c8a39ab9..c99bf7029a2e 100644
--- a/aspnetcore/data/ef-mvc/crud.md
+++ b/aspnetcore/data/ef-mvc/crud.md
@@ -1,6 +1,6 @@
---
title: Implement CRUD - ASP.NET MVC with EF Core
-description: Review and customize the autogenerated CRUD (create, read, update, delete) code produced by the MVC scaffolding in your controllers and views.
+description: Review and customize the autogenerated CRUD (create, read, update, delete) code produced by the MVC scaffolding in your controllers and views. This article is part of a tutorial series.
author: tdykstra
ms.author: tdykstra
ms.custom: mvc
@@ -13,7 +13,7 @@ uid: data/ef-mvc/crud
# Tutorial: Implement basic CRUD functionality - ASP.NET MVC with EF Core
-In the previous tutorial, you created an MVC application that stores and displays data by using the [Entity Framework (EF) Core](/ef/core/) and a SQL Server local database. In this tutorial, you review and customize the CRUD (create, read, update, delete) code that the MVC scaffolding automatically creates for you in controllers and views.
+In the previous tutorial, you created an MVC application that stores and displays data by using the [Entity Framework (EF) Core](/ef/core/) and a SQL Server local database. In this exercise, you review and customize the CRUD (create, read, update, delete) code that the MVC scaffolding automatically creates for you in controllers and views.
> [!NOTE]
> It's a common practice to implement the repository pattern in order to create an abstraction layer between your controller and the data access layer. To keep the examples simple and focused on demonstrating how to use the Entity Framework itself, the tutorials don't use repositories. For information about repositories with Entity Framework, see [the last tutorial in this series](advanced.md).
diff --git a/aspnetcore/data/ef-mvc/sort-filter-page.md b/aspnetcore/data/ef-mvc/sort-filter-page.md
index 95b87a4ab3fa..db8362bd02e0 100644
--- a/aspnetcore/data/ef-mvc/sort-filter-page.md
+++ b/aspnetcore/data/ef-mvc/sort-filter-page.md
@@ -1,106 +1,109 @@
---
-title: "Tutorial: Add sorting, filtering, and paging - ASP.NET MVC with EF Core"
-description: "In this tutorial you'll add sorting, filtering, and paging functionality to the Students Index page. You'll also create a page that does simple grouping."
+title: Add Sort, Filter, Paging - ASP.NET MVC with EF Core
+description: Add sorting, filtering, and paging functionality to the Students Index page, and create a page for simple grouping. This article is part of a tutorial series.
author: tdykstra
ms.author: tdykstra
-ms.date: 03/27/2019
+ms.date: 04/17/2026
ms.topic: tutorial
uid: data/ef-mvc/sort-filter-page
+
+# customer intent: As an ASP.NET developer, I want to add sorting, filtering, and paging functionality in ASP.NET MVC with EF Core, so I can customize my site pages and behavior.
---
# Tutorial: Add sorting, filtering, and paging - ASP.NET MVC with EF Core
-In the previous tutorial, you implemented a set of web pages for basic CRUD operations for Student entities. In this tutorial you'll add sorting, filtering, and paging functionality to the Students Index page. You'll also create a page that does simple grouping.
+In the previous tutorial, you implemented a set of web pages for basic CRUD operations for Student entities. You created an MVC application that stores and displays data by using the [Entity Framework (EF) Core](/ef/core/)
-The following illustration shows what the page will look like when you're done. The column headings are links that the user can click to sort by that column. Clicking a column heading repeatedly toggles between ascending and descending sort order.
+In this exercise, you add sorting, filtering, and paging functionality to the Students Index page. You also create a page that does simple grouping. The following illustration shows what the page looks like when you're done. The column headings are links the user can select to sort by that column. Selecting a column heading repeatedly causes the sort order to toggle between ascending and descending.
-
+:::image type="content" source="sort-filter-page/_static/paging.png" border="false" alt-text="Screenshot of the Students index page that includes sorting, filtering, and paging functionality.":::
In this tutorial, you:
> [!div class="checklist"]
-> * Add column sort links
-> * Add a Search box
-> * Add paging to Students Index
-> * Add paging to Index method
-> * Add paging links
-> * Create an About page
+> * Add links to support column sorting
+> * Add a Search box to support searches
+> * Add paging to the Students Index
+> * Add links to support the paging action
+> * Create an About page for the site
## Prerequisites
-* [Implement CRUD Functionality](crud.md)
+* Complete the previous tutorial, [Implement basic CRUD functionality - ASP.NET MVC with EF Core](crud.md)
## Add column sort links
-To add sorting to the Student Index page, you'll change the `Index` method of the Students controller and add code to the Student Index view.
+To add sorting to the **Students Index** page, you change the `Index` method of the Students controller and add code to the Student Index view.
-### Add sorting Functionality to the Index method
+### Add sorting functionality to the Index method
-In `StudentsController.cs`, replace the `Index` method with the following code:
+In the _StudentsController.cs_ file, replace the `Index` method with the following code:
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_SortOnly)]
-This code receives a `sortOrder` parameter from the query string in the URL. The query string value is provided by ASP.NET Core MVC as a parameter to the action method. The parameter will be a string that's either "Name" or "Date", optionally followed by an underscore and the string "desc" to specify descending order. The default sort order is ascending.
+This code receives a `sortOrder` parameter from the query string in the URL. The query string value is provided by ASP.NET Core MVC as a parameter to the action method. The parameter is a string value: "Name" or "Date." The value is optionally followed by an underscore and the string "desc" to specify descending order. The default sort order is ascending.
-The first time the Index page is requested, there's no query string. The students are displayed in ascending order by last name, which is the default as established by the fall-through case in the `switch` statement. When the user clicks a column heading hyperlink, the appropriate `sortOrder` value is provided in the query string.
+The first time the **Index** page is requested, there's no query string. The students are displayed in ascending order by last name, which is the default as established by the fall-through case in the `switch` statement. When the user selects a column heading hyperlink, the appropriate `sortOrder` value is provided in the query string.
-The two `ViewData` elements (NameSortParm and DateSortParm) are used by the view to configure the column heading hyperlinks with the appropriate query string values.
+The two `ViewData` elements (`NameSortParm` and `DateSortParm`) are used by the view to configure the column heading hyperlinks with the appropriate query string values.
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_SortOnly&highlight=3-4)]
-These are ternary statements. The first one specifies that if the `sortOrder` parameter is null or empty, NameSortParm should be set to "name_desc"; otherwise, it should be set to an empty string. These two statements enable the view to set the column heading hyperlinks as follows:
+These code changes include ternary statements for the elements. The first statement specifies that if the `sortOrder` parameter is null or empty, the `NameSortParm` element is set to "name_desc." Otherwise, the element is set to an empty string. These two statements enable the view to set the column heading hyperlinks as follows:
-| Current sort order | Last Name Hyperlink | Date Hyperlink |
-|:--------------------:|:-------------------:|:--------------:|
-| Last Name ascending | descending | ascending |
-| Last Name descending | ascending | ascending |
-| Date ascending | ascending | descending |
-| Date descending | ascending | ascending |
+| Current sort order | Last name hyperlink | Date hyperlink |
+|---|---|---|
+| Last Name ascending | Descending | Ascending |
+| Last Name descending | Ascending | Ascending |
+| Date ascending | Ascending | Descending |
+| Date descending | Ascending | Ascending |
-The method uses LINQ to Entities to specify the column to sort by. The code creates an `IQueryable` variable before the switch statement, modifies it in the switch statement, and calls the `ToListAsync` method after the `switch` statement. When you create and modify `IQueryable` variables, no query is sent to the database. The query isn't executed until you convert the `IQueryable` object into a collection by calling a method such as `ToListAsync`. Therefore, this code results in a single query that's not executed until the `return View` statement.
+The method uses `LINQ` statements to student entities to specify the column to sort by. The code creates an `IQueryable` variable before the `switch` statement, modifies it in the `switch` statement, and calls the `ToListAsync` method after the `switch` statement. When you create and modify `IQueryable` variables, no query is sent to the database. The query isn't executed until you convert the `IQueryable` object into a collection by calling a method such as `ToListAsync`. Therefore, this code results in a single query that isn't executed until the `return View` statement.
-This code could get verbose with a large number of columns. [The last tutorial in this series](advanced.md#dynamic-linq) shows how to write code that lets you pass the name of the `OrderBy` column in a string variable.
+This code can get verbose when you work with a large number of columns. The last tutorial in this series, [Learn about advanced scenarios > Use dynamic LINQ to simplify code](advanced.md#dynamic-linq), shows how to write code that lets you pass the name of the `OrderBy` column in a string variable.
### Add column heading hyperlinks to the Student Index view
-Replace the code in `Views/Students/Index.cshtml`, with the following code to add column heading hyperlinks. The changed lines are highlighted.
+Replace the code in the _Views/Students/Index.cshtml_ file with the following code that adds column heading hyperlinks. The changed lines are highlighted.
[!code-cshtml[](intro/samples/cu/Views/Students/Index2.cshtml?highlight=16,22)]
-This code uses the information in `ViewData` properties to set up hyperlinks with the appropriate query string values.
+This code uses the information in the `ViewData` properties to set up hyperlinks with the appropriate query string values.
-Run the app, select the **Students** tab, and click the **Last Name** and **Enrollment Date** column headings to verify that sorting works.
+Run the app, select the **Students** tab, and select the **Last Name** and **Enrollment Date** column headings to verify sorting works.
-
+:::image type="content" source="sort-filter-page/_static/name-order.png" border="false" alt-text="Screenshot of the Students Index page showing the student last names sorted in ascending order.":::
## Add a Search box
-To add filtering to the Students Index page, you'll add a text box and a submit button to the view and make corresponding changes in the `Index` method. The text box will let you enter a string to search for in the first name and last name fields.
+To add filtering to the **Students Index** page, you add a text box and a **Submit** button to the view and make corresponding changes in the `Index` method. In the text box, you can enter a string to search for in the Student first name and last name fields.
-### Add filtering functionality to the Index method
+### Add filtering support to the Index method
-In `StudentsController.cs`, replace the `Index` method with the following code (the changes are highlighted).
+In the _StudentsController.cs_ file, replace the `Index` method with the following code (the changes are highlighted).
[!code-csharp[](intro/samples/cu/Controllers/StudentsController.cs?name=snippet_SortFilter&highlight=1,5,9-13)]
-You've added a `searchString` parameter to the `Index` method. The search string value is received from a text box that you'll add to the Index view. You've also added to the LINQ statement a where clause that selects only students whose first name or last name contains the search string. The statement that adds the where clause is executed only if there's a value to search for.
+You added a `searchString` parameter to the `Index` method. The search string value is received from a text box that you add to the Index view. You also added a `where` clause to the `LINQ` statement that selects only students whose first name or last name match the search string. The statement that adds the `where` clause is executed only if there's a searchable value.
> [!NOTE]
-> Here you are calling the `Where` method on an `IQueryable` object, and the filter will be processed on the server. In some scenarios you might be calling the `Where` method as an extension method on an in-memory collection. (For example, suppose you change the reference to `_context.Students` so that instead of an EF `DbSet` it references a repository method that returns an `IEnumerable` collection.) The result would normally be the same but in some cases may be different.
+> In this scenario, you call the `Where` method on an `IQueryable` object, and the filter is processed on the server. In some scenarios, you might call the `Where` method as an extension method on an in-memory collection. Suppose you change the reference to `_context.Students`. Instead of calling the Entity Framework `DbSet` method, it references a repository method that returns an `IEnumerable` collection. The result is normally the same, but some cases might be different.
>
->For example, the .NET Framework implementation of the `Contains` method performs a case-sensitive comparison by default, but in SQL Server this is determined by the collation setting of the SQL Server instance. That setting defaults to case-insensitive. You could call the `ToUpper` method to make the test explicitly case-insensitive: *Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())*. That would ensure that results stay the same if you change the code later to use a repository which returns an `IEnumerable` collection instead of an `IQueryable` object. (When you call the `Contains` method on an `IEnumerable` collection, you get the .NET Framework implementation; when you call it on an `IQueryable` object, you get the database provider implementation.) However, there's a performance penalty for this solution. The `ToUpper` code would put a function in the WHERE clause of the TSQL SELECT statement. That would prevent the optimizer from using an index. Given that SQL is mostly installed as case-insensitive, it's best to avoid the `ToUpper` code until you migrate to a case-insensitive data store.
+> For example, the .NET Framework implementation of the `Contains` method performs a case-sensitive comparison by default. In SQL Server, the determination occurs by the collation setting of the SQL Server instance. The setting defaults to case-insensitive. You might call the `ToUpper` method to make the test explicitly case-insensitive: `Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`. This option ensures the results stay the same if you change the code later to use a repository that returns an `IEnumerable` collection rather than an `IQueryable` object. (When you call the `Contains` method on an `IEnumerable` collection, you get the .NET Framework implementation. When you call it on an `IQueryable` object, you get the database provider implementation.)
+>
+> However, there's a performance penalty for this solution. The `ToUpper` code puts a function in the `WHERE` clause of the `TSQL SELECT` statement. This behavior prevents the optimizer from using an index. Given that SQL is mostly installed as case-insensitive, it's best to avoid the `ToUpper` code until you migrate to a case-insensitive data store.
-### Add a Search Box to the Student Index View
+### Add a Search Box to the Student Index view
-In `Views/Student/Index.cshtml`, add the highlighted code immediately before the opening table tag in order to create a caption, a text box, and a **Search** button.
+In the _Views/Student/Index.cshtml_ file, add the highlighted code immediately before the opening table tag `` in order to create a caption, a text box, and a **Search** button.
[!code-cshtml[](intro/samples/cu/Views/Students/Index3.cshtml?range=9-23&highlight=5-13)]
-This code uses the `