cshttp Tutorial - Day 5: Dynamic Data & Memory State (Interactive Guestbook)

Back to cshttp Tutorial Main Menu

Day 5: Dynamic Data & Memory State (Interactive Guestbook)

In Day 4, when a visitor submitted a Contact message, our server displayed a "Thank You" page and printed the message to the console. However, once the request finished, the message was gone from the website.

Today, we will build our first Stateful Web Application: a live Community Guestbook!

Visitors will be able to post comments, and our C# server will remember them in memory (List<string>) so every visitor who opens the website can read all posted comments!

In Day 5, we will:

  1. Store data in Server Memory using a static List<string> field.
  2. Generate HTML dynamically from a list using StringBuilder and a foreach loop.
  3. Add a form to post new Guestbook entries (POST).
  4. Learn the Post-Redirect-Get (PRG) pattern using Response.Redirect("/guestbook") so refreshing the browser never submits duplicate comments.

1. Storing Data in Server Memory (static List<string>)

When you declare a variable inside a method (like string name = ...; inside HandleGreet()), that variable only lives for a few milliseconds while that single request is running. As soon as the page is sent to the browser, local variables disappear.

To make our server remember data across all requests and all visitors, we declare a static variable at the class Program level (outside of any method)!

First, make sure using System.Collections.Generic; is at the top of Program.cs so we can use List<string>. Then, add guestbookEntries right inside class Program:

using System;
using System.Collections.Generic;
using cshttp;

namespace ConsoleApp1
{
    internal class Program
    {
        // A list stored in server memory!
        // Every visitor to the website shares this exact same list.
        static List<string> guestbookEntries = new List<string>()
        {
            "[2026-10-22 09:00] System: Welcome to our new Guestbook!",
            "[2026-10-22 09:05] Alice: First comment! C# web dev is awesome."
        };

        static void Main(string[] args)
        {
            WebApp.Run(8080, OnRequest);
        }
        // ...

We pre-filled the list with two starter comments so we can immediately see how they look on our page!


2. Adding /guestbook to Our Router and Navigation Bar

Just like we did for /greet in Day 3, let's wire up /guestbook in two places:

Step 1: Add case "/guestbook": to OnRequest()

static void OnRequest()
{
    string path = Request.Path.ToLower();

    switch (path)
    {
        case "/":
        case "/home":
            HandleHome();
            break;
        case "/guestbook":   // <-- NEW: Route /guestbook to HandleGuestbook()
            HandleGuestbook();
            break;
        case "/greet":
            HandleGreet();
            break;
        case "/about":
            HandleAbout();
            break;
        case "/contact":
            HandleContact();
            break;
        default:
            HandleNotFound();
            break;
    }
}

Step 2: Add the Guestbook Link to RenderPage()

<nav>
    <a href='/'>Home</a> | 
    <a href='/guestbook'>Guestbook</a> | 
    <a href='/greet'>Greet</a> | 
    <a href='/about'>About</a> | 
    <a href='/contact'>Contact</a>
</nav>

3. Building an HTML List with StringBuilder and foreach

In HTML, a bulleted list is created using <ul> (Unordered List) and <li> (List Item) tags:

<ul>
    <li>[2026-10-22 09:00] System: Welcome to our new Guestbook!</li>
    <li>[2026-10-22 09:05] Alice: First comment! C# web dev is awesome.</li>
</ul>

Since our comments are stored in guestbookEntries (which will grow as users post more comments), we cannot hardcode the <li> tags. Instead, we loop through guestbookEntries using a C# foreach loop and build the HTML using StringBuilder!

What is StringBuilder? Found in the System.Text namespace (using System.Text;), StringBuilder is C#'s tool for efficiently piecing together a string inside a loop using .Append(...).

Let's write the first version of HandleGuestbook() that loops through guestbookEntries and displays them on the page:

static void HandleGuestbook()
{
    // 1. Build the HTML list of all current comments using a loop
    StringBuilder listHtml = new StringBuilder();
    listHtml.Append("<ul>");

    foreach (string entry in guestbookEntries)
    {
        listHtml.Append($"<li style='margin-bottom: 8px;'>{entry}</li>");
    }

    listHtml.Append("</ul>");

    // 2. Render the list of comments on the page
    string content = $@"
        <h1>Community Guestbook</h1>
        <h2>Recent Entries ({guestbookEntries.Count})</h2>
        {listHtml.ToString()}";

    RenderPage("Guestbook", content);
}

Notice {guestbookEntries.Count} inside the <h2> heading—it automatically shows the total number of comments currently in the list!


4. Adding the "Sign Guestbook" Form

Next, let's place an HTML <form method='post'> right above the comment list inside HandleGuestbook() so visitors can type their Name (author) and Comment (comment):

string content = $@"
    <h1>Community Guestbook</h1>
    <p>Leave a message for everyone visiting this site!</p>

