Skip to content
  • iImagine
  • Register
  • Log In

Web Development School

Learning made easy.

  • Books
    • Beginning Web Development with ASP.Net Core & Client-Side Technologies
      • TOC
      • Part 1
        • Chapter 1: Static HTML – Designing the landing page
      • Part 2
        • Chapter 2: ASP.Net Core – Let’s talk Dynamic
        • Chapter 3: Introduction to ASP.Net Core MVC
          [ASP.Net Core v9]
      • Part 4
        • Chapter 7: Using Server Side & Client Side technologies together
          [ASP.Net Core v7 & Angular 15]
  • Environment Setup
    • Installing Angular
    • Installing Visual Studio 2022
    • Installing SQL Server 2022 Express
    • Installing Postman
    • Installing Git for Windows
  • Blog
  • iImagine WebSolutions
  • Events
  • Learning Videos
  • Toggle search form

Create the Delete Page

In the last module we created the Update feature. In this module we will create our last needed piece of CRUD functionality; DELETE.

Table Of Contents
  1. Extend the Vehicle Repository
    • Modify the interface
    • Modify the Implementation
      • Implement the read-first approach
      • Implement the create – and – attach approach
      • Implement the no-reads-at-all approach
  2. Add Delete action methods to the controller
    • HttpGet Delete
    • HttpPost Delete
  3. Create the View
  4. Add Delete link buttons to vehicle results
  5. Unit Testing
    • Install the Microsoft.EntityFrameworkCore.Sqlite package
    • Create a SqliteInMemoryDbContextFactory
    • Unit test the Vehicle Repository
    • Unit test the Vehicles controller
  6. What's Next

Extend the Vehicle Repository

As usual the first thing we need to do is extend the Vehicle Repository.

Modify the interface

Modify the IVehicleRepository interface with the code 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);
        Task DeleteAsync(int id);
    }
}

Here we have simply added another method definition called DeleteAsync which returns a Task rather then void to support its being asynchronous. But where the other two methods take in a Vehicle object, DeleteAsync takes in an int parameter named id. The id is all we will need to delete a Vehicle as we will see.

Modify the Implementation

We are going to look at three ways to implement the DeleteAsync implementation.

The first will be the same way we implemented UpdateAsync called the read-first approach. The second will be a method called the create-and-attach approach. The third will be the no-reads-at-all approach.

Implement the read-first approach

Modify the EFVehicleRepository class 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;
        }

        ... existing code ...

        public async Task DeleteAsync(int id)
        {
            // DELETE: EF CORE read-first approach
            Vehicle? vehicle = await _context.Vehicles.FindAsync(id);

            if (vehicle != null)
            {
                _context.Vehicles.Remove(vehicle);
                await _context.SaveChangesAsync();
            }
        }
    }
}

In the code above, we once again use the DbContext's Vehicles DbSet/IQueryable property’s FindAsync method to take in the id of the parameter passed to the DeleteAsync action via ASP.Net Core model binding to fetch a Vehicle and store it in a variable named vehicle of type Vehicle?, or nullable Vehicle.

Keep in mind this method makes a request to the database.

Next, we create a null-check block using an if/then structure to remove the Vehicle from the database and save the changes.

if (vehicle != null)
{
    _context.Vehicles.Remove(vehicle);
    await _context.SaveChangesAsync();
}

In the above snippet, the EF Core Remove method begins tracking the Vehicle entity in the EntityState.DeletedState. The DbContext.SaveChangesAsync method actually saves the deletion to the database. This is the second hit to the database.

As you can see this approach takes two hits to the database to accomplish its task. Next, we will look at how to avoid this.

Implement the create – and – attach approach

If application performance is mission critical in your environment then hitting a database twice for each task may not be acceptable. To deal with this situation we will look at the “create-and-attach” approach in this section.

Modify the EFVehicleRepository class with the code shown below.

FredsCars\Models\Repositories\EFVehicleRepository.cs

