Lesson 4 Lab — OrdersController (Nested Data)¶
Build an OrdersController that returns orders with their related data nested:
the Staff member who took the order, and — on the detail view — the full list of
OrderItems, each with its MenuItem. This is a step up from the guide: two
navigation properties and a two-level ThenInclude.
Use the finished MenuItemsController as your model for the Include pattern.
The Order and OrderItem entities¶
Add Models/Order.cs:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace TableServe.Api.Models;
public class Order
{
public int Id { get; set; }
public int TableNumber { get; set; }
[StringLength(200)]
public string? Notes { get; set; }
[StringLength(10)]
public string Status { get; set; } = OrderStatus.Placed;
[StringLength(200)]
public string? CancellationReason { get; set; }
[Column(TypeName = "decimal(11,2)")]
public decimal Total { get; set; }
public DateTime OrderedAt { get; set; } = DateTime.Now;
public int StaffId { get; set; }
public Staff? Staff { get; set; }
public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>();
}
Add Models/OrderItem.cs (note the [JsonIgnore] from this lesson's guide):
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace TableServe.Api.Models;
public class OrderItem
{
public int Id { get; set; }
public int Quantity { get; set; } = 1;
[StringLength(200)]
public string? Notes { get; set; }
public int OrderId { get; set; }
[JsonIgnore]
public Order? Order { get; set; }
public int MenuItemId { get; set; }
public MenuItem? MenuItem { get; set; }
}
Add Models/OrderStatus.cs — status values live in a constants class, never as
magic strings:
namespace TableServe.Api.Models;
public static class OrderStatus
{
public const string Placed = "PLACED";
public const string Preparing = "PREPARING";
public const string Ready = "READY";
public const string Served = "SERVED";
public const string Cancelled = "CANCELLED";
}
The names are PascalCase; the values are UPPERCASE. Both on purpose.
OrderStatus.Placedis a C# identifier and follows C# naming."PLACED"is the value that goes in the database, comes back as JSON, and gets compared in the React app later — and string comparison is case-sensitive on every one of those hops. One"Placed"slipped in somewhere and the status filter silently matches nothing, the badge renders with no colour, and the workflow button never appears. Nothing throws.Picking one case and holding it everywhere is what makes that class of bug impossible. This course uses UPPERCASE for every status value, in both apps, in the seed data, in Insomnia, and in the front end. Use the constants and you get it for free — which is the actual reason the constants exist.
Steps¶
- Add
Order.cs,OrderItem.cs, andOrderStatus.cstoModels/ - Add
DbSet<Order> OrdersandDbSet<OrderItem> OrderItemstoTableServeDbContext - Run
Add-Migration AddOrdersthenUpdate-Database - Verify the
OrdersandOrderItemstables exist in SQL Server Object Explorer - Seed a few orders and items (Staff and MenuItems must already exist):
SET IDENTITY_INSERT [dbo].[Orders] ON
INSERT INTO [dbo].[Orders] ([Id],[TableNumber],[Notes],[Status],[CancellationReason],[Total],[OrderedAt],[StaffId])
VALUES (1, 4, NULL, 'PLACED', NULL, 0.00, '2026-07-02 17:15:00', 6)
INSERT INTO [dbo].[Orders] ([Id],[TableNumber],[Notes],[Status],[CancellationReason],[Total],[OrderedAt],[StaffId])
VALUES (2, 7, 'Anniversary dinner', 'PREPARING', NULL, 0.00, '2026-07-02 17:32:00', 7)
INSERT INTO [dbo].[Orders] ([Id],[TableNumber],[Notes],[Status],[CancellationReason],[Total],[OrderedAt],[StaffId])
VALUES (3, 12, NULL, 'READY', NULL, 0.00, '2026-07-02 17:48:00', 8)
SET IDENTITY_INSERT [dbo].[Orders] OFF
INSERT INTO [dbo].[OrderItems] ([Quantity],[Notes],[OrderId],[MenuItemId]) VALUES (1, 'no onions', 1, 4)
INSERT INTO [dbo].[OrderItems] ([Quantity],[Notes],[OrderId],[MenuItemId]) VALUES (2, NULL, 1, 6)
INSERT INTO [dbo].[OrderItems] ([Quantity],[Notes],[OrderId],[MenuItemId]) VALUES (1, 'medium rare', 2, 3)
INSERT INTO [dbo].[OrderItems] ([Quantity],[Notes],[OrderId],[MenuItemId]) VALUES (1, NULL, 3, 5)
The
StaffIdandMenuItemIdvalues above assume the seed ids from the Lesson 2 and Lesson 3 labs. If your ids differ, adjust them to point at rows that exist.
- Create
Controllers/OrdersController.cs: GetAll—.Include(order => order.Staff), and support an optional?status=query filter:GetById— IncludeStaff, and IncludeOrderItemswith.ThenInclude(orderItem => orderItem.MenuItem)Create,Update,Delete— same CRUD pattern as before; re-fetch with.Include(order => order.Staff)after Create/Update into anorderWithStaffvariable- Run the project and verify using the Orders folder in Insomnia
Verify in Insomnia¶
With the project running (no login required — the endpoints are open), expand the Orders folder:
- Get All Orders → expect 200 OK; each order has a nested
staffobject - Get Order by Id → expect 200 OK; the order has
staffand anorderItemsarray, and each item has a nestedmenuItem - Create Order → expect 201 Created with
staffpopulated - Update Order → expect 200 OK
- Delete Order → expect 204 No Content
- Check the Tests tab — all green
If you see a "possible object cycle" error on Get Order by Id, confirm the
[JsonIgnore] is on OrderItem.Order.
The Include/ThenInclude pattern you just used is exactly what you'll write for
RequestsController on PRS in the capstone — a request that nests its RequestLines,
each with its Product, plus the User who submitted it.
Stretch challenges¶
Optional — for when you finish early. Not needed for the capstone. [Reinforce] builds on what you just did; [Reach] goes past the guide and needs some research.
- Filter menu items by category — [Reinforce] — add an optional
?categoryId=filter toMenuItemsController.GetAll, returning only that category's items. (Same shape as PRS's?status=filter you'll write on requests.) - Deeper include — [Reinforce] — on Get Order by Id, extend the include
chain so each
OrderItem'sMenuItemalso brings back itsCategory, and confirm you don't trip a "possible object cycle" error. - Availability flag — [Reinforce] — add a
bool IsAvailableproperty toMenuItem, then return only available items from a new?available=truefilter or a dedicatedmenuendpoint. Since you're adding a column to a table that already has rows, give it a database default oftrueso existing menu items stay available. EF Core ignores[DefaultValue]for the schema, so set the default with the Fluent API inTableServeDbContext.OnModelCreating(add that method if it isn't there yet, and keep thebase.OnModelCreating(modelBuilder)call): Then runAdd-Migration AddMenuItemIsAvailableandUpdate-Databaseto apply the change to your database.
Finished these and want more? See stretch-api-challenges.md for bigger challenges that span the whole API pass.