1 2_Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application Mon Jul 06, 2015 8:05 am
Admin
Admin
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 and focused on teaching how to use the Entity Framework itself, they don't use repositories. For information about how to implement repositories, see the ASP.NET Data Access Content Map.
In this tutorial, you'll create the following web pages:
Create a Details Page
The scaffolded code for the Students
- Code:
Index
- Code:
Enrollments
- Code:
Details
In Controllers\StudentController.cs, the action method for the
- Code:
Details
- Code:
Student
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
The key value is passed to the method as the
- Code:
id
Route data
Route data is data that the model binder found in a URL segment specified in the routing table. For example, the default route specifies- Code:
controller
- Code:
action
- Code:
id
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
In the following URL, the default route maps
- Code:
Instructor
- Code:
controller
- Code:
Index
- Code:
action
- Code:
id
- Code:
http://localhost:1230/Instructor/Index/1?courseID=2021
- Code:
id
- Code:
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
- Code:
ActionLink
- Code:
id
- Code:
id
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
In the following code,
- Code:
courseID
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
[list defaultattr=]
[*]Open Views\Student\Details.cshtml. Each field is displayed using a
- Code:
DisplayFor
@Html.DisplayNameFor(model => model.LastName)
@Html.DisplayFor(model => model.LastName)
[*]After the
- Code:
EnrollmentDate
- Code:
</dl>
@Html.DisplayNameFor(model => model.EnrollmentDate)
@Html.DisplayFor(model => model.EnrollmentDate)
@Html.DisplayNameFor(model => model.Enrollments)
Course Title | Grade |
---|---|
@Html.DisplayFor(modelItem => item.Course.Title) | @Html.DisplayFor(modelItem => item.Grade) |
@Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
@Html.ActionLink("Back to List", "Index")
If code indentation is wrong after you paste the code, press CTRL-K-D to correct it.
This code loops through the entities in the
- Code:
Enrollments
- Code:
Enrollment
- Code:
Course
- Code:
Course
- Code:
Enrollments
- Code:
Courses
- Code:
Enrollments
[*]Run the page by selecting the Students tab and clicking a Details link for Alexander Carson. (If you press CTRL+F5 while the Details.cshtml file is open, you'll get an HTTP 400 error because Visual Studio tries to run the Details page but it wasn't reached from a link that specifies the student to display. In that case, just remove "Student/Details" from the URL and try again, or close the browser, right-click the project, and click View, and then click View in Browser.)
You see the list of courses and grades for the selected student:
[/list]
Update the Create Page
[list defaultattr=]
[*]In Controllers\StudentController.cs, replace the
- Code:
HttpPost
- Code:
Create
- Code:
try-catch
- Code:
ID
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "LastName, FirstMidName, EnrollmentDate")]Student student)
{
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
This code adds the
- Code:
Student
- Code:
Students
- Code:
Student
- Code:
Form
You removed
- Code:
ID
- Code:
ID
- Code:
ID
Security Note: The
- Code:
ValidateAntiForgeryToken
- Code:
Html.AntiForgeryToken()
The
- Code:
Bind
- Code:
Student
- Code:
Secret
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; }
public virtual ICollection
}
Even if you don't have a
- Code:
Secret
- Code:
Secret
- Code:
Student
- Code:
Secret
- Code:
Student
- Code:
Secret
- Code:
Secret
The value "OverPost" would then be successfully added to the
- Code:
Secret
It's a security best practice to use the
- Code:
Include
- Code:
Bind
- Code:
Exclude
- Code:
Include
- Code:
Exclude
You can prevent overposting in edit scenarios is by reading the entity from the database first and then calling
- Code:
TryUpdateModel
An alternative way to prevent overposting that is preferrred 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 db.Entry on the entity instance to set its state to Unchanged, and then set Property("PropertyName").IsModified to true on each entity property that is included in the view model. This method works in both edit and create scenarios.
Other than the
- Code:
Bind
- Code:
try-catch
The code in Views\Student\Create.cshtml is similar to what you saw in Details.cshtml, except that
- Code:
EditorFor
- Code:
ValidationMessageFor
- Code:
DisplayFor
@Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
Create.chstml also includes
- Code:
@Html.AntiForgeryToken()
- Code:
ValidateAntiForgeryToken
No changes are required in Create.cshtml.
[*]Run the page by selecting the Students tab and clicking Create New.
[*]Enter names and an invalid date and click Create to see the error message.
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.
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
[*]Change the date to a valid value and click Create to see the new student appear in the Index page.
[/list]
Update the Edit HttpPost Method
In Controllers\StudentController.cs, the
- Code:
HttpGet
- Code:
Edit
- Code:
HttpPost
- Code:
Find
- Code:
Student
- Code:
Details
However, replace the
- Code:
HttpPost
- Code:
Edit
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var studentToUpdate = db.Students.Find(id);
if (TryUpdateModel(studentToUpdate, "",
new string[] { "LastName", "FirstMidName", "EnrollmentDate" }))
{
try
{
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DataException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
return View(studentToUpdate);
}
These changes implement a security best practice to prevent overposting, The scaffolder generated a
- Code:
Bind
- Code:
Bind
- Code:
Include
- Code:
Bind
The new code reads the existing entity and calls TryUpdateModel to update fields from user input in the posted form data. The Entity Framework's automatic change tracking sets the Modified flag on the entity. When the SaveChanges method is called, the
- Code:
Modified
As a best practice to prevent overposting, the fields that you want to be updateable by the Edit page are whitelisted in the
- Code:
TryUpdateModel
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.
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- Code:
SaveChanges
- Code:
Added
- Code:
INSERT
An entity may be in one of the following states:
-
- Code:
Added
- Code:
SaveChanges
- Code:
INSERT
-
- Code:
Unchanged
- Code:
SaveChanges
-
- Code:
Modified
- Code:
SaveChanges
- Code:
UPDATE
-
- Code:
Deleted
- Code:
SaveChanges
- Code:
DELETE
-
- Code:
Detached
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
- Code:
Modified
- Code:
SaveChanges
- Code:
UPDATE
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
- Code:
HttpPost
- Code:
Edit
- Code:
Modified.
- Code:
SaveChanges
If you want the SQL
- Code:
Update
- Code:
HttpPost
- Code:
Edit
- Code:
Student
- Code:
Attach
- Code:
SaveChanges.
The HTML and Razor code in Views\Student\Edit.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\StudentController.cs, the template code for the
- Code:
HttpGet
- Code:
Delete
- Code:
Find
- Code:
Student
- Code:
Details
- Code:
Edit
- Code:
SaveChanges
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
- Code:
HttpPost
- Code:
Delete
You'll add a
- Code:
try-catch
- Code:
HttpPost
- Code:
Delete
- Code:
HttpPost
- Code:
Delete
- Code:
HttpGet
- Code:
Delete
- Code:
HttpGet Delete
[list defaultattr=]
[*]Replace the
- Code:
HttpGet
- Code:
Delete
public ActionResult Delete(int? id, bool? saveChangesError=false)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
if (saveChangesError.GetValueOrDefault())
{
ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
}
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
This code accepts an optional parameter that indicates whether the method was called after a failure to save changes. This parameter is
- Code:
false
- Code:
HttpGet
- Code:
Delete
- Code:
HttpPost
- Code:
Delete
- Code:
true
[*]Replace the
- Code:
HttpPost
- Code:
Delete
- Code:
DeleteConfirmed
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
try
{
Student student = db.Students.Find(id);
db.Students.Remove(student);
db.SaveChanges();
}
catch (DataException/* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
return RedirectToAction("Delete", new { id = id, saveChangesError = true });
}
return RedirectToAction("Index");
}
This code retrieves the selected entity, then calls the Remove method to set the entity's status to
- Code:
Deleted
- Code:
SaveChanges
- Code:
DELETE
- Code:
DeleteConfirmed
- Code:
Delete
- Code:
HttpPost
- Code:
Delete
- Code:
DeleteConfirmed
- Code:
HttpPost
- Code:
HttpPost
- Code:
HttpGet
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
- Code:
Find
- Code:
Remove
Student studentToDelete = new Student() { ID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;
This code instantiates a
- Code:
Student
- Code:
Deleted
As noted, the
- Code:
HttpGet
- Code:
Delete
[*]In Views\Student\Delete.cshtml, add an error message between the
- Code:
h2
- Code:
h3
Delete
@ViewBag.ErrorMessage
Are you sure you want to delete this?
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 concurrency tutorial.)
[/list]
Closing Database Connections
To close database connections and free up the resources they hold as soon as possible, dispose the context instance when you are done with it. That is why the scaffolded code provides a Dispose method at the end of the
- Code:
StudentController
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
The base
- Code:
Controller
- Code:
IDisposable
- Code:
Dispose(bool)
Handling Transactions
By default the Entity Framework implicitly implements transactions. In scenarios where you make changes to multiple rows or tables and then call
- Code:
SaveChanges
Summary
You now have a complete set of pages that perform simple CRUD operations for
- Code:
Student
In the next tutorial you'll expand the functionality of the Index page by adding sorting and paging.
Please leave feedback on how you liked this tutorial and what we could improve. You can also request new topics at Show Me How With Code.
Links to other Entity Framework resources can be found in ASP.NET Data Access - Recommended Resources.
This article was originally created on February 14, 2014