using FredsCars.Data;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Models.Repositories
{
    public class EFVehicleRepository : IVehicleRepository
    {
        private FredsCarsDbContext _context;

        public EFVehicleRepository(FredsCarsDbContext context)
        {
            _context = context;
        }

        ... existing code ...

        public async Task DeleteAsync(int id)
        {
            // DELETE: EF CORE read-first approach
            //Vehicle? vehicle = await _context.Vehicles.FindAsync(id);

            //if (vehicle != null)
            //{
            //    _context.Vehicles.Remove(vehicle);
            //    await _context.SaveChangesAsync();
            //}

            // DELETE: EF CORE create - and - attach approach
            // --create vehicle and attach to EF Core Entity State/Changes tracker
            // -- -- very challenging to unit test 
            Vehicle vehicleToDelete = new Vehicle() { Id = id };
            _context.Entry(vehicleToDelete).State = EntityState.Deleted;

            await _context.SaveChangesAsync();
        }
    }
}

In the code above we have commented out the read-first approach and added code for the create-and-attach approach.

The first line creates a new Vehicle object and sets its Id to the incoming id of the action method. This is all EF Core needs to delete the Vehicle from the database. We do not have to set any other properties of the Vehicle object.

Vehicle vehicleToDelete = new Vehicle() { Id = id };

In the next line we set the Vehicle’s entity state to Deleted. This attaches the Vehicle entity to EF Core’s tracking system.

_context.Entry(vehicleToDelete).State = EntityState.Deleted;

Lastly, we save the changes to the database.

await _context.SaveChangesAsync();

Implement the no-reads-at-all approach

The create-and-attach approach in the last section improves the performance in mission critical or high traffic environments. However, this approach can prove to be challenging to unit test.

Here is the code again:

 Vehicle vehicleToDelete = new Vehicle() { Id = id };
 _context.Entry(vehicleToDelete).State = EntityState.Deleted;

await _context.SaveChangesAsync();

When we save the changes in runtime with SaveChangesAsync, the web application sends an SQL statement to SQL Server that looks like this:

DELETE FROM Vehicles WHERE Id = 123

But, the in-memory data provider in .Net Core does not run or use SQL. It uses object relational mapping.

Because of this we can run into exceptions in unit tests after creating Vehicle objects for test data and then trying to delete one by id like the following.

"An entity with the same key is already being tracked"

We can get around this little hiccup by using the ExecuteDeleteAsync method.

Modify the EFVehicleRepository class once again with the following code where we comment out the create-and-attach approach and add the no-reads-at-all approach.

FredsCars\Models\Repositories\EFVehicleRepository.cs

... existing code ...

        public async Task DeleteAsync(int id)
        {
            // DELETE: EF CORE read-first approach
            //Vehicle? vehicle = await _context.Vehicles.FindAsync(id);

            //if (vehicle != null)
            //{
            //    _context.Vehicles.Remove(vehicle);
            //    await _context.SaveChangesAsync();
            //}

            // DELETE: EF CORE create-and-attach approach
            // --create vehicle and attach to EF Core Entity State/Changes tracker
            // -- -- very challenging to unit test 
            //Vehicle vehicleToDelete = new Vehicle() { Id = id };
            //_context.Entry(vehicleToDelete).State = EntityState.Deleted;

            //await _context.SaveChangesAsync();

            // DELETE: EF CORE no-reads-at-all approach
            await _context.Vehicles.Where(v => v.Id == id)
                        .ExecuteDeleteAsync();
        }
... existing code ...

Here we find the vehicle we want to delete with a lambda expression but without sending a query to the database. The ExecuteDeleteAsync method then will delete database rows that match the LINQ query. The operation executes immediately against the database without being deferred until DbContext.SaveChanges is called.

Add Delete action methods to the controller

HttpGet Delete

Modify the VehiclesController class 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
    {
        private IVehicleRepository _vehicleRepo;
        private IVehicleTypeRepository _vehicleTypeRepo;

        public VehiclesController(IVehicleRepository vRepo,
            IVehicleTypeRepository vtRepo)
        {
            _vehicleRepo = vRepo;
            _vehicleTypeRepo = vtRepo;
        }

		... existing code ...
        
        public async Task<ViewResult> Delete(int id)
        {
            Vehicle? vehicle = await _vehicleRepo.Vehicles
                .Include(v => v.VehicleType)
                .FirstOrDefaultAsync(v => v.Id == id);

            return View(vehicle);
        }

    }
}

In the code above we added in the HTTPGet action method for the Delete feature. This is very simple code and should be old hat by now.

We simply fetch the vehicle we are dealing with by id from the Vehicles DbSet in the DbContext and pass the vehicle as the model to the view.

HttpPost Delete

