Persist Honey Orders with MVC
Build an MVC honey store with validated forms and persistent SQLite orders.
Introduction
30 Second Summary
A honey catalog can look ready for customers while every order vanishes as soon as the page closes. That leaves no record to review when you return.
In this project, you will turn Honeybee Harvest into a local order app with ASP.NET Core MVC. Valid submissions are stored in SQLite, so they remain available after the app restarts.
What You'll Build
You'll place a honey order in Safari before restarting the app to find the same record waiting on the Saved Orders page.
By the end of this project, you'll have:
- A server-rendered catalog that turns C# product data into three reusable product cards through a strongly typed Razor view.
- A validated order form that displays a specific message beside every unusable field.
- A persistent order history that keeps submitted orders available after the local app restarts.
- Secret Mission: Add a product filter to Saved Orders so you can isolate one honey variety without changing the database schema.
Are there any prerequisites?
This project builds on the Honeybee Harvest frontend from Build a Bee and Honey Landing Page. You should have prior ASP.NET Core experience plus Visual Studio Code with C# Dev Kit installed on your Mac.
Before We Start
The project needs a clear destination before the hands-on work begins. You are rebuilding Honeybee Harvest as a local ASP.NET Core MVC application.
The finished catalog comes from C# data. Its server-side order form rejects invalid submissions.
Accepted orders are stored in SQLite. The saved records remain available after the application restarts.
Set Up the MVC Project
The Honeybee Harvest frontend can show a catalog. Static files cannot run controllers or C#, so the project needs a local web host before any storefront code can move across.
You will verify the .NET SDK first. You will then create an ASP.NET Core MVC project that serves a starter page in Safari.
In this step, get ready to:
- Verify that .NET SDK 10.0.401 is installed.
- Trust the local HTTPS development certificate.
- Prove the new MVC project runs in Safari.
Verify the .NET SDK
The .NET SDK supplies the compiler, project templates, and command-line tools used throughout this project. Version 10.0.401 includes the .NET 10 tools needed by the MVC application.
- Press Cmd+Space to open Spotlight.
- Type Terminal and press Enter to open it.
- Check the installed SDK by running this command:
dotnet --info
What does this check show?
- The SDK section identifies the .NET tools available for creating projects.
- The runtime section identifies the installed components that can run ASP.NET Core applications.
- Finding 10.0.401 in the SDK section confirms that this project can target net10.0.
✔️ I see the required SDK
You should see 10.0.401 in the SDK details. Your .NET tools are ready for the MVC scaffold.
ⓧ I see a different SDK version
Choosing the correct installer can feel fiddly. Apple silicon Macs use Arm64. Intel Macs use x64.
- Visit the official .NET 10 download page.
- Find the SDK 10.0.401 macOS installer.
- Choose Arm64 for an Apple silicon Mac.
- Choose x64 for an Intel Mac.
- Run the downloaded installer.
- Return to Terminal after the installation completes.
- Check the installed SDK again by running this command:
dotnet --info
What confirms the upgrade?
The SDK details should now include 10.0.401. This confirms the required tools are available.
ⓧ Command not found
Terminal cannot find the .NET command because the SDK is not installed yet. The official macOS installer adds the SDK plus the required .NET and ASP.NET Core runtimes.
- Visit the official .NET 10 download page.
- Find the SDK 10.0.401 macOS installer.
- Choose Arm64 for an Apple silicon Mac.
- Choose x64 for an Intel Mac.
- Run the downloaded installer.
- Close Terminal after the installation completes.
- Open a new Terminal window through Spotlight.
- Check the installed SDK by running this command:
dotnet --info
What confirms the installation?
The SDK details should include 10.0.401. A fresh Terminal window ensures the newly installed command is available.
Still missing the required SDK?
- Confirm that you downloaded the SDK installer instead of a runtime-only installer.
- Confirm that the installer architecture matches your Mac processor.
- Close every Terminal window after installation before checking again.
Share the SDK section from your output when asking the NextWork community for help. Help me diagnose why .NET SDK 10.0.401 is missing on my Mac.
Create the MVC project
Local HTTPS lets the development application serve a secure browser address. Trusting the development certificate prevents Safari from treating that local connection as unknown.
macOS may ask you to approve the certificate change. This affects the local development certificate used by ASP.NET Core projects on your Mac.
- Trust the local development certificate by running this command:
dotnet dev-certs https --trust
What does this command do?
The command asks macOS to trust the ASP.NET Core HTTPS development certificate. This allows local projects to use secure browser addresses during development.
You should see confirmation that the HTTPS development certificate is trusted. Approve the macOS prompt if it appears.
Certificate trust did not complete?
- Check for a macOS approval prompt behind the Terminal window.
- Confirm that the SDK check lists 10.0.401.
- Run the certificate command again after approving any pending macOS prompt.
Need another pair of eyes? Help me troubleshoot trusting the ASP.NET Core HTTPS development certificate on macOS.
The MVC template creates the project structure for controllers, views, configuration, and static assets. Creating it on your Desktop gives the project a predictable location.
- Move to your Desktop by running this command:
cd ~/Desktop
What does this command do?
The cd command changes Terminal's current folder. The ~ character represents your macOS home folder.
- Create the HoneybeeHarvest MVC project by running this command:
dotnet new mvc -o HoneybeeHarvest --framework net10.0
What does this command create?
- The MVC template creates the starter controllers and Razor views.
- The output option places every generated file inside HoneybeeHarvest.
- The framework option targets net10.0.
You should see messages confirming that the MVC template was created. The command also restores the starter project's dependencies.
Project scaffold failed?
- Confirm that the SDK check lists 10.0.401.
- Confirm that Terminal is still on your Desktop.
- Check whether a folder named HoneybeeHarvest already exists before retrying the scaffold.
Need help reading the scaffold output? Help me troubleshoot why my ASP.NET Core MVC scaffold did not complete.
- Press Cmd+Space to open Spotlight.
- Type Visual Studio Code and press Enter to open Visual Studio Code.
- Select the folder-opening option on the Visual Studio Code welcome screen.
- Choose the HoneybeeHarvest folder on your Desktop.
The scaffold is in place. You should see the generated project files in Visual Studio Code's Explorer sidebar.
✔️ I see the generated project
Your HoneybeeHarvest folder is open in Visual Studio Code. The starter project is ready to run.
ⓧ I'd like to double check the scaffold
- Confirm that Controllers/HomeController.cs exists and serves the starter home page.
- Confirm that HoneybeeHarvest.csproj exists and targets net10.0.
- Confirm that Program.cs exists and configures MVC services, middleware, static assets, and the default Home controller route.
- Confirm that Views/Home/Index.cshtml exists and contains the starter home page.
- Confirm that appsettings.json exists.
- Confirm that wwwroot/css/site.css exists.
Run the starter application
Running the scaffold proves that its code compiles and that Kestrel can serve the MVC application. The printed localhost address connects Safari to that local web host.
- Switch back to the Terminal window from earlier.
- Enter the generated project folder by running this command:
cd HoneybeeHarvest
What does this command do?
Terminal moves from your Desktop into HoneybeeHarvest. The project run command can now find HoneybeeHarvest.csproj.
The app keeps this Terminal session busy while it runs. You can stop it later with Control+C.
Before you run this, do you expect the project to stop during its build or start listening for browser requests?
- Build and start the MVC application by running this command:
dotnet run
What does this command do?
- The command builds the generated C# project.
- Kestrel starts the local web host after the build succeeds.
- The terminal prints one or more localhost addresses that Safari can open.
You should see a localhost address in the Terminal output. That address proves Kestrel is listening for browser requests.
- Record the printed address here: your printed localhost URL.
- Press Cmd+Space to open Spotlight.
- Type Safari and press Enter to open it.
- Select Safari's address field.
- Paste your printed localhost URL into the address field.
- Press Enter to load the application.
Safari should display the ASP.NET Core starter page. This proves the generated controller route, Razor view, static assets, and local host are working together.
Starter page did not load?
- Confirm that Terminal still shows the running application.
- Copy the complete localhost address printed by Kestrel.
- Check that Terminal ran the project from the HoneybeeHarvest folder.
Still stuck? Help me diagnose why my ASP.NET Core MVC starter page does not load in Safari.
You have cleared the setup hurdle. Your MVC host now builds successfully and serves browser requests from the HoneybeeHarvest project directory.
Your local MVC application is ready. Next, you will replace the starter page with the C#-rendered Honeybee Harvest catalog.
Render the Honey Catalog
Your ASP.NET Core MVC host already answers local requests in Safari. The starter page still hides the product data that makes Honeybee Harvest useful.
A visible catalog makes the MVC data flow concrete. In this step, a controller sends three C# product objects to a strongly typed Razor view.
The controller chooses the data for a request. The view turns that data into the HTML you see in the browser.
The @model Product[] declaration gives the view a known model type. Razor can then check each product property while the project builds.
In this step, get ready to:
- Define the three Honeybee Harvest products in C#.
- Pass the catalog from the home controller into a strongly typed Razor view.
- Style the catalog with Honeybee Harvest navigation and responsive product cards.
Define the catalog data
A model describes the data your application works with. The catalog uses one product shape for every jar of honey.
- In the VS Code Explorer sidebar, right-click the Models folder.
- Select New File.
- Enter Product.cs as the file name.
- Define the product shape by pasting this code:
namespace HoneybeeHarvest.Models;
public sealed class Product
{
public string Name { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public decimal Price { get; init; }
}
What does this model hold?
- The Product class gives every catalog item the same structure.
- The Name property holds the product label shown on its card.
- The Description property holds the short catalog copy.
- The Price property uses a decimal value for the displayed price.
- Save Models/Product.cs.
- Confirm Product.cs appears inside the Models folder in the Explorer sidebar.
Cannot find Product.cs?
- Expand the Models folder in the Explorer sidebar.
- Check that the file name ends with .cs.
Ask for help with finding the product model.
The product class defines one catalog item. A separate catalog class now creates the three items that the controller can reuse.
- Right-click the Models folder in the Explorer sidebar.
- Select New File.
- Enter Catalog.cs as the file name.
- Create the shared product collection by pasting this code:
namespace HoneybeeHarvest.Models;
public static class Catalog
{
public static Product[] Products { get; } =
[
new Product
{
Name = "Wildflower Honey",
Description = "A mellow fictional blend with a floral finish.",
Price = 12.00m
},
new Product
{
Name = "Citrus Blossom",
Description = "A bright fictional jar inspired by spring orchards.",
Price = 14.00m
},
new Product
{
Name = "Forest Honey",
Description = "A dark, rich fictional variety for the demo catalog.",
Price = 16.00m
}
];
}
How does the catalog work?
- The static Catalog class exposes one shared product collection.
- Each Product initializer supplies the values shown on one catalog card.
- The Products array gives the controller all three items in one value.
- Save Models/Catalog.cs.
- Switch back to the terminal from the previous step.
- Stop Kestrel with Control+C if it is still running.
- Compile the new model files by starting the app with this command:
dotnet run
What does this run prove?
The application has to compile Product.cs together with Catalog.cs before Kestrel starts. A printed localhost address confirms that both model files compile successfully.
- Confirm the terminal prints a localhost address for the running application.
- Stop Kestrel with Control+C before editing the controller.
Good progress. Your catalog data now has a reusable C# structure that the MVC request flow can deliver.
Does the model fail to compile?
- Check that both files use the HoneybeeHarvest.Models namespace.
- Compare every property name in Catalog.cs with the properties in Product.cs.
- Check the closing braces at the bottom of both files.
Ask for help with debugging the catalog models.
Connect the controller to the Razor view
The model data exists in memory. The home controller must pass that data into the view that handles the home page.
- Select Controllers/HomeController.cs in the Explorer sidebar.
- Select all of the existing template code.
- Replace the selected code with this controller:
using HoneybeeHarvest.Models;
using Microsoft.AspNetCore.Mvc;
namespace HoneybeeHarvest.Controllers;
public sealed class HomeController : Controller
{
public IActionResult Index()
{
return View(Catalog.Products);
}
}
What does this controller do?
- The models import makes Catalog available to the controller.
- The Index() action handles the default home request.
- The View(Catalog.Products) result passes the product array into the matching view.
- Save Controllers/HomeController.cs.
- Start the updated controller by running this command in the terminal:
dotnet run
What does the controller check prove?
The run confirms that the controller can resolve Catalog.Products. The existing starter view can receive the request while you prepare its catalog markup.
- Return to the starter page in Safari.
- Refresh the page to confirm the home request still succeeds.
- Stop Kestrel with Control+C.
Does the home request fail?
- Check that HomeController.cs imports HoneybeeHarvest.Models.
- Check that the action passes Catalog.Products to View().
Ask for help with connecting the catalog to HomeController.
A strongly typed view declares the exact data it expects. The view can then loop through the product array without repeating any catalog values in HTML.
- Select Views/Home/Index.cshtml in the Explorer sidebar.
- Select all of the starter page markup.
- Replace the selected markup with this catalog view:
@model Product[]
@{
ViewData["Title"] = "Honey Catalog";
}
<section class="hero">
<div class="page-shell">
<p class="eyebrow">Server-rendered honey shop</p>
<h1>Choose your honey</h1>
<p>This catalog is rendered from C# product data through an MVC controller and Razor view.</p>
</div>
</section>
<section class="page-shell section-space">
<div class="product-grid">
@foreach (var product in Model)
{
<article class="product-card">
<div class="jar-art" aria-hidden="true"></div>
<h2>@product.Name</h2>
<p>@product.Description</p>
<p class="price">$@product.Price</p>
<a class="button" asp-controller="Orders" asp-action="Create" asp-route-product="@product.Name">
Order this honey
</a>
</article>
}
</div>
</section>
How does the view render three cards?
- The @model Product[] directive declares the view model type.
- The @foreach loop renders one article for every product.
- Each Razor expression reads a value from the current product.
- The route attribute carries the selected product name into the order URL.
- Save Views/Home/Index.cshtml.
- Start the catalog version of the app by running this command:
dotnet run
What does this run connect?
The home request now travels through Index() into the strongly typed view. Razor repeats the card markup once for each product in the model.
Before you refresh, how many product cards do you expect the loop to render?
- Return to Safari.
- Refresh the home page.
You should see Wildflower Honey, Citrus Blossom, and Forest Honey as three unpolished catalog cards. Each card includes its own description, price, and order link.
That is the main data flow working. One C# collection now drives every product card you see in Safari.
Do the product cards fail to appear?
- Check that the first line of Index.cshtml is @model Product[].
- Check that the loop reads from Model with an uppercase first letter.
- Check that HomeController passes Catalog.Products.
Ask for help with debugging the Razor catalog.
Style the Honeybee Harvest catalog
The product loop proves that the data flow works. A shared layout now gives every page the same navigation before CSS turns the catalog into a responsive storefront.
- Select Views/Shared/_Layout.cshtml in the Explorer sidebar.
- Select all of the existing layout markup.
- Replace the selected markup with this Honeybee Harvest layout:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@ViewData["Title"] - Honeybee Harvest</title>
<link rel="stylesheet" href="/css/site.css" />
</head>
<body>
<header class="site-header">
<nav class="nav-shell" aria-label="Primary navigation">
<a class="brand" asp-controller="Home" asp-action="Index">Honeybee Harvest</a>
<div class="nav-links">
<a asp-controller="Home" asp-action="Index">Catalog</a>
<a asp-controller="Orders" asp-action="Create">Order Honey</a>
<a asp-controller="Orders" asp-action="Index">Saved Orders</a>
</div>
</nav>
</header>
<main>
@RenderBody()
</main>
<footer class="site-footer">
<p>Honeybee Harvest is a fictional learning project.</p>
</footer>
</body>
</html>
What does the shared layout provide?
- The shared header gives every page the same Honeybee Harvest navigation.
- The @RenderBody() expression inserts the current view inside the shared page structure.
- The stylesheet link loads wwwroot/css/site.css for every rendered page.
- Save Views/Shared/_Layout.cshtml.
- Stop Kestrel with Control+C.
- Restart the app with the new layout by running this command:
dotnet run
What does the layout restart load?
The restarted application renders the new shared layout around the home view. This gives the catalog a common header, navigation area, and footer.
- Return to Safari.
- Refresh the catalog page.
You should see Honeybee Harvest above the catalog with links for Catalog, Order Honey, and Saved Orders. The footer identifies the site as a fictional learning project.
Is the old layout still showing?
- Check that you edited Views/Shared/_Layout.cshtml.
- Confirm the layout still contains @RenderBody().
- Refresh Safari after restarting Kestrel.
Ask for help with loading the shared layout.
The stylesheet is easiest to verify in small layers. Each layer changes a specific part of the catalog so you can spot mistakes close to the edit that caused them.
- Select wwwroot/css/site.css in the Explorer sidebar.
- Select all of the existing template styles.
- Replace the selected styles with these color and page foundations:
:root {
--ink: #2f2a24;
--brown: #69452f;
--amber: #e99a12;
--gold: #ffc43d;
--cream: #fffaf0;
--soft-gold: #fff1c7;
--green: #315c45;
--white: #ffffff;
--danger: #9d1c1c;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--ink);
background: var(--cream);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
}
a {
color: var(--brown);
}
What do the foundation styles control?
- The variables keep the Honeybee Harvest color palette in one place.
- The universal selector makes element sizing predictable.
- The body rules apply the cream background, readable type, and dark text across the site.
- Save wwwroot/css/site.css.
- Refresh Safari.
You should see the page switch to a cream background with dark text and brown links.
Are the foundation colors missing?
- Check that the layout links to /css/site.css.
- Check that every color variable starts with two hyphens.
Ask for help with loading the foundation CSS.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the shared typography and page-width styles by pasting this code:
h1,
h2,
p {
margin-top: 0;
}
h1,
h2,
.brand {
line-height: 1.1;
font-weight: 800;
}
h1 {
font-size: clamp(2.4rem, 7vw, 4.8rem);
letter-spacing: -0.045em;
}
.site-header {
background: rgba(255, 250, 240, 0.96);
border-bottom: 1px solid rgba(105, 69, 47, 0.18);
}
.nav-shell,
.page-shell,
.site-footer {
width: min(1100px, calc(100% - 2rem));
margin-inline: auto;
}
How do these rules shape the page?
- The heading rules create a clear visual hierarchy.
- The shared width keeps the navigation, page content, and footer aligned.
- The header border separates navigation from the page content.
- Save wwwroot/css/site.css.
- Refresh Safari.
You should see a larger catalog heading inside a centered page width.
Is the page still full width?
- Check the commas between .nav-shell, .page-shell, and .site-footer.
- Check that the shared rule includes margin-inline: auto.
Ask for help with centering the catalog layout.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the responsive navigation and hero styles by pasting this code:
.nav-shell {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: space-between;
min-height: 4.5rem;
}
.brand,
.nav-links a {
text-decoration: none;
font-weight: 800;
}
.nav-links {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.hero {
padding: 5rem 0;
background: radial-gradient(circle at top right, #ffe399 0, transparent 36%), var(--soft-gold);
}
What changes in the header and hero?
- The navigation uses flexible rows that can wrap on a narrow screen.
- The brand and navigation links use heavier text without underlines.
- The hero uses spacing and a soft radial highlight to frame the page title.
- Save wwwroot/css/site.css.
- Refresh Safari.
You should see the navigation spread across the header and a gold-toned hero behind the catalog title.
Is the hero background missing?
- Check that the view uses class="hero" on its first section.
- Check that the radial gradient rule ends with var(--soft-gold).
Ask for help with styling the navigation and hero.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the page spacing and responsive product grid by pasting this code:
.section-space {
padding-block: 4rem;
}
.narrow-shell {
max-width: 720px;
}
.eyebrow {
color: var(--green);
font-size: 0.78rem;
font-weight: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.product-grid {
display: grid;
gap: 1.25rem;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
How does the catalog become responsive?
- The section spacing separates the catalog from the hero.
- The eyebrow style creates a compact label above the main heading.
- The product grid automatically chooses the number of columns that fit the available width.
- Save wwwroot/css/site.css.
- Refresh Safari.
You should see the three products arranged in responsive columns with space between them.
Are the products still stacked tightly?
- Check that the catalog container uses class="product-grid".
- Check the spelling of grid-template-columns.
Ask for help with fixing the responsive product grid.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the card surfaces and honey jar artwork by pasting this code:
.product-card,
.order-form,
.empty-state,
.table-shell {
padding: 1.5rem;
background: var(--white);
border: 2px solid rgba(105, 69, 47, 0.18);
border-radius: 1.2rem;
box-shadow: 0 18px 40px rgba(75, 56, 45, 0.08);
}
.product-card {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.jar-art {
width: 88px;
height: 96px;
margin-bottom: 1rem;
background: linear-gradient(#ffd970 0 25%, #e99a12 25% 100%);
border: 5px solid var(--brown);
border-radius: 14px 14px 24px 24px;
box-shadow: inset 0 0 0 10px rgba(255, 255, 255, 0.28);
}
How do the products become cards?
- The shared surface rules give cards and future form elements a consistent white panel.
- The product card uses a vertical layout that keeps its content aligned.
- The jar artwork uses a gradient, border, and rounded corners without an image file.
- Save wwwroot/css/site.css.
- Refresh Safari.
You should see three white product cards with a small illustrated honey jar at the top of each card.
Are the cards or jars missing?
- Check that each article uses class="product-card".
- Check that each card contains an element with class="jar-art".
Ask for help with rendering the product cards and jars.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the price and order button styles by pasting this code:
.price {
color: var(--green);
font-size: 1.15rem;
font-weight: 900;
}
.button {
display: inline-block;
margin-top: auto;
padding: 0.75rem 1rem;
color: var(--ink);
background: var(--gold);
border: 2px solid var(--ink);
border-radius: 0.65rem;
box-shadow: 4px 4px 0 var(--ink);
font: inherit;
font-weight: 900;
text-decoration: none;
cursor: pointer;
}
What makes the order link stand out?
- The price uses green text with a heavier weight.
- The button uses gold, a dark border, and an offset shadow for contrast.
- The automatic top margin keeps each order link aligned near the bottom of its card.
- Save wwwroot/css/site.css.
- Refresh Safari.
You should see green prices and gold order buttons aligned across the product cards.
Do the order links look like plain text?
- Check that every order link uses class="button".
- Check that the .button selector has its closing brace.
Ask for help with styling the order links.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the interactive button and reusable form layout styles by pasting this code:
.button:hover,
.button:focus {
color: var(--white);
background: var(--brown);
box-shadow: none;
}
.order-form {
display: grid;
gap: 1rem;
}
.field-group {
display: grid;
gap: 0.35rem;
}
label {
font-weight: 800;
}
What do these reusable styles prepare?
- The hover and focus states make an active button easy to identify.
- The order form rules prepare a spaced grid for the form you build next.
- The field group rules keep each future label close to its input.
- Save wwwroot/css/site.css.
- Refresh Safari.
- Move the pointer over an order button.
You should see the button switch to a brown background with white text.
Does the button stay gold?
- Check that both selectors begin with .button.
- Check the comma between the hover selector and the focus selector.
Ask for help with fixing the button interaction.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the reusable input and focus styles by pasting this code:
input,
select {
width: 100%;
padding: 0.75rem;
color: var(--ink);
background: var(--white);
border: 2px solid rgba(105, 69, 47, 0.4);
border-radius: 0.55rem;
font: inherit;
}
input:focus,
select:focus {
border-color: var(--amber);
outline: 3px solid rgba(233, 154, 18, 0.22);
}
Why add input styles now?
The shared stylesheet also supports the order form introduced in the next step. Inputs and selects inherit the catalog type while their focus outline makes keyboard position visible.
- Save wwwroot/css/site.css.
- Confirm the input selectors appear below the button rules in the file.
Are the input rules misplaced?
- Check that the new selectors sit outside the closing brace for label.
- Check that input and select share the same declaration block.
Ask for help with checking the reusable input CSS.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Add the validation and responsive table styles by pasting this code:
.validation-message,
.field-validation-error,
.validation-summary-errors {
color: var(--danger);
font-weight: 800;
}
.table-shell {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 0.8rem;
border-bottom: 1px solid rgba(105, 69, 47, 0.2);
text-align: left;
}
What do these future-facing rules support?
- Validation messages use the danger color with heavier text.
- The table wrapper allows wide order data to scroll on small screens.
- The table rules keep saved-order columns aligned and separated.
- Save wwwroot/css/site.css.
- Confirm the table rules appear below the input focus styles.
Does the CSS file show a syntax warning?
- Check the commas in the validation selector group.
- Check that every table declaration block has a closing brace.
Ask for help with checking the validation and table CSS.
- Place the cursor after the final closing brace in wwwroot/css/site.css.
- Finish the stylesheet with the footer rules by pasting this code:
.site-footer {
padding-block: 1.5rem;
border-top: 1px solid rgba(105, 69, 47, 0.18);
}
.site-footer p {
margin: 0;
}
What finishes the page frame?
The footer gets its own spacing and a subtle top border. Removing the paragraph margin keeps the footer compact.
- Save wwwroot/css/site.css.
- Refresh Safari.
You should see a compact footer separated from the catalog by a thin border.
Is the footer spacing missing?
- Check that the layout uses class="site-footer".
- Check that the footer rules sit at the bottom of site.css.
Ask for help with finishing the footer styles.
Use this comparison if you want to check every file changed in this step.
✔️ Awesome, I've got everything!
Great. Save every changed file before the final catalog check.
ⓧ I'd like to double check the full code
- Compare each changed file with the complete versions below.
namespace HoneybeeHarvest.Models;
public sealed class Product
{
public string Name { get; init; } = string.Empty;
public string Description { get; init; } = string.Empty;
public decimal Price { get; init; }
}
namespace HoneybeeHarvest.Models;
public static class Catalog
{
public static Product[] Products { get; } =
[
new Product
{
Name = "Wildflower Honey",
Description = "A mellow fictional blend with a floral finish.",
Price = 12.00m
},
new Product
{
Name = "Citrus Blossom",
Description = "A bright fictional jar inspired by spring orchards.",
Price = 14.00m
},
new Product
{
Name = "Forest Honey",
Description = "A dark, rich fictional variety for the demo catalog.",
Price = 16.00m
}
];
}
using HoneybeeHarvest.Models;
using Microsoft.AspNetCore.Mvc;
namespace HoneybeeHarvest.Controllers;
public sealed class HomeController : Controller
{
public IActionResult Index()
{
return View(Catalog.Products);
}
}
@model Product[]
@{
ViewData["Title"] = "Honey Catalog";
}
<section class="hero">
<div class="page-shell">
<p class="eyebrow">Server-rendered honey shop</p>
<h1>Choose your honey</h1>
<p>This catalog is rendered from C# product data through an MVC controller and Razor view.</p>
</div>
</section>
<section class="page-shell section-space">
<div class="product-grid">
@foreach (var product in Model)
{
<article class="product-card">
<div class="jar-art" aria-hidden="true"></div>
<h2>@product.Name</h2>
<p>@product.Description</p>
<p class="price">$@product.Price</p>
<a class="button" asp-controller="Orders" asp-action="Create" asp-route-product="@product.Name">
Order this honey
</a>
</article>
}
</div>
</section>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@ViewData["Title"] - Honeybee Harvest</title>
<link rel="stylesheet" href="/css/site.css" />
</head>
<body>
<header class="site-header">
<nav class="nav-shell" aria-label="Primary navigation">
<a class="brand" asp-controller="Home" asp-action="Index">Honeybee Harvest</a>
<div class="nav-links">
<a asp-controller="Home" asp-action="Index">Catalog</a>
<a asp-controller="Orders" asp-action="Create">Order Honey</a>
<a asp-controller="Orders" asp-action="Index">Saved Orders</a>
</div>
</nav>
</header>
<main>
@RenderBody()
</main>
<footer class="site-footer">
<p>Honeybee Harvest is a fictional learning project.</p>
</footer>
</body>
</html>
:root {
--ink: #2f2a24;
--brown: #69452f;
--amber: #e99a12;
--gold: #ffc43d;
--cream: #fffaf0;
--soft-gold: #fff1c7;
--green: #315c45;
--white: #ffffff;
--danger: #9d1c1c;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--ink);
background: var(--cream);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
}
a {
color: var(--brown);
}
h1,
h2,
p {
margin-top: 0;
}
h1,
h2,
.brand {
line-height: 1.1;
font-weight: 800;
}
h1 {
font-size: clamp(2.4rem, 7vw, 4.8rem);
letter-spacing: -0.045em;
}
.site-header {
background: rgba(255, 250, 240, 0.96);
border-bottom: 1px solid rgba(105, 69, 47, 0.18);
}
.nav-shell,
.page-shell,
.site-footer {
width: min(1100px, calc(100% - 2rem));
margin-inline: auto;
}
.nav-shell {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: space-between;
min-height: 4.5rem;
}
.brand,
.nav-links a {
text-decoration: none;
font-weight: 800;
}
.nav-links {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.hero {
padding: 5rem 0;
background: radial-gradient(circle at top right, #ffe399 0, transparent 36%), var(--soft-gold);
}
.section-space {
padding-block: 4rem;
}
.narrow-shell {
max-width: 720px;
}
.eyebrow {
color: var(--green);
font-size: 0.78rem;
font-weight: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.product-grid {
display: grid;
gap: 1.25rem;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.product-card,
.order-form,
.empty-state,
.table-shell {
padding: 1.5rem;
background: var(--white);
border: 2px solid rgba(105, 69, 47, 0.18);
border-radius: 1.2rem;
box-shadow: 0 18px 40px rgba(75, 56, 45, 0.08);
}
.product-card {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.jar-art {
width: 88px;
height: 96px;
margin-bottom: 1rem;
background: linear-gradient(#ffd970 0 25%, #e99a12 25% 100%);
border: 5px solid var(--brown);
border-radius: 14px 14px 24px 24px;
box-shadow: inset 0 0 0 10px rgba(255, 255, 255, 0.28);
}
.price {
color: var(--green);
font-size: 1.15rem;
font-weight: 900;
}
.button {
display: inline-block;
margin-top: auto;
padding: 0.75rem 1rem;
color: var(--ink);
background: var(--gold);
border: 2px solid var(--ink);
border-radius: 0.65rem;
box-shadow: 4px 4px 0 var(--ink);
font: inherit;
font-weight: 900;
text-decoration: none;
cursor: pointer;
}
.button:hover,
.button:focus {
color: var(--white);
background: var(--brown);
box-shadow: none;
}
.order-form {
display: grid;
gap: 1rem;
}
.field-group {
display: grid;
gap: 0.35rem;
}
label {
font-weight: 800;
}
input,
select {
width: 100%;
padding: 0.75rem;
color: var(--ink);
background: var(--white);
border: 2px solid rgba(105, 69, 47, 0.4);
border-radius: 0.55rem;
font: inherit;
}
input:focus,
select:focus {
border-color: var(--amber);
outline: 3px solid rgba(233, 154, 18, 0.22);
}
.validation-message,
.field-validation-error,
.validation-summary-errors {
color: var(--danger);
font-weight: 800;
}
.table-shell {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
padding: 0.8rem;
border-bottom: 1px solid rgba(105, 69, 47, 0.2);
text-align: left;
}
.site-footer {
padding-block: 1.5rem;
border-top: 1px solid rgba(105, 69, 47, 0.18);
}
.site-footer p {
margin: 0;
}
- Stop Kestrel with Control+C.
Before you run the final check, what should Safari show when the controller, view, layout, and stylesheet all work together?
- Start the completed catalog by running this command:
dotnet run
What does the final run prove?
The final run compiles the C# models, controller, and Razor view as one application. Kestrel then serves the shared layout and stylesheet to Safari.
- Return to the localhost page in Safari.
- Refresh the page.
You should see Wildflower Honey, Citrus Blossom, and Forest Honey as three styled cards. You should also see the Honeybee Harvest navigation with an order link on every card.
You made the catalog dynamic. The home page now turns reusable C# product data into a responsive storefront.
Your MVC catalog can now display server-side data. Next, you will connect each product link to an order form handled by a controller.
Post an Order to a Controller
Your catalog now proves that ASP.NET Core MVC can send product data from a controller into a view. Visitors still need a way to send their order details back to the server.
This step creates that return path through model binding. You will deliberately leave out validation so you can see why accepting posted fields is only the first half of a reliable form.
In this step, get ready to:
- Define the customer details that an order form can submit.
- Handle the order form through GET and POST controller actions.
- Submit incomplete data to reveal the missing validation rules.
Create the order models
The input model describes the fields that MVC accepts from the request. A separate view model carries those submitted fields alongside the catalog choices needed by the form.
- In the VS Code Explorer sidebar, use the new-file control beside the Models folder.
- Name the file OrderInputModel.cs.
- Define the posted order fields by adding this code:
namespace HoneybeeHarvest.Models;
public sealed class OrderInputModel
{
public string CustomerName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; } = 1;
}
What Do These Properties Hold?
- CustomerName and Email hold the visitor details posted by the form.
- ProductName identifies the selected catalog item.
- Quantity starts at 1 so a fresh form has a usable default.
These properties have no data annotation attributes yet. MVC can populate them without deciding whether their values meet your business rules.
- Save OrderInputModel.cs.
✔️ Awesome, I've got everything!
Your input model now contains the four fields the form can post.
ⓧ I'd like to double check the full code
namespace HoneybeeHarvest.Models;
public sealed class OrderInputModel
{
public string CustomerName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; } = 1;
}
- Use the new-file control beside the Models folder again.
- Name the file OrderCreateViewModel.cs.
- Combine the posted input with the catalog choices by adding this code:
namespace HoneybeeHarvest.Models;
public sealed class OrderCreateViewModel
{
public OrderInputModel Input { get; set; } = new();
public Product[] Products { get; set; } = [];
}
Why Use a View Model?
Input groups the fields that arrive from the form. Products supplies the choices that the view renders in its honey selector.
Keeping these responsibilities in one view model gives the view a single strongly typed object. The controller can also repopulate the product choices whenever it returns the form.
- Save OrderCreateViewModel.cs.
✔️ Awesome, I've got everything!
The form can now receive its input fields and product choices through one model.
ⓧ I'd like to double check the full code
namespace HoneybeeHarvest.Models;
public sealed class OrderCreateViewModel
{
public OrderInputModel Input { get; set; } = new();
public Product[] Products { get; set; } = [];
}
- Check that both model files compile by running this command in the integrated terminal:
dotnet run
What Does This Check Prove?
The command rebuilds the project before starting the local MVC host. A successful start confirms that both model classes compile with the existing catalog code.
You will see the local host start without compiler errors. Your existing catalog remains available in Safari.
Seeing a Model Compilation Error?
- Check that both files use the HoneybeeHarvest.Models namespace.
- Confirm that OrderCreateViewModel.cs spells OrderInputModel exactly as the first class does.
- Check that each property sits inside its class braces.
Help me fix the model compilation error.
Add the order controller
A GET action prepares a form for display. A POST action receives the submitted values through the same view model.
Why Separate GET and POST?
The GET request asks the server for a form. The POST request sends the completed form back as a state-changing request.
The Form Tag Helper adds an anti-forgery token to the form. The ValidateAntiForgeryToken attribute checks that token when the POST action runs.
- Stop the running app in the integrated terminal.
- In the VS Code Explorer sidebar, use the new-file control beside the Controllers folder.
- Name the file OrdersController.cs.
- Create the GET-to-POST request loop by adding this code:
using HoneybeeHarvest.Models;
using Microsoft.AspNetCore.Mvc;
namespace HoneybeeHarvest.Controllers;
public sealed class OrdersController : Controller
{
[HttpGet]
public IActionResult Create(string? product)
{
return View(new OrderCreateViewModel
{
Input = new OrderInputModel
{
ProductName = product ?? string.Empty
},
Products = Catalog.Products
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(OrderCreateViewModel model)
{
model.Products = Catalog.Products;
ViewData["Submitted"] = true;
return View(model);
}
}
What Does This Controller Do?
- Create(string? product) reads the product value from the catalog link. It uses that value to preselect a honey product.
- Catalog.Products supplies the product choices needed by the form.
- Create(OrderCreateViewModel model) receives the posted fields through model binding.
- ViewData["Submitted"] tells the view to show the temporary preview.
The POST action returns the submitted model directly to the view. It performs no validity check and writes nothing to a database.
- Save OrdersController.cs.
- Check that the controller compiles by running this command:
dotnet run
What Does This Run Confirm?
A successful start confirms that MVC recognizes the controller class and both action signatures. The form view comes next.
You will see the local host start without controller compilation errors.
Controller Not Compiling?
- Confirm that OrdersController.cs sits inside the Controllers folder.
- Check that the file imports HoneybeeHarvest.Models and Microsoft.AspNetCore.Mvc.
- Confirm that both action methods are inside the OrdersController braces.
Help me debug the order controller.
✔️ Awesome, I've got everything!
Your controller now prepares the form and receives its posted values.
ⓧ I'd like to double check the full code
using HoneybeeHarvest.Models;
using Microsoft.AspNetCore.Mvc;
namespace HoneybeeHarvest.Controllers;
public sealed class OrdersController : Controller
{
[HttpGet]
public IActionResult Create(string? product)
{
return View(new OrderCreateViewModel
{
Input = new OrderInputModel
{
ProductName = product ?? string.Empty
},
Products = Catalog.Products
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(OrderCreateViewModel model)
{
model.Products = Catalog.Products;
ViewData["Submitted"] = true;
return View(model);
}
}
Build and test the order form
A strongly typed Razor view connects each visible form control to a property under Input. Tag Helpers generate the matching field names that model binding expects.
- Stop the running app in the integrated terminal.
- In the VS Code Explorer sidebar, use the new-folder control beside the Views folder.
- Name the folder Orders.
- Use the new-file control beside the Views/Orders folder.
- Name the file Create.cshtml.
- Create the first visible form fields by adding this code:
@model OrderCreateViewModel
@{
ViewData["Title"] = "Order Honey";
}
<section class="page-shell section-space narrow-shell">
<p class="eyebrow">MVC order form</p>
<h1>Order honey</h1>
<p>Submit a fictional order to preview the posted values.</p>
<form asp-action="Create" method="post" class="order-form">
<div asp-validation-summary="ModelOnly" class="validation-message"></div>
<div class="field-group">
<label asp-for="Input.CustomerName">Name</label>
<input asp-for="Input.CustomerName" />
<span asp-validation-for="Input.CustomerName" class="validation-message"></span>
</div>
<div class="field-group">
<label asp-for="Input.Email">Email</label>
<input asp-for="Input.Email" />
<span asp-validation-for="Input.Email" class="validation-message"></span>
</div>
<button class="button" type="submit">Save order</button>
</form>
</section>
How Do These Tag Helpers Connect?
- asp-action="Create" sends the form to the controller action named Create.
- asp-for="Input.CustomerName" connects the name control to the nested input property.
- asp-validation-for reserves a place for field errors once validation rules exist.
The POST action uses ValidateAntiForgeryToken. The Form Tag Helper supplies the matching hidden token automatically.
- Save Create.cshtml.
- Start the app with the updated form by running this command:
dotnet run
What Does This Run Load?
The running host can now resolve OrdersController.Create to Views/Orders/Create.cshtml. This proves the controller and view follow MVC's naming convention.
- Return to Safari.
- Select Order Honey in the navigation.
You will see the Honeybee Harvest order page with name and email fields.
Order Page Not Loading?
- Check that the folder path is Views/Orders.
- Confirm that the view file is named Create.cshtml.
- Check that the first line uses OrderCreateViewModel.
Help me connect my order controller to its Razor view.
- Stop the running app in the integrated terminal.
- In Create.cshtml find the email field group.
- Insert the product and quantity controls below that field group:
<div class="field-group">
<label asp-for="Input.ProductName">Honey</label>
<select asp-for="Input.ProductName">
<option value="">Choose a product</option>
@foreach (var product in Model.Products)
{
<option value="@product.Name">@product.Name</option>
}
</select>
<span asp-validation-for="Input.ProductName" class="validation-message"></span>
</div>
<div class="field-group">
<label asp-for="Input.Quantity">Quantity</label>
<input asp-for="Input.Quantity" min="1" max="12" />
<span asp-validation-for="Input.Quantity" class="validation-message"></span>
</div>
How Does Product Selection Work?
The loop renders one option for every item in Model.Products. The selector binds the chosen value to Input.ProductName.
A catalog link sends the product name through the query string. The GET action places it into Input.ProductName so the matching option becomes selected.
- Save Create.cshtml.
- Restart the app by running this command:
dotnet run
What Should the Updated Form Contain?
The view now renders four bound controls. The catalog choices come from the view model prepared by the GET action.
- Return to the catalog in Safari.
- Select Order this honey on the Citrus Blossom card.
You will see all four order fields. Citrus Blossom is selected in the honey field.
Product Not Preselected?
- Confirm that the catalog link uses asp-route-product="@product.Name".
- Check that the GET action assigns product ?? string.Empty to ProductName.
- Confirm that the select uses asp-for="Input.ProductName".
Help me debug the preselected honey value.
- Stop the running app in the integrated terminal.
- In Create.cshtml find the closing form tag.
- Add the temporary preview below the form:
@if (ViewData["Submitted"] is true)
{
<div class="empty-state">
<h2>Submitted order preview</h2>
<p><strong>Name:</strong> @Model.Input.CustomerName</p>
<p><strong>Email:</strong> @Model.Input.Email</p>
<p><strong>Honey:</strong> @Model.Input.ProductName</p>
<p><strong>Quantity:</strong> @Model.Input.Quantity</p>
</div>
}
Why Show a Temporary Preview?
The preview makes model binding visible. Every value displayed here came from the posted form and was placed into OrderCreateViewModel.
No database exists in this request flow. Restarting the app removes the preview because the controller has not persisted an order.
- Save Create.cshtml.
- Start the completed request loop by running this command:
dotnet run
What Is Running Now?
The app now has a complete GET-to-form-to-POST loop. The next check reveals what this loop still fails to protect.
- Return to the catalog in Safari.
- Select Order this honey on any product card.
Before you submit, pause on this question: do you expect MVC to reject the blank customer fields.
- Clear the Name field.
- Clear the Email field.
- Click Save order.
You will see the submitted-order preview even though the name and email values are blank. This shortfall is intentional because the model contains no validation rules.
What Did This Prove?
Model binding transferred the posted values into your C# object. MVC had no rules that classified the empty strings as invalid.
The validation Tag Helpers had no errors to display. Those helpers render validation results after the model defines rules and the controller checks them.
✔️ Awesome, I've got everything!
Your form posts all four values and displays the temporary preview.
ⓧ I'd like to double check the full code
@model OrderCreateViewModel
@{
ViewData["Title"] = "Order Honey";
}
<section class="page-shell section-space narrow-shell">
<p class="eyebrow">MVC order form</p>
<h1>Order honey</h1>
<p>Submit a fictional order to preview the posted values.</p>
<form asp-action="Create" method="post" class="order-form">
<div asp-validation-summary="ModelOnly" class="validation-message"></div>
<div class="field-group">
<label asp-for="Input.CustomerName">Name</label>
<input asp-for="Input.CustomerName" />
<span asp-validation-for="Input.CustomerName" class="validation-message"></span>
</div>
<div class="field-group">
<label asp-for="Input.Email">Email</label>
<input asp-for="Input.Email" />
<span asp-validation-for="Input.Email" class="validation-message"></span>
</div>
<div class="field-group">
<label asp-for="Input.ProductName">Honey</label>
<select asp-for="Input.ProductName">
<option value="">Choose a product</option>
@foreach (var product in Model.Products)
{
<option value="@product.Name">@product.Name</option>
}
</select>
<span asp-validation-for="Input.ProductName" class="validation-message"></span>
</div>
<div class="field-group">
<label asp-for="Input.Quantity">Quantity</label>
<input asp-for="Input.Quantity" min="1" max="12" />
<span asp-validation-for="Input.Quantity" class="validation-message"></span>
</div>
<button class="button" type="submit">Save order</button>
</form>
@if (ViewData["Submitted"] is true)
{
<div class="empty-state">
<h2>Submitted order preview</h2>
<p><strong>Name:</strong> @Model.Input.CustomerName</p>
<p><strong>Email:</strong> @Model.Input.Email</p>
<p><strong>Honey:</strong> @Model.Input.ProductName</p>
<p><strong>Quantity:</strong> @Model.Input.Quantity</p>
</div>
}
</section>
That request loop is working from the catalog link through the controller preview. Next, you will add validation rules that stop incomplete orders before they reach the accepted path.
Validate Order Submissions
Your order form now uses ASP.NET Core MVC model binding to turn posted fields into an OrderCreateViewModel. Blank customer details still reach the preview because the input model has no rules.
Data annotations define which values count as valid. The POST action then reads ModelState to decide whether the submitted order can continue.
In this step, get ready to:
- Add server-side validation rules to OrderInputModel.
- Return invalid submissions to the order form with field errors.
- Prove that a valid preview disappears after an app restart.
Add validation rules to the input model
The input model represents the values a customer is allowed to submit. Validation attributes place the business rules beside those properties.
- In the Visual Studio Code Explorer sidebar from earlier, select Models/OrderInputModel.cs.
- Replace the file contents with the validated input model below:
using System.ComponentModel.DataAnnotations;
namespace HoneybeeHarvest.Models;
public sealed class OrderInputModel
{
[Required(ErrorMessage = "Enter your name.")]
public string CustomerName { get; set; } = string.Empty;
[Required(ErrorMessage = "Enter your email address.")]
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Choose a honey product.")]
public string ProductName { get; set; } = string.Empty;
[Range(1, 12, ErrorMessage = "Quantity must be between 1 and 12.")]
public int Quantity { get; set; } = 1;
}
How Do the Rules Work?
- Required rejects an empty customer name or product choice.
- EmailAddress checks whether the submitted email has a valid email address format.
- Range(1, 12) limits each order to between 1 and 12 jars.
- Each ErrorMessage gives the customer a specific correction to make.
- Save Models/OrderInputModel.cs.
Before you submit the form, do you think the annotations can stop the controller from building its preview on their own?
- Switch back to Safari.
- Click Order Honey in the navigation.
- Leave the Name field blank.
- Leave the Email field blank.
- Select Choose a product in the Honey field.
- Click Save order.
You will see validation messages appear while the incomplete preview is still accepted. The annotations record the problems in ModelState, but the controller does not act on that result yet.
No Validation Messages Yet?
- Confirm that you saved Models/OrderInputModel.cs.
- Check that each attribute sits directly above its matching property.
- Return to the terminal from earlier to confirm the app is still running.
Get help with missing validation messages.
✔️ Awesome, I've got everything!
Your input model now contains every server-side validation rule for this step.
ⓧ I'd like to double check the full code
using System.ComponentModel.DataAnnotations;
namespace HoneybeeHarvest.Models;
public sealed class OrderInputModel
{
[Required(ErrorMessage = "Enter your name.")]
public string CustomerName { get; set; } = string.Empty;
[Required(ErrorMessage = "Enter your email address.")]
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Choose a honey product.")]
public string ProductName { get; set; } = string.Empty;
[Range(1, 12, ErrorMessage = "Quantity must be between 1 and 12.")]
public int Quantity { get; set; } = 1;
}
What Should Match?
This reference combines the data annotation import with all four validated properties. Compare the attribute placement and error messages with your file.
Stop invalid submissions in the controller
The controller must branch when validation records an error. Returning the submitted view model preserves the entered values while the Validation Tag Helpers display the corrections.
- In the Explorer sidebar, select Controllers/OrdersController.cs.
- Find the POST Create(OrderCreateViewModel model) action.
- Add the validity check below the existing product-list assignment by copying this code:
if (!ModelState.IsValid)
{
return View(model);
}
What Does This Check Do?
- ModelState.IsValid becomes false when model binding or validation records an error.
- return View(model) redisplays the submitted form instead of continuing to the preview.
- The existing product assignment restores the honey choices before the view renders again.
- Leave the existing valid preview branch below the new check unchanged.
- Save Controllers/OrdersController.cs.
Before you submit another blank form, do you expect the preview to appear this time?
- Return to Safari.
- Click Order Honey in the navigation.
- Leave the Name field blank.
- Leave the Email field blank.
- Select Choose a product in the Honey field.
- Click Save order.
You will stay on the order form without an accepted preview. You will see Enter your name., Enter your email address., and Choose a honey product. beside the invalid fields.
That closes the validation gap because your controller now keeps incomplete orders on the form with specific guidance.
Still Seeing the Invalid Preview?
- Confirm that the validity check appears inside the POST action.
- Check that the validity check sits above the existing preview branch.
- Confirm that the condition includes the negation symbol before ModelState.IsValid.
Get help with the controller validation branch.
Prove the valid preview is temporary
A valid submission can now reach the temporary preview. The app still has no durable store for keeping that order after its process stops.
- Enter Avery Stone in the Name field.
- Enter avery@example.com in the Email field.
- Select Citrus Blossom in the Honey field.
- Enter 2 in the Quantity field.
- Click Save order.
You will see the valid order preview with Avery Stone, Citrus Blossom, and a quantity of 2. Your invalid and valid paths now produce different outcomes.
- Return to the terminal where the app is running.
- Press Control+C to stop the server.
Before you restart the app, do you expect Avery's preview to remain available?
- Restart the MVC app by running this command:
dotnet run
What Does the Restart Prove?
This command starts a fresh local web process for the same project. The new process begins with a clean runtime session.
- Return to the existing Honeybee Harvest tab in Safari.
- Click Order Honey in the navigation.
You will see a clean order form with no Avery Stone preview. This confirms that the valid submission lived only in the previous app process.
Still Seeing the Old Preview?
- Use the Order Honey navigation link to request a clean form.
- Confirm that the previous server process stopped before you ran dotnet run again.
Get help verifying the restart behavior.
Your form now protects the preview from incomplete submissions. Next, you will replace that temporary preview with order records that survive an app restart.
Persist Orders with SQLite
Your ASP.NET Core MVC form now rejects invalid submissions. A valid order still disappears whenever the app restarts.
In this step, Entity Framework Core maps accepted orders into a local SQLite database. The database file gives each valid submission a durable home.
A migration records the database structure as generated source files. You can apply that structure consistently without creating the table by hand.
In this step, get ready to:
- Install the pinned EF Core tooling plus the SQLite packages.
- Create the migrated order schema.
- Persist valid orders through the controller.
Install EF Core and SQLite
The local tool manifest keeps dotnet-ef attached to this project. Matching version 10.0.12 packages prevent the command-line tooling from drifting away from the EF Core runtime.
Why SQLite for this project?
SQLite stores the database in one local file. It keeps this project focused on MVC request handling plus persistence.
A separate SQL Server process would add infrastructure that this local storefront does not need.
- Return to the VS Code terminal inside the existing HoneybeeHarvest project.
- Stop the running app with Control+C if the server is still active.
- Create the local tool manifest and install the persistence dependencies by running these commands:
dotnet new tool-manifest
dotnet tool install --local dotnet-ef --version 10.0.12
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.12
dotnet add package Microsoft.EntityFrameworkCore.Design --version 10.0.12
What do these commands add?
- The first command creates .config/dotnet-tools.json for project-local tools.
- The second command installs dotnet-ef version 10.0.12 into that manifest.
- The third command adds the SQLite database provider to HoneybeeHarvest.csproj.
- The fourth command adds the design-time package used to generate migrations.
- Check the project-local tool version by running this command:
dotnet tool list --local
What does this check?
This command reads the nearest local tool manifest. Its output identifies the installed package plus its pinned version.
✔️ I see version 10.0.12
Your local EF Core tool matches the project packages. The migration commands can now use the intended version.
ⓧ I see an older version
- Update the local tool to version 10.0.12 by running this command:
dotnet tool update dotnet-ef --local --version 10.0.12
What does this update?
The command updates the tool referenced by the local manifest. It pins the replacement to version 10.0.12.
ⓧ Command not found
- Confirm that the terminal is inside the HoneybeeHarvest folder.
- Install the local tool from that folder by running this command:
dotnet tool install --local dotnet-ef --version 10.0.12
Why does the folder matter?
Local tools are resolved through the manifest associated with the current project folder. Running the command from HoneybeeHarvest connects the tool to the correct manifest.
- Verify that the EF Core commands are available by running:
dotnet ef --help
What should you see?
You should see EF Core command help with options for database operations plus migrations. This confirms that the local tool can run from the project.
EF Core tool unavailable?
- Check that .config/dotnet-tools.json appears inside the HoneybeeHarvest folder.
- Check your internet connection if a package download did not complete.
- Run the installation commands again from the terminal attached to the project.
Need help with the local tool setup?
Create the database schema
The database needs an entity that describes one stored order. A dedicated DbContext exposes those orders to EF Core queries.
- In the VS Code Explorer sidebar, right-click the existing Models folder.
- Select New File.
- Enter Order.cs as the file name.
- Define the persisted order entity by adding this code:
namespace HoneybeeHarvest.Models;
public sealed class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; }
public DateTime CreatedAtUtc { get; set; }
}
What does this model store?
- The Id property identifies each persisted row.
- The customer fields copy accepted values from OrderInputModel.
- The CreatedAtUtc property records when the controller accepted the order.
Separating Order from OrderInputModel keeps database-managed values outside the posted form.
- Save Models/Order.cs.
- Confirm that Order.cs appears under Models in the Explorer sidebar.
Order model showing errors?
- Check that the namespace is HoneybeeHarvest.Models.
- Check that every property sits inside the Order class braces.
Ask for help with the entity code.
- In the Explorer sidebar, right-click the top-level HoneybeeHarvest folder.
- Select New Folder.
- Enter Data as the folder name.
- Right-click the new Data folder.
- Select New File.
- Enter HoneybeeContext.cs as the file name.
- Expose the order table through the context by adding this code:
using HoneybeeHarvest.Models;
using Microsoft.EntityFrameworkCore;
namespace HoneybeeHarvest.Data;
public sealed class HoneybeeContext(DbContextOptions<HoneybeeContext> options)
: DbContext(options)
{
public DbSet<Order> Orders { get; set; } = null!;
}
What does this context do?
The constructor receives configured database options from the app. Passing those options to DbContext connects the context to its provider.
The Orders set represents the order rows that EF Core can add or query.
- Save Data/HoneybeeContext.cs.
- Confirm that the new file appears under Data in the Explorer sidebar.
- Check that the entity plus context compile by running the app:
dotnet run
What does this run prove?
You should see the local server start without a C# compilation error. This proves that the new entity plus context types are valid.
- Stop the server with Control+C after the localhost address appears.
Context not compiling?
- Check that the SQLite plus Design package commands completed successfully.
- Check that HoneybeeContext.cs imports both required namespaces.
- Check that Order is spelled exactly the same in both files.
Ask for help with the context error.
The context now needs a connection string plus dependency injection registration. These settings tell each controller request how to reach honeybee.db.
- Select the existing appsettings.json file in the Explorer sidebar.
- Replace its contents with this configuration:
{
"ConnectionStrings": {
"HoneybeeContext": "Data Source=honeybee.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
What does this setting control?
The HoneybeeContext connection string points SQLite to honeybee.db. The relative file name places the database with the local project runtime files.
- Save appsettings.json.
- Select the existing Program.cs file in the Explorer sidebar.
- Replace its contents with this startup configuration:
using HoneybeeHarvest.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("HoneybeeContext")
?? throw new InvalidOperationException("Connection string 'HoneybeeContext' not found.");
builder.Services.AddDbContext<HoneybeeContext>(options =>
options.UseSqlite(connectionString));
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapStaticAssets();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();
app.Run();
How does the app reach SQLite?
- The configuration lookup reads the connection string named HoneybeeContext.
- The AddDbContext registration makes HoneybeeContext available to controllers.
- The UseSqlite call selects SQLite as the EF Core provider.
- Save Program.cs.
- Generate the initial migration and apply it to the local database by running these commands:
dotnet ef migrations add InitialCreate
dotnet ef database update
What do the migration commands create?
The first command generates a timestamped InitialCreate migration plus a model snapshot inside Migrations.
The second command applies that migration. It creates the Orders table inside honeybee.db.
- Confirm that a Migrations folder appears in the Explorer sidebar.
- Confirm that the folder contains a timestamped migration with InitialCreate in its name.
- Confirm that HoneybeeContextModelSnapshot.cs appears under Migrations.
- Confirm that honeybee.db appears inside the top-level HoneybeeHarvest folder.
Migration command failed?
- Check that the terminal is inside the HoneybeeHarvest folder.
- Check that Program.cs registers HoneybeeContext with UseSqlite.
- Check that the connection string name matches in Program.cs plus appsettings.json.
Ask for help with the migration failure.
Save and reload orders
The database exists now. The controller must map validated form values into an Order entity before EF Core can save it.
The controller receives HoneybeeContext through constructor injection. This gives each request access to the configured database context.
- Select Controllers/OrdersController.cs in the Explorer sidebar.
- Replace the import group at the top with these namespaces:
using HoneybeeHarvest.Data;
using HoneybeeHarvest.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
Why add these namespaces?
The data namespace exposes HoneybeeContext. The EF Core namespace exposes the asynchronous query extension used by the saved-orders action.
- Change the OrdersController declaration so its primary constructor receives HoneybeeContext context.
- Add a private readonly HoneybeeContext field named _context inside the class.
- Initialize _context from the constructor parameter named context.
- Save Controllers/OrdersController.cs.
- Check that constructor injection compiles by running the app:
dotnet run
What does this compile check prove?
You should see the local server start successfully. This confirms that the controller can receive the registered context.
- Stop the server with Control+C after the localhost address appears.
- Replace the existing POST Create method with this persisted version:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(OrderCreateViewModel model)
{
model.Products = Catalog.Products;
if (!ModelState.IsValid)
{
return View(model);
}
var order = new Order
{
CustomerName = model.Input.CustomerName,
Email = model.Input.Email,
ProductName = model.Input.ProductName,
Quantity = model.Input.Quantity,
CreatedAtUtc = DateTime.UtcNow
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
How does the POST save an order?
- The validation branch still returns the form when ModelState.IsValid is false.
- The mapping copies accepted input into a new Order entity.
- The CreatedAtUtc value records the acceptance time in UTC.
- The Add call tracks the entity for insertion.
- The awaited SaveChangesAsync call writes the row to SQLite.
- Add this GET Index action inside OrdersController after the POST action:
[HttpGet]
public async Task<IActionResult> Index()
{
var orders = await _context.Orders
.OrderByDescending(order => order.CreatedAtUtc)
.ToListAsync();
return View(orders);
}
How does the query load orders?
The query sorts orders with the newest timestamp first. The awaited ToListAsync call executes the query before the controller passes the results to the view.
- Save Controllers/OrdersController.cs.
- Check that the persistence actions compile by running the app:
dotnet run
What does this compile check prove?
You should see the local server start without controller errors. The save path plus query path now compile against HoneybeeContext.
- Stop the server with Control+C after the localhost address appears.
- In the Explorer sidebar, right-click the existing Views/Orders folder.
- Select New File.
- Enter Index.cshtml as the file name.
- Set the view model to IReadOnlyList<Order>.
- Render an empty state when the model contains no orders.
- Render saved orders in a table when records exist.
- Convert each CreatedAtUtc value to local time in the table.
✔️ Awesome, I've got everything!
Save the new view after you finish. Use the next tab if you want to compare every authored file changed in this step.
ⓧ I'd like to double check the full code
The CLI generated the local tool manifest. It also generated the migration files.
Compare the authored files below with your project.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.12">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />
</ItemGroup>
</Project>
namespace HoneybeeHarvest.Models;
public sealed class Order
{
public int Id { get; set; }
public string CustomerName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; }
public DateTime CreatedAtUtc { get; set; }
}
using HoneybeeHarvest.Models;
using Microsoft.EntityFrameworkCore;
namespace HoneybeeHarvest.Data;
public sealed class HoneybeeContext(DbContextOptions<HoneybeeContext> options)
: DbContext(options)
{
public DbSet<Order> Orders { get; set; } = null!;
}
{
"ConnectionStrings": {
"HoneybeeContext": "Data Source=honeybee.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
using HoneybeeHarvest.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("HoneybeeContext")
?? throw new InvalidOperationException("Connection string 'HoneybeeContext' not found.");
builder.Services.AddDbContext<HoneybeeContext>(options =>
options.UseSqlite(connectionString));
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapStaticAssets();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();
app.Run();
using HoneybeeHarvest.Data;
using HoneybeeHarvest.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace HoneybeeHarvest.Controllers;
public sealed class OrdersController(HoneybeeContext context) : Controller
{
private readonly HoneybeeContext _context = context;
[HttpGet]
public IActionResult Create(string? product)
{
return View(new OrderCreateViewModel
{
Input = new OrderInputModel
{
ProductName = product ?? string.Empty
},
Products = Catalog.Products
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(OrderCreateViewModel model)
{
model.Products = Catalog.Products;
if (!ModelState.IsValid)
{
return View(model);
}
var order = new Order
{
CustomerName = model.Input.CustomerName,
Email = model.Input.Email,
ProductName = model.Input.ProductName,
Quantity = model.Input.Quantity,
CreatedAtUtc = DateTime.UtcNow
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
[HttpGet]
public async Task<IActionResult> Index()
{
var orders = await _context.Orders
.OrderByDescending(order => order.CreatedAtUtc)
.ToListAsync();
return View(orders);
}
}
@model IReadOnlyList<Order>
@{
ViewData["Title"] = "Saved Orders";
}
<section class="page-shell section-space">
<p class="eyebrow">SQLite order history</p>
<h1>Saved orders</h1>
@if (Model.Count == 0)
{
<div class="empty-state">
<p>No orders have been saved yet.</p>
<a class="button" asp-action="Create">Create the first order</a>
</div>
}
else
{
<div class="table-shell">
<table>
<thead>
<tr>
<th>Customer</th>
<th>Email</th>
<th>Honey</th>
<th>Quantity</th>
<th>Created</th>
</tr>
</thead>
<tbody>
@foreach (var order in Model)
{
<tr>
<td>@order.CustomerName</td>
<td>@order.Email</td>
<td>@order.ProductName</td>
<td>@order.Quantity</td>
<td>@order.CreatedAtUtc.ToLocalTime()</td>
</tr>
}
</tbody>
</table>
</div>
}
</section>
- Save Views/Orders/Index.cshtml.
Before you run the app, do you expect the next valid submission to reach the saved-orders table?
- Start the completed app by running:
dotnet run
What does this run start?
The command starts the MVC app with its SQLite configuration. The localhost address printed in the terminal is ready for the browser test.
- Switch back to Safari.
- Refresh the localhost page.
- Select Order Honey in the navigation.
- Enter Avery Bee in the Name field.
- Enter avery@example.com in the Email field.
- Select Wildflower Honey in the Honey field.
- Enter 2 in the Quantity field.
- Click Save order.
You should arrive on Saved Orders. The table should show Avery Bee with the submitted email, honey product, quantity, and creation time.
- Return to the VS Code terminal.
- Stop the server with Control+C.
Before restarting the app, do you expect the Avery Bee order to remain in the table?
- Restart the app by running:
dotnet run
What does the restart test prove?
Restarting creates a fresh application process. Any order that remains afterward came from honeybee.db instead of temporary process memory.
- Switch back to Safari.
- Refresh the Saved Orders page.
- Confirm that the Avery Bee order still appears in the table.
That is the durable request flow working. Your validated form now writes an order that survives a full app restart.
Saved order missing after restart?
- Check that honeybee.db still exists inside the HoneybeeHarvest folder.
- Check that the POST action awaits _context.SaveChangesAsync() before redirecting.
- Check that the Index action queries _context.Orders.
Ask for help tracing the persistence path.
Secret mission
Filter the Saved Orders
Turn the saved-orders page into a focused operational view. You will filter persisted orders by honey product through a GET form without changing the database schema.
Clean Up Your Resources
Clean Up Your Resources
Everything you created runs on your Mac. There are no cloud services or ongoing charges.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The Kestrel process serving the app locally.
- The HoneybeeHarvest project folder containing your ASP.NET Core MVC source code.
- The .config/dotnet-tools.json local tool manifest for dotnet-ef version 10.0.12.
- The Migrations folder containing generated Entity Framework Core migration files.
- The honeybee.db local SQLite database containing your saved orders.
Keep everything running
No action needed. Choose this if you are still testing the app or adding orders.
- Leave the dotnet run process active in the Terminal panel.
- Keep the HoneybeeHarvest folder in its current location.
- Return to Safari whenever you want to use the app.
- Continue without worrying about ongoing charges.
Pause - I'll come back to this later
Shut down Kestrel to free its local process. Keep all project files for your next session.
- Return to the Terminal panel where the app is running.
- Press Control+C to stop Kestrel.
You are safely paused once the terminal prompt returns. Your saved orders remain inside honeybee.db.
Delete - I don't want to use this again
Deleting the folder is permanent. Your saved orders disappear with the code.
- Return to the Terminal panel from earlier.
- Press Control+C if Kestrel is still running.
- Move to the directory above HoneybeeHarvest and delete the project by running these commands:
cd ..
rm -rf HoneybeeHarvest
These commands move up one directory. They permanently delete the complete HoneybeeHarvest folder.
- In Finder, check the directory that previously contained HoneybeeHarvest.
Cleanup complete. You should no longer see the HoneybeeHarvest folder.
Nice Work!
Nice Work!
You did it. Honeybee Harvest now turns validated form submissions into persistent SQLite orders.
You've learned how to:
- Render a catalog through an ASP.NET Core MVC request flow from C# product data to a strongly typed Razor view.
- Reject invalid orders with server-side validation through a dedicated input model before any data reaches storage.
- Persist orders asynchronously with Entity Framework Core in SQLite so submitted records survive an application restart.
- Secret Mission: Filter saved orders with an optional product query while keeping every SQLite record intact.
Ready to quiz yourself?