    <form action='/guestbook' method='post' style='background: #f9f9f9; padding: 15px; border: 1px solid #ddd; max-width: 450px;'>
        <div>
            <label>Your Name:</label><br />
            <input type='text' name='author' required style='width: 100%;' />
        </div>
        <br />
        <div>
            <label>Your Comment:</label><br />
            <textarea name='comment' rows='3' required style='width: 100%;'></textarea>
        </div>
        <br />
        <button type='submit'>Sign Guestbook</button>
    </form>

    <hr />
    <h2>Recent Entries ({guestbookEntries.Count})</h2>
    {listHtml.ToString()}";

5. Saving New Comments & The "Post-Redirect-Get" Pattern

Now, what should happen at the top of HandleGuestbook() when a visitor clicks Sign Guestbook (Request.HttpMethod == "POST")?

  1. We read author and comment from Request.Form.
  2. We check !string.IsNullOrWhiteSpace(...) to make sure they didn't submit empty spaces.
  3. We format the new comment with the current date/time (DateTime.Now) and add it to our list using guestbookEntries.Add(entry);.
  4. Finally, we call Response.Redirect("/guestbook");!
// 1. If user submitted a new comment:
if (Request.HttpMethod == "POST")
{
    string author = Request.Form["author"];
    string comment = Request.Form["comment"];

    if (!string.IsNullOrWhiteSpace(author) && !string.IsNullOrWhiteSpace(comment))
    {
        string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
        string entry = $"[{timestamp}] {author}: {comment}";

        // Add to our server list
        guestbookEntries.Add(entry);
    }

    // Redirect back to the guestbook page so the browser refreshes cleanly
    Response.Redirect("/guestbook");
    return;
}

Why Do We Call Response.Redirect("/guestbook")?

You might wonder: why don't we just let the POST request continue down the method and render the page directly?

Have you ever submitted a form on a website, pressed the Refresh (F5) button afterward, and seen an annoying browser popup saying "Confirm Form Resubmission: Are you sure you want to send this form again?"?

If we don't redirect, the browser's last action remains a POST request. Every time the user refreshes the page, the browser will re-send that POST request and add the exact same comment to the guestbook 2, 3, or 4 times!

By calling Response.Redirect("/guestbook"), our server tells the browser:

"I have saved your POST data! Now, please perform a fresh, clean GET request to /guestbook."

In web development, this famous technique is called the PRG (Post-Redirect-Get) Pattern. Because the final page load is a clean GET request, the user can refresh the page as many times as they like without ever duplicating their comment!


6. Complete Code for Day 5

Here is the complete Program.cs for Day 5. Notice using System.Collections.Generic; and using System.Text; at the very top:

using System;
using System.Collections.Generic;
using System.Text;
using cshttp;

namespace ConsoleApp1
{
    internal class Program
    {
        // NEW IN DAY 5: A list stored in server memory!
        // Every visitor to the website shares this exact same list.
        static List<string> guestbookEntries = new List<string>()
        {
            "[2026-10-22 09:00] System: Welcome to our new Guestbook!",
            "[2026-10-22 09:05] Alice: First comment! C# web dev is awesome."
        };

        static void Main(string[] args)
        {
            WebApp.Run(8080, OnRequest);
        }

        static void OnRequest()
        {
            string path = Request.Path.ToLower();

            switch (path)
            {
                case "/":
                case "/home":
                    HandleHome();
                    break;
                case "/guestbook":
                    HandleGuestbook();
                    break;
                case "/greet":
                    HandleGreet();
                    break;
                case "/about":
                    HandleAbout();
                    break;
                case "/contact":
                    HandleContact();
                    break;
                default:
                    HandleNotFound();
                    break;
            }
        }

        static void RenderPage(string title, string bodyContent)
        {
            string html = $@"
<html>
    <head>
        <title>{title}</title>
    </head>
    <body>
        <nav>
            <a href='/'>Home</a> | 
            <a href='/guestbook'>Guestbook</a> | 
            <a href='/greet'>Greet</a> | 
            <a href='/about'>About</a> | 
            <a href='/contact'>Contact</a>
        </nav>
        <hr />
        
        {bodyContent}

        <hr />
        <footer>
            <small>Server Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss}</small>
        </footer>
    </body>
</html>";

            Response.Write(html);
        }