Modify the VehiclesController class 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
    {
        private IVehicleRepository _vehicleRepo;
        private IVehicleTypeRepository _vehicleTypeRepo;

        public VehiclesController(IVehicleRepository vRepo,
            IVehicleTypeRepository vtRepo)
        {
            _vehicleRepo = vRepo;
            _vehicleTypeRepo = vtRepo;
        }

		... existing code ...
        
        public async Task<ViewResult> Delete(int id)
        {
            Vehicle? vehicle = await _vehicleRepo.Vehicles
                .Include(v => v.VehicleType)
                .FirstOrDefaultAsync(v => v.Id == id);

            return View(vehicle);
        }
		
	[HttpPost]
	[ActionName("Delete")]
	[ValidateAntiForgeryToken]
	public async Task<IActionResult> DeleteConfirmed(int id)
	{
		   await _vehicleRepo.DeleteAsync(id);
		   return RedirectToAction("Index", "Home");
	}
    }
}

In the HTTPPost version of the Delete action method shown above, we again had to name of the actual method to DeleteConfirmed rather than just Delete similar to what we did with the Edit methods to avoid a C# signature conflict error for two methods with the same name having the same parameter types list. Then we mark the method with an ActionName attribute to let ASP.Net Core know that when looking for the POST Delete method it should use DeleteConfirmed.

We also use the HttpPost and ValidateAntiForgeryToken attributes as for all of our Post methods.

As for the actual code in the body of the method, we really don’t have to do much here except let the repo do all the work which keeps our controller code nice and clean, tidy, and readable.

We simply call the repo’s DeleteAsync method passing the Id and let it do all of the work for us and then redirect to the Home/Index page.

We don’t have to worry about error handling here like we did in the Post Edit action which was just an example because we are going to come back in a later module and implement a global wide error handling system for the web application. So if an error occurs here it will log it and go to the error page of the website.

Create the View

In the Views/Vehicles folder of the FredsCars project, create a new Razor file named Delete.cshtml and modify it with the code shown below.

FredsCars\Views\Vehicles\Delete.cshtml

@model Vehicle

@{
    ViewData["Title"] = "Delete";
}

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Delete Vehicle: @Model?.VIN
    </h3>
    <h3 class="mt-5">Are you sure you want to delete this vehicle?</h3>

    <hr />
</div>

<div class="container">
    <div class="row">
        <div class="col-2"></div>
        <div class="col-4">
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Status)</b>
                <br />
                @Html.DisplayFor(model => model.Status)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Year)</b>
                <br />
                @Html.DisplayFor(model => model.Year)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Make)</b>
                <br />
                @Html.DisplayFor(model => model.Make)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Color)</b>
                <br />
                @Html.DisplayFor(model => model.Color)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.ImagePath)</b>
                <br />
                @Html.DisplayFor(model => model.ImagePath)
            </div>
        </div>
        <div class="col-4">
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.VehicleType)</b>
                <br />
                @Html.DisplayFor(model => model.VehicleType!.Name)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Price)</b>
                <br />
                @Html.DisplayFor(model => model.Price)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Model)</b>
                <br />
                @Html.DisplayFor(model => model.Model)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.VIN)</b>
                <br />
                @Html.DisplayFor(model => model.VIN)
            </div>
        </div>
        <div class="col-2"></div>
    </div>
</div>

<form asp-action="Delete" class="mt-3">
    <div class="container">
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4 text-center">
                <input type="hidden" asp-for="Id" />
                <input type="submit" value="Delete" class="btn btn-danger" />
                <a asp-controller="Home"
                   asp-action="Index"
                   class="btn btn-secondary">Cancel</a>
            </div>
            <div class="col-2"></div>
        </div>
    </div>
</form>

We have seen all of the pieces in the Razor Code above before but let’s review.

First, we declare that the view’s model type will be a Vehicle object and set the value of ViewData["Title"] to Delete for the layout to use.

@model Vehicle

@{
    ViewData["Title"] = "Delete";
}

The next block of code, a div element, creates a top blue subheader like we did for the Edit page with the VIN number of the Vehicle to delete but also adds an, “Are you sure…” message along with an hr element to separate the header area from the details of the Vehicle we are about to delete.

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Delete Vehicle: @Model?.VIN
    </h3>
    <h3 class="mt-5">Are you sure you want to delete this vehicle?</h3>

    <hr />
