In the last module we added the Create feature in order to let users add Vehicles to the database.
In this module we will add an Update feature so that users can update vehicle information.
A lot of the steps in creating the update/edit feature are going to be similar to the steps we took in developing the create feature in the last module.
The first thing we need to do is again extend the vehicles repo to include a method for updating a vehicle.
Extend the Vehicles Repository
Modify IVehicleRepository.cs with the code shown below.
FredsCars\Models\Repositories\IVehicleRepository.cs
namespace FredsCars.Models.Repositories
{
public interface IVehicleRepository
{
IQueryable<Vehicle> Vehicles { get; }
Task CreateAsync(Vehicle vehicle);
Task UpdateAsync(Vehicle vehicle);
}
}
Next modify EFVehicleRepository.cs with the code shown below.
FredsCars\Models\Repositories\EFVehicleRepository.cs
using FredsCars.Data;
namespace FredsCars.Models.Repositories
{
public class EFVehicleRepository : IVehicleRepository
{
private FredsCarsDbContext _context;
public EFVehicleRepository(FredsCarsDbContext context)
{
_context = context;
}
public IQueryable<Vehicle> Vehicles => _context.Vehicles;
public async Task CreateAsync(Vehicle vehicle)
{
await _context.Vehicles.AddAsync(vehicle);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(Vehicle vehicle)
{
_context.Vehicles.Update(vehicle);
await _context.SaveChangesAsync();
}
}
}
In this section we simply added a method definition to the Vehicle repo interface called UpdateAsync
which takes in a Vehicle object and returns a Task and implemented it the EFVehicleRepository
class much like we did for the Create feature in the last module. The implimentation uses the Vehicles DbSet’s Update method where the CreateAsync method used the Vehicles DbSet’s AddAsync method. A DbSet only has an Upddate method and no UpdateAsync method. But the change to the Entity’s state will be saved to the database asyncronously through the SaveChangesAsync method.
Add the Edit action methods to the Vehicles controller
HttpGet Edit
First let’s add the HttpGet Edit action method. Modify the Vehicles controller with the following code.
FredsCars\Controllers\VehiclesController.cs
using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
namespace FredsCars.Controllers
{
public class VehiclesController : Controller
{
private IVehicleRepository _vehicleRepo;
private IVehicleTypeRepository _vehicleTypeRepo;
public VehiclesController(IVehicleRepository vRepo,
IVehicleTypeRepository vtRepo)
{
_vehicleRepo = vRepo;
_vehicleTypeRepo = vtRepo;
}
... existing code ...
public async Task<ViewResult> Edit(int id)
{
Vehicle? vehicle = await _vehicleRepo.Vehicles
.Include(v => v.VehicleType)
.FirstOrDefaultAsync(v => v.Id == id);
if (vehicle == null)
{
ViewBag.NoVehicleMessage =
"Sorry, no vehicle with that id could be found.";
}
var vehicleTypes =
_vehicleTypeRepo.VehicleTypes;
ViewBag.VehicleTypeList = new SelectList(vehicleTypes,
"Id", "Name");
return View(vehicle);
}
}
}
In the code above for the GET Edit action method we start off by fetching a vehicle by its Id using a LINQ query very similar to the Details action method and assign the result to a variable named vehicle of Type Vehicle?
(nullable Vehicle). Except here we do not include the AsNoTracking
method of the Vehicles DbSet because we need EF Core to track the changes made to the entity update.
Vehicle? vehicle = await _vehicleRepo.Vehicles
.Include(v => v.VehicleType)
.FirstOrDefaultAsync(v => v.Id == id);
Next we also do a similar null check as in the Details action method and store the same “..no vehicle with that id…” message into a ViewBag dynamic property with the same name.
if (vehicle == null)
{
ViewBag.NoVehicleMessage =
"Sorry, no vehicle with that id could be found.";
}
If the vehicle object is not null, we borrow the next two code statements from the Create action method’s set up to create a SelectList
object we will need for the Category select form control in the Edit view. To refresh our memory we use the VehicleTypes IQueryable
property of the VehicleTypes repo as a parameter to the SelectList
constructor, and pass “Id” and “Name” as the parameters for the dataValueField and dataTextField arguments for the drop down options in the View’s select form control.
var vehicleTypes =
_vehicleTypeRepo.VehicleTypes;
ViewBag.VehicleTypeList = new SelectList(vehicleTypes,
"Id", "Name");
As the last step we return a ViewResult
with the vehicle to update as the model and only parameter of the View
method.
return View(vehicle);
HttpPost Edit
Next let’s add the post edit action. Modify the Vehicles controller once more with the code shown below.
FredsCars\Controllers\VehiclesController.cs
using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
namespace FredsCars.Controllers
{
public class VehiclesController : Controller
{
... existing code ...
public async Task<ViewResult> Edit(int id)
{
Vehicle? vehicle = await _vehicleRepo.Vehicles
.Include(v => v.VehicleType)
.FirstOrDefaultAsync(v => v.Id == id);
if (vehicle == null)
{
ViewBag.NoVehicleMessage =
"Sorry, no vehicle with that id could be found.";
}
return View(vehicle);
}
[HttpPost]
[ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int id)
{
var vehicle = await _vehicleRepo.Vehicles
.Include(v => v.VehicleType)
.FirstOrDefaultAsync(v => v.Id == id);
if (await TryUpdateModelAsync<Vehicle>(vehicle!,
"",
v => v.Status, v => v.Year, v => v.Make,
v => v.Model, v => v.Color, v => v.Price,
v => v.Price, v => v.VIN, v => v.ImagePath
))
{
try
{
await _vehicleRepo.UpdateAsync(vehicle!);
return RedirectToAction("Index", "Home");
}
catch (DbUpdateException ex)
{
// Log the exception
//ex.Message, ex.Source, ex.StackTrace
ModelState.AddModelError("",
"There was a problem updating the vehicle."
+ "Please contact your system administrator." );
}
}
var vehicleTypes =
_vehicleTypeRepo.VehicleTypes;
ViewBag.VehicleTypeList = new SelectList(vehicleTypes,
"Id", "Name");
return View();
}
}
}
The new Post Edit action method takes in an id as the single parameter. If we named it Edit with the same parameter list as the Get Edit action, C# would complain that we have to methods with the same parameter types. C# does not allow this so we name the new action method EditPost. But when the user submits a post from the Get Edit action, ASP.Net core will be looking for a Post Edit action; not a Get EditPost method. To resolve this issue we decorate the action method with the ActionName
attribute and tell it the real name of this method is Edit; not EditPost.
[HttpPost]
[ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int id)
{
...
}
Notice we also decorate the Post Edit action method with the HttpPost
and ValidateAntiForgeryToken
attributes to mark this method as the Post version of the Edit action and to Validate the CSRF token that will be sent up with the form post data from the form post of the Edit View just like in the Post Create action method.
The Edit method starts off again fetching the vehicle to update from the database using a LINQ query and assigning the result to a variable named vehicle.
The TryUpdateModel pattern
In the Post Create action method we used the Bind attribute with a list of Vehicle object properties that are allowed to be bound to form post fields to protect from OverPosting.
In the next part of the Post Edit method we use the TryUpdateModel
pattern as an alternative way to defend against overposting in an Update/Edit method.
The setup part of the TryUpdateModel pattern here is to use the asynchronous version of the ControllerBase
class’s generic TryUpdateModel
method, TryUpdateModelAsync
of type type Vehicle, along with the await keyword to force the method to actually perform asynchronously, in an if statement.
if (await TryUpdateModelAsync<Vehicle>(vehicle!,
"",
v => v.Status, v => v.Year, v => v.Make,
v => v.Model, v => v.Color, v => v.Price,
v => v.Price, v => v.VIN, v => v.ImagePath
))
{
...
}
The first parameter passed to TryUpdateModel[Async] is the Vehicle object to be updated; the same vehicle entity we just fetched with the LINQ statement.
The second parameter is a prefix that will be in front of form field names in the form post. It is set to blank, or no characters, with double quotes here (""
) but this parameter will come in handy in the next chapter on Razor Pages.
A list of the object’s properties to update by model binding to matching fields in the form post comes at the end of the parameter list.
The TryUpdateMode[Async]
method returns a boolean; true if the update succeeds and false if not.
Error Handling with the try/catch pattern
In the body of the if statement, if the model binding to update the object succeeds, we use the try/catch pattern to handle any exceptions that may occur.
The try/catch pattern looks like the following.
try
{
// try to do something
}
catch(1stExpectedExceptionType ex)
{
// handle the exception
}
catch(2ndExpectedExceptionType ex)
{
// handle the exception
}
catch(Exception ex // the base exception class comes last)
{
// handle the exception
}
In a try catch block of code we try to do something and if it fails we can have a multiple series of catch blocks to handle specific types of exceptions. The base exception comes last to handle the most generic type of exception.
In our try block of code we try to Update the Vehicle object with the Vehicle repo and redirect to the Home/Index action. But sometimes an error beyond our control can occur such as a bad network connection or a database being down.
try
{
await _vehicleRepo.UpdateAsync(vehicle!);
return RedirectToAction("Index", "Home");
}
Exceptions
If our catch block of code we check for a specific type of exception, DbUpdateException.
catch (DbUpdateException ex)
{
// Log the exception
//ex.Message, ex.Source, ex.StackTrace
ModelState.AddModelError("",
"There was a problem updating the vehicle."
+ "Please contact your system administrator." );
}
An exception is…