Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application (2 of 10)
by Tom Dykstra
The Contoso University sample web application demonstrates how to create ASP.NET MVC 4 applications using the Entity Framework 5 Code First and Visual Studio 2012. For information about the tutorial series, see the first tutorial in the series. You can start the tutorial series from the beginning or download a starter project for this chapter and start here.
[!NOTE]
If you run into a problem you can’t resolve, download the completed chapter and try to reproduce your problem. You can generally find the solution to the problem by comparing your code to the completed code. For some common errors and how to solve them, see Errors and Workarounds.
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.
[!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, you won’t implement a repository until a later tutorial in this series.
In this tutorial, you’ll create the following web pages:
Creating a 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.
In Controllers.cs, the action method for the Details
view uses the Find
method to retrieve a single Student
entity.
[!code-csharpMain]
1: public ActionResult Details(int id = 0)
2: {
3: Student student = db.Students.Find(id);
4: if (student == null)
5: {
6: return HttpNotFound();
7: }
8: return View(student);
9: }
The key value is passed to the method as the id
parameter and comes from route data in the Details hyperlink on the Index page.
Open Views.cshtml. Each field is displayed using a
[!code-cshtmlMain]DisplayFor
helper, as shown in the following example:1: <div class="display-label">
2: @Html.DisplayNameFor(model => model.LastName)
3: </div>
4: <div class="display-field">
5: @Html.DisplayFor(model => model.LastName)
6: </div>
After the
EnrollmentDate
field and immediately before the closingfieldset
tag, add code to display a list of enrollments, as shown in the following example:[!code-cshtmlMain]
1: <div class="display-label">
2: @Html.LabelFor(model => model.Enrollments)
3: </div>
4: <div class="display-field">
5: <table>
6: <tr>
7: <th>Course Title</th>
8: <th>Grade</th>
9: </tr>
10: @foreach (var item in Model.Enrollments)
11: {
12: <tr>
13: <td>
14: @Html.DisplayFor(modelItem => item.Course.Title)
15: </td>
16: <td>
17: @Html.DisplayFor(modelItem => item.Grade)
18: </td>
19: </tr>
20: }
21: </table>
22: </div>
23: </fieldset>
24: <p>
25: @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) |
26: @Html.ActionLink("Back to List", "Index")
27: </p>
Enrollments
navigation property. For eachEnrollment
entity in the property, it displays the course title and the grade. The course title is retrieved from theCourse
entity that’s stored in theCourse
navigation property of theEnrollments
entity. All of this data is retrieved from the database automatically when it’s needed. (In other words, you are using lazy loading here. You did not specify eager loading for theCourses
navigation property, so the first time you try to access that property, a query is sent to the database to retrieve the data. You can read more about lazy loading and eager loading in the Reading Related Data tutorial later in this series.)Run the page by selecting the Students tab and clicking a Details link for Alexander Carson. You see the list of courses and grades for the selected student:
Updating the Create Page
In Controllers.cs, replace the
HttpPost``Create
action method with the following code to add atry-catch
block and the Bind attribute to the scaffolded method:[!code-csharpMain]
1: [HttpPost]
2: [ValidateAntiForgeryToken]
3: public ActionResult Create(
4: [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
5: Student student)
6: {
7: try
8: {
9: if (ModelState.IsValid)
10: {
11: db.Students.Add(student);
12: db.SaveChanges();
13: return RedirectToAction("Index");
14: }
15: }
16: catch (DataException /* dex */)
17: {
18: //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
19: ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
20: }
21: return View(student);
22: }
This code adds the
Student
entity created by the ASP.NET MVC model binder to theStudents
entity set and then saves the changes to the database. (Model binder refers to the ASP.NET 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 aStudent
entity for you using property values from theForm
collection.)The
ValidateAntiForgeryToken
attribute helps prevent cross-site request forgery attacks.
> [!WARNING]
> Security - The `Bind` attribute is added to protect against *over-posting*. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to update.
>
> [!code-csharpMain]
1: public class Student
2: {
3: public int StudentID { get; set; }
4: public string LastName { get; set; }
5: public string FirstMidName { get; set; }
6: public DateTime EnrollmentDate { get; set; }
7: public string Secret { get; set; }
8:
9: public virtual ICollection<Enrollment> Enrollments { get; set; }
10: }
>
> Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as [fiddler](http://fiddler2.com/home), or write some JavaScript, to post a `Secret` form value. Without the [Bind](https://msdn.microsoft.com/en-us/library/system.web.mvc.bindattribute(v=vs.108).aspx) 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 update 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.
>
> ![](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image6.png)
>
> 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 update that property.
>
> It's a security best practice to use the `Include` parameter with the `Bind` attribute to *whitelist* fields. It's also possible to use the `Exclude` parameter to *blacklist* fields you want to exclude. The reason `Include` is more secure is that when you add a new property to the entity, the new field is not automatically protected by an `Exclude` list.
>
> Another alternative approach, and one preferred by many, is to use only view models with model binding. The view model contains only the properties you want to bind. Once the MVC model binder has finished, you copy the view model properties to the entity instance.
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 [DataException](https://msdn.microsoft.com/en-us/library/system.data.dataexception.aspx) is caught while the changes are being saved, a generic error message is displayed. [DataException](https://msdn.microsoft.com/en-us/library/system.data.dataexception.aspx) 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 (and non-null inner exceptions ) with a logging mechanism such as [ELMAH](https://code.google.com/p/elmah/).
The code in *Views\Student\Create.cshtml* is similar to what you saw in *Details.cshtml*, except that `EditorFor` and `ValidationMessageFor` helpers are used for each field instead of `DisplayFor`. The following example shows the relevant code:
[!code-cshtmlMain]
1: <div class="editor-label">
2: @Html.LabelFor(model => model.LastName)
3: </div>
4: <div class="editor-field">
5: @Html.EditorFor(model => model.LastName)
6: @Html.ValidationMessageFor(model => model.LastName)
7: </div>
*Create.chstml* also includes `@Html.AntiForgeryToken()`, which works with the `ValidateAntiForgeryToken` attribute in the controller to help prevent [cross-site request forgery](../../security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages.md) attacks.
No changes are required in *Create.cshtml*.
Run the page by selecting the Students tab and clicking Create New.
Some data validation works by default. Enter names and an invalid date and click Create to see the error message.
The following highlighted code shows the model validation check.
[!code-csharpMain]
1: [HttpPost]
2: [ValidateAntiForgeryToken]
3: public ActionResult Create(Student student)
4: {
5: if (ModelState.IsValid)
6: {
7: db.Students.Add(student);
8: db.SaveChanges();
9: return RedirectToAction("Index");
10: }
11:
12: return View(student);
13: }
Change the date to a valid value such as 9/1/2005 and click Create to see the new student appear in the Index page.
Updating the Edit POST Page
In Controllers.cs, the HttpGet
Edit
method (the one without the HttpPost
attribute) uses the Find
method to retrieve the selected Student
entity, as you saw in the Details
method. You don’t need to change this method.
However, replace the HttpPost
Edit
action method with the following code to add a try-catch
block and the Bind attribute:
[!code-csharpMain]
1: [HttpPost]
2: [ValidateAntiForgeryToken]
3: public ActionResult Edit(
4: [Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
5: Student student)
6: {
7: try
8: {
9: if (ModelState.IsValid)
10: {
11: db.Entry(student).State = EntityState.Modified;
12: db.SaveChanges();
13: return RedirectToAction("Index");
14: }
15: }
16: catch (DataException /* dex */)
17: {
18: //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
19: ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
20: }
21: return View(student);
22: }
This code is similar to what you saw in the HttpPost
Create
method. However, instead of adding the entity created by the model binder to the entity set, this code sets a flag on the entity indicating it has been changed. When the SaveChanges method is called, the Modified flag causes the Entity Framework to create SQL statements to update the database row. All columns of the database row will be updated, including those that the user didn’t change, and concurrency conflicts are ignored. (You’ll learn how to handle concurrency in a later tutorial in this series.)
Entity States and the Attach and SaveChanges Methods
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.
An entity may be in one of thefollowing states:
Added
. The entity does not yet exist in the database. TheSaveChanges
method must issue anINSERT
statement.Unchanged
. Nothing needs to be done with this entity by theSaveChanges
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. TheSaveChanges
method must issue anUPDATE
statement.Deleted
. The entity has been marked for deletion. TheSaveChanges
method must issue aDELETE
statement.Detached
. The entity isn’t being tracked by the database context.
In a desktop application, state changes are typically set automatically. In a desktop type of application, 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.
The disconnected nature of web apps doesn’t allow for this continuous sequence. The DbContext that reads an entity is disposed after a page is rendered. When the HttpPost
Edit
action method is called, a new request is made and you have a new instance of the DbContext, so you have to manually set the entity state to Modified.
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 want the SQL Update
statement to update only the fields that the user actually changed, you can save the original values in some way (such as hidden fields) so that they are 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.
For more information, see Entity states and SaveChanges and Local Data in the MSDN Data Developer Center.
The code in Views.cshtml is similar to what you saw in Create.cshtml, and no changes are required.
Run the page by selecting the Students tab and then clicking an Edit hyperlink.
Change some of the data and click Save. You see the changed data in the Index page.
Updating the Delete Page
In Controllers.cs, the template code for the HttpGet
Delete
method uses the Find
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.
As you saw for update and create operations, delete operations require two action methods. The method that is 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.
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.
Replace the
HttpGet
Delete
action method with the following code, which manages error reporting:[!code-csharpMain]
1: public ActionResult Delete(bool? saveChangesError=false, int id = 0)
2: {
3: if (saveChangesError.GetValueOrDefault())
4: {
5: ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
6: }
7: Student student = db.Students.Find(id);
8: if (student == null)
9: {
10: return HttpNotFound();
11: }
12: return View(student);
13: }
false
when theHttpGet
Delete
method is called without a previous failure. When it is called by theHttpPost
Delete
method in response to a database update error, the parameter istrue
and an error message is passed to the view.
Replace the
HttpPost
Delete
action method (namedDeleteConfirmed
) with the following code, which performs the actual delete operation and catches any database update errors.[!code-csharpMain]
1: [HttpPost]
2: [ValidateAntiForgeryToken]
3: public ActionResult Delete(int id)
4: {
5: try
6: {
7: Student student = db.Students.Find(id);
8: db.Students.Remove(student);
9: db.SaveChanges();
10: }
11: catch (DataException/* dex */)
12: {
13: // uncomment dex and log error.
14: return RedirectToAction("Delete", new { id = id, saveChangesError = true });
15: }
16: return RedirectToAction("Index");
17: }
This code retrieves the selected entity, then calls the Remove method to set the entity’s status to
Deleted
. WhenSaveChanges
is called, a SQLDELETE
command is generated. You have also changed the action method name fromDeleteConfirmed
toDelete
. The scaffolded code named theHttpPost
Delete
methodDeleteConfirmed
to give theHttpPost
method a unique signature. ( The CLR requires overloaded methods to have different method parameters.) Now that the signatures are unique, you can stick with the MVC convention and use the same name for theHttpPost
andHttpGet
delete methods.If improving performance in a high-volume application is a priority, you could avoid an unnecessary SQL query to retrieve the row by replacing the lines of code that call the
Find
andRemove
methods with the following code as shown in yellow highlight:[!code-csharpMain]
1: Student studentToDelete = new Student() { StudentID = id };
2: db.Entry(studentToDelete).State = EntityState.Deleted;
This code instantiates a
As noted, theStudent
entity using only the primary key value and then sets the entity state toDeleted
. That’s all that the Entity Framework needs in order to delete the entity.HttpGet
Delete
method doesn’t delete the data. Performing a delete operation in response to a GET request (or for that matter, performing any edit operation, create operation, or any other operation that changes data) creates a security risk. For more information, see ASP.NET MVC Tip #46 — Don’t use Delete Links because they create Security Holes on Stephen Walther’s blog.In Views.cshtml, add an error message between the
h2
heading and theh3
heading, as shown in the following example:[!code-cshtmlMain]
1: <h2>Delete</h2>
2: <p class="error">@ViewBag.ErrorMessage</p>
3: <h3>Are you sure you want to delete this?</h3>
Run the page by selecting the Students tab and clicking a Delete hyperlink:
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 Handling Concurrency tutorial later in this series.)
Ensuring that Database Connections Are Not Left Open
To make sure that database connections are properly closed and the resources they hold freed up, you should see to it that the context instance is disposed. That is why the scaffolded code provides a Dispose method at the end of the StudentController
class in StudentController.cs, as shown in the following example:
[!code-csharpMain]
1: protected override void Dispose(bool disposing)
2: {
3: db.Dispose();
4: base.Dispose(disposing);
5: }
The base Controller
class already implements the IDisposable
interface, so this code simply adds an override to the Dispose(bool)
method to explicitly dispose the context instance.
Summary
You now have a complete set of pages that perform simple CRUD operations for Student
entities. You used MVC helpers to generate UI elements for data fields. For more information about MVC helpers, see Rendering a Form Using HTML Helpers (the page is for MVC 3 but is still relevant for MVC 4).
In the next tutorial you’ll expand the functionality of the Index page by adding sorting and paging.
Links to other Entity Framework resources can be found in the ASP.NET Data Access Content Map.
|