</div>

This will render like the screenshot below.

The next block of code is another div element which shows the details of the vehicle to delete.

<div class="container">
    <div class="row">
        <div class="col-2"></div>
        <div class="col-4">
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Status)</b>
                <br />
                @Html.DisplayFor(model => model.Status)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Year)</b>
                <br />
                @Html.DisplayFor(model => model.Year)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Make)</b>
                <br />
                @Html.DisplayFor(model => model.Make)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Color)</b>
                <br />
                @Html.DisplayFor(model => model.Color)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.ImagePath)</b>
                <br />
                @Html.DisplayFor(model => model.ImagePath)
            </div>
        </div>
        <div class="col-4">
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.VehicleType)</b>
                <br />
                @Html.DisplayFor(model => model.VehicleType!.Name)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Price)</b>
                <br />
                @Html.DisplayFor(model => model.Price)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Model)</b>
                <br />
                @Html.DisplayFor(model => model.Model)
            </div>
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.VIN)</b>
                <br />
                @Html.DisplayFor(model => model.VIN)
            </div>
        </div>
        <div class="col-2"></div>
    </div>
</div>

This is the same structure we used in the Edit Form to layout the form controls but here instead of form controls here we use the Html helper object’s DisplayNameFor and DisplayFor methods to layout the label and value of each of the Vehicle’s properties.

Details div structure – row of 2, 4, 4, 2 just like in the Edit form.

<div class="container">
    <div class="row">
        <div class="col-2"></div>
        <div class="col-4">
            ... div groups of label and value
        </div>
        <div class="col-4">
            ... div groups of label and value
        </div>
        <div class="col-2"></div>
    </div>
</div>

This will render as follows

The last part of the razor file is the actual form.

<form asp-action="Delete" class="mt-3">
    <div class="container">
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4 text-center">
                <input type="hidden" asp-for="Id" />
                <input type="submit" value="Delete" class="btn btn-danger" />
                <a asp-controller="Home"
                   asp-action="Index"
                   class="btn btn-secondary">Cancel</a>
            </div>
            <div class="col-2"></div>
        </div>
    </div>
</form>

In the above snippet we use a form tag helper to create the form. The tag helper’s asp-action attribute will direct the form request to the HttpPost Delete action method of the Vehicles controller when the user clicks the Delete button which is an input control of type submit. The form contains another input control of type hidden with its asp-for attribute set to the Id property of the model, our Vehicle to delete, to send up as form post data so the HttpPost Delete method will know which vehicle to delete. The Delete submit button also has the Bootstrap btn-danger class to give it a red appearance indicating the user should double check if they want to take this action.

The form block will render like the following screenshot.

Add Delete link buttons to vehicle results

Now let’s add the Delete icon buttons to the Vehicle results on the Home/Index page. Make the following changes in the _VehicleTableRowResult.cshtml partial view.

@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>
        <a asp-controller="Vehicles"
           asp-action="Delete"
           asp-route-id="@Model.Id">
            <i class="bi bi-trash text-danger"></i>
        </a>
    </td>
    
... existing code ...

Restart the application, navigate to https://localhost:40443, click the Jeeps Category button, and from there click the new Delete icon for the 2025 Jeep Gladiator Rubicon.

You’ll be taken to the new Delete page for the 2025 Jeep Gladiator Rubicon which will look like the following.

Click the Delete button at the bottom of the page and you will be redirected back to Home/Index or https://localhost:40443. Again click the Jeeps category button and you’ll no longer see the 2025 Jeep Gladiator Rubicon in the results.

Unit Testing

We left the Vehicle repository in a state using the no-reads-at-all approach to help with unit testing. But we still have a slight problem.

The .Net Core InMemory provider is not a relational database — it’s a lightweight object store designed for speed, not realism. It is not a relational database but stores C# objects directly in memory.

SQLite, on the other hand, translates LINQ to real SQL (though a limited dialect)

So, we are going to use the SQLite data provider and it’s version of an in-memory database to test the Vehicle Repository’s DeleteAsync method. The first thing we need to do is install the Microsoft.EntityFrameworkCore.Sqlite package.

Install the Microsoft.EntityFrameworkCore.Sqlite package

Navigate to the FredsCars.Tests project in a command window and run the following command.

 dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 9.0.6

Create a SqliteInMemoryDbContextFactory

