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.
Extend the Vehicles Repository
The first thing we’ll need to do is again extend the vehicles repo to include a method for updating a vehicle.
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, NoVehicleMessage.
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 also 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.VIN, v => v.ImagePath, v => v.VehicleTypeID
))
{
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 two 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 Post 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 data from the form post of the Edit View just like in the Post Create action method.
The action 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 uses the asynchronous version of the Controller base class’s generic TryUpdateModel method, TryUpdateModelAsync of 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,
v => v.VehicleTypeId
))
{
...
}
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 be updated by the model binder to matching fields in the form post comes at the end of the parameter list. Only the fields listed here will be updated in order to protect against overposting.
The TryUpdateModel[Async] method returns a boolean value; true if the update succeeds and false if not.
Error Handling with the try/catch pattern
In the body of the if statement, if TryUpdateModelAsync 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’s UpdateAsync method 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
In our catch block of code we check for a specific type of exception, DbUpdateException. And we give the exception an optional variable name, in this case ex, so that we can work with and access the exception within the catch block.
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." );
}
In C#, an exception is an object that represents an error or unexpected behavior that occurs during the execution of a program. When something goes wrong — like trying to divide by zero, access a null object, or open a file that doesn’t exist — the .NET runtime throws an exception.
An exception object has useful properties we can inspect to figure out what the source of the error is like Message (a hopefully human friendly message to explain what happened) and StackTrace.
The StackTrace property of an exception object provides a string that describes the sequence of method calls that led to the exception being thrown. It helps developers trace back through the call stack to identify where exactly the error occurred in the code.
Each line in the stack trace typically shows:
- The method name
- The file name (if debug symbols are available)
- The line number where the method was called
Within the catch block we have a comment to come back and log the exception. We will come back to this step in a later module.
Also within the catch block we add an error to the ModelState using the ModelState’s AddModelError method. The first parameter of the AddModelError method is a key for the error. The second parameter is a message for the error. If we want the error to correspond to a specific property of the model, a Vehicle object, we would pass something like “Make” as the key string. Here we have set it to empty double quotes, "", meaning the error does not pertain to a specific property but to the model itself.
If TryUpdateModelAsync does not succeed in the if statement and returns false, the try/catch block is skipped over and we again setup the SelectList for categories or Vehicle Types before finally returning a ViewResult with the View method to re-render the form with any errors.
Create the View
Create a new Razor file in the Views/Vehicles folder of the FredsCars project and name it Edit.cshtml. Modify it with the code shown below.
FredsCars\FredsCars\Views\Vehicles\Edit.cshtml
@model Vehicle
@{
ViewData["Title"] = "Edit";
}
<div class="container-fluid mt-3">
<h3 class="text-center bg-primary-subtle py-2"
style="border: 1px solid black;">
Edit Vehicle: @Model?.VIN
</h3>
</div>
<form asp-action="Edit">
<div class="container">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="row">
<div class="col-2"></div>
<div class="col-4">
<div class="mb-3">
<label asp-for="Status" class="form-label fw-bold"></label>
<Select asp-for="Status" class="form-select">
<option id=""><-- Select One --></></option>
<option id="0">New</option>
<option id="1">Used</option>
</Select>
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Year" class="form-label fw-bold"></label>
<input asp-for="Year" class="form-control" />
<span asp-validation-for="Year" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Make" class="form-label fw-bold"></label>
<input asp-for="Make" class="form-control" />
<span asp-validation-for="Make" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Color" class="form-label fw-bold"></label>
<input asp-for="Color" class="form-control" />
<span asp-validation-for="Color" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="ImagePath" class="form-label fw-bold"></label>
<br />
<span class="text-info-emphasis">
Format as:<br />
/images/[Cars|Trucks|Jeeps]/filename.ext
</span>
<input asp-for="ImagePath" class="form-control" />
<span asp-validation-for="ImagePath" class="text-danger"></span>
</div>
</div>
<div class="col-4">
<div class="mb-3">
<label asp-for="VehicleTypeId" class="form-label fw-bold"></label>
<Select asp-for="VehicleTypeId"
asp-items="@ViewBag.VehicleTypeList"
class="form-select">
<option id=""><-- Select One --></></option>
</Select>
<span asp-validation-for="VehicleTypeId" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Price" class="form-label fw-bold"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Model" class="form-label fw-bold"></label>
<input asp-for="Model" class="form-control" />
<span asp-validation-for="Model" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="VIN" class="form-label fw-bold"></label>
<input asp-for="VIN" class="form-control" />
<span asp-validation-for="VIN" class="text-danger"></span>
</div>
</div>
<div class="col-2"></div>
</div>
<div class="row">
<div class="col-2"></div>
<div class="col-4 text-center">
<div>
<input type="submit"
value="Edit"
class="btn btn-success" />
<a asp-controller="Home"
asp-action="Index"
class="btn btn-secondary">Cancel</a>
</div>
</div>
<div class="col-2"></div>
</div>
</div>
</form>
The code above is almost exactly the same as the Create form in the last module except for a few of differences.
Number one the light blue page header in the h3 heading element has literal text in the Create page that says, “Create a Vehicle”.
<h3 class="text-center bg-primary-subtle py-2"
style="border: 1px solid black;">
Create a Vehicle
</h3>

