Build a Security Incident Tracker
Build a full-stack C# web app with ASP.NET Core MVC and Entity Framework Core.
Introduction
30 Second Summary
Every organization faces cybersecurity threats. When incidents happen, teams need a reliable way to log, track, and manage them so nothing falls through the cracks.
In this project, you will build a Security Incident Tracker web application using C# and ASP.NET Core MVC. You'll create a full application that lets you log, view, edit, and delete cybersecurity incidents with color-coded severity levels and form validation.
What You'll Build
A web application built with ASP.NET Core MVC that tracks security incidents with color-coded severity badges, validated forms, and full create-read-update-delete functionality powered by Entity Framework Core.
By the end of this project, you'll have:
- A working ASP.NET Core MVC web application with a dashboard displaying all logged security incidents.
- A custom data model with validation annotations connected to an in-memory database through Entity Framework Core.
- Full CRUD operations (Create, Read, Update, Delete) through a controller with proper form validation and anti-forgery protection.
- Secret Mission: Add a search and filter feature to find incidents by severity or keyword.
Are there any prerequisites?
You should have basic C# knowledge (variables, classes, and methods) and Visual Studio 2022 installed on your machine. You may also need to install the .NET 10 SDK, which Step 1 walks you through.
Set Up Your .NET 10 Project
Before you can build a security incident tracker, you need a web framework that handles incoming requests, talks to a database, and renders pages. ASP.NET Core MVC is the industry-standard framework for building data-driven web apps with C#.
In this step, you will confirm your .NET SDK version, scaffold a brand-new MVC project, and verify it runs in your browser.
In this step, get ready to:
- Confirm your .NET 10 SDK is installed and ready.
- Create an ASP.NET Core MVC project called SecurityIncidentTracker.
- Run the default template in your browser and explore the generated project structure.
Check your .NET SDK version
This project requires .NET 10 because it uses the latest ASP.NET Core MVC templates and Entity Framework Core packages. Your existing Visual Studio 2022 install may ship with an older SDK version, so let's confirm what you have.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
- Check your installed .NET SDK version by running this command:
dotnet --version
Look at the version number that appears. It will be in the format 10.x.xxx if you have .NET 10 installed.
✔️ I see version 10.0 or higher
Your .NET 10 SDK is ready to go. Move on to the next substep.
ⓧ I see a version lower than 10.0
Your machine has an older .NET SDK (likely 8.x or 9.x from your Visual Studio 2022 install). .NET supports side-by-side installation, so you can add .NET 10 without breaking existing projects.
- Download the .NET 10 SDK installer from the official page:
- https://dotnet.microsoft.com/en-us/download/dotnet/10.0
- Run the installer and follow the prompts to completion.
- Close and reopen your terminal, then run dotnet --version again to confirm it shows 10.0 or higher.
ⓧ Command not found
Your terminal cannot find the dotnet command, which means no .NET SDK is on your system PATH.
- Download the .NET 10 SDK installer from the official page:
- https://dotnet.microsoft.com/en-us/download/dotnet/10.0
- Run the installer and follow the prompts to completion.
- Close and reopen your terminal, then run dotnet --version to confirm it shows 10.0 or higher.
Still not working after install?
On Windows, try opening a new Developer PowerShell for VS 2022 from your Start menu instead of a regular terminal. This shell has the .NET SDK paths pre-configured.
Create your ASP.NET Core MVC project
The dotnet CLI can scaffold a complete MVC web application with a single command. This generates all the folders, configuration files, and starter code you need.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project by running this command:
dotnet new mvc -o SecurityIncidentTracker
What does this command do?
- dotnet new mvc tells the CLI to use the ASP.NET Core MVC template.
- -o SecurityIncidentTracker creates a new folder with that name and places all generated files inside it.
- Confirm the project folder was created by running:
ls SecurityIncidentTracker
You should see files like Program.cs and folders like Controllers/, Models/, Views/, and wwwroot/ in the output.
- Open Visual Studio 2022.
- Click Open a project or solution on the start screen.
- Navigate to your Desktop, open the SecurityIncidentTracker folder, and select the SecurityIncidentTracker.csproj file.
- Click Open.
Why ASP.NET Core MVC?
Microsoft recommends Blazor for new web projects, but MVC teaches the most transferable architectural pattern in web development. Model-View-Controller is used across frameworks like Django, Rails, and Spring.
Learning MVC here means the pattern transfers directly to other languages and frameworks you encounter in your career.
Visual Studio won't open the project?
Make sure you selected the .csproj file, not the folder itself. If Visual Studio shows a compatibility warning, confirm you installed the .NET 10 SDK in the previous substep.
help me open my .NET project in Visual Studio 2022
Run the application and explore the project
The scaffolded project includes a working homepage out of the box. Let's run it to confirm everything is set up correctly before you start adding your own code.
- In Visual Studio, open the integrated terminal by clicking View in the menu bar, then selecting Terminal.
- Start the development server with hot reload by running this command:
dotnet watch
What does dotnet watch do?
This command compiles and runs your application, then watches for file changes. When you save a code edit, it automatically recompiles and refreshes your browser.
Your default browser opens automatically to https://localhost: followed by a port number. You should see the ASP.NET Core welcome page with a Welcome heading and navigation links for Home and Privacy.
Browser shows a certificate warning?
ASP.NET Core uses HTTPS by default with a development certificate. If your browser warns about an untrusted certificate, click Advanced and then Proceed to continue to the page.
help me fix the HTTPS certificate warning when running my ASP.NET Core app
Now take a look at the project structure in the Solution Explorer panel on the right side of Visual Studio. Here are the key folders the MVC template generated:
- Controllers/ contains classes that handle incoming web requests and decide what to send back.
- Models/ holds classes that define the shape of your data.
- Views/ stores the Razor templates that generate the HTML pages users see.
- wwwroot/ is for static files like CSS, JavaScript, and images.
- Program.cs is the application entry point where services are registered and the request pipeline is configured.
What is the MVC pattern?
MVC separates your app into three responsibilities. The Model defines data structure and rules. The View renders the user interface. The Controller handles user input and coordinates between Model and View.
When a user visits /Home/Index, ASP.NET Core routes the request to the HomeController class, calls its Index method, and renders the matching View template.
- In Solution Explorer, double-click Program.cs to open it.
This is the file that configures your entire application. It should look like this:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
What is this route pattern?
The pattern {controller=Home}/{action=Index}/{id?} tells ASP.NET Core how to map URLs to controller actions. A request to /Incidents/Details/3 would call the Details method on IncidentsController with id = 3.
✔️ Awesome, I've got everything!
Great. Make sure dotnet watch is still running in your terminal. You will keep it running throughout the project.
ⓧ I'd like to double check the full code
Here is the complete Program.cs as generated by the template:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Your development environment is ready and your MVC project is running. Next up, you will create the Incident model that defines what a security incident looks like in your database.
Create the Incident Model and Database Context
Your project scaffolding is in place and the default template runs. Now it is time to define the data your app works with.
Every MVC application needs a Model that describes the shape of its data. You will create an Incident model, then connect it to a database so the app can store and retrieve incidents.
In this step, get ready to:
- Define an Incident model class with validation rules using data annotations.
- Install the Entity Framework Core InMemory NuGet package.
- Create a database context with seed data and register it in your app.
Create the Incident model class
A model in ASP.NET Core MVC is a C# class that represents the shape of your data. Each property becomes a column when stored in a database. Data annotations are attributes you place above properties to enforce validation rules and control how labels appear in forms.
- In the Solution Explorer panel in Visual Studio, right-click the Models folder.
- Select Add then Class....
- Name the file Incident.cs and click Add.
- Replace the entire contents of the file with the following code:
using System.ComponentModel.DataAnnotations;
namespace SecurityIncidentTracker.Models;
public class Incident
{
public int Id { get; set; }
[Required(ErrorMessage = "Title is required")]
[StringLength(100, ErrorMessage = "Title cannot exceed 100 characters")]
[Display(Name = "Incident Title")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "Description is required")]
[StringLength(500, ErrorMessage = "Description cannot exceed 500 characters")]
public string Description { get; set; } = string.Empty;
[Required]
[Display(Name = "Severity Level")]
public string Severity { get; set; } = "Low";
[Required]
public string Category { get; set; } = "Other";
[Required]
public string Status { get; set; } = "Open";
[Display(Name = "Reported At")]
[DataType(DataType.DateTime)]
public DateTime ReportedAt { get; set; } = DateTime.Now;
}
What does this code do?
This class defines the structure of a security incident. Each property maps to a piece of information you want to track.
- Id is an auto-generated primary key that uniquely identifies each incident.
- [Required] prevents the form from submitting if that field is empty.
- [StringLength(100)] caps the title at 100 characters so the database column stays bounded.
- [Display(Name = "Incident Title")] controls the label text that appears on forms and tables.
- Default values like "Low" and "Open" pre-fill the form when a user creates a new incident.
- Save Incident.cs by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Seeing red squiggles in the file?
Make sure the namespace matches your project name exactly: SecurityIncidentTracker.Models. If you see errors on DataAnnotations, confirm the using System.ComponentModel.DataAnnotations; line is at the top of the file.
help me fix red squiggles in my Incident model
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
using System.ComponentModel.DataAnnotations;
namespace SecurityIncidentTracker.Models;
public class Incident
{
public int Id { get; set; }
[Required(ErrorMessage = "Title is required")]
[StringLength(100, ErrorMessage = "Title cannot exceed 100 characters")]
[Display(Name = "Incident Title")]
public string Title { get; set; } = string.Empty;
[Required(ErrorMessage = "Description is required")]
[StringLength(500, ErrorMessage = "Description cannot exceed 500 characters")]
public string Description { get; set; } = string.Empty;
[Required]
[Display(Name = "Severity Level")]
public string Severity { get; set; } = "Low";
[Required]
public string Category { get; set; } = "Other";
[Required]
public string Status { get; set; } = "Open";
[Display(Name = "Reported At")]
[DataType(DataType.DateTime)]
public DateTime ReportedAt { get; set; } = DateTime.Now;
}
Install the Entity Framework Core InMemory package
Entity Framework Core is an ORM (Object-Relational Mapper) that lets you interact with a database using C# objects instead of raw SQL. The InMemory provider stores data in memory, which means zero database setup for this project.
- Install the InMemory package by running this command in the terminal inside your project folder:
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 10.0.9
- You should see output confirming the package was added, including a line like PackageReference for package 'Microsoft.EntityFrameworkCore.InMemory' version '10.0.9' added.
Why an in-memory database?
The InMemory provider lets you focus on learning the MVC pattern without installing a database engine. The code patterns you write (DbContext, DbSet, LINQ queries) transfer directly to production databases like SQL Server or PostgreSQL later.
The trade-off is that data disappears when you stop the app. That is fine for learning.
Command not found or package error?
Make sure your terminal is inside the SecurityIncidentTracker project folder (the one containing the .csproj file). If you are in the parent folder, run cd SecurityIncidentTracker first.
help me fix a NuGet package install error
Create the database context and register it
A DbContext is the bridge between your C# model and the database. It tells Entity Framework Core which models to track and how to seed initial data.
- In Solution Explorer, right-click the project name SecurityIncidentTracker (not a subfolder).
- Select Add then New Folder.
- Name the folder Data.
- Right-click the new Data folder, select Add then Class..., and name it ApplicationDbContext.cs.
- Replace the entire contents of the file with the following code:
using Microsoft.EntityFrameworkCore;
using SecurityIncidentTracker.Models;
namespace SecurityIncidentTracker.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Incident> Incidents { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Incident>().HasData(
new Incident
{
Id = 1,
Title = "Phishing Email Campaign",
Description = "Multiple users reported receiving suspicious emails impersonating IT support requesting password resets.",
Severity = "High",
Category = "Phishing",
Status = "Investigating",
ReportedAt = DateTime.Now.AddDays(-2)
},
new Incident
{
Id = 2,
Title = "Unauthorized VPN Access Attempt",
Description = "Detected multiple failed VPN login attempts from an unrecognized IP address outside business hours.",
Severity = "Medium",
Category = "Unauthorized Access",
Status = "Open",
ReportedAt = DateTime.Now.AddDays(-1)
},
new Incident
{
Id = 3,
Title = "Malware Detected on Workstation",
Description = "Endpoint protection flagged a trojan on workstation WS-045 in the finance department.",
Severity = "Critical",
Category = "Malware",
Status = "Investigating",
ReportedAt = DateTime.Now.AddHours(-6)
}
);
}
}
What does this code do?
This class sets up the database layer for your app.
- DbSet<Incident> Incidents tells Entity Framework Core to create an "Incidents" table that stores Incident objects.
- OnModelCreating seeds three sample incidents into the database so you have data to display immediately.
- The constructor accepts DbContextOptions which lets the app configure which database provider to use (InMemory in this case).
- Save ApplicationDbContext.cs.
You will not see a change yet. The next chunk registers this context in your app's startup.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before continuing.
ⓧ I'd like to double check the full code
using Microsoft.EntityFrameworkCore;
using SecurityIncidentTracker.Models;
namespace SecurityIncidentTracker.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Incident> Incidents { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Incident>().HasData(
new Incident
{
Id = 1,
Title = "Phishing Email Campaign",
Description = "Multiple users reported receiving suspicious emails impersonating IT support requesting password resets.",
Severity = "High",
Category = "Phishing",
Status = "Investigating",
ReportedAt = DateTime.Now.AddDays(-2)
},
new Incident
{
Id = 2,
Title = "Unauthorized VPN Access Attempt",
Description = "Detected multiple failed VPN login attempts from an unrecognized IP address outside business hours.",
Severity = "Medium",
Category = "Unauthorized Access",
Status = "Open",
ReportedAt = DateTime.Now.AddDays(-1)
},
new Incident
{
Id = 3,
Title = "Malware Detected on Workstation",
Description = "Endpoint protection flagged a trojan on workstation WS-045 in the finance department.",
Severity = "Critical",
Category = "Malware",
Status = "Investigating",
ReportedAt = DateTime.Now.AddHours(-6)
}
);
}
}
Now you need to tell your application about this database context. Dependency injection in ASP.NET Core lets you register services (like your database) in one place so any controller can use them automatically.
- Open Program.cs from the project root in Solution Explorer.
- Replace the entire contents of the file with the following code:
using Microsoft.EntityFrameworkCore;
using SecurityIncidentTracker.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("SecurityIncidentDb"));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
context.Database.EnsureCreated();
}
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
What changed in Program.cs?
- The using statements at the top now import Entity Framework Core and your Data namespace.
- AddDbContext<ApplicationDbContext> registers your database context with the dependency injection container so controllers can request it.
- UseInMemoryDatabase("SecurityIncidentDb") tells EF Core to store data in memory under that name.
- The using (var scope = ...) block runs EnsureCreated() at startup, which triggers the seed data from your OnModelCreating method.
- Save Program.cs.
- Run your app by pressing Ctrl+F5 in Visual Studio (or run dotnet watch in the terminal).
- Your browser should open the default home page with no errors. The app is now backed by an in-memory database with three seeded incidents.
Build errors when you run the app?
Check that both using Microsoft.EntityFrameworkCore; and using SecurityIncidentTracker.Data; are at the top of Program.cs. If you see an error about ApplicationDbContext not being found, confirm the namespace in your ApplicationDbContext.cs file is SecurityIncidentTracker.Data.
help me fix build errors in my ASP.NET Core project after adding the database context
✔️ Awesome, I've got everything!
Great. Double check you have saved both Program.cs and ApplicationDbContext.cs.
ⓧ I'd like to double check the full code
using Microsoft.EntityFrameworkCore;
using SecurityIncidentTracker.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseInMemoryDatabase("SecurityIncidentDb"));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
context.Database.EnsureCreated();
}
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Your data layer is ready. The app knows about incidents, has sample data seeded, and builds cleanly. Next up, you will create a controller that handles web requests and connects the model to views.
Build the Incidents Controller
You have a model that defines what an incident looks like and a database context that knows how to store them. Now you need the "traffic controller" that handles incoming web requests and decides what to do with them.
In ASP.NET Core MVC, a controller is a C# class that receives HTTP requests, interacts with the database, and chooses which view to display back to the user. You will build one controller with all the actions needed to create, read, update, and delete incidents.
In this step, get ready to:
- Create an IncidentsController with an Index action that lists all incidents.
- Add Create actions with form validation and anti-forgery protection.
- Add Details, Edit, and Delete actions to complete the CRUD operations.
Create the controller with an Index action
The controller class uses dependency injection to receive the database context you registered in Program.cs. This gives every action method access to the incidents database without manually creating connections.
- In the Solution Explorer sidebar, right-click the Controllers folder.
- Select Add, then Class....
- Name the file IncidentsController.cs and click Add.
- Replace the entire contents of the new file with this code:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SecurityIncidentTracker.Data;
using SecurityIncidentTracker.Models;
namespace SecurityIncidentTracker.Controllers;
public class IncidentsController : Controller
{
// Database context injected by the framework
private readonly ApplicationDbContext _context;
// Constructor receives the database context automatically
public IncidentsController(ApplicationDbContext context)
{
_context = context;
}
// GET: /Incidents - displays all incidents sorted by newest first
public async Task<IActionResult> Index()
{
var incidents = await _context.Incidents
.OrderByDescending(i => i.ReportedAt)
.ToListAsync();
return View(incidents);
}
}
What does this code do?
This sets up the controller's foundation with one action method.
- The class inherits from Controller, which gives it access to helper methods like View(), RedirectToAction(), and NotFound().
- The constructor stores the ApplicationDbContext in a private field so all action methods can query the database.
- The Index() action retrieves every incident, sorts them by ReportedAt descending (newest first), and passes the list to a view.
- Save IncidentsController.cs.
- Check the terminal running dotnet watch. You should see the build succeed with no errors.
Seeing build errors?
- Make sure the namespace is SecurityIncidentTracker.Controllers. A mismatched namespace prevents the framework from finding your controller.
- Check that both using Microsoft.EntityFrameworkCore; and using SecurityIncidentTracker.Data; are at the top of the file.
Help me fix build errors in my IncidentsController.
Add Create actions with validation
CRUD operations need two actions for creating a record: one to display the empty form (GET) and one to process the submitted data (POST). The POST action validates the data before saving, protecting against incomplete or malicious submissions.
- In IncidentsController.cs, add the following Create methods directly below the closing brace of the Index() method (but still inside the class):
// GET: /Incidents/Create - displays the empty form
public IActionResult Create()
{
return View();
}
// POST: /Incidents/Create - processes the submitted form data
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Title,Description,Severity,Category,Status,ReportedAt")] Incident incident)
{
if (ModelState.IsValid)
{
_context.Add(incident);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(incident);
}
What does this code do?
- The GET Create() simply returns an empty view. The framework will look for a file at Views/Incidents/Create.cshtml automatically.
- The [HttpPost] attribute restricts the second Create() to only handle form submissions.
- The [ValidateAntiForgeryToken] attribute prevents cross-site request forgery attacks by requiring a matching token in the form.
- The [Bind] attribute specifies exactly which properties the form is allowed to set. This prevents attackers from injecting values into properties like Id.
- If ModelState.IsValid passes (all data annotations are satisfied), the incident is saved. Otherwise the form is re-displayed with validation errors.
- Save IncidentsController.cs.
- Confirm the build succeeds in the terminal with no errors.
Build failed after adding Create?
- Make sure both Create methods are inside the IncidentsController class braces, not outside the class definition.
- Check that the [Bind] attribute uses escaped double quotes around each property name string.
Help me fix my Create action methods.
Add Details, Edit, and Delete actions
The remaining CRUD actions follow the same pattern you just used for Create. Each operation that modifies data needs both a GET action (to show a confirmation page or form) and a POST action (to execute the change).
- Below the Create POST method, add the Details action:
// GET: /Incidents/Details/5 - displays a single incident
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var incident = await _context.Incidents.FirstOrDefaultAsync(m => m.Id == id);
if (incident == null)
{
return NotFound();
}
return View(incident);
}
What does this code do?
- The int? id parameter is nullable. If someone navigates to /Incidents/Details without an ID, the action returns a 404 page.
- The FirstOrDefaultAsync query finds the incident by its ID. If no match exists, it also returns a 404.
- Below the Details method, add both Edit actions:
// GET: /Incidents/Edit/5 - displays the edit form with existing data
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var incident = await _context.Incidents.FindAsync(id);
if (incident == null)
{
return NotFound();
}
return View(incident);
}
// POST: /Incidents/Edit/5 - saves updated incident data
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Description,Severity,Category,Status,ReportedAt")] Incident incident)
{
if (id != incident.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
_context.Update(incident);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(incident);
}
What does this code do?
- The Edit GET action uses FindAsync to load the existing incident into the edit form.
- The Edit POST includes Id in the [Bind] list (unlike Create) because the record already exists and needs its ID preserved.
- The id != incident.Id check prevents a tampered form from updating the wrong record.
- Below the Edit POST method, add both Delete actions:
// GET: /Incidents/Delete/5 - displays the delete confirmation page
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var incident = await _context.Incidents.FirstOrDefaultAsync(m => m.Id == id);
if (incident == null)
{
return NotFound();
}
return View(incident);
}
// POST: /Incidents/Delete/5 - permanently removes the incident
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var incident = await _context.Incidents.FindAsync(id);
if (incident != null)
{
_context.Incidents.Remove(incident);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
Why is Delete structured differently?
C# does not allow two methods with the same name and same parameters. Since both Delete actions take an id, the POST method is named DeleteConfirmed in code.
The [ActionName("Delete")] attribute tells the routing system to still map it to the /Incidents/Delete URL. The GET shows a confirmation page first so users do not accidentally delete records.
- Save IncidentsController.cs.
- Confirm the build succeeds in the terminal with no errors. You should see a hot reload or rebuild message with zero warnings.
Seeing a build error about duplicate methods?
- Make sure the Delete POST method is named DeleteConfirmed (not Delete). The [ActionName("Delete")] attribute handles the URL mapping.
- Verify that the Edit POST signature is Edit(int id, ...) (non-nullable int) while the Edit GET is Edit(int? id) (nullable). This differentiates the overloads.
Help me fix duplicate method errors in my controller.
✔️ Awesome, I've got everything!
Great. Double check you have saved IncidentsController.cs and the build is green.
ⓧ I'd like to double check the full code
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SecurityIncidentTracker.Data;
using SecurityIncidentTracker.Models;
namespace SecurityIncidentTracker.Controllers;
public class IncidentsController : Controller
{
private readonly ApplicationDbContext _context;
public IncidentsController(ApplicationDbContext context)
{
_context = context;
}
public async Task<IActionResult> Index()
{
var incidents = await _context.Incidents
.OrderByDescending(i => i.ReportedAt)
.ToListAsync();
return View(incidents);
}
public IActionResult Create()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Title,Description,Severity,Category,Status,ReportedAt")] Incident incident)
{
if (ModelState.IsValid)
{
_context.Add(incident);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(incident);
}
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var incident = await _context.Incidents.FirstOrDefaultAsync(m => m.Id == id);
if (incident == null)
{
return NotFound();
}
return View(incident);
}
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var incident = await _context.Incidents.FindAsync(id);
if (incident == null)
{
return NotFound();
}
return View(incident);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,Description,Severity,Category,Status,ReportedAt")] Incident incident)
{
if (id != incident.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
_context.Update(incident);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(incident);
}
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var incident = await _context.Incidents.FirstOrDefaultAsync(m => m.Id == id);
if (incident == null)
{
return NotFound();
}
return View(incident);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var incident = await _context.Incidents.FindAsync(id);
if (incident != null)
{
_context.Incidents.Remove(incident);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
}
Your controller is ready to handle every CRUD operation. Next up, you will build the Razor views that give users a visual interface for interacting with these actions.
Create the Razor Views
Your controller has action methods ready to handle every request, but right now it has no views to render. Without views, visiting any Incidents URL returns an error because ASP.NET Core MVC cannot find the matching .cshtml template.
In this step you will build the Razor view templates that form the user interface of your incident tracker. Each view maps to an action method in your controller, completing the V in the MVC pattern.
In this step, get ready to:
- Build the Index view with a table of incidents and color-coded severity badges.
- Create the form views for reporting and editing incidents using Tag Helpers.
- Add Details, Edit, and Delete views plus a navigation link in the shared layout.
Build the Index view with the incidents table
The Razor view engine looks for views inside a folder matching the controller name. Your IncidentsController needs a Views/Incidents/ folder to store its templates.
- In the Solution Explorer panel in Visual Studio, right-click the Views folder.
- Select Add, then New Folder.
- Name the folder Incidents.
Now create the Index view file that displays all incidents in a table.
- Right-click the new Views/Incidents folder.
- Select Add, then New Item.
- Choose Razor View - Empty and name it Index.cshtml.
- Replace the contents of Index.cshtml with the following code:
@model IEnumerable<SecurityIncidentTracker.Models.Incident>
@{
ViewData["Title"] = "Security Incidents";
}
<h1>Security Incidents</h1>
<p>
<a asp-action="Create" class="btn btn-primary">Report New Incident</a>
</p>
<table class="table table-striped">
<thead>
<tr>
<th>@Html.DisplayNameFor(model => model.Title)</th>
<th>@Html.DisplayNameFor(model => model.Severity)</th>
<th>@Html.DisplayNameFor(model => model.Category)</th>
<th>@Html.DisplayNameFor(model => model.Status)</th>
<th>@Html.DisplayNameFor(model => model.ReportedAt)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@Html.DisplayFor(modelItem => item.Title)</td>
<td>
@{
var badgeClass = item.Severity switch
{
"Critical" => "bg-danger",
"High" => "bg-warning text-dark",
"Medium" => "bg-info text-dark",
_ => "bg-secondary"
};
}
<span class="badge @badgeClass">@item.Severity</span>
</td>
<td>@Html.DisplayFor(modelItem => item.Category)</td>
<td>@Html.DisplayFor(modelItem => item.Status)</td>
<td>@Html.DisplayFor(modelItem => item.ReportedAt)</td>
<td>
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
What does this view do?
The @model directive tells Razor this view expects a list of Incident objects. This matches what the Index action passes with return View(incidents).
The C# switch expression assigns a Bootstrap badge color based on severity. Critical incidents get a red bg-danger badge, High gets yellow bg-warning, Medium gets blue bg-info, and everything else defaults to grey.
Tag Helpers like asp-action and asp-route-id generate the correct URLs for each incident's Details, Edit, and Delete pages.
- Save Views/Incidents/Index.cshtml.
- In the terminal, make sure dotnet watch is still running. Navigate to https://localhost:{port}/Incidents in your browser.
- You should see a table with three seeded incidents, each displaying a colored severity badge.
Seeing a blank page or 404?
- Double-check the folder name is exactly Incidents (capital I) inside Views/. The folder must match the controller name without the "Controller" suffix.
- Make sure the file is named Index.cshtml (capital I). Razor view file names are case-sensitive on some systems.
- Confirm dotnet watch shows no build errors in the terminal output.
Still stuck? Why is my Incidents Index view not loading?
✔️ Awesome, I've got everything!
Great. Make sure you have saved Index.cshtml before moving on.
ⓧ I'd like to double check the full code
@model IEnumerable<SecurityIncidentTracker.Models.Incident>
@{
ViewData["Title"] = "Security Incidents";
}
<h1>Security Incidents</h1>
<p>
<a asp-action="Create" class="btn btn-primary">Report New Incident</a>
</p>
<table class="table table-striped">
<thead>
<tr>
<th>@Html.DisplayNameFor(model => model.Title)</th>
<th>@Html.DisplayNameFor(model => model.Severity)</th>
<th>@Html.DisplayNameFor(model => model.Category)</th>
<th>@Html.DisplayNameFor(model => model.Status)</th>
<th>@Html.DisplayNameFor(model => model.ReportedAt)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td>@Html.DisplayFor(modelItem => item.Title)</td>
<td>
@{
var badgeClass = item.Severity switch
{
"Critical" => "bg-danger",
"High" => "bg-warning text-dark",
"Medium" => "bg-info text-dark",
_ => "bg-secondary"
};
}
<span class="badge @badgeClass">@item.Severity</span>
</td>
<td>@Html.DisplayFor(modelItem => item.Category)</td>
<td>@Html.DisplayFor(modelItem => item.Status)</td>
<td>@Html.DisplayFor(modelItem => item.ReportedAt)</td>
<td>
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Create the form view for reporting new incidents
Forms in ASP.NET Core MVC use Tag Helpers to bind HTML elements directly to model properties. When you write asp-for="Title", Razor generates the correct name, id, and validation attributes automatically.
- Right-click the Views/Incidents folder, select Add then New Item.
- Choose Razor View - Empty and name it Create.cshtml.
- Replace its contents with the following code:
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Report New Incident";
}
<h1>Report New Incident</h1>
<hr />
<div class="row">
<div class="col-md-6">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control" rows="4"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Severity" class="control-label"></label>
<select asp-for="Severity" class="form-select">
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
<option value="Critical">Critical</option>
</select>
<span asp-validation-for="Severity" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Category" class="control-label"></label>
<select asp-for="Category" class="form-select">
<option value="Phishing">Phishing</option>
<option value="Malware">Malware</option>
<option value="Unauthorized Access">Unauthorized Access</option>
<option value="Data Breach">Data Breach</option>
<option value="Other">Other</option>
</select>
<span asp-validation-for="Category" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Status" class="control-label"></label>
<select asp-for="Status" class="form-select">
<option value="Open">Open</option>
<option value="Investigating">Investigating</option>
<option value="Resolved">Resolved</option>
</select>
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="ReportedAt" class="control-label"></label>
<input asp-for="ReportedAt" class="form-control" type="datetime-local" />
<span asp-validation-for="ReportedAt" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Submit Incident" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
What are Tag Helpers doing here?
Each asp-for attribute binds the input to an Incident model property. Razor reads the [Display] annotations to generate labels and the [Required]/[StringLength] annotations to drive client-side validation.
The asp-validation-for spans display error messages when input fails validation. The @section Scripts block loads jQuery validation scripts that make these messages appear without a page reload.
The Form Tag Helper (<form asp-action="Create">) automatically injects a hidden anti-forgery token into the rendered HTML. This pairs with the [ValidateAntiForgeryToken] attribute on your controller to prevent cross-site request forgery attacks.
- Save Views/Incidents/Create.cshtml.
- In your browser, click the Report New Incident button on the Incidents page.
- You should see a form with fields for Title, Description, Severity, Category, Status, and Reported At, plus a Submit Incident button.
Form not appearing?
- Check that Create.cshtml is inside Views/Incidents/ (not inside Views/Home/).
- Look at the terminal for build errors. A missing closing tag in the Razor markup will prevent the page from compiling.
- Verify the @model directive at the top matches the full namespace: SecurityIncidentTracker.Models.Incident.
Need help? Why is my Create form view not loading?
✔️ Awesome, I've got everything!
Great. Make sure you have saved Create.cshtml before moving on.
ⓧ I'd like to double check the full code
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Report New Incident";
}
<h1>Report New Incident</h1>
<hr />
<div class="row">
<div class="col-md-6">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group mb-3">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control" rows="4"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Severity" class="control-label"></label>
<select asp-for="Severity" class="form-select">
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
<option value="Critical">Critical</option>
</select>
<span asp-validation-for="Severity" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Category" class="control-label"></label>
<select asp-for="Category" class="form-select">
<option value="Phishing">Phishing</option>
<option value="Malware">Malware</option>
<option value="Unauthorized Access">Unauthorized Access</option>
<option value="Data Breach">Data Breach</option>
<option value="Other">Other</option>
</select>
<span asp-validation-for="Category" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Status" class="control-label"></label>
<select asp-for="Status" class="form-select">
<option value="Open">Open</option>
<option value="Investigating">Investigating</option>
<option value="Resolved">Resolved</option>
</select>
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="ReportedAt" class="control-label"></label>
<input asp-for="ReportedAt" class="form-control" type="datetime-local" />
<span asp-validation-for="ReportedAt" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Submit Incident" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Create the Details, Edit, and Delete views and add navigation
The remaining views follow the same patterns you have already seen. The Details view displays a read-only card, the Edit view reuses the form structure with a hidden Id field, and the Delete view asks for confirmation before removing a record.
- Create a new file Details.cshtml in the Views/Incidents folder with the following code:
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Incident Details";
}
<h1>Incident Details</h1>
<div class="card">
<div class="card-body">
<h4 class="card-title">@Model.Title</h4>
<hr />
<dl class="row">
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Severity)</dt>
<dd class="col-sm-9">
@{
var badgeClass = Model.Severity switch
{
"Critical" => "bg-danger",
"High" => "bg-warning text-dark",
"Medium" => "bg-info text-dark",
_ => "bg-secondary"
};
}
<span class="badge @badgeClass">@Model.Severity</span>
</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Category)</dt>
<dd class="col-sm-9">@Model.Category</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Status)</dt>
<dd class="col-sm-9">@Model.Status</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Description)</dt>
<dd class="col-sm-9">@Model.Description</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.ReportedAt)</dt>
<dd class="col-sm-9">@Model.ReportedAt.ToString("g")</dd>
</dl>
</div>
</div>
<div class="mt-3">
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">Edit</a>
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
What does this view do?
The Details view uses a Bootstrap card with a definition list (<dl>) to display each property as a label-value pair. It reuses the same severity badge logic from the Index view so the visual language stays consistent.
- Save Details.cshtml.
- Create a new file Edit.cshtml in the same Views/Incidents folder with the following code:
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Edit Incident";
}
<h1>Edit Incident</h1>
<hr />
<div class="row">
<div class="col-md-6">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group mb-3">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control" rows="4"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Severity" class="control-label"></label>
<select asp-for="Severity" class="form-select">
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
<option value="Critical">Critical</option>
</select>
<span asp-validation-for="Severity" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Category" class="control-label"></label>
<select asp-for="Category" class="form-select">
<option value="Phishing">Phishing</option>
<option value="Malware">Malware</option>
<option value="Unauthorized Access">Unauthorized Access</option>
<option value="Data Breach">Data Breach</option>
<option value="Other">Other</option>
</select>
<span asp-validation-for="Category" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Status" class="control-label"></label>
<select asp-for="Status" class="form-select">
<option value="Open">Open</option>
<option value="Investigating">Investigating</option>
<option value="Resolved">Resolved</option>
</select>
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="ReportedAt" class="control-label"></label>
<input asp-for="ReportedAt" class="form-control" type="datetime-local" />
<span asp-validation-for="ReportedAt" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save Changes" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div class="mt-3">
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
How does Edit differ from Create?
The Edit form includes <input type="hidden" asp-for="Id" /> so the Id value gets posted back to the controller. Without this hidden field, the controller cannot match the submitted data to the correct database record.
- Save Edit.cshtml.
- Create a new file Delete.cshtml in the Views/Incidents folder with the following code:
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Delete Incident";
}
<h1>Delete Incident</h1>
<h3>Are you sure you want to delete this incident?</h3>
<div class="card">
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Title)</dt>
<dd class="col-sm-9">@Model.Title</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Severity)</dt>
<dd class="col-sm-9">@Model.Severity</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Category)</dt>
<dd class="col-sm-9">@Model.Category</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Status)</dt>
<dd class="col-sm-9">@Model.Status</dd>
</dl>
</div>
</div>
<form asp-action="Delete" class="mt-3">
<input type="hidden" asp-for="Id" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-action="Index">Back to List</a>
</form>
Why show a confirmation page?
The Delete view displays the incident details and asks the user to confirm. When they click Delete, the form posts to the DeleteConfirmed action via the [ActionName("Delete")] attribute. This two-step pattern prevents accidental deletions from a simple link click.
- Save Delete.cshtml.
The last piece is adding a navigation link so users can reach the Incidents page from anywhere in the app.
- In Solution Explorer, expand Views then Shared, and open _Layout.cshtml.
- Find the <ul class="navbar-nav flex-grow-1"> section that contains the Home and Privacy links.
- Find the Privacy <li> element. It looks like this:
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
- Add the following new <li> element directly below the Privacy list item:
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Incidents" asp-action="Index">Incidents</a>
</li>
- Save _Layout.cshtml.
- In your browser, refresh any page. You should see an Incidents link in the top navigation bar next to Home and Privacy.
- Click Incidents to confirm it loads the Index view.
- Test the full CRUD flow: click Details on any incident, then try Edit, and then Delete on one of the seeded records.
Navigation link not appearing?
- Make sure you added the new <li> inside the existing <ul class="navbar-nav flex-grow-1"> element, not outside it.
- Check that asp-controller="Incidents" matches your controller name exactly (capital I, no "Controller" suffix).
- If dotnet watch stopped, restart it and refresh your browser.
Still stuck? Help me add a navigation link to my Incidents page in the layout.
✔️ Awesome, I've got everything!
Great. Make sure all five view files are saved and your nav link is working.
ⓧ I'd like to double check the full code
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Incident Details";
}
<h1>Incident Details</h1>
<div class="card">
<div class="card-body">
<h4 class="card-title">@Model.Title</h4>
<hr />
<dl class="row">
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Severity)</dt>
<dd class="col-sm-9">
@{
var badgeClass = Model.Severity switch
{
"Critical" => "bg-danger",
"High" => "bg-warning text-dark",
"Medium" => "bg-info text-dark",
_ => "bg-secondary"
};
}
<span class="badge @badgeClass">@Model.Severity</span>
</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Category)</dt>
<dd class="col-sm-9">@Model.Category</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Status)</dt>
<dd class="col-sm-9">@Model.Status</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Description)</dt>
<dd class="col-sm-9">@Model.Description</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.ReportedAt)</dt>
<dd class="col-sm-9">@Model.ReportedAt.ToString("g")</dd>
</dl>
</div>
</div>
<div class="mt-3">
<a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">Edit</a>
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
</div>
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Edit Incident";
}
<h1>Edit Incident</h1>
<hr />
<div class="row">
<div class="col-md-6">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group mb-3">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control" rows="4"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Severity" class="control-label"></label>
<select asp-for="Severity" class="form-select">
<option value="Low">Low</option>
<option value="Medium">Medium</option>
<option value="High">High</option>
<option value="Critical">Critical</option>
</select>
<span asp-validation-for="Severity" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Category" class="control-label"></label>
<select asp-for="Category" class="form-select">
<option value="Phishing">Phishing</option>
<option value="Malware">Malware</option>
<option value="Unauthorized Access">Unauthorized Access</option>
<option value="Data Breach">Data Breach</option>
<option value="Other">Other</option>
</select>
<span asp-validation-for="Category" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Status" class="control-label"></label>
<select asp-for="Status" class="form-select">
<option value="Open">Open</option>
<option value="Investigating">Investigating</option>
<option value="Resolved">Resolved</option>
</select>
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="ReportedAt" class="control-label"></label>
<input asp-for="ReportedAt" class="form-control" type="datetime-local" />
<span asp-validation-for="ReportedAt" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save Changes" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div class="mt-3">
<a asp-action="Index">Back to List</a>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
@model SecurityIncidentTracker.Models.Incident
@{
ViewData["Title"] = "Delete Incident";
}
<h1>Delete Incident</h1>
<h3>Are you sure you want to delete this incident?</h3>
<div class="card">
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Title)</dt>
<dd class="col-sm-9">@Model.Title</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Severity)</dt>
<dd class="col-sm-9">@Model.Severity</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Category)</dt>
<dd class="col-sm-9">@Model.Category</dd>
<dt class="col-sm-3">@Html.DisplayNameFor(model => model.Status)</dt>
<dd class="col-sm-9">@Model.Status</dd>
</dl>
</div>
</div>
<form asp-action="Delete" class="mt-3">
<input type="hidden" asp-for="Id" />
<input type="submit" value="Delete" class="btn btn-danger" /> |
<a asp-action="Index">Back to List</a>
</form>
Your incident tracker now has a fully functional user interface. Every action in the controller has a matching Razor view, and users can navigate to the Incidents page from anywhere in the app. Next up, you will extend this with a search and filter feature in the Secret Mission.
Secret mission
Add Search and Filtering
Your incident tracker works, but imagine having 200 incidents in the list. Finding a specific one would be painful. In this secret mission, you'll add a search bar and severity filter so users can quickly narrow down the incidents that matter most.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with an in-memory database, so there are no ongoing costs. Decide whether to keep your project files, pause the running server, or delete everything entirely.
Resources you used:
- SecurityIncidentTracker project folder on your Desktop (all source code, views, models, and configuration).
- .NET 10 SDK (if you installed or upgraded it for this project).
Keep everything running
No action needed. Choose this if you want to keep building on your Security Incident Tracker or reference the code later.
- Your SecurityIncidentTracker folder stays on your Desktop.
- The .NET 10 SDK remains installed and available for other projects.
- If dotnet watch is still running in your terminal, you can leave it running or press Ctrl+C to stop it at any time.
Pause - I'll come back to this later
Stop the running development server to free up your terminal. Your project files stay on disk for next time.
- In the terminal where dotnet watch is running, press Ctrl+C to stop the server.
- Close Visual Studio 2022 if you no longer need it open.
- When you come back, open the SecurityIncidentTracker folder in Visual Studio and run dotnet watch again to resume.
Delete - I don't want to use this again
Remove all project files and start fresh. This cannot be undone.
- Stop the development server by pressing Ctrl+C in the terminal where dotnet watch is running.
- Close Visual Studio 2022.
- Delete the project folder by running this command on macOS/Linux:
rm -rf ~/Desktop/SecurityIncidentTracker
On Windows, run this in PowerShell:
Remove-Item -Recurse -Force ~\Desktop\SecurityIncidentTracker
- To uninstall the .NET 10 SDK on Windows, open Settings, select Apps, then Installed apps. Search for ".NET SDK" and click Uninstall.
- On macOS, you can remove the .NET SDK using the official dotnet-core-uninstall tool from Microsoft. Only do this if you no longer need .NET for any other projects.
Nice Work!
Nice Work!
You just built a fully functional security incident tracking web application using C# and ASP.NET Core MVC. That is a production-grade architectural pattern powering enterprise applications across the industry.
You've learned how to:
- Build a complete ASP.NET Core MVC web application from scratch using C# and .NET 10, including models, views, and controllers working together.
- Create a data model with data annotations for validation and connect it to Entity Framework Core with an in-memory database.
- Implement full CRUD operations through a controller with Razor views, Tag Helpers, form validation, and anti-forgery protection.
- (Secret Mission) Added search and filtering using LINQ queries and query parameters to find incidents by keyword or severity.
Ready to quiz yourself?