To make it easier to work with an Sqlite in-memory database, we are going to first create a factory in which to create an Sqlite in-memory db.

In the Infrastructure folder of the FredsCars.Tests project, create a class called SqliteInMemoryDbContextFactory and modify it with the code shown below.

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using FredsCars.Data;

namespace FredsCars.Tests.Infrastructure
{
    public class SqliteInMemoryDbContextFactory : IDisposable
    {
        private readonly SqliteConnection _connection;

        public SqliteInMemoryDbContextFactory()
        {
            _connection = new SqliteConnection("DataSource=:memory:");
            _connection.Open();
        }

        public FredsCarsDbContext CreateContext()
        {
            var options = new DbContextOptionsBuilder<FredsCarsDbContext>()
                    .UseSqlite(_connection)
                    .EnableSensitiveDataLogging() // helps debug
                    .Options;

            var context = new FredsCarsDbContext(options);

            // Ensure schema is created just once
            context.Database.EnsureCreated();

            return context;
        }

        public void Dispose()
        {
            _connection.Close();
            _connection.Dispose();
        }
    }
}

In the code above SqliteInMemoryDbContextFactory implements IDisposable so that it can dispose of the SqlliteConnection when it is no longer needed. When the C# garbage collector disposes of the factory it will also remove the connection from memory.

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using FredsCars.Data;

namespace FredsCars.Tests.Infrastructure
{
    public class SqliteInMemoryDbContextFactory : IDisposable
    {
        private readonly SqliteConnection _connection;

        public SqliteInMemoryDbContextFactory()
        {
            _connection = new SqliteConnection("DataSource=:memory:");
            _connection.Open();
        }

        public FredsCarsDbContext CreateContext()
        {
            ... existing code ...
        }

        public void Dispose()
        {
            _connection.Close();
            _connection.Dispose();
        }
    }
}

The constructor of the factory sets a private field member named _connection to a new SqliteConnection with a connection string that specifies the location of the relational database will be in-memory rather then on an Sqlite Server. It also opens the connection, setting it to an open state using the SqliteConnection.Open method.

private readonly SqliteConnection _connection;

public SqliteInMemoryDbContextFactory()
{
    _connection = new SqliteConnection("DataSource=:memory:");
            _connection.Open();
}

The CreateContext method returns an instance of the FredsCarsDbContext via a DbContextOptionsBuilder which uses the connection set in the constructor pointing to the in-memory version of an Sqlite db.

The EnsureCreated method ensures the database is created. If not, it creates the database. And that the schema is created. If not it creates the tables specified by the DbSet entity classes in FredsCarsDbContext.

public FredsCarsDbContext CreateContext()
        {
            var options = new DbContextOptionsBuilder<FredsCarsDbContext>()
                    .UseSqlite(_connection)
                    .EnableSensitiveDataLogging() // helps debug
                    .Options;

            var context = new FredsCarsDbContext(options);

            // Ensure schema is created just once
            context.Database.EnsureCreated();

            return context;
        }

Unit test the Vehicle Repository

Modify the EFVehicleRepositoryTests class with the code shown below.

FredsCars.Tests\Models\Repositories\EFVehicleRepositoryTests.cs

using FredsCars.Data;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using FredsCars.Tests.Infrastructure;
using Microsoft.EntityFrameworkCore;
using MockQueryable.Moq;
using Moq;

namespace FredsCars.Tests.Models.Repositories
{
    public class EFVehicleRepositoryTests
    {
        #region class fields for tests that use SqliteInMemory db
        private readonly SqliteInMemoryDbContextFactory _factory;
        private readonly FredsCarsDbContext _context;
        private readonly EFVehicleRepository _repo;
        #endregion

        public EFVehicleRepositoryTests()
        {
            #region Setup for for tests that use SqliteInMemory db
            _factory = new SqliteInMemoryDbContextFactory();
            _context = _factory.CreateContext();
            _repo = new EFVehicleRepository(_context);
            #endregion
        }

       	... existing code ...

