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:
MenuItemis the class, or blueprint.paneerWrapis oneMenuItemobject.Customerrepresents restaurant customers.Orderrepresents one purchase by one customer.OrderLinerepresents a specific menu item and the quantity ordered.
Instead of relying on array positions, each OrderLine keeps its own related information together:
| Earlier design | Class-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 match | The 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().
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:
| Class | Main responsibility |
|---|---|
MenuItem | Describe something that can appear on the menu. |
Customer | Store customer identity and delivery-address information. |
OrderLine | Pair one menu item with the quantity requested. |
Order | Associate a customer with order lines and calculate the subtotal. |
Program | Create 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 form | Who can read it? | Who can assign it? | Restaurant use |
|---|---|---|---|
{ get; } | Any caller | Only the class constructor | A menu item ID or unit price established when created |
{ get; set; } | Any caller | Any caller | A simple value that is allowed to change freely |
{ get; private set; } | Any caller | Only code inside the class | A 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:
- Has exactly the same name as its class.
- Has no return type, not even
void. - Runs when
newcreates an object. - 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:
Customerrefers to the customer who placed the order.Linesis an array ofOrderLineobjects.- Every
OrderLinerefers to aMenuItem.
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:
| Value | Expected amount |
|---|---|
| Item count | 6 |
| Subtotal | 890.00 |
| Tax | 44.50 |
| Final total | 934.50 |
With SAVE10, the final total should be , as in the previous lesson.
When inspecting the code, notice these changes in responsibility:
| Earlier procedural responsibility | New location |
|---|---|
| A loop combines three separate arrays | Order contains OrderLine[] objects |
Program.cs multiplies a price and quantity | OrderLine.CalculateLineTotal() |
Program.cs calculates the subtotal | Order.CalculateSubtotal() |
| A customer name is only a text variable | A 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, andOrdermake the relationships in an order explicit.OrderLine.CalculateLineTotal()andOrder.CalculateSubtotal()keep calculation behavior close to the data it uses.get-only andprivate setproperties 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