Skip to content
wannacode

Hotel reservations

Problem

One room, two confirmations

A small hotel has four rooms. Reservations come from everywhere: from the website, from the front desk, from travel agencies. One Friday evening, two agencies reserve the last double room in the same second. Both get a confirmation. On Saturday, two families arrive at the room.

We will build a system where the server keeps the rooms and the reservations, and clients send requests over the network. A client can:

  • see the free rooms;
  • check one room;
  • reserve a room in their own name;
  • cancel their own reservation.

And when two clients reserve the same room at the same time, only one of them succeeds. We will prove it with a program that gives the same output on every run.

We build from the bottom up: the model without a network, the rules and the errors, the protocol, the server and the client, the simultaneous requests, and finally everything in one program.

More precisely, if you are curious
The model is the same in Java and C#. In Java the clients send lines of text over a socket, and each client is in its own thread. In C# the server is a web service with ASP.NET Core, and the client calls it with HttpClient. In both languages the same key idea guards the rooms: lock.

The idea

The room

A room has a number, a type, a price per night and a state: free or taken.

class Room {
private final int number;
private final String type;
private final int price;
private boolean free = true;
public Room(int number, String type, int price) {
this.number = number;
this.type = type;
this.price = price;
}
public int getNumber() {
return number;
}
public String getType() {
return type;
}
public int getPrice() {
return price;
}
public boolean isFree() {
return free;
}
public void occupy() {
free = false;
}
public void release() {
free = true;
}
@Override
public String toString() {
return number + ", " + type + ", €" + price + " per night, " + (free ? "free" : "taken");
}
}
  • The number, the type and the price do not change. Only the state changes, and only through two methods with clear names: Occupy and Release.
  • The room does not know who is staying in it. The room goes out to every client who asks, and the guest's name should not leave the hotel.
More precisely, if you are curious
IsFree has a public set because the client gets the room as JSON, and System.Text.Json has to write the state. The number, the type and the price come through the constructor. Without set, the client would see every room as free. For more than two states you use an enumeration: enum RoomState { Free, Taken, Repair }.

See it

The hotel and the reservation

The hotel keeps the rooms in a list and the guests in a dictionary "room number → name":
private final List<Room> rooms = new ArrayList<>();
private final Map<Integer, String> guests = new HashMap<>();
public void add(Room room) {
rooms.add(room);
}
public List<Room> freeRooms() {
return rooms.stream().filter(Room::isFree).toList();
}
public Room check(int number) {
return find(number);
}

Find is private: it looks up the room by number or throws an exception. That way nobody from outside can grab a room and take it around the rules:

private Room find(int number) {
for (Room room : rooms) {
if (room.getNumber() == number) {
return room;
}
}
throw new RoomNotFoundException("There is no room " + number + ".");
}

The reservation checks the name, finds the room and checks whether it is free. Only then does it take it:

public void reserve(int number, String guest) {
if (guest.isBlank()) {
throw new HotelException("The guest's name is required.");
}
Room room = find(number);
if (!room.isFree()) {
throw new RoomTakenException("Room " + number + " is already taken.");
}
room.occupy();
guests.put(number, guest);
}
Hotel hotel = new Hotel();
hotel.add(new Room(101, "single", 60));
hotel.add(new Room(102, "double", 85));
hotel.add(new Room(201, "double", 95));
hotel.add(new Room(202, "suite", 140));
for (Room room : hotel.freeRooms()) {
System.out.println(room);
}
hotel.reserve(102, "Anna Petrova");
System.out.println("After the reservation: " + hotel.check(102));
System.out.println("Free rooms: " + hotel.freeRooms().size());
Output
101, single, €60 per night, free
102, double, €85 per night, free
201, double, €95 per night, free
202, suite, €140 per night, free
After the reservation: 102, double, €85 per night, taken
Free rooms: 3

The idea

Canceling and the rules

Not every request can be carried out: the room is taken, there is no room 305, a guest cancels someone else's reservation. These are refusals by the rules, so they are your own exceptions with a common parent:

class HotelException extends RuntimeException {
public HotelException(String message) {
super(message);
}
}
class RoomNotFoundException extends HotelException {
public RoomNotFoundException(String message) {
super(message);
}
}
class RoomTakenException extends HotelException {
public RoomTakenException(String message) {
super(message);
}
}