        [Fact]
        public async Task Can_Delete_Vehicle()
        {
            // Arrange
            #region Arrange - seed VehicleTypes test 
            await _context.VehicleTypes.AddRangeAsync(
                new VehicleType
                {
                    Id = 1,
                    Name = "Cars"
                },
                new VehicleType
                {
                    Id = 2,
                    Name = "Trucks"
                },
                new VehicleType
                {
                    Id = 3,
                    Name = "Jeeps"
                }
            );
            await _context.SaveChangesAsync();
            #endregion

            // Arrange - add vehicles and detach
            var v1 = new Vehicle
            {
                Status = Status.New,
                Year = "2025",
                Make = "Make",
                Model = "Gladiator Rubicon 4 X 4",
                Color = "Joose",
                Price = 64125,
                VIN = "1C6RJTBG0SL532163",
                VehicleTypeId = 3,
                ImagePath = "/images/jeeps/jeep5.jpg"
            };

            var v2 = new Vehicle
            {
                Status = Status.Used,
                Year = "2020",
                Make = "Ford",
                Model = "Escape",
                Color = "Joose",
                Price = 22999,
                VIN = "1FMCU0F63LUC25826",
                VehicleTypeId = 1,
                ImagePath = "/images/cars/car2.webp"
            };
            await _context.Vehicles.AddRangeAsync(v1, v2);
            await _context.SaveChangesAsync();
            
            // Check vehicles were created
            Assert.True(_repo.Vehicles.Count() == 2);

            // Act
            await _repo.DeleteAsync(v2.Id);

            // Assert
            Assert.Equal(1, _repo.Vehicles.Count());
            // - get remaining vehicle
            var vehicle =
                await _repo.Vehicles.FirstOrDefaultAsync(v => v.Id == v1.Id);
            Assert.Equal(v1.VIN, vehicle?.VIN);
        }
    }
}

In the code above we have added three new class level private variables to the top of the file. These will be used to hold a DbContext and Vehicle repo based on a Sqlite in-memory db for any unit tests that want to use it and the setup for this is done in the class constructor.

The setup routine in the constructor creates an instance of the SqliteInMemoryDbContextFactory class and stores in the private class variable named _factory. We call the factory’s CreateContext method to retrieve an instance of the FredsCarsDbContext service now pointing to an in-memory Sqlite database and store it in the class level private variable named _context.

Finally, we create an instance of the EFVehicleRepository by passing the FredsCarsDbContext instance to its constructor.

... existing code ...

public class EFVehicleRepositoryTests
{
    #region class fields for tests that use SqliteInMemory db
    private readonly SqliteInMemoryDbContextFactory _factory;
    private readonly FredsCarsDbContext _context;
    private readonly EFVehicleRepository _repo;
    #endregion

    public EFVehicleRepositoryTests()
    {
        #region Setup for for tests that use SqliteInMemory db
        _factory = new SqliteInMemoryDbContextFactory();
        _context = _factory.CreateContext();
        _repo = new EFVehicleRepository(_context);
        #endregion
    }
    
... existing code ...

We then add a unit test to the bottom of the class file to test the EFVehicleRepository class’s DeleteAsync method and name the test Can_Delete_Vehilce.

We start off with some familiar code by seeding VehicleTypes and Vehicles test data using the DbContext.VehicleTypes.AddRangeAsync method and save the changes with DbContext.SaveChangesAsync.

At this point we stop and Assert that there are indeed two Vehicles in the database.

// Check vehicles were created
Assert.True(_repo.Vehicles.Count() == 2);

In the Act section we call the the repo’s DeleteAsync method passing it the id of the Vehicle stored in v2 which should have the VIN# value of “1FMCU0F63LUC25826”.

// Act
await _repo.DeleteAsync(v2.Id);

In the Assert section we verify that there is only one Vehicle left in the Vehicles DbSet, fetch the remaining Vehicle whose Id should match that stored in v1 where the VIN# value should be “1C6RJTBG0SL532163”, and verify that the VIN in the remaining Vehicle just fetched and stored in vehicle should match that of v1.

 // Assert
 Assert.Equal(1, _repo.Vehicles.Count());
 // - get remaining vehicle
 var vehicle =
     await _repo.Vehicles.FirstOrDefaultAsync(v => v.Id == v1.Id);
 Assert.Equal(v1.VIN, vehicle?.VIN);

Unit test the Vehicles controller

Modify the VehiclesControllerTests class to setup for Sqlite like we did for EFVehicleRepositoryTests and add a unit test to test the POST Delete action, DeleteConfirmed.

FredsCars.Tests\Controllers\VehiclesControllerTests.cs

... existing code ...
public class VehiclesControllerTests 
{
	#region class fields for tests that use SqliteInMemory db
	private readonly SqliteInMemoryDbContextFactory _factory;
	private readonly FredsCarsDbContext _context;
	private readonly EFVehicleRepository _vRepo;
	private readonly EFVehicleTypeRepository _vtRepo;
	#endregion

