Skip to content

Latest commit

 

History

History
300 lines (169 loc) · 24.4 KB

File metadata and controls

300 lines (169 loc) · 24.4 KB
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. This article is part of a tutorial series.
author tdykstra
ms.author tdykstra
ms.custom mvc
ms.date 04/16/2026
ms.topic tutorial
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 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.

In this tutorial, you:

[!div class="checklist"]

  • 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

Customize the Details page

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 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]

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 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.

Configure the route data

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]

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.

http://localhost:1230/Instructor/Index/1?courseID=2021

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:

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 the id value is added to the route data.

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>

This change generates the following HTML when the item.ID value is 6:

<a href="/Students/Edit/6">Edit</a>

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.

<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>

This change generates the following HTML when the item.ID value is 6:

<a href="/Students/Edit?studentID=6">Edit</a>

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.

[!code-cshtml]

After the last field, and immediately before the closing </dl> tag, add the following code to display a list of enrollments:

[!code-cshtml]

If the code indentation is misaligned after you paste the snippet, use the CTRL+K+D keyboard shortcut to correct it.

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 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 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]

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 by using property values from the Form collection.

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.

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.

The ValidateAntiForgeryToken attribute helps prevent cross-site request forgery (CSRF) attacks. 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 tPrevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core and FormTagHelper Class reference.

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.

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstMidName { get; set; }
    public DateTime EnrollmentDate { get; set; }
    public string Secret { get; set; }
}

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" 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 the TryUpdateModel method. Pass in an explicit allowed properties list, which is how the method is used in these tutorials.

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 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.

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 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]

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 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

Replace the HttpPost Edit method with the following code.

[!code-csharp]

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 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.) 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. 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. The changes basically rename the method to EditPost.

Use alternate HttpPost Edit code: Create and attach

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]

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.

Understand entity states

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 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.

  • 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.

  • 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 web app, the DbContext object 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 class. 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.

Test the Edit page

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 select Save. The Index page opens and you see the changed data.

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.

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 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]

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.

Use read-first approach to HttpPost Delete

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]

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.

Use create-and-attach approach to HttpPost Delete

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]

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 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]

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.":::

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're finished. The ASP.NET Core built-in dependency injection takes care of the clean-up task.

In the Startup.cs file, you call the AddDbContext extension method 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 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. The examples cover how to include operations completed outside of Entity Framework in a transaction.

Disable tracking of entity objects (no-tracking queries)

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. 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 Entity Framework to automatically load navigation properties with entities retrieved by separate queries. These conditions are frequently met in a controller's HttpGet Action methods.

  • 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 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 versus no-tracking queries.

Get the code

Download or view the completed application,

Next step

[!div class="nextstepaction"] Tutorial: Add sorting, filtering, and paging - ASP.NET MVC with EF Core