How to Scaffold .NET 8 MVC Controller and Views
One of the useful feature of .NET MVC development is the ability to scaffold a new controller for one of your database tables, typically represented as a DbContext Model. The resulting output may not be your final code but it can serve as a useful starting point.
Assumptions
I am assuming here a solution folder with the following structure, you may need to adjust the -dc Scaffold command path to match the location of your application DbContext,
solution root
src (folder to contain all projects)
API
Controllers
appsettings.json
program.cs
Data (project - seperate data layer)
Models
appSettings.json
Site (MVC UI layer)
Dependencies
wwwroot(
Controllers
Models
Views
appSettings.json
program.cs
Steps to Scaffold in .NET 8 Using .NET CLI
1: Install NuGet Packages
You will probably need to add the following NuGet packages to the Project you are doing the scaffolding from:
Microsoft.VisualStudio.Web.CodeGeneration.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
If there is a problem scaffolding then you may need to Install the Code Generation Tool Globally to do this make sure the code generator tool (dotnet-aspnet-codegenerator
) is installed globally. Run this in your terminal:
dotnet tool install --global dotnet-aspnet-codegenerator
TIP: If the dotnet aspnet-codegenerator command (above or below) doesnt work, Save All and then restart visual studio.
Navigate to Your MVC Project Directory In your Terminal (Command Prompt, PowerShell), navigate to the directory where your Site.csproj
file is located.
For example:
cd path\to\your\mvc\project\Site
Run the Correct Scaffolding Command Now, run the following .NET CLI
scaffolding command to generate the controller and views for the Contact
model:
dotnet aspnet-codegenerator controller -name ContactsController -m Contact -dc Data.Models.ApplicationDbContext --relativeFolderPath Controllers --useDefaultLayout --referenceScriptLibraries
As mentioned earlier, adjust the -dc path to your DbContext as required.
Here’s what each argument means:
-name ContactsController
: The name of the controller to be generated.
-m Contact
: The model class (Contact
) for which CRUD operations will be scaffolded.
-dc Data.ApplicationDbContext
: The DbContext class (ApplicationDbContext
) in your Data
project. Make sure the namespace (Data
) matches where your ApplicationDbContext
is located.
--relativeFolderPath Controllers
: This specifies where to generate the controller. It will be placed in the Controllers
folder.
--useDefaultLayout
: This ensures that the views generated will use the default layout (i.e., _Layout.cshtml
).
--referenceScriptLibraries
: This includes script libraries (e.g., for validation) in the generated views.
Check the Results After running this command:
- A
ContactsController.cs
file should be generated in the Controllers
folder of your MVC project.
- Corresponding views (for
Create
, Edit
, Delete
, Details
, and Index
) will be generated in a Views/Contacts
folder.
Additional Considerations
-
DbContext Location: Since your ApplicationDbContext
is in the Data
project, make sure the Data
project is successfully referenced in your MVC project. This is confirmed by the <ProjectReference>
tag in your Site.csproj
, so that part looks good.
Optional additional Step
Add the following navigation links to your shared/_layout.cshtml file.
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="contactsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Contacts
</a>
<ul class="dropdown-menu" aria-labelledby="contactsDropdown">
<li><a class="dropdown-item" asp-controller="Contacts" asp-action="Index">List Contacts</a></li>
<li><a class="dropdown-item" asp-controller="Contacts" asp-action="Create">Create Contact</a></li>
</ul>
</li>
How to Scaffold .NET 8 API Controller
Lets assume that our project looks like this when completed
src (folder to contain all projects)
API
Controllers
ContactsController.cs
appsettings.json
program.cs
Data (project - seperate data layer)
Models
ApplicationDbContext.cs
Contact.cs
appSettings.json
Site (MVC UI layer)
Dependencies
wwwroot(
Controllers
Models
Views
appSettings.json
program.cs
Add NuGet Packages
Add the following NuGet packages to the API project
Microsoft.EntityFrameworkCore.Tools
Microsoft.VisualStudio.Web.CodeGeneration.Design
- Right click on the Controllers folder and Choose Add | Controller
- In the left panel ensure you Click Common | API
- In the Dialog that pops up
- Choose the Model class for the API
- Select the DbContext class
When you click Create the controller will be created in the Controllers folder
Example output:
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Data.Models;
namespace API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ContactsController : ControllerBase
{
private readonly ApplicationDbContext _context;
public ContactsController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/Contacts
[HttpGet]
public async Task<ActionResult<IEnumerable<Contact>>> GetContacts()
{
return await _context.Contacts.ToListAsync();
}
// GET: api/Contacts/5
[HttpGet("{id}")]
public async Task<ActionResult<Contact>> GetContact(Guid id)
{
var contact = await _context.Contacts.FindAsync(id);
if (contact == null)
{
return NotFound();
}
return contact;
}
// PUT: api/Contacts/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutContact(Guid id, Contact contact)
{
if (id != contact.ContactId)
{
return BadRequest();
}
_context.Entry(contact).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ContactExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Contacts
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Contact>> PostContact(Contact contact)
{
_context.Contacts.Add(contact);
await _context.SaveChangesAsync();
return CreatedAtAction("GetContact", new { id = contact.ContactId }, contact);
}
// DELETE: api/Contacts/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteContact(Guid id)
{
var contact = await _context.Contacts.FindAsync(id);
if (contact == null)
{
return NotFound();
}
_context.Contacts.Remove(contact);
await _context.SaveChangesAsync();
return NoContent();
}
private bool ContactExists(Guid id)
{
return _context.Contacts.Any(e => e.ContactId == id);
}
}
}
Modify your Client MVC controller to call the API
1: Dependency Injection setup:
: In the client program.cs we need to configure the HttpClient
for dependency injection:
services.AddControllersWithViews();
services.AddHttpClient(); // Registers HttpClient
var app = builder.Build();
Add the httpClient to the controller constructor
private readonly HttpClient _httpClient;
public ContactsController(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://localhost:5001/api/"); // Replace with your API's base URL
}
Here is the modified MVC controller with all endpoints converted to using the API.
NOTE: I have left the original code in place just remmed out in case its useful.
using Data.Models;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Text;
namespace Site.Controllers
{
public class ContactsController : Controller
{
private readonly ApplicationDbContext _context;
private readonly HttpClient _httpClient;
public ContactsController(ApplicationDbContext context, HttpClient httpClient)
{
_context = context;
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("http://localhost:5111/api/"); // Replace with your API's base URL
}
// GET: Contacts
public async Task<IActionResult> Index()
{
var response = await _httpClient.GetAsync("contacts");
if (response.IsSuccessStatusCode)
{
var jsonData = await response.Content.ReadAsStringAsync();
var contacts = JsonConvert.DeserializeObject<List<Contact>>(jsonData);
return View(contacts);
}
return View("Error"); // Error handling
//return View(await _context.Contacts.ToListAsync());
}
// GET: Contacts/Details/5
public async Task<IActionResult> Details(Guid? id)
{
var response = await _httpClient.GetAsync($"contacts/{id}");
if (response.IsSuccessStatusCode)
{
var jsonData = await response.Content.ReadAsStringAsync();
var contact = JsonConvert.DeserializeObject<Contact>(jsonData);
return View(contact);
}
return View("Error");
//if (id == null)
//{
// return NotFound();
//}
//var contact = await _context.Contacts
// .FirstOrDefaultAsync(m => m.ContactId == id);
//if (contact == null)
//{
// return NotFound();
//}
//return View(contact);
}
// GET: Contacts/Create
public IActionResult Create()
{
return View();
}
// POST: Contacts/Create
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ContactId,Firstname,Lastname,Email,Winner")] Contact contact)
{
if (ModelState.IsValid)
{
contact.ContactId = Guid.NewGuid();
var jsonContent = JsonConvert.SerializeObject(contact);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("contacts", content);
if (response.IsSuccessStatusCode)
{
return RedirectToAction(nameof(Index));
}
}
return View(contact);
//if (ModelState.IsValid)
//{
// contact.ContactId = Guid.NewGuid();
// _context.Add(contact);
// await _context.SaveChangesAsync();
// return RedirectToAction(nameof(Index));
//}
//return View(contact);
}
// GET: Contacts/Edit/5
public async Task<IActionResult> Edit(Guid? id)
{
var response = await _httpClient.GetAsync($"contacts/{id}");
if (response.IsSuccessStatusCode)
{
var jsonData = await response.Content.ReadAsStringAsync();
var contact = JsonConvert.DeserializeObject<Contact>(jsonData);
return View(contact);
}
return View("Error");
//if (id == null)
//{
// return NotFound();
//}
//var contact = await _context.Contacts.FindAsync(id);
//if (contact == null)
//{
// return NotFound();
//}
//return View(contact);
}
// POST: Contacts/Edit/5
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Guid id, [Bind("ContactId,Firstname,Lastname,Email,Winner")] Contact contact)
{
if (ModelState.IsValid)
{
var jsonContent = JsonConvert.SerializeObject(contact);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await _httpClient.PutAsync($"contacts/{id}", content);
if (response.IsSuccessStatusCode)
{
return RedirectToAction(nameof(Index));
}
}
return View(contact);
//if (id != contact.ContactId)
//{
// return NotFound();
//}
//if (ModelState.IsValid)
//{
// try
// {
// _context.Update(contact);
// await _context.SaveChangesAsync();
// }
// catch (DbUpdateConcurrencyException)
// {
// if (!ContactExists(contact.ContactId))
// {
// return NotFound();
// }
// else
// {
// throw;
// }
// }
// return RedirectToAction(nameof(Index));
//}
//return View(contact);
}
// GET: Contacts/Delete/5
public async Task<IActionResult> Delete(Guid? id)
{
var response = await _httpClient.GetAsync($"contacts/{id}");
if (response.IsSuccessStatusCode)
{
var jsonData = await response.Content.ReadAsStringAsync();
var contact = JsonConvert.DeserializeObject<Contact>(jsonData);
return View(contact);
}
return View("Error");
//if (id == null)
//{
// return NotFound();
//}
//var contact = await _context.Contacts
// .FirstOrDefaultAsync(m => m.ContactId == id);
//if (contact == null)
//{
// return NotFound();
//}
//return View(contact);
}
// POST: Contacts/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(Guid id)
{
var response = await _httpClient.DeleteAsync($"contacts/{id}");
if (response.IsSuccessStatusCode)
{
return RedirectToAction(nameof(Index));
}
return View("Error");
//var contact = await _context.Contacts.FindAsync(id);
//if (contact != null)
//{
// _context.Contacts.Remove(contact);
//}
//await _context.SaveChangesAsync();
//return RedirectToAction(nameof(Index));
}
private bool ContactExists(Guid id)
{
//No longer required at this end, this is now done in the API Controller
return _context.Contacts.Any(e => e.ContactId == id);
}
}
}