	public VehiclesControllerTests()
	{
		#region Setup for for tests that use SqliteInMemory db
		_factory = new SqliteInMemoryDbContextFactory();
		_context = _factory.CreateContext();
		_vRepo = new EFVehicleRepository(_context);
		_vtRepo = new EFVehicleTypeRepository(_context);
		#endregion
	}
	
	... existing code ...

	[Fact]
	public async Task Can_Delete_Vehicle()
	{
		// Arrange
		#region Arrange - seed VehicleTypes
		await _context.VehicleTypes.AddRangeAsync(
			new VehicleType
			{
				Id = 1,
				Name = "Cars"
			},
			new VehicleType
			{
				Id = 2,
				Name = "Trucks"
			},
			new VehicleType
			{
				Id = 3,
				Name = "Jeeps"
			}
		);
		await _context.SaveChangesAsync();
		#endregion

		#region Arrange - seed Vehicles test data
		var v1 = new Vehicle
		{
			Status = Status.New,
			Year = "2025",
			Make = "Make",
			Model = "Gladiator Rubicon 4 X 4",
			Color = "Joose",
			Price = 64125,
			VIN = "1C6RJTBG0SL532163",
			VehicleTypeId = 3,
			ImagePath = "/images/jeeps/jeep5.jpg"
		};

		var v2 = new Vehicle
		{
			Status = Status.Used,
			Year = "2020",
			Make = "Ford",
			Model = "Escape",
			Color = "Joose",
			Price = 22999,
			VIN = "1FMCU0F63LUC25826",
			VehicleTypeId = 1,
			ImagePath = "/images/cars/car2.webp"
		};
		await _context.Vehicles.AddRangeAsync(v1, v2);
		await _context.SaveChangesAsync();
		#endregion

		// Check vehicles were created
		Assert.True(_vRepo.Vehicles.Count() == 2);

		// Arrange - controller
		var target = new VehiclesController(_vRepo, _vtRepo);

		// Act
		RedirectToActionResult? result =
			await target.DeleteConfirmed(v1.Id)
				as RedirectToActionResult;

		// Assert
		Assert.Equal(1, _context.Vehicles.Count());
		Assert.Equal("1FMCU0F63LUC25826",
			_context.Vehicles.FirstOrDefault()!.VIN);
		Assert.Equal("/Home/Index",
			$"/{result?.ControllerName}/{result?.ActionName}");
	}
}
... existing code ...

In the code above we first setup for Sqlite in the same fashion we did for the EFVehicleRepositoryTests tests setting up class level members at the top of the file to represent a DbContext and repos for Sqlite and use the custom SqliteInMemoryDbContextFactory to create the context based on Sqlite and use that context to create the repos in the constructor.

Next, we add a test called Can_Delete_Vehicle.

In the Arrange section we seed the VehicleTypes and Vehicles test data using the Sqlite DbContext and its DbSet properties and verify that two Vehicles were created in the Sqlite in-memory db. We also instantiate the Vehicles controller passing the repos both of which were created with the Sqlite DbContext pointing to the Sqlite in-memory db.

In the Act section we call the Vehicle controller’s DeleteConfirmed method (representing the HTTPost Delete action) and capture the result in a variable named result of type RedirectToActionResult?.

In the Assert section we verify that there is only one Vehicle left, its VIN is what we expected and that the result redirects back to the Home/Index page.

What’s Next

Well, once again we certainly covered a lot in this module. We added the Delete feature which completes the CRUD (create, retrieve, update, and delete) functionality for our web application.

We also learned how use an Sqlite in-memory db in our unit tests to overcome some limitations of the .Net Core in-memory db.

In the next several modules we are going to revisit our default server-side validation and update it to use custom client-side validation, build an application wide error handling system, create a logging system, add security, and deploy the applicaton.

< Prev
Next >

Leave a ReplyCancel reply

Chapter 1: Static HTML – Designing the landing page.

