Create your own
Lesson illustration

Modeling Menu Items, Customers, and Orders in C#

Welcome back. In the previous lesson, the receipt calculator kept an item’s name, price, and quantity in three parallel arrays. That worked, but it depended on a fragile rule: values at the same array position had to belong together.

This lesson replaces that arrangement with small C# classes. You will model a restaurant’s menu items, customers, and orders, plus an OrderLine class that connects a menu item to a quantity. By the end, your console program will still calculate and print a receipt, but its data will have a structure that resembles the beginning of a real ASP.NET Core application.


Classes turn related data into meaningful objects

A class is a custom type you define to represent a concept in the system. An object is one actual instance created from that type while the program runs.

For example:

  • MenuItem is the class, or blueprint.
  • paneerWrap is one MenuItem object.
  • Customer represents restaurant customers.
  • Order represents one purchase by one customer.
  • OrderLine represents a specific menu item and the quantity ordered.

Instead of relying on array positions, each OrderLine keeps its own related information together:

Earlier designClass-based design
"Paneer Wrap" in itemNames[0]A MenuItem object with a Name property
250.00m in unitPrices[0]The same MenuItem object’s UnitPrice property
2 in quantities[0]An OrderLine object’s Quantity property
The relationship exists only because indexes matchThe relationship is explicit: OrderLine.Item refers to one MenuItem

A class normally contains:

  • Properties: data that describes the object’s current state.
  • Methods: behavior the object can perform.
  • Constructors: code that initializes an object when it is created with new.

For example, an order should know its order number, customer, and lines. It should also be able to calculate its subtotal. That gives the calculation a natural home: Order.CalculateSubtotal().

C# Classes Tutorial | Mosh

Watch “C# Classes Tutorial” by Programming with Mosh for a compact visual explanation of classes, objects, members, and object creation.

Watch class and object to distinguish a type definition from an instance in memory. Then watch class syntax for members and C# naming conventions, followed by object creation to see new and var in use. Keep the restaurant model in mind: MenuItem is the type, while paneerWrap will be one instance.

A useful responsibility split for this version of the restaurant program is:

ClassMain responsibility
MenuItemDescribe something that can appear on the menu.
CustomerStore customer identity and delivery-address information.
OrderLinePair one menu item with the quantity requested.
OrderAssociate a customer with order lines and calculate the subtotal.
ProgramCreate sample objects, read console input, and print the receipt.

Notice that Program remains responsible for console input and output. The domain classes should represent restaurant concepts and calculations, rather than knowing how a terminal works.


Properties express what can be known and changed

C# properties often look like fields when you use them:

menuItem.Name

But properties are more controlled than public fields. They can expose a value for reading, permit or restrict assignment, and later contain validation logic without forcing every caller to change.

Three property forms are especially useful here:

public string Name { get; }
public bool IsVegetarian { get; set; }
public string? DeliveryAddress { get; private set; }

Their meaning differs:

Property formWho can read it?Who can assign it?Restaurant use
{ get; }Any callerOnly the class constructorA menu item ID or unit price established when created
{ get; set; }Any callerAny callerA simple value that is allowed to change freely
{ get; private set; }Any callerOnly code inside the classA customer address changed through a customer method

For this lesson, core values such as MenuItemId, Name, and UnitPrice will be read-only after construction. A caller must provide them when creating the object. This removes the possibility of accidentally constructing a menu item with missing basic information and assigning it later.

A constructor:

  1. Has exactly the same name as its class.
  2. Has no return type, not even void.
  3. Runs when new creates an object.
  4. Receives values needed for the initial object state.

C# Constructors Tutorial | Mosh

Watch “C# Constructors Tutorial” by Programming with Mosh to reinforce why constructors establish an object’s initial state.

Watch constructor purpose for the definition and syntax. Then skip to parameters and this to see how constructor arguments become object data. In the code below, the property names and constructor parameter names differ only by casing, so assignments such as Name = name; are unambiguous.

