Create, Read, Update, and Delete - EF Core with ASP.NET Core MVC tutorial (2 of 10)
By Tom Dykstra and Rick Anderson
The Contoso University sample web application demonstrates how to create ASP.NET Core MVC web applications using Entity Framework Core and Visual Studio. For information about the tutorial series, see the first tutorial in the series.
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 repositories with EF, see the last tutorial in this series.
In this tutorial, you’ll work with the following web pages:
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.
In Controllers/StudentsController.cs, the action method for the Details view uses the SingleOrDefaultAsync
method to retrieve a single Student
entity. Add code that calls Include
. ThenInclude
, and AsNoTracking
methods, as shown in the following highlighted code.
[!code-csharpMain]
1: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
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 reading related data tutorial.
The AsNoTracking
method improves performance in scenarios where the entities returned will not be updated in the current context’s lifetime. You’ll learn more about AsNoTracking
at the end of this tutorial.
Route data
The key value that is 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:
[!code-csharpMain]
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Threading.Tasks;
5: using Microsoft.AspNetCore.Builder;
6: using Microsoft.AspNetCore.Hosting;
7: using Microsoft.Extensions.Configuration;
8: using Microsoft.Extensions.DependencyInjection;
9: #region snippet_Usings
10: using ContosoUniversity.Data;
11: using Microsoft.EntityFrameworkCore;
12: #endregion
13:
14: namespace ContosoUniversity
15: {
16: public class Startup
17: {
18: public Startup(IConfiguration configuration)
19: {
20: Configuration = configuration;
21: }
22:
23: public IConfiguration Configuration { get; }
24:
25: // This method gets called by the runtime. Use this method to add services to the container.
26: #region snippet_SchoolContext
27: public void ConfigureServices(IServiceCollection services)
28: {
29: services.AddDbContext<SchoolContext>(options =>
30: options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
31:
32: services.AddMvc();
33: }
34: #endregion
35:
36: // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
37: public void Configure(IApplicationBuilder app, IHostingEnvironment env)
38: {
39: if (env.IsDevelopment())
40: {
41: app.UseDeveloperExceptionPage();
42: app.UseBrowserLink();
43: }
44: else
45: {
46: app.UseExceptionHandler("/Home/Error");
47: }
48:
49: app.UseStaticFiles();
50:
51: #region snippet_Route
52: app.UseMvc(routes =>
53: {
54: routes.MapRoute(
55: name: "default",
56: template: "{controller=Home}/{action=Index}/{id?}");
57: });
58: #endregion
59: }
60: }
61: }
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.
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 Details
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 id
is added to the route data.
This generates the following HTML when item.ID
is 6:
In the following Razor code, studentID
doesn’t match a parameter in the default route, so it’s added as a query string.
This generates the following HTML when item.ID
is 6:
For more information about tag helpers, see (xref:)Tag helpers in ASP.NET Core.
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:
[!code-htmlMain]
1: @model ContosoUniversity.Models.Student
2:
3: @{
4: ViewData["Title"] = "Details";
5: }
6:
7: <h2>Details</h2>
8:
9: <div>
10: <h4>Student</h4>
11: <hr />
12: <dl class="dl-horizontal">
13: <dt>
14: @Html.DisplayNameFor(model => model.LastName)
15: </dt>
16: <dd>
17: @Html.DisplayFor(model => model.LastName)
18: </dd>
19: <dt>
20: @Html.DisplayNameFor(model => model.FirstMidName)
21: </dt>
22: <dd>
23: @Html.DisplayFor(model => model.FirstMidName)
24: </dd>
25: <dt>
26: @Html.DisplayNameFor(model => model.EnrollmentDate)
27: </dt>
28: <dd>
29: @Html.DisplayFor(model => model.EnrollmentDate)
30: </dd>
31: <dt>
32: @Html.DisplayNameFor(model => model.Enrollments)
33: </dt>
34: <dd>
35: <table class="table">
36: <tr>
37: <th>Course Title</th>
38: <th>Grade</th>
39: </tr>
40: @foreach (var item in Model.Enrollments)
41: {
42: <tr>
43: <td>
44: @Html.DisplayFor(modelItem => item.Course.Title)
45: </td>
46: <td>
47: @Html.DisplayFor(modelItem => item.Grade)
48: </td>
49: </tr>
50: }
51: </table>
52: </dd>
53: </dl>
54: </div>
55: <div>
56: <a asp-action="Edit" asp-route-id="@Model.ID">Edit</a> |
57: <a asp-action="Index">Back to List</a>
58: </div>
After the last field and immediately before the closing </dl>
tag, add the following code to display a list of enrollments:
[!code-htmlMain]
1: @model ContosoUniversity.Models.Student
2:
3: @{
4: ViewData["Title"] = "Details";
5: }
6:
7: <h2>Details</h2>
8:
9: <div>
10: <h4>Student</h4>
11: <hr />
12: <dl class="dl-horizontal">
13: <dt>
14: @Html.DisplayNameFor(model => model.LastName)
15: </dt>
16: <dd>
17: @Html.DisplayFor(model => model.LastName)
18: </dd>
19: <dt>
20: @Html.DisplayNameFor(model => model.FirstMidName)
21: </dt>
22: <dd>
23: @Html.DisplayFor(model => model.FirstMidName)
24: </dd>
25: <dt>
26: @Html.DisplayNameFor(model => model.EnrollmentDate)
27: </dt>
28: <dd>
29: @Html.DisplayFor(model => model.EnrollmentDate)
30: </dd>
31: <dt>
32: @Html.DisplayNameFor(model => model.Enrollments)
33: </dt>
34: <dd>
35: <table class="table">
36: <tr>
37: <th>Course Title</th>
38: <th>Grade</th>
39: </tr>
40: @foreach (var item in Model.Enrollments)
41: {
42: <tr>
43: <td>
44: @Html.DisplayFor(modelItem => item.Course.Title)
45: </td>
46: <td>
47: @Html.DisplayFor(modelItem => item.Grade)
48: </td>
49: </tr>
50: }
51: </table>
52: </dd>
53: </dl>
54: </div>
55: <div>
56: <a asp-action="Edit" asp-route-id="@Model.ID">Edit</a> |
57: <a asp-action="Index">Back to List</a>
58: </div>
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 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. You see the list of courses and grades for the selected student:
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.
[!code-csharpMain]
1: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
This code adds the Student entity created by the ASP.NET MVC model binder to the Students 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 a Student entity for you using property values from the Form collection.)
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 does not set the ID value.
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).
The ValidateAntiForgeryToken
attribute helps prevent cross-site request forgery (CSRF) attacks. The token is automatically injected into the view by the (xref:)FormTagHelper and is included when the form is submitted by the user. The token is validated by the ValidateAntiForgeryToken
attribute. For more information about CSRF, see Anti-Request Forgery.
### Security note about 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 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.
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.
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 is the method used in these tutorials.
An alternative way to prevent overposting that is 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 is 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.
Run the app, select the Students tab, and click 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.
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.
[!code-csharpMain]
1: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
Change the date to a valid value and click 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 SingleOrDefaultAsync
method to retrieve the selected Student entity, as you saw in the Details
method. You don’t need to change this method.
Recommended HttpPost Edit code: Read and update
Replace the HttpPost Edit action method with the following code.
[!code-csharpMain]
1: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
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 is not recommended for many scenarios because the Bind
attribute clears out any pre-existing 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 (xref:)based on user input in the posted form data. 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.)
As a best practice to prevent overposting, the fields that you want to be updateable by the Edit page are whitelisted 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 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
.
Alternative 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.)
[!code-csharpMain]
1: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
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.
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.
An entity may be in one of the following states:
Added
. The entity does not yet exist in the database. TheSaveChanges
method issues an INSERT 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 issues an UPDATE statement.Deleted
. The entity has been marked for deletion. TheSaveChanges
method issues a DELETE statement.Detached
. The entity isn’t being tracked by the database context.
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 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.
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 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 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
.
Test the Edit page
Run the app, select the Students tab, then click an Edit hyperlink.
Change some of the data and click 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 SingleOrDefaultAsync
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: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
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 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
Replace the HttpPost Delete
action method (named DeleteConfirmed
) with the following code, which performs the actual delete operation and catches any database update errors.
[!code-csharpMain]
1: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
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 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.)
[!code-csharpMain]
1: #define SortFilterPage //or ScaffoldedIndex or SortOnly or SortFilter or DynamicLinq
2: #define ReadFirst //or CreateAndAttach
3: #define DeleteWithReadFirst // or DeleteWithoutReadFirst
4:
5: using System.Linq;
6: using System.Threading.Tasks;
7: using Microsoft.AspNetCore.Mvc;
8: using Microsoft.AspNetCore.Mvc.Rendering;
9: using Microsoft.EntityFrameworkCore;
10: using ContosoUniversity.Data;
11: using ContosoUniversity.Models;
12: using System;
13: using Microsoft.Extensions.Logging;
14:
15: #region snippet_Context
16: namespace ContosoUniversity.Controllers
17: {
18: public class StudentsController : Controller
19: {
20: private readonly SchoolContext _context;
21:
22: public StudentsController(SchoolContext context)
23: {
24: _context = context;
25: }
26: #endregion
27:
28: // GET: Students
29:
30: #if (ScaffoldedIndex)
31: #region snippet_ScaffoldedIndex
32: public async Task<IActionResult> Index()
33: {
34: return View(await _context.Students.ToListAsync());
35: }
36: #endregion
37: #elif (SortOnly)
38: #region snippet_SortOnly
39: public async Task<IActionResult> Index(string sortOrder)
40: {
41: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
42: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
43: var students = from s in _context.Students
44: select s;
45: switch (sortOrder)
46: {
47: case "name_desc":
48: students = students.OrderByDescending(s => s.LastName);
49: break;
50: case "Date":
51: students = students.OrderBy(s => s.EnrollmentDate);
52: break;
53: case "date_desc":
54: students = students.OrderByDescending(s => s.EnrollmentDate);
55: break;
56: default:
57: students = students.OrderBy(s => s.LastName);
58: break;
59: }
60: return View(await students.AsNoTracking().ToListAsync());
61: }
62: #endregion
63: #elif (SortFilter)
64: #region snippet_SortFilter
65: public async Task<IActionResult> Index(string sortOrder, string searchString)
66: {
67: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
68: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
69: ViewData["CurrentFilter"] = searchString;
70:
71: var students = from s in _context.Students
72: select s;
73: if (!String.IsNullOrEmpty(searchString))
74: {
75: students = students.Where(s => s.LastName.Contains(searchString)
76: || s.FirstMidName.Contains(searchString));
77: }
78: switch (sortOrder)
79: {
80: case "name_desc":
81: students = students.OrderByDescending(s => s.LastName);
82: break;
83: case "Date":
84: students = students.OrderBy(s => s.EnrollmentDate);
85: break;
86: case "date_desc":
87: students = students.OrderByDescending(s => s.EnrollmentDate);
88: break;
89: default:
90: students = students.OrderBy(s => s.LastName);
91: break;
92: }
93: return View(await students.AsNoTracking().ToListAsync());
94: }
95: #endregion
96: #elif (SortFilterPage)
97: #region snippet_SortFilterPage
98: public async Task<IActionResult> Index(
99: string sortOrder,
100: string currentFilter,
101: string searchString,
102: int? page)
103: {
104: ViewData["CurrentSort"] = sortOrder;
105: ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
106: ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
107:
108: if (searchString != null)
109: {
110: page = 1;
111: }
112: else
113: {
114: searchString = currentFilter;
115: }
116:
117: ViewData["CurrentFilter"] = searchString;
118:
119: var students = from s in _context.Students
120: select s;
121: if (!String.IsNullOrEmpty(searchString))
122: {
123: students = students.Where(s => s.LastName.Contains(searchString)
124: || s.FirstMidName.Contains(searchString));
125: }
126: switch (sortOrder)
127: {
128: case "name_desc":
129: students = students.OrderByDescending(s => s.LastName);
130: break;
131: case "Date":
132: students = students.OrderBy(s => s.EnrollmentDate);
133: break;
134: case "date_desc":
135: students = students.OrderByDescending(s => s.EnrollmentDate);
136: break;
137: default:
138: students = students.OrderBy(s => s.LastName);
139: break;
140: }
141:
142: int pageSize = 3;
143: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), page ?? 1, pageSize));
144: }
145: #endregion
146: #elif (DynamicLinq)
147: #region snippet_DynamicLinq
148: public async Task<IActionResult> Index(
149: string sortOrder,
150: string currentFilter,
151: string searchString,
152: int? page)
153: {
154: ViewData["CurrentSort"] = sortOrder;
155: ViewData["NameSortParm"] =
156: String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
157: ViewData["DateSortParm"] =
158: sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";
159:
160: if (searchString != null)
161: {
162: page = 1;
163: }
164: else
165: {
166: searchString = currentFilter;
167: }
168:
169: ViewData["CurrentFilter"] = searchString;
170:
171: var students = from s in _context.Students
172: select s;
173:
174: if (!String.IsNullOrEmpty(searchString))
175: {
176: students = students.Where(s => s.LastName.Contains(searchString)
177: || s.FirstMidName.Contains(searchString));
178: }
179:
180: if (string.IsNullOrEmpty(sortOrder))
181: {
182: sortOrder = "LastName";
183: }
184:
185: bool descending = false;
186: if (sortOrder.EndsWith("_desc"))
187: {
188: sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
189: descending = true;
190: }
191:
192: if (descending)
193: {
194: students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
195: }
196: else
197: {
198: students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
199: }
200:
201: int pageSize = 3;
202: return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(),
203: page ?? 1, pageSize));
204: }
205: #endregion
206: #endif
207:
208: // GET: Students/Details/5
209: #region snippet_Details
210: public async Task<IActionResult> Details(int? id)
211: {
212: if (id == null)
213: {
214: return NotFound();
215: }
216:
217: var student = await _context.Students
218: .Include(s => s.Enrollments)
219: .ThenInclude(e => e.Course)
220: .AsNoTracking()
221: .SingleOrDefaultAsync(m => m.ID == id);
222:
223: if (student == null)
224: {
225: return NotFound();
226: }
227:
228: return View(student);
229: }
230: #endregion
231:
232: // GET: Students/Create
233: public IActionResult Create()
234: {
235: return View();
236: }
237:
238: // POST: Students/Create
239: #region snippet_Create
240: [HttpPost]
241: [ValidateAntiForgeryToken]
242: public async Task<IActionResult> Create(
243: [Bind("EnrollmentDate,FirstMidName,LastName")] Student student)
244: {
245: try
246: {
247: if (ModelState.IsValid)
248: {
249: _context.Add(student);
250: await _context.SaveChangesAsync();
251: return RedirectToAction(nameof(Index));
252: }
253: }
254: catch (DbUpdateException /* ex */)
255: {
256: //Log the error (uncomment ex variable name and write a log.
257: ModelState.AddModelError("", "Unable to save changes. " +
258: "Try again, and if the problem persists " +
259: "see your system administrator.");
260: }
261: return View(student);
262: }
263: #endregion
264:
265: // GET: Students/Edit/5
266: public async Task<IActionResult> Edit(int? id)
267: {
268: if (id == null)
269: {
270: return NotFound();
271: }
272:
273: var student = await _context.Students
274: .AsNoTracking()
275: .SingleOrDefaultAsync(m => m.ID == id);
276: if (student == null)
277: {
278: return NotFound();
279: }
280: return View(student);
281: }
282:
283: // POST: Students/Edit/5
284: #if (CreateAndAttach)
285: #region snippet_CreateAndAttach
286: public async Task<IActionResult> Edit(int id, [Bind("ID,EnrollmentDate,FirstMidName,LastName")] Student student)
287: {
288: if (id != student.ID)
289: {
290: return NotFound();
291: }
292: if (ModelState.IsValid)
293: {
294: try
295: {
296: _context.Update(student);
297: await _context.SaveChangesAsync();
298: return RedirectToAction(nameof(Index));
299: }
300: catch (DbUpdateException /* ex */)
301: {
302: //Log the error (uncomment ex variable name and write a log.)
303: ModelState.AddModelError("", "Unable to save changes. " +
304: "Try again, and if the problem persists, " +
305: "see your system administrator.");
306: }
307: }
308: return View(student);
309: }
310: #endregion
311: #elif (ReadFirst)
312: #region snippet_ReadFirst
313: [HttpPost, ActionName("Edit")]
314: [ValidateAntiForgeryToken]
315: public async Task<IActionResult> EditPost(int? id)
316: {
317: if (id == null)
318: {
319: return NotFound();
320: }
321: var studentToUpdate = await _context.Students.SingleOrDefaultAsync(s => s.ID == id);
322: if (await TryUpdateModelAsync<Student>(
323: studentToUpdate,
324: "",
325: s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
326: {
327: try
328: {
329: await _context.SaveChangesAsync();
330: return RedirectToAction(nameof(Index));
331: }
332: catch (DbUpdateException /* ex */)
333: {
334: //Log the error (uncomment ex variable name and write a log.)
335: ModelState.AddModelError("", "Unable to save changes. " +
336: "Try again, and if the problem persists, " +
337: "see your system administrator.");
338: }
339: }
340: return View(studentToUpdate);
341: }
342: #endregion
343: #endif
344:
345: // GET: Students/Delete/5
346: #region snippet_DeleteGet
347: public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
348: {
349: if (id == null)
350: {
351: return NotFound();
352: }
353:
354: var student = await _context.Students
355: .AsNoTracking()
356: .SingleOrDefaultAsync(m => m.ID == id);
357: if (student == null)
358: {
359: return NotFound();
360: }
361:
362: if (saveChangesError.GetValueOrDefault())
363: {
364: ViewData["ErrorMessage"] =
365: "Delete failed. Try again, and if the problem persists " +
366: "see your system administrator.";
367: }
368:
369: return View(student);
370: }
371: #endregion
372: // POST: Students/Delete/5
373: #if (DeleteWithReadFirst)
374: #region snippet_DeleteWithReadFirst
375: [HttpPost, ActionName("Delete")]
376: [ValidateAntiForgeryToken]
377: public async Task<IActionResult> DeleteConfirmed(int id)
378: {
379: var student = await _context.Students
380: .AsNoTracking()
381: .SingleOrDefaultAsync(m => m.ID == id);
382: if (student == null)
383: {
384: return RedirectToAction(nameof(Index));
385: }
386:
387: try
388: {
389: _context.Students.Remove(student);
390: await _context.SaveChangesAsync();
391: return RedirectToAction(nameof(Index));
392: }
393: catch (DbUpdateException /* ex */)
394: {
395: //Log the error (uncomment ex variable name and write a log.)
396: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
397: }
398: }
399: #endregion
400: #elif (DeleteWithoutReadFirst)
401: #region snippet_DeleteWithoutReadFirst
402: [HttpPost]
403: [ValidateAntiForgeryToken]
404: public async Task<IActionResult> DeleteConfirmed(int id)
405: {
406: try
407: {
408: Student studentToDelete = new Student() { ID = id };
409: _context.Entry(studentToDelete).State = EntityState.Deleted;
410: await _context.SaveChangesAsync();
411: return RedirectToAction(nameof(Index));
412: }
413: catch (DbUpdateException /* ex */)
414: {
415: //Log the error (uncomment ex variable name and write a log.)
416: return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
417: }
418: }
419: #endregion
420: #endif
421: }
422: }
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.
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:
[!code-htmlMain]
1: @model ContosoUniversity.Models.Student
2:
3: @{
4: ViewData["Title"] = "Delete";
5: }
6:
7: <h2>Delete</h2>
8: <p class="text-danger">@ViewData["ErrorMessage"]</p>
9: <h3>Are you sure you want to delete this?</h3>
10: <div>
11: <h4>Student</h4>
12: <hr />
13: <dl class="dl-horizontal">
14: <dt>
15: @Html.DisplayNameFor(model => model.LastName)
16: </dt>
17: <dd>
18: @Html.DisplayFor(model => model.LastName)
19: </dd>
20: <dt>
21: @Html.DisplayNameFor(model => model.FirstMidName)
22: </dt>
23: <dd>
24: @Html.DisplayFor(model => model.FirstMidName)
25: </dd>
26: <dt>
27: @Html.DisplayNameFor(model => model.EnrollmentDate)
28: </dt>
29: <dd>
30: @Html.DisplayFor(model => model.EnrollmentDate)
31: </dd>
32: </dl>
33:
34: <form asp-action="Delete">
35: <div class="form-actions no-color">
36: <input type="submit" value="Delete" class="btn btn-default" /> |
37: <a asp-action="Index">Back to List</a>
38: </div>
39: </form>
40: </div>
Run the app, select the Students tab, and click 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.)
Closing 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 takes care of that task for you.
In Startup.cs, you call the AddDbContext extension method to provision the DbContext
class in the ASP.NET 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.
Handling 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.
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.
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:
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. Frequently these conditions are 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 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.
For more information, see Tracking vs. No-Tracking.
Summary
You now have a complete set of pages that perform simple CRUD operations for Student entities. In the next tutorial you’ll expand the functionality of the Index page by adding sorting, filtering, and paging.
<SITEMAP> <MVC> <ASP> <NET> <DATA> <KIOSK> <FLEX> <SQL> <NOTES> <LINUX> <MONO> <FREEWARE> <DOCS> <ENG> <CHAT ME> <ABOUT ME> < THANKS ME> |