  • Static HTML – Designing the landing page.
  • Let’s get started!
  • Mock your site with HTML
  • Make CSS easy with Bootstrap
  • Mock your content
  • Introducing JavaScript
  • JavaScript Code Improvements
  • Results Data
  • Images and the HTML Image Element.
  • Revisiting Reusability for CSS and JavaScript
  • Reuse for HTML: PART 1
  • Reuse for HTML: PART 2
  • Details Page – Using a Bootstrap Component
  • Creating Links
  • Chapter One Conclusion

Chapter 2: ASP.Net Core – Let’s talk Dynamic

  • Introduction to ASP.Net Core
  • What is .Net?
  • What is ASP.Net
  • Introduction to Entity Framework Core

Chapter 3: ASP.Net MVC Core – Models, Views, and Controllers [ASP.Net Core v9]

  • Introduction to ASP.Net Core MVC
  • Create the project: ASP.Net Core MVC
  • Explore the ASP.Net Core Empty Web Project Template
  • Configure the Application for MVC
  • Create a Controller: Home Controller
  • Create a View: Index View for the Home Controller
  • Install Bootstrap using Libman
  • Create the Layout template
  • Create the Model
  • Install EF Core & Create the Database
  • Seed the Database: Loading test data
  • DI (Dependency Injection): Display a List of Vehicles
  • Repository Pattern: The Vehicles Repo
  • Unit Test 1: Home Controller Can Use Vehicle Repository
  • Unit Test 2: Vehicle Repository Can Return List
  • Add the ImagePath Migration and Thumbnail images to results
  • Pagination: Create a Custom Tag Helper
  • Sorting
  • Category Filter
  • Partial View: Break out the vehicle results
  • View Component: Create dynamic category buttons
  • Create the Details page
  • Create the Create Page
  • Create the Update Page
  • Create the Delete Page
  • Validation
  • Logging & Configuration
  • Storing Secrets
  • Error Handling
  • Security & Administration

Chapter 7: Using Server Side & Client Side technologies together. [ASP.Net Core v7 & Angular v15]

  • Intro to Full Stack Development
  • Fred’s Cars – Full Stack Development
  • Prepare the environment
  • Create the Visual Studio Solution
  • Add the ASP.Net Core Web API project
  • Add the Angular Project
  • Wire it up!
  • WeatherForecast: Understanding the basics
  • Vehicles API Controller: Mock Data
  • Vehicles Angular Component: Consuming Data
  • Routing and Navigation
  • Using a Component Library: Angular Material
  • Our first Angular Material Component: MatToolbar
  • Configuring for Saas: CSS with superpowers
  • Create the Header & Footer components
  • Displaying Results with MatTable
  • Loading: Using a Progress Spinner
  • MatTable: Client-Side Paging and Sorting
  • MatSidenav: Create a Search Sidebar
  • MatCheckbox: Category Search UI
  • Adding an image to the welcome page
  • Create the database with Entity Framework Core migrations
  • MatPaginator & PageEvent: Custom Server-Side Paging
  • Unit Testing: Custom Server-Side Paging
  • Repository Pattern: VehicleRepository
  • Unit Test: Paging in the Vehicles controller
  • Server-Side Sorting
  • Unit Tests: Sorting
  • Filter (Quick Search)
  • Unit Tests: Filter feature
  • Advanced Search: Categories
  • Unit Tests: Search by category
  • Progress Spinner: Final Fix

TOC

  • What were WebForms?
  • Enter MVC
    • Understanding MVC
    • Advantages of MVC
  • ASP.Net Core MVC – A total rewrite
  • ASP.Net Core 2 MVC – Here come Razor Pages
    • Understanding Razor Pages
  • ASP.Net Core 3 – Dropping the MVC reference
    • Understanding Blazor
  • Dropping the MVC reference
  • Hello .Net 5!
  • What’s Next? – Here comes .Net 6.

Recent Posts

  • Angular Commands Cheat Sheet
  • Installing Git for Windows
  • Installing Postman
  • Installing SQL Server 2022 Express
  • Installing Visual Studio 2022

Recent Comments

No comments to show.

Archives

  • November 2023
  • October 2023
  • June 2023
  • October 2021

Categories

  • Angular
  • ASP.Net
  • Environment Setup
  • See All
  • SQL Server
  • Visual Studio
  • Web API & Rest Services

WordPress Theme Editor

Copyright © 2025 Web Development School.

Powered by PressBook Blog WordPress theme