        // NEW IN DAY 5: Combining POST forms, C# Lists, and foreach loops!
        static void HandleGuestbook()
        {
            // 1. If user submitted a new comment:
            if (Request.HttpMethod == "POST")
            {
                string author = Request.Form["author"];
                string comment = Request.Form["comment"];

                if (!string.IsNullOrWhiteSpace(author) && !string.IsNullOrWhiteSpace(comment))
                {
                    string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm");
                    string entry = $"[{timestamp}] {author}: {comment}";

                    // Add to our server list
                    guestbookEntries.Add(entry);
                }

                // Redirect back to the guestbook page so the browser refreshes cleanly
                Response.Redirect("/guestbook");
                return;
            }

            // 2. Build the HTML list of all current comments using a loop
            StringBuilder listHtml = new StringBuilder();
            listHtml.Append("<ul>");
            foreach (string entry in guestbookEntries)
            {
                listHtml.Append($"<li style='margin-bottom: 8px;'>{entry}</li>");
            }
            listHtml.Append("</ul>");

            // 3. Render the Guestbook form + the list of comments
            string content = $@"
                <h1>Community Guestbook</h1>
                <p>Leave a message for everyone visiting this site!</p>

                <form action='/guestbook' method='post' style='background: #f9f9f9; padding: 15px; border: 1px solid #ddd; max-width: 450px;'>
                    <div>
                        <label>Your Name:</label><br />
                        <input type='text' name='author' required style='width: 100%;' />
                    </div>
                    <br />
                    <div>
                        <label>Your Comment:</label><br />
                        <textarea name='comment' rows='3' required style='width: 100%;'></textarea>
                    </div>
                    <br />
                    <button type='submit'>Sign Guestbook</button>
                </form>

                <hr />
                <h2>Recent Entries ({guestbookEntries.Count})</h2>
                {listHtml.ToString()}";

            RenderPage("Guestbook", content);
        }

        static void HandleContact()
        {
            if (Request.HttpMethod == "POST")
            {
                string senderName = Request.Form["sender"];
                string email = Request.Form["email"];
                string message = Request.Form["message"];

                Console.WriteLine($"[New Message] From: {senderName} ({email}) - {message}");

                string thankYouContent = $@"
                    <h1>Thank You, {senderName}!</h1>
                    <p>We received your message and will reply to <b>{email}</b> soon.</p>
                    <div style='background: #f0f0f0; padding: 10px; border-radius: 4px;'>
                        <i>""{message}""</i>
                    </div>
                    <p><a href='/contact'>Send another message</a></p>";

                RenderPage("Message Received", thankYouContent);
                return;
            }

            string formContent = @"
                <h1>Contact Us</h1>
                <p>Send us a message below:</p>
                <form action='/contact' method='post'>
                    <div>
                        <label>Your Name:</label><br />
                        <input type='text' name='sender' required style='width: 300px;' />
                    </div>
                    <br />
                    <div>
                        <label>Email Address:</label><br />
                        <input type='email' name='email' required style='width: 300px;' />
                    </div>
                    <br />
                    <div>
                        <label>Message:</label><br />
                        <textarea name='message' rows='5' cols='40' required></textarea>
                    </div>
                    <br />
                    <button type='submit'>Send Message</button>
                </form>";

            RenderPage("Contact", formContent);
        }

        static void HandleGreet()
        {
            string name = Request.QueryString["name"];
            string greetingMessage = "";

            if (string.IsNullOrEmpty(name))
            {
                greetingMessage = "<p>Please enter your name below, or try clicking a quick link!</p>";
            }
            else
            {
                greetingMessage = $"<h2>Hello, {name}! Welcome to C# Web Development!</h2>";
            }

            string content = $@"
                <h1>Interactive Greeting</h1>
                {greetingMessage}

                <form action='/greet' method='get'>
                    <label>Your Name:</label>
                    <input type='text' name='name' placeholder='Type your name...' />
                    <button type='submit'>Say Hello</button>
                </form>

                <p>Or test query links directly: 
                    <a href='/greet?name=Alice'>Greet Alice</a> | 
                    <a href='/greet?name=Bob'>Greet Bob</a>
                </p>";

            RenderPage("Greet", content);
        }

        static void HandleHome()
        {
            string content = @"
                <h1>Welcome cshttp!</h1>
                <p>Check out our new <a href='/guestbook'>Guestbook</a> to see shared server state in action!</p>";

            RenderPage("Home", content);
        }

        static void HandleAbout()
        {
            string content = @"
                <h1>About Us</h1>
                <p>It's Me!</p>";

            RenderPage("About Us", content);
        }

        static void HandleNotFound()
        {
            Response.StatusCode = 404;
            string content = @"
                <h1>404 Not Found</h1>
                <p>The page that you are looking for does not exist.</p>";

            RenderPage("404 Not Found", content);
        }
    }
}

7. Test It in Your Browser

  1. Run the application (dotnet run or press Start in Visual Studio).
  2. Open your browser and visit http://localhost:8080/guestbook.
  3. Type your name and a comment, then click Sign Guestbook.
  4. Watch how the page refreshes and your new comment appears at the bottom of the list, while the counter increases to Recent Entries (3)!
  5. Try refreshing the page (F5)—because we used Response.Redirect("/guestbook"), the page refreshes cleanly without reposting your comment!
  6. Open a second browser window (or an Incognito tab) and visit http://localhost:8080/guestbook—you will see the exact same comments there too, because guestbookEntries lives in shared server memory!

In Day 6, we will discover a hidden security flaw in our Guestbook (what happens if a mischievous visitor types HTML tags like <b> or <script> into a comment?) and learn how to protect our website with HttpUtility.HtmlEncode().