Properties (C# Programming Guide)

Read Microsoft Learn’s “Properties (C# Programming Guide)” to understand the property forms used in the restaurant classes.

In “Automatically implemented properties,” read from automatic properties. Focus on how C# supplies the backing storage for a simple get and set property. Next, in “Expression body definitions,” read computed properties. You will not need the shorter expression syntax yet, but notice the distinction between stored data and data calculated when requested. Finally, in “Access control,” read from restricted setters. Relate this to Customer.DeliveryAddress, which outside code can read but only Customer itself can modify.

The private set on DeliveryAddress is an early example of restricting changes. In the next lesson, you will take this further by enforcing meaningful rules about what states an order can have. For now, focus on modeling and using the objects clearly.


Build the restaurant model

Create a branch for this refactor:

git switch main
git pull --ff-only
git switch -c feature/restaurant-domain-classes

Inside src/Restaurant.Console, create a Models folder. Add the following four files. Keeping one class per file makes a project easier to navigate as it grows.

Models/MenuItem.cs

namespace Restaurant.Console.Models;

public class MenuItem
{
    public string MenuItemId { get; }
    public string Name { get; }
    public decimal UnitPrice { get; }
    public bool IsVegetarian { get; }

    public MenuItem(
        string menuItemId,
        string name,
        decimal unitPrice,
        bool isVegetarian)
    {
        MenuItemId = menuItemId;
        Name = name;
        UnitPrice = unitPrice;
        IsVegetarian = isVegetarian;
    }
}

MenuItem describes an item that can be ordered. Its constructor requires the menu ID, display name, price, and vegetarian flag. UnitPrice is a decimal, just as in the previous receipt calculator.

The properties use { get; }. They can be assigned in the constructor, but code elsewhere cannot later write something like this:

// Not allowed:
// paneerWrap.UnitPrice = 9999m;

That restriction is intentional. The menu item’s basic details should exist at creation time.

Models/Customer.cs

namespace Restaurant.Console.Models;

public class Customer
{
    public string CustomerId { get; }
    public string FullName { get; }
    public string? DeliveryAddress { get; private set; }

    public Customer(string customerId, string fullName)
    {
        CustomerId = customerId;
        FullName = fullName;
    }

    public void UpdateDeliveryAddress(string? deliveryAddress)
    {
        DeliveryAddress = deliveryAddress;
    }
}

DeliveryAddress is nullable because a customer may exist before supplying a delivery address. The UpdateDeliveryAddress method provides a meaningful operation on a customer. At this stage it simply stores the value; later, business rules can be added in one place inside this method.

Models/OrderLine.cs

namespace Restaurant.Console.Models;

public class OrderLine
{
    public MenuItem Item { get; }
    public int Quantity { get; }

    public OrderLine(MenuItem item, int quantity)
    {
        Item = item;
        Quantity = quantity;
    }

    public decimal CalculateLineTotal()
    {
        return Item.UnitPrice * Quantity;
    }
}

This small support class is the key replacement for the earlier parallel arrays. An OrderLine directly holds:

  • the selected MenuItem
  • the quantity selected
  • the calculation for that line’s total

For a paneer wrap priced at with a quantity of , CalculateLineTotal() returns .

Models/Order.cs

namespace Restaurant.Console.Models;

public class Order
{
    public string OrderNumber { get; }
    public Customer Customer { get; }
    public DateTime CreatedAt { get; }
    public OrderLine[] Lines { get; }

    public Order(
        string orderNumber,
        Customer customer,
        DateTime createdAt,
        OrderLine[] lines)
    {
        OrderNumber = orderNumber;
        Customer = customer;
        CreatedAt = createdAt;
        Lines = lines;
    }

    public decimal CalculateSubtotal()
    {
        decimal subtotal = 0m;

        for (int index = 0; index < Lines.Length; index++)
        {
            subtotal += Lines[index].CalculateLineTotal();
        }

        return subtotal;
    }

    public int CalculateItemCount()
    {
        int itemCount = 0;

        for (int index = 0; index < Lines.Length; index++)
        {
            itemCount += Lines[index].Quantity;
        }

        return itemCount;
    }
}

The Order class has references to other objects:

  • Customer refers to the customer who placed the order.
  • Lines is an array of OrderLine objects.
  • Every OrderLine refers to a MenuItem.

This creates an object model rather than separate unrelated values. Order.CalculateSubtotal() also moves the subtotal calculation out of Program.cs and into the object that owns the order data.

This version deliberately uses an array because arrays and loops are already familiar from the previous lesson. A later lesson will replace this with generic collections such as List<OrderLine> and will make ownership of order lines safer.


Use the model from the console application

Replace the contents of src/Restaurant.Console/Program.cs with the following code:

using Restaurant.Console.Models;

const decimal TaxRate = 0.05m;

var paneerWrap = new MenuItem(
    "M-101",
    "Paneer Wrap",
    250.00m,
    true);

var vegBiryani = new MenuItem(
    "M-102",
    "Veg Biryani",
    180.00m,
    true);

var lemonSoda = new MenuItem(
    "M-103",
    "Lemon Soda",
    70.00m,
    true);

var customer = new Customer("C-1001", "Asha Patel");
customer.UpdateDeliveryAddress("12 Lake Road");

OrderLine[] lines =
{
    new OrderLine(paneerWrap, 2),
    new OrderLine(vegBiryani, 1),
    new OrderLine(lemonSoda, 3)
};

var order = new Order(
    "ORD-2025-001",
    customer,
    DateTime.Now,
    lines);

Console.Write("Discount code (SAVE10 or press Enter for none): ");
string? discountCode = Console.ReadLine();

decimal? discountRate = null;

if (string.Equals(discountCode, "SAVE10", StringComparison.OrdinalIgnoreCase))
{
    discountRate = 0.10m;
}
else if (!string.IsNullOrWhiteSpace(discountCode))
{
    Console.WriteLine("The discount code was not recognized. No discount will be applied.");
}

Console.WriteLine();
Console.WriteLine("----- Restaurant Receipt -----");
Console.WriteLine($"Order:    {order.OrderNumber}");
Console.WriteLine($"Customer: {order.Customer.FullName}");
Console.WriteLine("------------------------------");

for (int index = 0; index < order.Lines.Length; index++)
{
    OrderLine line = order.Lines[index];
    decimal lineTotal = line.CalculateLineTotal();

    Console.WriteLine(
        $"{line.Item.Name,-16} {line.Quantity,2} x {line.Item.UnitPrice,8:C} = {lineTotal,8:C}");
}

decimal subtotal = order.CalculateSubtotal();
int totalItemCount = order.CalculateItemCount();

decimal appliedDiscountRate = discountRate ?? 0m;
decimal discount = subtotal * appliedDiscountRate;
decimal taxableAmount = subtotal - discount;
decimal tax = taxableAmount * TaxRate;
decimal finalTotal = taxableAmount + tax;

Console.WriteLine("------------------------------");
Console.WriteLine($"Items:    {totalItemCount}");
Console.WriteLine($"Subtotal: {subtotal:C}");

if (discountRate.HasValue)
{
    Console.WriteLine($"Discount ({discountRate.Value:P0}): -{discount:C}");
}

Console.WriteLine($"Tax:      {tax:C}");
Console.WriteLine($"Total:    {finalTotal:C}");

Read the object creation code from inside out:

var paneerWrap = new MenuItem("M-101", "Paneer Wrap", 250.00m, true);

This calls the MenuItem constructor and stores the resulting object in paneerWrap.

new OrderLine(paneerWrap, 2)

This calls the OrderLine constructor. The first argument is not a string or an ID; it is the actual MenuItem object. The relationship is therefore explicit in the code.

var order = new Order("ORD-2025-001", customer, DateTime.Now, lines);

This creates the Order object by combining a customer, creation time, and the lines that belong to that order.

When the program calls:

decimal subtotal = order.CalculateSubtotal();

it asks the Order object to calculate its own subtotal. Program.cs does not need to know the internal loop used by that method. This separation is valuable because the calculation remains in one place as the application becomes larger.


Run, inspect, and commit the refactor

Run the program from the solution root:

dotnet run --project src/Restaurant.Console/Restaurant.Console.csproj

With no discount code, the key values should still be:

ValueExpected amount
Item count6
Subtotal890.00
Tax44.50
Final total934.50

With SAVE10, the final total should be , as in the previous lesson.

When inspecting the code, notice these changes in responsibility:

Earlier procedural responsibilityNew location
A loop combines three separate arraysOrder contains OrderLine[] objects
Program.cs multiplies a price and quantityOrderLine.CalculateLineTotal()
Program.cs calculates the subtotalOrder.CalculateSubtotal()
A customer name is only a text variableA Customer object has identity, name, and an address operation

A practical debugging technique is to place a breakpoint on this line:

decimal subtotal = order.CalculateSubtotal();

Step into the method and inspect Lines[index]. You should see an OrderLine, its Item, and its Quantity grouped together. That is the structural improvement classes provide.

When the program builds and produces the expected receipt, commit the feature:

git status
git add src/Restaurant.Console
git commit -m "feat: model restaurant domain classes"
git push -u origin feature/restaurant-domain-classes

Open a pull request into main. In its description, note that the change replaces parallel arrays with MenuItem, Customer, OrderLine, and Order classes while preserving the existing receipt calculation. This is useful evidence of deliberate refactoring when you later present the project.


Wrap-up

You have moved from loosely connected primitive values to a small restaurant object model:

  • A class defines a custom C# type; an object is an instance created with new.
  • Constructors establish the initial state an object needs to be useful.
  • Properties represent state, while methods express behavior.
  • MenuItem, Customer, OrderLine, and Order make the relationships in an order explicit.
  • OrderLine.CalculateLineTotal() and Order.CalculateSubtotal() keep calculation behavior close to the data it uses.
  • get-only and private set properties begin to limit accidental changes to important data.

The model is clearer, but it still trusts callers to create sensible values. For example, nothing currently prevents new OrderLine(paneerWrap, 0) or a negative quantity. In the next lesson, you will apply encapsulation and enums to protect valid restaurant state more deliberately.

Can't find a good explanation? Sign up and we'll make it for you

Sign up