Here the Edit page’s h3 heading element contains the literal text, “Edit Vehicle:”, a blank white space, and then the VIN number of Model, the vehicle we are updating.
<h3 class="text-center bg-primary-subtle py-2"
style="border: 1px solid black;">
Edit Vehicle: @Model?.VIN
</h3>

In the code snippet above we transition into C# using the at sign (@) to access the Model (a Vehicle object) with the null-conditional operator (?) to check if the Vehicle is null before trying to access its VIN property.
@Model?.VIN
The null-conditional (Elvis) operator
In C#, the null-conditional operator (often referred to as the “Elvis operator” because of its appearance ?.) is a powerful tool for safely accessing members (properties, methods, etc.) or elements of an object that might be null, without risking a NullReferenceException.
- Safe Access: When you use
?.before accessing a member or?[]before accessing an element, the operation is only performed if the object on the left side of the operator is not null. - Short-Circuiting: If the object is null, the entire expression using the null-conditional operator short-circuits (stops evaluating) and returns
null. This avoids the dreadedNullReferenceExceptionthat would occur if you attempted to access a member of a null object.
So, nstead of writing verbose null checks like this:
string name = null;
if (customer != null && customer.Address != null)
{
name = customer.Address.StreetName;
}
You can use the null-conditional operator:
var name = customer?.Address?.StreetName;
If customer or customer.Address is null, name will be assigned null.
Using a hidden input form control
The second difference between the Create and Edit razor code is that in Edit we have added an input control of type hidden using a built in tag helper with the asp-for tag helper attribute having a value of Id to signify this control is bound to the Id property of a Vehicle.
<input type="hidden" asp-for="Id" />
The Id of an object being updated isn’t usually shown to the user and that is why we use a hidden field but, it still needs to be in the form post along with all the other form data in order for the Post Edit action method to know which vehicle to fetch and use for the updates.
Let’s test it out. Navigate to https://localhost:40443/ and click the details link for one of the vehicles.

From the Details page click the Edit link at the bottom of the page.

Then on the Edit page try changing any combination of the fields and click the Edit button. Remember the original values so you can change them back.

Once you click the Edit button the changes are saved and you are redirected back to the Home/Index page where you can see the change on most fields and see the changes to all properties on the Details page.
One last difference here between Create and Edit is that the Create button uses the btn-primary Bootstrap class which makes the button blue and the Edit button uses the btn-success Bootstrap class which makes the button Green.
Add an Edit link button to Vehicle rows on the Home/Index page
A user can now get to the Edit page for any vehicle albeit in a roundabout way with multiple clicks. It would be nice though if we could get there in one click. In this section we are going to add an Edit button/icon link next to the details link in each Vehicle row in the results table on the Home/Index page.
Add a CDN link to the Bootstrap icons library
To save room, rather than use another text link with the text, “Edit”, next to the “Details” text link, we are going to use an icon that most users associate with an Edit type of action, a pencil.
In module 7, we installed the client-side Bootstrap package to use Bootstrap CSS classes in order to give our web pages a consistent look and feel and to be standardized across all modern browsers.
Here we are going to use an icon from the Bootstrap-icons library. Rather then install the package we are going to go a different route here and use a CDN link to the library.
A CDN (Content Delivery Network) is a geographically distributed network of servers that delivers content (such as JavaScript libraries, CSS files, fonts, images, etc.) to users fast and reliably, by serving the resources from servers closest to the user.
First modify the layout file by adding the CDN link to the header tag.
FredsCars\Views\Shared_Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-ixwidth" />
<title>Fred's Cars - @ViewBag.Title</title>
<link href="~/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css" rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
</head>
... existing code ...
The new link in the above code snippet points to the jsdelivr CDN to deliver the bootstrap-icons library as opposed to the Bootstrap package which we downloaded using library manager (libman) from the cdnjs CDN.
Now modify the _VehicleTableRowResult.cshtml partial view with the following code.
FredsCars\Views\Home_VehicleTableRowResult.cshtml
@model Vehicle
<tr>
<td>
<a asp-controller="Vehicles"
asp-action="Details"
asp-route-id="@Model.Id">
<img src="@Model.ImagePath"
class="result-image" />Details</a>
<a asp-controller="Vehicles"
asp-action="Edit"
asp-route-id="@Model.Id">
<i class="bi bi-pencil text-success"></i></a>
</td>
... existing code ...
In the above snippet we added an i tag and dressed it up as a pencil icon using the bootstrap-icon classes. We used the bi class to specify we are using a bootstrap-icon on this tag, and bi-pencil to specify which icon, in this case the pencil icon. We also assign the text-success class to make the icon button green.
Restart the application and navigate to https://localhost:40443/ in your browser.
The new pencil icon should appear to the right of the Details text link for each Vehicle row and act as a link to the Edit page for each specific vehicle.