Canceling checks that the room is taken and that the same guest is canceling it:

public void cancel(int number, String guest) {
Room room = find(number);
if (room.isFree()) {
throw new HotelException("Room " + number + " is not reserved.");
}
if (!guests.get(number).equals(guest)) {
throw new HotelException("Room " + number + " is reserved by another guest.");
}
room.release();
guests.remove(number);
}

The project brief asks for the system to be checked in different scenarios: reserving, checking and canceling, both when they are valid and when they are not. Each scenario is a line in the output:

Hotel hotel = new Hotel();
hotel.add(new Room(101, "single", 60));
hotel.add(new Room(102, "double", 85));
hotel.add(new Room(201, "double", 95));
hotel.add(new Room(202, "suite", 140));
hotel.reserve(102, "Anna Petrova");
System.out.println("1. Free room: " + hotel.check(102));
try {
hotel.reserve(102, "Boris Iliev");
} catch (RoomTakenException e) {
System.out.println("2. Taken room: " + e.getMessage());
}
try {
hotel.cancel(102, "Boris Iliev");
} catch (HotelException e) {
System.out.println("3. Someone else's reservation: " + e.getMessage());
}
hotel.cancel(102, "Anna Petrova");
System.out.println("4. Cancellation: " + hotel.check(102));
try {
hotel.cancel(102, "Anna Petrova");
} catch (HotelException e) {
System.out.println("5. Second cancellation: " + e.getMessage());
}
try {
hotel.reserve(305, "Anna Petrova");
} catch (RoomNotFoundException e) {
System.out.println("6. No such room: " + e.getMessage());
}
try {
hotel.reserve(201, " ");
} catch (HotelException e) {
System.out.println("7. No name: " + e.getMessage());
}
Output
1. Free room: 102, double, €85 per night, taken
2. Taken room: Room 102 is already taken.
3. Someone else's reservation: Room 102 is reserved by another guest.
4. Cancellation: 102, double, €85 per night, free
5. Second cancellation: Room 102 is not reserved.
6. No such room: There is no room 305.
7. No name: The guest's name is required.

The specific exception says what the problem is. HotelException catches all refusals at once: the server will catch only that one and turn it into a reply for the client.

More precisely, if you are curious
  • C# has no checked exceptions. So the server has to catch them in one place, or a refusal by the rules goes out as a 500.
  • Scenarios like these are natural tests. In C# there are libraries for this, such as xUnit: each scenario becomes a method, and Assert.Throws<RoomTakenException>(...) checks the exception.

Try it

Who is in 201

What will the program print?

Hotel hotel = new Hotel();
hotel.Add(new Room(101, "single", 60));
hotel.Add(new Room(102, "double", 85));
hotel.Add(new Room(201, "double", 95));
hotel.Add(new Room(202, "suite", 140));
hotel.Reserve(101, "Anna Petrova");
hotel.Reserve(201, "Boris Iliev");
hotel.Cancel(101, "Anna Petrova");
try
{
hotel.Reserve(201, "Vera Koleva");
Console.WriteLine("Vera is in 201");
}
catch (RoomTakenException e)
{
Console.WriteLine("Vera is turned away");
}
Console.WriteLine(hotel.FreeRooms().Count);
Answer

The idea

The protocol: addresses and JSON

Every action is a method and an address, the data is JSON, and the status says how it went:

Request Body Response
GET /rooms/free 200 and the free rooms
GET /rooms/102 200 and the room, or 404
POST /rooms/102/reservation {"guest":"Anna Petrova"} 200 and a confirmation, 409 for a taken room, 404 for a room that does not exist
DELETE /rooms/102/reservation?guest=Anna Petrova 200 and a confirmation, or 400 with the reason

Every refusal of the model has its own status. RoomNotFoundException is 404 Not Found, RoomTakenException is 409 Conflict: the request is fine, but it clashes with the current state. Any other HotelException is 400 Bad Request.

The body of the reservation is a small class just for carrying data:

public class ReservationRequest
{
public string Guest { get; set; }
}
More precisely, if you are curious
  • The commands could be an enumeration: enum Command { Free, Check, Reserve, Cancel }. Then they are a separate type with a closed list of values. In the course they are constants, because we have not covered enumerations.
  • DELETE has no body by convention, so the name is in the address. In a real system, instead of a name the client logs in with an account, and the server knows who it is. Then nobody can cancel someone else's reservation by typing someone else's name.

See it

The server

The start is as in the lesson on the first server: the port, no ASP.NET messages, and JSON that does not escape non-English letters:

var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All));
var app = builder.Build();
app.Urls.Add("http://localhost:5331");

The model's refusals become statuses in one place, in Safe:

IResult Safe(Func<IResult> action)
{
try
{
return action();
}
catch (RoomNotFoundException e)
{
return Results.NotFound(e.Message);
}
catch (RoomTakenException e)
{
return Results.Conflict(e.Message);
}
catch (HotelException e)
{
return Results.BadRequest(e.Message);
}
}

The addresses call the hotel. Reserving and canceling are lambdas with a body, because they do two things: call the model and return a confirmation:

app.MapGet("/rooms/free", () => hotel.FreeRooms());
app.MapGet("/rooms/{number:int}", (int number) => Safe(() => Results.Ok(hotel.Check(number))));
app.MapPost("/rooms/{number:int}/reservation", (int number, ReservationRequest request) => Safe(() =>
{
hotel.Reserve(number, request.Guest);
return Results.Ok("Room " + number + " is yours, " + request.Guest + ".");
}));
app.MapDelete("/rooms/{number:int}/reservation", (int number, string guest) => Safe(() =>
{
hotel.Cancel(number, guest);
return Results.Ok("The reservation for room " + number + " is canceled.");
}));

Here is what goes over the network:

using var http = new HttpClient { BaseAddress = new Uri("http://localhost:5331") };
async Task Show(string request, HttpResponseMessage response)
{
Console.WriteLine(request + " -> " + (int)response.StatusCode + " " + await response.Content.ReadAsStringAsync());
}
var anna = new ReservationRequest { Guest = "Anna Petrova" };
var boris = new ReservationRequest { Guest = "Boris Iliev" };
await Show("POST /rooms/102/reservation", await http.PostAsJsonAsync("/rooms/102/reservation", anna));
await Show("POST /rooms/102/reservation", await http.PostAsJsonAsync("/rooms/102/reservation", boris));
await Show("GET /rooms/102", await http.GetAsync("/rooms/102"));
await Show("DELETE /rooms/102/reservation?guest=Boris Iliev", await http.DeleteAsync("/rooms/102/reservation?guest=Boris Iliev"));
await Show("DELETE /rooms/102/reservation?guest=Anna Petrova", await http.DeleteAsync("/rooms/102/reservation?guest=Anna Petrova"));
await Show("GET /rooms/305", await http.GetAsync("/rooms/305"));
Output
POST /rooms/102/reservation -> 200 "Room 102 is yours, Anna Petrova."
POST /rooms/102/reservation -> 409 "Room 102 is already taken."
GET /rooms/102 -> 200 {"number":102,"type":"double","price":85,"isFree":false}
DELETE /rooms/102/reservation?guest=Boris Iliev -> 400 "Room 102 is reserved by another guest."
DELETE /rooms/102/reservation?guest=Anna Petrova -> 200 "The reservation for room 102 is canceled."
GET /rooms/305 -> 404 "There is no room 305."

See it

The client

The client hides HTTP behind methods. A response with an error becomes the same exception the server threw: 404, 409 and 400 come back as RoomNotFoundException, RoomTakenException and HotelException:

public class HotelClient
{
private readonly HttpClient http;
public HotelClient(string address)
{
http = new HttpClient { BaseAddress = new Uri(address) };
}
public async Task<List<Room>> FreeRoomsAsync()
{
return await http.GetFromJsonAsync<List<Room>>("/rooms/free");
}
public async Task<Room> CheckAsync(int number)
{
HttpResponseMessage response = await http.GetAsync("/rooms/" + number);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<Room>();
}
public async Task<string> ReserveAsync(int number, string guest)
{
var request = new ReservationRequest { Guest = guest };
HttpResponseMessage response = await http.PostAsJsonAsync("/rooms/" + number + "/reservation", request);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
public async Task<string> CancelAsync(int number, string guest)
{
HttpResponseMessage response = await http.DeleteAsync("/rooms/" + number + "/reservation?guest=" + guest);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
private static async Task ThrowIfError(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}
string message = await response.Content.ReadFromJsonAsync<string>();
int status = (int)response.StatusCode;
if (status == 404)
{
throw new RoomNotFoundException(message);
}
if (status == 409)
{
throw new RoomTakenException(message);
}
throw new HotelException(message);
}
}
var client = new HotelClient("http://localhost:5335");
Console.WriteLine(await client.ReserveAsync(102, "Anna Petrova"));
Console.WriteLine(await client.CheckAsync(102));
foreach (Room room in await client.FreeRoomsAsync())
{
Console.WriteLine("Free: " + room);
}
Console.WriteLine(await client.CancelAsync(102, "Anna Petrova"));
try
{
await client.CheckAsync(305);
}
catch (RoomNotFoundException e)
{
Console.WriteLine("Refused: " + e.Message);
}
Output
Room 102 is yours, Anna Petrova.
102, double, €85 per night, taken
Free: 101, single, €60 per night, free
Free: 201, double, €95 per night, free
Free: 202, suite, €140 per night, free
The reservation for room 102 is canceled.
Refused: There is no room 305.

The idea

Two for one room

A reservation is two steps: check that the room is free, then take it. If two requests run at the same time, this can happen:
Agency A Agency B
1 checks: 102 is free
2 checks: 102 is free
3 takes 102 for its guest
4 takes 102 for its guest and records that guest

Both get a confirmation. There is no mistake in any single line of code. The mistake is that the steps of the two requests interleave.

The fix: only one request at a time gets into the hotel. Then "check and take" is atomic: nobody can get in between the check and taking the room. In C#, a lock in every public method of Hotel does this:
private readonly List<Room> rooms = new List<Room>();
private readonly Dictionary<int, string> guests = new Dictionary<int, string>();
private readonly object sync = new object();
public void Reserve(int number, string guest)
{
lock (sync)
{
if (string.IsNullOrWhiteSpace(guest))
{
throw new HotelException("The guest's name is required.");
}
Room room = Find(number);
if (!room.IsFree)
{
throw new RoomTakenException("Room " + number + " is already taken.");
}
room.Occupy();
guests[number] = guest;
}
}

public void Reserve(int number, string guest)
{
lock (sync)
{
if (string.IsNullOrWhiteSpace(guest))
{
throw new HotelException("The guest's name is required.");
}
Room room = Find(number);
if (!room.IsFree)
{
throw new RoomTakenException("Room " + number + " is already taken.");
}
room.Occupy();
guests[number] = guest;
}
}
Step by step, in words
  1. Room 102 is free. Anna Petrova and Boris Iliev reserve it at the same moment.
  2. First without lock. This is one possible order: Anna's request checks and sees "free".
  3. Boris's request checks before Anna has taken the room: it sees "free" too.
  4. Anna takes the room and records her name.
  5. Boris takes the room too and records his name. Both have a confirmation for the same room.
  6. From the start, with lock (sync): only one request at a time gets into the hotel.
  7. Say Anna is first: she takes the lock. The check and the taking are one step, and nobody gets in between them.
  8. Boris reaches the hotel and waits until the lock is free.
  9. Boris has the lock. Now the check sees "taken".
  10. RoomTakenException. The server sends it back as 409 Conflict, and the client turns it into RoomTakenException again.
  11. The program from the lesson counts the ones that succeed: 1 for each room. Who was first is different on every run.

The proof: two clients reserve 102 at once, then twenty agencies reserve 201 at once. The replies are collected, and the program prints how many succeeded:

var client = new HotelClient("http://localhost:5332");
async Task<bool> TryReserve(HotelClient client, int number, string guest)
{
try
{
await client.ReserveAsync(number, guest);
return true;
}
catch (RoomTakenException)
{
return false;
}
}
async Task<int> Race(HotelClient client, int number, string[] guests)
{
var attempts = new List<Task<bool>>();
foreach (string guest in guests)
{
attempts.Add(TryReserve(client, number, guest));
}
bool[] reserved = await Task.WhenAll(attempts);
return reserved.Count(r => r);
}
string[] two = { "Anna Petrova", "Boris Iliev" };
Console.WriteLine("Two for room 102, succeeded: " + await Race(client, 102, two));
string[] agencies = new string[20];
for (int i = 0; i < agencies.Length; i++)
{
agencies[i] = "Agency " + (i + 1);
}
Console.WriteLine("Twenty for room 201, succeeded: " + await Race(client, 201, agencies));
Console.WriteLine("Free: " + string.Join("; ", await client.FreeRoomsAsync()));
Output
Two for room 102, succeeded: 1
Twenty for room 201, succeeded: 1
Free: 101, single, €60 per night, free; 202, suite, €140 per night, free

Task.WhenAll starts all the requests at once and waits for the replies. The refusal is RoomTakenException, and TryReserve turns it into false.

Who won the room is different on every run, so the program does not print it. How many succeeded is always the same: one for each room.

More precisely, if you are curious
  • Without the locking the program would still almost always say 1: the window between the check and taking the room is small. But "almost always" means that once in thousands of reservations two guests arrive at the room. You do not catch a bug like that by running the program, but by thinking: which data is shared, and who changes it at the same time.
  • lock (sync) locks the whole hotel. While one request reserves 102, another waits even for 201. With four rooms that is fine. With thousands you lock per room, for example lock (room) { ... }.
  • In a real system the rooms are in a database, and the guarding happens there: a transaction or a condition in the query itself (UPDATE rooms SET free = false WHERE number = 102 AND free = true). Then it holds even with several servers, not only inside one program.

Try it

Locked, but not quite

A colleague split the reservation into two methods, both with lock. The server calls the check first, then the taking. With many requests at once, two people still sometimes get the same room. Which line is the bug on?

Pick the line with the bug.

See it

The whole hotel

The test scenarios from the brief, this time over the network, with two clients connected at once. At the end two people reserve 201 at the same moment:

var anna = new HotelClient("http://localhost:5333");
var boris = new HotelClient("http://localhost:5333");
async Task Say(string who, Task<string> request)
{
try
{
Console.WriteLine(who + ": " + await request);
}
catch (HotelException e)
{
Console.WriteLine(who + ": refused, " + e.Message);
}
}
async Task<bool> TryReserve(HotelClient client, int number, string guest)
{
try
{
await client.ReserveAsync(number, guest);
return true;
}
catch (RoomTakenException)
{
return false;
}
}
async Task<int> Race(HotelClient client, int number, string[] guests)
{
var attempts = new List<Task<bool>>();
foreach (string guest in guests)
{
attempts.Add(TryReserve(client, number, guest));
}
bool[] reserved = await Task.WhenAll(attempts);
return reserved.Count(r => r);
}
await Say("Anna", anna.ReserveAsync(102, "Anna Petrova"));
await Say("Boris", boris.ReserveAsync(102, "Boris Iliev"));
Console.WriteLine("Boris: " + await boris.CheckAsync(102));
await Say("Boris", boris.CancelAsync(102, "Boris Iliev"));
await Say("Anna", anna.CancelAsync(102, "Anna Petrova"));
await Say("Boris", boris.ReserveAsync(102, "Boris Iliev"));
try
{
await anna.CheckAsync(305);
}
catch (RoomNotFoundException e)
{
Console.WriteLine("Anna: refused, " + e.Message);
}
string[] two = { "Anna Petrova", "Vera Koleva" };
Console.WriteLine("Two for room 201 at once, succeeded: " + await Race(anna, 201, two));
Console.WriteLine("Free: " + string.Join("; ", await anna.FreeRoomsAsync()));
Output
Anna: Room 102 is yours, Anna Petrova.
Boris: refused, Room 102 is already taken.
Boris: 102, double, €85 per night, taken
Boris: refused, Room 102 is reserved by another guest.
Anna: The reservation for room 102 is canceled.
Boris: Room 102 is yours, Boris Iliev.
Anna: refused, There is no room 305.
Two for room 201 at once, succeeded: 1
Free: 101, single, €60 per night, free; 202, suite, €140 per night, free
More precisely, if you are curious

The whole program, from start to finish:

Program.cs
using System.Text.Encodings.Web;
using System.Text.Unicode;
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All));
var app = builder.Build();
app.Urls.Add("http://localhost:5333");
Hotel hotel = new Hotel();
hotel.Add(new Room(101, "single", 60));
hotel.Add(new Room(102, "double", 85));
hotel.Add(new Room(201, "double", 95));
hotel.Add(new Room(202, "suite", 140));
IResult Safe(Func<IResult> action)
{
try
{
return action();
}
catch (RoomNotFoundException e)
{
return Results.NotFound(e.Message);
}
catch (RoomTakenException e)
{
return Results.Conflict(e.Message);
}
catch (HotelException e)
{
return Results.BadRequest(e.Message);
}
}
app.MapGet("/rooms/free", () => hotel.FreeRooms());
app.MapGet("/rooms/{number:int}", (int number) => Safe(() => Results.Ok(hotel.Check(number))));
app.MapPost("/rooms/{number:int}/reservation", (int number, ReservationRequest request) => Safe(() =>
{
hotel.Reserve(number, request.Guest);
return Results.Ok("Room " + number + " is yours, " + request.Guest + ".");
}));
app.MapDelete("/rooms/{number:int}/reservation", (int number, string guest) => Safe(() =>
{
hotel.Cancel(number, guest);
return Results.Ok("The reservation for room " + number + " is canceled.");
}));
await app.StartAsync();
var anna = new HotelClient("http://localhost:5333");
var boris = new HotelClient("http://localhost:5333");
async Task Say(string who, Task<string> request)
{
try
{
Console.WriteLine(who + ": " + await request);
}
catch (HotelException e)
{
Console.WriteLine(who + ": refused, " + e.Message);
}
}
async Task<bool> TryReserve(HotelClient client, int number, string guest)
{
try
{
await client.ReserveAsync(number, guest);
return true;
}
catch (RoomTakenException)
{
return false;
}
}
async Task<int> Race(HotelClient client, int number, string[] guests)
{
var attempts = new List<Task<bool>>();
foreach (string guest in guests)
{
attempts.Add(TryReserve(client, number, guest));
}
bool[] reserved = await Task.WhenAll(attempts);
return reserved.Count(r => r);
}
await Say("Anna", anna.ReserveAsync(102, "Anna Petrova"));
await Say("Boris", boris.ReserveAsync(102, "Boris Iliev"));
Console.WriteLine("Boris: " + await boris.CheckAsync(102));
await Say("Boris", boris.CancelAsync(102, "Boris Iliev"));
await Say("Anna", anna.CancelAsync(102, "Anna Petrova"));
await Say("Boris", boris.ReserveAsync(102, "Boris Iliev"));
try
{
await anna.CheckAsync(305);
}
catch (RoomNotFoundException e)
{
Console.WriteLine("Anna: refused, " + e.Message);
}
string[] two = { "Anna Petrova", "Vera Koleva" };
Console.WriteLine("Two for room 201 at once, succeeded: " + await Race(anna, 201, two));
Console.WriteLine("Free: " + string.Join("; ", await anna.FreeRoomsAsync()));
await app.StopAsync();
public class Room
{
public int Number { get; }
public string Type { get; }
public int Price { get; }
public bool IsFree { get; set; } = true;
public Room(int number, string type, int price)
{
Number = number;
Type = type;
Price = price;
}
public void Occupy()
{
IsFree = false;
}
public void Release()
{
IsFree = true;
}
public override string ToString()
{
return Number + ", " + Type + ", €" + Price + " per night, " + (IsFree ? "free" : "taken");
}
}
public class Hotel
{
private readonly List<Room> rooms = new List<Room>();
private readonly Dictionary<int, string> guests = new Dictionary<int, string>();
private readonly object sync = new object();
public void Add(Room room)
{
lock (sync)
{
rooms.Add(room);
}
}
public List<Room> FreeRooms()
{
lock (sync)
{
return rooms.Where(room => room.IsFree).ToList();
}
}
public Room Check(int number)
{
lock (sync)
{
return Find(number);
}
}
public void Reserve(int number, string guest)
{
lock (sync)
{
if (string.IsNullOrWhiteSpace(guest))
{
throw new HotelException("The guest's name is required.");
}
Room room = Find(number);
if (!room.IsFree)
{
throw new RoomTakenException("Room " + number + " is already taken.");
}
room.Occupy();
guests[number] = guest;
}
}
public void Cancel(int number, string guest)
{
lock (sync)
{
Room room = Find(number);
if (room.IsFree)
{
throw new HotelException("Room " + number + " is not reserved.");
}
if (guests[number] != guest)
{
throw new HotelException("Room " + number + " is reserved by another guest.");
}
room.Release();
guests.Remove(number);
}
}
private Room Find(int number)
{
Room room = rooms.FirstOrDefault(r => r.Number == number);
if (room == null)
{
throw new RoomNotFoundException("There is no room " + number + ".");
}
return room;
}
}
public class HotelException : Exception
{
public HotelException(string message) : base(message)
{
}
}
public class RoomNotFoundException : HotelException
{
public RoomNotFoundException(string message) : base(message)
{
}
}
public class RoomTakenException : HotelException
{
public RoomTakenException(string message) : base(message)
{
}
}
public class ReservationRequest
{
public string Guest { get; set; }
}
public class HotelClient
{
private readonly HttpClient http;
public HotelClient(string address)
{
http = new HttpClient { BaseAddress = new Uri(address) };
}
public async Task<List<Room>> FreeRoomsAsync()
{
return await http.GetFromJsonAsync<List<Room>>("/rooms/free");
}
public async Task<Room> CheckAsync(int number)
{
HttpResponseMessage response = await http.GetAsync("/rooms/" + number);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<Room>();
}
public async Task<string> ReserveAsync(int number, string guest)
{
var request = new ReservationRequest { Guest = guest };
HttpResponseMessage response = await http.PostAsJsonAsync("/rooms/" + number + "/reservation", request);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
public async Task<string> CancelAsync(int number, string guest)
{
HttpResponseMessage response = await http.DeleteAsync("/rooms/" + number + "/reservation?guest=" + guest);
await ThrowIfError(response);
return await response.Content.ReadFromJsonAsync<string>();
}
private static async Task ThrowIfError(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
{
return;
}
string message = await response.Content.ReadFromJsonAsync<string>();
int status = (int)response.StatusCode;
if (status == 404)
{
throw new RoomNotFoundException(message);
}
if (status == 409)
{
throw new RoomTakenException(message);
}
throw new HotelException(message);
}
}

Exercise

Keep going on your own

Work with the whole program from the previous card.

1. Changing rooms. The guest moves from one room to another with POST /rooms/102/move and the body {"guest":"Anna Petrova","to":202}. The reply is "Anna Petrova is in room 202.", and the client has a method MoveAsync. If the new room is taken, the guest stays in the old one. The move is one method in Hotel, in a lock: nobody should take the new room between freeing the old one and taking the new one.

Output
Room 102 is yours, Anna Petrova.
Room 201 is yours, Boris Iliev.
Refused: Room 201 is already taken.
Anna Petrova is in room 202.
Free: 101, single, €60 per night, free; 102, double, €85 per night, free

More ideas

  • Nights and total. A reservation is for a number of nights, and the confirmation says the total: "3 nights, €255".
  • Dates. A room is reserved for days, for example from December 14 to 16. Two reservations for the same room must not overlap. Whether the room is free depends on the dates.
  • My reservations. GET /reservations?guest=Anna Petrova says which rooms belong to this guest.
  • Waiting list. If the room is taken, the guest signs up to wait. When it is canceled, it goes to the first one on the list.
  • A server that does not stop. Move the server into its own program with await app.RunAsync(), and the client into another one, run in another terminal.

Test

Check yourself

1. Two agencies reserve room 102 at the same moment, and the reservation is not synchronized. What can happen?
2. The "is it free" check and taking the room are in two separate synchronized methods. Is that enough?
3. Room 102 is taken. Which status does POST /rooms/102/reservation answer with?
4. Why does the program print how many reservations succeeded, and not who won the room?
5. Why are the guests' names in a dictionary in the hotel, and not in the room?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·