Manually test the DbUpdateException error
I want to go back to the controller for a moment and manually test the DbUpdateException error we catch and throw if there is a problem updating a vehicle and test out a piece of the corresponding front end validation.
Modify the try block in the EditPost action method with the following code.
FredsCars\Controllers\VehiclesController.cs
... existing code...
try
{
throw new DbUpdateException("500 error on vehicle update.");
await _vehicleRepo.UpdateAsync(vehicle!);
return RedirectToAction("Index", "Home");
}
... existing code...
In the code snippet above we added a line to intentionally throw an exception before we try to update the Vehicle in the try block. We do this using the C# throw keyword followed by a new DbUpdateException since that is the specific type of Exception we want to test in the Catch block. And we pass the string message “500 error on vehicle update.” to the DbUpdateException's constructor.
A 500 type of error would indicate some kind of server error. We will explore other types of errors like 404 – not found errors when we dig more into error handling in a later module.
Recall that in the catch block where we catch and handle DbUpdateException errors we add add a model error to the ModelState with the string.
“There was a problem updating the vehicle. Please contact your system administrator.“
... existing code ...
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." );
}
... existing code ...
Now restart the application and navigate to the update page for a vehicle and click the Edit button. Your browser should look similar to the results below with the ModeState error we added at the top of the page.

The ModelState error is rendered in the Edit.cshtml Razor file by the ValidationSummary tag helper using a div element.
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
The tag helper attribute asp-validation-summary is set to the value ModelOnly so it will only show errors related to the model and not a specific property as opposed to the property specific ValidationMessage tag helpers we use for each form control.
<div class="mb-3">
<label asp-for="Make" class="form-label fw-bold"></label>
<input asp-for="Make" class="form-control" />
<span asp-validation-for="Make" class="text-danger"></span>
</div>
Let’s return the application to its normal working state and comment out the line that throws the test Exception in the try block.
FredsCars\Controllers\VehiclesController.cs
... existing code ...
try
{
// TEST DbUpdateException
// throw new DbUpdateException("500 error on vehicle update.");
await _vehicleRepo.UpdateAsync(vehicle!);
return RedirectToAction("Index", "Home");
}
... existing code ...
This section serves as a good introduction to error handling. But honestly, in enterprise application development, we mostly just let errors bubble up to the top and log them. Bubbling up refers to called code that can be several layers deep that throw an exception. Control of execution then goes to the calling code which will also throw an exception. The calling code will have its own exception message while the called code’s exception message will be stored in an InnerMessage property. Again this can be multiple layers deep.
We will come back to error handling in a later module and set up a global wide error handling system for the web application. But for now let’s finish up this module with some unit testing.
Unit Testing
Unit Test the vehicle repo update
Add the following test to the EFVehicleRepositoryTests class.
FredsCars.Tests\Models\Repositories\EFVehicleRepositoryTests.cs
[Fact]
public async Task Can_Update_Vehicle()
{
// Arrange
var options =
new DbContextOptionsBuilder<FredsCarsDbContext>()
.UseInMemoryDatabase($"FredCars-{Guid.NewGuid().ToString()}")
.Options;
using var context = new FredsCarsDbContext(options);
var repo = new EFVehicleRepository(context);
Vehicle vehicle = new Vehicle
{
Status = 0,
Year = "2025",
Make = "Make",
Model = "Gladiator Rubicon 4 X 4",
Color = "Joose",
Price = 64125,
VIN = "1C6RJTBG0SL532163",
VehicleTypeId = 3,
ImagePath = "/images/jeeps/jeep5.jpg"
};
// Arrange - create vehicle: SQL Server assigns an ID to vehicle.
await repo.CreateAsync(vehicle);
// Arrange - get vehicle to update
Vehicle? vehicleToUpdate =
await repo.Vehicles.FirstOrDefaultAsync(v => v.Id == vehicle.Id);
// Arrange - change vehicle's price
vehicleToUpdate!.Price = 58345.00M;
// Act
await repo.UpdateAsync(vehicleToUpdate);
// Assert
// - get updated vehicle
Vehicle? updatedVehicle =
await repo.Vehicles.FirstOrDefaultAsync(v => v.Id == vehicle.Id);
Assert.Equal(58345.00M, updatedVehicle!.Price);
}
In the code above we are testing that the Vehicle Repository can indeed update a vehicle.
The first portion of the test in the Arrange section creates a vehicle almost identically to the Can_Create_Vehicle test except that the repo.CreateAsync statement is also considered part of the Arrange section since we need an existing vehicle in the database in order to test the update functionality.
SQL Server assigns an ID to the vehicle we just inserted into the database so the object stored in the variable named vehicle now has an ID of 1 as the first vehicle in the database.
In the final two Arrange statements we fetch the vehicle we want to update using the LINQ FirstOrDefaultAsync method of the Vehicles DbSet/IQueryable property where the first vehicle’s Id in the DbSet matches that of the vehicle stored in our local vehicle object variable and change the fetched vehicles price.
// Arrange - get vehicle to update
Vehicle? vehicleToUpdate =
await repo.Vehicles.FirstOrDefaultAsync(v => v.Id == vehicle.Id);
// Arrange - change vehicle's price
vehicleToUpdate!.Price = 58345.00M;
The Act portion of the test is very simple but also the crux of the test. We just call the UpdateAsync method of the vehicle repo and pass it the vehicle to update.
// Act
await repo.UpdateAsync(vehicleToUpdate);
Finally, in the Assert section, we fetch the vehicle we just updated into a variable named updatedVehicle from the database and verify it’s price matches that in the local vehicle object named vehicleToUpdate.
// Assert
// - get updated vehicle
Vehicle? updatedVehicle =
await repo.Vehicles.FirstOrDefaultAsync(v => v.Id == vehicle.Id);
Assert.Equal(58345.00M, updatedVehicle!.Price);
Unit Test the Post Edit action
The EditPost action method takes in an integer as a parameter but the TryUpdateModelAsync method it uses relies on ASP.Net model binding and gets the Vehicle property values to use in the update from the request body. To simulate sending post data in the body of a request we need to set up Integration testing.
Install the Microsoft.AspNetCore.Mvc.Testing package.
To set up integration testing for the FredsCars.Tests project we need to install the Testing package.
Open a command prompt, navigate to the FredsCars.Tests project and run the following command.
dotnet add package Microsoft.AspNetCore.Mvc.Testing --version 9.0.6
The Microsoft.AspNetCore.Mvc.Testing package provides support for writing integration tests for ASP.NET Core apps that utilize MVC or Minimal APIs.
Setup Integration Testing
To set up integration testing we are going to create a custom web application factory.
Create the CustomWebApplicationFactory
Create a new folder called Infrastructure on the root of the FredsCars.Tests project. In the new Infrastructure folder create a class called CustomWebApplicationFactory and modify its contents with the code shown below.
FredsCars.Tests\Infrastructure\CustomWebApplicationFactory.cs
using FredsCars.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
// Required for services.RemoveAll
using Microsoft.Extensions.DependencyInjection.Extensions;
using FredsCars.Models.Repositories;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace FredsCars.Tests.Infrastructure
{
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
private readonly string _dbName;
public CustomWebApplicationFactory(string dbName)
{
_dbName = dbName;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
// Force a test-specific environment
// -- to prevent seeding routing from kicking off in Program.cs
builder.UseEnvironment("Test");
builder.ConfigureServices(services =>
{
// Remove any existing DbContextOptions
services.RemoveAll<DbContextOptions<FredsCarsDbContext>>();
services.RemoveAll(typeof(IDbContextOptions));
// Remove any existing DbContext registration
services.RemoveAll<FredsCarsDbContext>();
// Add InMemory EF Core provider with its own internal service (data) provider
// -- Force EF Core to build its own data provider
services.AddDbContext<FredsCarsDbContext>((serviceProvider, options) =>
{
options.UseInMemoryDatabase(_dbName);
});
// register repos that depend on shared dbContext
services.AddScoped<IVehicleRepository, EFVehicleRepository>();
services.AddScoped<IVehicleTypeRepository, EFVehicleTypeRepository>();
});
}
}
}
The actual unit test for the EditPost action method we are going to write in a later section is going to use the CustomWebApplicationFactory class shown above to create an HttpClient to use in order to send up form post data with a request.
Let’s break this code down. First, the CustomWebApplicationFactory class inherits WebApplicationFactory<T>, or in this case WebApplicationFactory<Program>.
The WebApplicationFactory class is a factory for bootstrapping an application in memory for end to end tests. In our case, it will bootstrap the FredsCars web application into memory.
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
The WebApplicationFactory class lives in the Microsoft.AspNetCore.Mvc.Testing namespace of the package we installed earlier.
The constructor takes in the name of an in-memory database and assigns it to a private string variable named _dbName. The trick here is to get the CustomWebApplicationFactory and the unit test to share the same in-memory database. They will both create their own DbContext but with the same options. One, to use an in-memory database. And two to both look for the in-memory database with the same name. Remember, a DbContext is a bridge from C# code to a database. So the WebApplicationFactory and the Unit test can each have their own separate context pointing to the same in-memory db.
private readonly string _dbName;
public CustomWebApplicationFactory(string dbName)
{
_dbName = dbName;
}
The bulk of the custom web app factory is in the ConfigureWebHost method of the WebApplicationFactory<T> base class which we override. ConfigureWebHost takes in a variable named builder of type IWebHostBuilder used to build up an instance of and add services to the FredsCars web application bootstrapped in memory for testing. This is similar to how Program.cs uses a WebApplicationBuilder to build up the real web application in runtime.
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
...
}
The first line in the ConfigureWebHost method sets the Environment to Test. This is so that the bootstrapped in-memory version of the FredsCars web application will not run the seeding routine in Program.cs (which only runs when the environment is Development). We want to seed test data here from within the CustomWebApplicationFactory.
// Force a test-specific environment
// -- to prevent seeding routing from kicking off in Program.cs
builder.UseEnvironment("Test");
Next we call the IWebHostBuiler‘s ConfigureServices method and pass to it a Lambda expression. The parameter that goes into the expression named services is the IServiceCollection or DI container.
Here we want to add an instance of a FredsCarsDbContext object (pointing to the same in-memory database as the unit test and finding it by _dbName passed into the class constructor by the unit test) and two scoped services: IVehicleRepository and IVehicleTypeRepository.
builder.ConfigureServices(services =>
{
... existing code ...
// Add InMemory EF Core provider with its own internal service (data) provider
// -- Force EF Core to build its own data provider
services.AddDbContext<FredsCarsDbContext>((serviceProvider, options) =>
{
options.UseInMemoryDatabase(_dbName);
});
// register repos that depend on shared dbContext
services.AddScoped<IVehicleRepository, EFVehicleRepository>();
services.AddScoped<IVehicleTypeRepository, EFVehicleTypeRepository>();
});
}
But first we need to remove any stagnant objects left around by EF Core. EF Core has some issues with integration testing where DbContextOptions and/or DbContext objects can be left hanging around in memory.
builder.ConfigureServices(services =>
{
// Remove any existing DbContextOptions
services.RemoveAll<DbContextOptions<FredsCarsDbContext>>();
// Remove any existing DbContext registration
services.RemoveAll<FredsCarsDbContext>();
services.RemoveAll(typeof(IDbContextOptions));
... existing code -> register services ...
});
Note again our CustomWebApplicationFactory inherits from WebApplicationFactory<T> which again is a factory for bootstrapping an application in memory for functional end-to-end tests.
We actually use the Program class as Type of T for the base WebApplicationFactory here:
public class CustomWebApplicationFactory : WebApplicationFactory<Program>
This is saying we want to bootstrap the Program class from the Program.cs file in the FredsCars web application. But if you open up Program.cs and inspect it you will see that no Program class actually exist. As a matter of fact you won’t see any class defined at all. How can this be? All statements must be in some kind of class right? Well, Program.cs is using a C# feature called Top Level statements here and we are going to talk about that next.
Top level statements
A feature called top level statements was introduced in .Net 5. Top-level statements is a C# feature that lets you write programs and web applications without needing to explicitly define a Main method or a containing class for simple applications. The Main method in an application has long been considered the “entry point” for many programming languages.
.NET 6 projects adopted top-level statements by default, especially in Program.cs where it is often referred to as the minimal hosting model popular in developing minimal APIs.
This removed the need for CreateHostBuilder, Startup.cs, etc., in ASP.Net Core web project templates making the Program.cs file much more clean and readable.
Define the Program class
Because Program.cs uses top-level statements, and there is no actual Program class defined, we need to define an actual Program class at the end of the Program.cs file. Otherwise the base class for our custom web application factory, WebApplicationFactory<Program>, will throw an error complaining that no Program class can be found.
Make the following modifications to the Program.cs file.
FredsCars\FredsCars\Program.cs
... existing code ...
/*** Add endpoints for controller actions and
the default route ***/
app.MapDefaultControllerRoute();
// Log chosen route to console
app.Use(async (context, next) =>
{
var endpoint = context.GetEndpoint();
var routeNameMetadata = endpoint?.Metadata.GetMetadata<RouteNameMetadata>();
var routeName = routeNameMetadata?.RouteName;
if (endpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine($"Matched route: {routeEndpoint.RoutePattern.RawText}");
Console.WriteLine($"RouteName: {routeName}");
}
await next();
});
/*** Seed the database ***/
if (builder.Environment.IsDevelopment())
{
// Alternative C# 8 using declaration.
using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider
.GetRequiredService<FredsCarsDbContext>();
SeedData.Initialize(context);
}
app.Run();
// Needed for Test Server
// and Integration Tests
public partial class Program { }
Ensure the bootstrapped web app has only one data provider
There is one more modification we need to make in Program.cs. In our custom web application factory, we are telling the bootstrapped app to use an in-memory database and there for an in-memory data provider.
// Add InMemory EF Core provider with its own internal service (data) provider
// -- Force EF Core to build its own data provider
services.AddDbContext<FredsCarsDbContext>((serviceProvider, options) =>
{
options.UseInMemoryDatabase(_dbName);
});
But the bootstrapped app will also run through the setup in Program.cs and add a second provider for SQL Server.
builder.Services.AddDbContext<FredsCarsDbContext>(opts =>
{
opts.UseSqlServer(
builder.Configuration["ConnectionStrings:FredsCarsMvcConnection"]
);
});
Because of this the unit test will throw an exception complaining that the FredsCarsDbContext has two data providers registered.
To solve this we can tell Program.cs to add a FredsCarsDbContext with an SQL Server data provider to the Service Collection only if the environment is not test.
Make the following modifications to Program.cs in the code shown below.
C:\Development\FredsCars\MVC\Module24\FredsCars\FredsCars\Program.cs
... existing code ...
// Add Services
builder.Services.AddControllersWithViews();
if (!builder.Environment.IsEnvironment("Test"))
{
builder.Services.AddDbContext<FredsCarsDbContext>(opts =>
{
opts.UseSqlServer(
builder.Configuration["ConnectionStrings:FredsCarsMvcConnection"]
);
});
}
builder.Services.AddScoped<IVehicleRepository, EFVehicleRepository>();
builder.Services.AddScoped<IVehicleTypeRepository, EFVehicleTypeRepository>();
var app = builder.Build();
... existing code ...
Write the unit test
Now that we have our custom web application factory in place to help us with integration testing, let’s add a unit test called Can_Update_Vehicle to the VehiclesControllerTests class. Modify the code in VehiclesControllerTests.cs with the following changes.
FredsCars.Tests\Controllers\VehiclesControllerTests.cs
using FredsCars.Controllers;
using FredsCars.Data;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MockQueryable;
using Moq;
using FredsCars.Tests.Infrastructure;
using System.Text.RegularExpressions;
namespace FredsCars.Tests.Controllers
{
public class VehiclesControllerTests
{
... existing code ...
[Fact]
public async Task Can_Update_Vehicle()
{
// Arrange - create in-memory database options for DbContext
string dbName = $"FredCars-{Guid.NewGuid().ToString()}";
var options = new DbContextOptionsBuilder<FredsCarsDbContext>()
.UseInMemoryDatabase(dbName)
.Options;
// Arrange - create DbContext
using (var context = new FredsCarsDbContext(options))
{
// Arrange - add test data to vehicle types repo
await context.VehicleTypes.AddRangeAsync(
new VehicleType
{
Id = 1,
Name = "Cars"
},
new VehicleType
{
Id = 2,
Name = "Trucks"
},
new VehicleType
{
Id = 3,
Name = "Jeeps"
}
);
// Arrange - add test data to vehicle repo
await context.Vehicles.AddRangeAsync(
new Vehicle
{
Id = 1,
Make = "Make1",
Model = "Model1",
VehicleTypeId = 1
},
new Vehicle
{
Id = 2,
Make = "Make2",
Model = "Model2",
VehicleTypeId = 2
},
new Vehicle
{
Id = 3,
Make = "Make3",
Model = "Model3",
VehicleTypeId = 3
}
);
await context.SaveChangesAsync();
}
var factory = new CustomWebApplicationFactory(dbName);
HttpClient client = factory.CreateClient();
// Need to get anti forgery token
// -- Simulate a full round-trip
// Step 1: GET Edit Form to Retrieve Token and Cookie
var getResponse = await client.GetAsync("/Vehicles/Edit/2");
var getContent = await getResponse.Content.ReadAsStringAsync();
// Step 2:Extract the Anti-Forgery Token from the HTML
// -- Use Regex to extract the hidden __RequestVerificationToken value
var match = Regex.Match(getContent,
@"<input name=""__RequestVerificationToken"" type=""hidden"" value=""([^""]+)""");
Assert.True(match.Success, "Anti-forgery token not found in the form.");
var token = match.Groups[1].Value;
// Step 3: Add token to form post
// Arrange - create form data
var formData = new Dictionary<string, string>
{
{ "__RequestVerificationToken", token },
{"Status", "0" },
{ "Year", "2025" },
{ "Make", "Ford" },
{ "Model", "Escape" },
{ "Color", "Blue" },
{ "Price", "48995.34" },
{ "VIN", "1C6RJTBG0SL532163" },
{ "ImagePath", "/images/cars/car2.jpg" },
{ "VehicleTypeId", "1" }
};
var postBody = new FormUrlEncodedContent(formData);
// Step 4: Copy the Cookie from GET to POST
// -- contains the anti-forgery cookie
client.DefaultRequestHeaders.Add("Cookie",
string.Join("; ", getResponse.Headers
.Where(h => h.Key == "Set-Cookie")
.SelectMany(h => h.Value)
.Select(c => c.Split(';')[0])));
// Act
// Step 5: send the post
var response =
await client.PostAsync("/Vehicles/Edit/2", postBody);
// Assert
using (var context = new FredsCarsDbContext(options))
{
// get fresh entity to verify update
var updatedVehicle = context.Vehicles.Find(2);
Assert.Equal("1C6RJTBG0SL532163", context.Vehicles.Find(2)?.VIN);
}
}
}
}
In the unit test code above we have quite a bit of setup work in the Arrange section.
The first thing we do is define the in-memory database options. Using string interpolation we create a unique name for the in-memory db by using the Guid object’s NewGuid method, convert the guid to a string, and add it to the end of the literal-string “FredsCars-“. We assign the result to a variable named dbName of type string.
We then pass the unique string name in dbName to the UseInMemoryDatabase method of a DbContextOptionsBuilder<T>, where T is of type FredsCarsDbContext, and assign the result in the Options property of the builder to a variable named options.
// Arrange - create in-memory database options for DbContext
string dbName = $"FredCars-{Guid.NewGuid().ToString()}";
var options = new DbContextOptionsBuilder<FredsCarsDbContext>()
.UseInMemoryDatabase(dbName)
.Options;
In C#, a Guid (short for Globally Unique Identifier) is a 128-bit (16-byte) value used to uniquely identify something—such as objects, database entries, sessions, or anything else where uniqueness is important.
A Guid looks something like: 6a8d9c44-845a-438c-a285-73a9845ff6b5
So the value in DbName will look something like:FredCars-6a8d9c44-845a-438c-a285-73a9845ff6b5
This unique name is what enables us to create two DbContexts, one for the CustomWebFactory object, and one for the unit test itself, which both point to the same in-memory db as discussed when we created the CustomWebFactory class earlier.
The next part of the Arrange section is to create and seed a new FredsCarsDbContext using the DbContextOptions we created above with the unique name for our in-memory db in a using block.
// Arrange - create DbContext
using (var context = new FredsCarsDbContext(options))
{
// Arrange - add test data to vehicle types repo
await context.VehicleTypes.AddRangeAsync(
new VehicleType
{
Id = 1,
Name = "Cars"
},
new VehicleType
{
Id = 2,
Name = "Trucks"
},
new VehicleType
{
Id = 3,
Name = "Jeeps"
}
);
// Arrange - add test data to vehicle repo
await context.Vehicles.AddRangeAsync(
new Vehicle
{
Id = 1,
Make = "Make1",
Model = "Model1",
VehicleTypeId = 1
},
new Vehicle
{
Id = 2,
Make = "Make2",
Model = "Model2",
VehicleTypeId = 2
},
new Vehicle
{
Id = 3,
Make = "Make3",
Model = "Model3",
VehicleTypeId = 3
}
);
await context.SaveChangesAsync();
}
In the code above we also seed the VehicleTypes and Vehicles DbSets of the context and save the changes with the DbContext.SaveChagesAsync method. We need to seed the VehicleTypes first to satisfy the VehicleType navigation property of each Vehicle or the TryUpdateModel pattern in the Vehicles controller will fail during the unit test.
The next step is where our CustomWebApplicationFactory comes in handy in order to create an HttpClient for our unit test so we can simulate making requests to the controller.
var factory = new CustomWebApplicationFactory(dbName);
HttpClient client = factory.CreateClient();
In the snippet above we pass the unique dbName to the constructor of the CustomWebApplicationFactory and assign the resulting object to a variable named factory. Next we use the base class’ WebApplication.CreateClient method to create the HttpClient and store it in a variable named client.
The next part of the Arrange section starts a routine to get an anti forgery token which we will need to send up with the form post data of the POST Edit request in order to satisfy the ValidateAntiForgeryToken attribute the action method in the controller is marked with.
Step 1 of this routine is to make a GET request to the Edit action in order to retrieve the Edit form so we can pick out the the token value in the __RequestVerificationToken hidden field of the form.
// Need to get anti forgery token
// -- Simulate a full round-trip
// Step 1: GET Edit Form to Retrieve Token and Cookie
var getResponse = await client.GetAsync("/Vehicles/Edit/2");
var getContent = await getResponse.Content.ReadAsStringAsync();
In the snippet above we use the HttpClient.GetAsync method to make a GET request call to the following route: /Vehicles/Edit/2 which will go to GET Edit action method of the Vehicles controller and pass the value of 2 to the int id parameter of the method. The result is an HttpResponseMessage we store in a variable named getResponse.
We then use the HttpResponseMessage.Content.ReadAsStringAsync method to read the response’s HTML which includes the edit form with the __RequestVerificationToken hidden field and assign the resulting string to a variable named getContent.
In Step 2 we actually extract the token from the HTML using a regular expression.
// Step 2:Extract the Anti-Forgery Token from the HTML
// -- Use Regex to extract the hidden __RequestVerificationToken value
var match = Regex.Match(getContent,
@"<input name=""__RequestVerificationToken"" type=""hidden"" value=""([^""]+)""");
Assert.True(match.Success, "Anti-forgery token not found in the form.");
var token = match.Groups[1].Value;
In the code snippet above, we use the C# Regex object’s Match method to try to find a match for a hidden input field named __RequestVerificationToken with a guid value. The Match method returns a Match object with information about the match we assign to a variable named match. Then we assert that the match was successful. If the assert statement fails it will throw an exception with the message, “Anti-forgery token not found in the form.” Otherwise, we assign the match’s first and only group value or matched string to a string variable named token.
In programming, a regular expression (regex) is a sequence of characters that defines a search pattern. Regular expressions are used for pattern matching within strings. This means you can search for, extract, replace, or validate portions of text based on specific patterns.
In C#, the Regex class is part of the System.Text.RegularExpressions namespace and provides powerful functionality for working with regular expressions.
In step 3 we create a FormUrlEncodedContent object to hold the name/value pairs we will send up as simulated form field data. First we create a Dictionary<TKey, TValue> where TKey is a string holding the key of a key/value pair and TValue is a string holding the value of a key/value pair. A Dictionary is simply a collection like a List<T> but holds name/value pairs instead of indexed values. We assign the Dictionary to a variable named formData and pass it to the constructor of the new FormUrlEncodedContent object stored in a variable named postBody.
// Step 3: Add token to form post
// Arrange - create form data
var formData = new Dictionary<string, string>
{
{ "__RequestVerificationToken", token },
{"Status", "0" },
{ "Year", "2025" },
{ "Make", "Ford" },
{ "Model", "Escape" },
{ "Color", "Blue" },
{ "Price", "48995.34" },
{ "VIN", "1C6RJTBG0SL532163" },
{ "ImagePath", "/images/cars/car2.jpg" },
{ "VehicleTypeId", "1" }
};
var postBody = new FormUrlEncodedContent(formData);
The anti forgery token we send up with the POST request in the form data also needs to match the same value stored in the request’s cookie or the ValidateAntiForgeryToken's CSRF check will fail and the POST Edit action will not fire.
The Cookie header in HTTP requests is used to send stored cookies from the client (usually a browser) to the server. These cookies are typically set by the server using the Set-Cookie header in its responses. The Cookie header allows the server to maintain session information, user preferences, or other stateful data across multiple requests.
Because of this step 4 needs to copy the cookie from the Get response we just captured in Step 1 to the Post request.
// Step 4: Copy the Cookie from GET to POST
// -- contains the anti-forgery cookie
client.DefaultRequestHeaders.Add("Cookie",
string.Join("; ", getResponse.Headers
.Where(h => h.Key == "Set-Cookie")
.SelectMany(h => h.Value)
.Select(c => c.Split(';')[0])));
In the snippet above we are setting a default header of the HttpClient object called “Cookie” to the value of the “Set-Cookie” header we just received in the GET response from step 1.
Similarly to how HttpClient.GetAsync sends a GET request as in Step 1, the HttpClient.PostAsync method sends a POST request. Step 5 is our Act statement where we use PostAsync to send a post request to the POST Edit action in the vehicles controller and strore the response, an HttpResponseMessage object, in a variable named response.
// Act
// Step 5: send the post
var response =
await client.PostAsync("/Vehicles/Edit/2", postBody);
In the above snippet we pass the POST Edit route as the first parameter with 2 as the int id value for the Post Edit action method and the formData stored as a FormUrlEncodedContent object as the second parameter.
Finally, the last step is our actual assert. Here, we use a new instance of the FredsCarsDbContext service in a using block (so that it will be disposed of properly when we are done with it) to first fetch a fresh copy of a Vehicle with an Id of 2 from the in-memory db and assert that the updated VIN number in the db actually matches the VIN number we sent up with the update.
// Assert
using (var context = new FredsCarsDbContext(options))
{
// get fresh entity to verify update
var updatedVehicle = context.Vehicles.Find(2);
Assert.Equal("1C6RJTBG0SL532163", context.Vehicles.Find(2)?.VIN);
}
You should always get a fresh copy of an object you are working with or need to verify a fact about in an assert statement in a unit test or your application code for that matter.
Sometimes writing a unit test can be a brutal, brittle process to figure out and it takes some patience, trial, and error to figure out. Here, because of the TryUpdateModel pattern in the Post Edit action, we needed to dig into integration testing. But, in the end, it’s nice to know that our code is tested and verified.
What’s Next
We covered a lost of information in this module. The basics were a lot like creating the Create feature. But here we dove into integration testing as well and learned some more about C# along the way about Top Level Statements and the null-conditional/Elvis (?.) operator.
So far we have created three out four of the needed CRUD features.
- CREATE
- RETREIVE
- UPDATE
In the next module we are going to create the DELETE feature of the web application.
