cshttp Tutorial - Day 8: Saving Data Permanently to a File

Back to cshttp Tutorial Main Menu

Day 8: Saving Data Permanently to a File

Try this quick experiment with your Day 7 program:

  1. Run the server and post two or three comments in the Guestbook.
  2. Stop the server (Ctrl + C or press Stop in Visual Studio).
  3. Start the server again and refresh the Guestbook page.

All of your posted comments are gone!

Why did they disappear? Because our List<GuestbookEntry> lives inside the computer's temporary memory (RAM). Whenever a program stops running, Windows clears its RAM.

To keep our Guestbook comments forever—even after the server stops or the computer restarts—we need to save them to permanent storage on the hard disk: a text file (guestbook.txt).

Today, we will:

  1. Design a simple text file format using a pipe character (|) to separate Time, Name, and Message on each line.
  2. Locate our data file on disk using WebApp.MapPath("~/guestbook.txt").
  3. Write a SaveEntry() helper method using File.AppendAllText() to save each new comment to the file immediately.
  4. Write a LoadEntries() helper method using File.ReadAllLines() and string.Split() to load all saved comments back into our List<GuestbookEntry> when the server starts up!

1. Designing Our File Format (One Entry Per Line)

A .txt file on your hard drive is just a sequence of text lines. How do we store a GuestbookEntry object (which has 3 properties: Time, Name, and Message) on a single line of text?

We can separate the three properties using a | (pipe) character!

2026-10-27 14:30:05|Alice|First comment! C# web dev is awesome.
2026-10-27 14:31:40|Bob|Hello everyone! Nice website!

Look how easy it is to read each line:

  • Part 0 (before the first |): 2026-10-27 14:30:05 (Time)
  • Part 1 (between the two |s): Alice (Name)
  • Part 2 (after the second |): First comment! C# web dev is awesome. (Message)

Two Small Safety Rules for Our File Format:

Because each comment must fit on one single line and uses | as a separator:

  1. What if a visitor types | inside their Name? That would add an extra | and confuse our reader! So before saving, we replace any | in their name with /.
  2. What if a visitor presses the Enter key (a line break) inside their comment? That would split their comment across two lines in guestbook.txt! So before saving, we replace line breaks (\r and \n) with a space " ".

2. Where Should We Save guestbook.txt?

First, add using System.IO; at the very top of Program.cs — this gives us C#'s File class for reading and writing files.

Next, inside class Program, let's declare our guestbookEntries list and the path to guestbook.txt:

internal class Program
{
    // NEW IN DAY 8: The list starts empty; we will load saved entries from the file!
    static List<GuestbookEntry> guestbookEntries = new List<GuestbookEntry>();

    // NEW IN DAY 8: Get the full file path of guestbook.txt inside our app folder
    static string dataFile = WebApp.MapPath("~/guestbook.txt");

What Does WebApp.MapPath("~/guestbook.txt") Do?

In cshttp, the ~/ symbol means "the folder where my web application is running". WebApp.MapPath("~/guestbook.txt") automatically converts "~/guestbook.txt" into a complete Windows file path on your computer (for example: C:\Projects\ConsoleApp1\bin\Debug\guestbook.txt).


3. Saving a New Comment to the File (SaveEntry)

Let's write a helper method called SaveEntry(GuestbookEntry entry) that takes one GuestbookEntry object, turns it into a |-separated line of text, and adds it to the bottom of guestbook.txt:

static void SaveEntry(GuestbookEntry entry)
{
    string time = entry.Time.ToString("yyyy-MM-dd HH:mm:ss");
    string line = time + "|" + entry.Name + "|" + entry.Message;

    File.AppendAllText(dataFile, line + Environment.NewLine);
}

How Does SaveEntry Work?

  1. entry.Time.ToString("yyyy-MM-dd HH:mm:ss"):

    Formats the date and time with seconds (2026-10-27 14:30:05).

  2. time + "|" + entry.Name + "|" + entry.Message:

    Glues the three fields together with | separators.

  3. File.AppendAllText(dataFile, line + Environment.NewLine):
    • If guestbook.txt does not exist yet, File.AppendAllText creates it automatically!
    • If guestbook.txt already exists, it adds our new line (followed by Environment.NewLine, which moves to the next line) right at the very end of the file without erasing previous lines.

4. Calling SaveEntry When a Visitor Posts a Comment

Now let's update the POST block inside HandleGuestbook() so that whenever a visitor submits a comment, we:

  1. Clean any | or line breaks (\r, \n) out of their input.
  2. Add entry to our in-memory guestbookEntries list.
  3. Call SaveEntry(entry); so it is also written to guestbook.txt on disk!
if (Request.HttpMethod == "POST")
{
    string author = Request.Form["author"];
    string comment = Request.Form["comment"];

    if (!string.IsNullOrWhiteSpace(author) && !string.IsNullOrWhiteSpace(comment))
    {
        GuestbookEntry entry = new GuestbookEntry
        {
            // NEW IN DAY 8: Clean the text so it fits cleanly on one line of our file
            Name = author.Replace("|", "/").Replace("\r", " ").Replace("\n", " "),
            Message = comment.Replace("\r", " ").Replace("\n", " "),
            Time = DateTime.Now
        };

        guestbookEntries.Add(entry);
        SaveEntry(entry); // <-- NEW IN DAY 8: Also write it to guestbook.txt!
    }

    Response.Redirect("/guestbook");
    return;
}

5. Loading Saved Comments on Startup (LoadEntries)

Saving to guestbook.txt is half the job. Now, when our server starts up inside Main(), we need to read guestbook.txt and turn every line back into a GuestbookEntry object inside guestbookEntries!

Let's build the LoadEntries() method step by step:

static void LoadEntries()
{
    // 1. Check if guestbook.txt exists yet
    if (!File.Exists(dataFile))
    {
        return; // No file yet (first time running the app). Leave the list empty!
    }

    // 2. Read all lines from the file into a string[] array
    string[] lines = File.ReadAllLines(dataFile);

    // 3. Loop through each line and split it at the '|' character
    foreach (string line in lines)
    {
        // Split into at most 3 parts: [0] = Time, [1] = Name, [2] = Message
        string[] parts = line.Split(new char[] { '|' }, 3);
        if (parts.Length != 3)
        {
            continue; // Skip any blank or broken lines
        }

        GuestbookEntry entry = new GuestbookEntry
        {
            Time = DateTime.Parse(parts[0]),
            Name = parts[1],
            Message = parts[2]
        };

        guestbookEntries.Add(entry);
    }
}

Let's Look Closely at Three Key Lines in LoadEntries():

  1. if (!File.Exists(dataFile)) return;:

    The very first time you run your program, guestbook.txt hasn't been created yet. Trying to read a file that doesn't exist would crash the program! Checking File.Exists(dataFile) first makes sure we only read the file if it is actually there.

  2. line.Split(new char[] { '|' }, 3):

    Cuts the line at the | character into an array of strings (parts[0], parts[1], parts[2]). Why did we pass 3 as the maximum number of pieces? Because if the visitor typed a | inside their comment message (parts[2]), Split stops cutting after the 2nd | and keeps the entire message intact!

  3. DateTime.Parse(parts[0]):

    Does the exact opposite of .ToString(...): it converts the text "2026-10-27 14:30:05" back into a real C# DateTime value!

Finally, we call LoadEntries() inside Main() right before WebApp.Run(8080, OnRequest);:

static void Main(string[] args)
{
    // NEW IN DAY 8: Load saved entries from disk BEFORE the server starts!
    LoadEntries();
    Console.WriteLine("Guestbook file: " + dataFile);

    WebApp.Run(8080, OnRequest);
}

6. Complete Code for Day 8

Here is the complete Program.cs with permanent file storage:

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

namespace ConsoleApp1
{
    internal class GuestbookEntry
    {
        public string Name { get; set; }
        public string Message { get; set; }
        public DateTime Time { get; set; }
    }

    internal class Program
    {
        // NEW IN DAY 8: The list starts empty. Saved entries are loaded from guestbook.txt!
        static List<GuestbookEntry> guestbookEntries = new List<GuestbookEntry>();

        // NEW IN DAY 8: The full path of our data file on disk
        static string dataFile = WebApp.MapPath("~/guestbook.txt");

        static void Main(string[] args)
        {
            // NEW IN DAY 8: Load saved entries BEFORE the server starts
            LoadEntries();
            Console.WriteLine("Guestbook file: " + dataFile);

            WebApp.Run(8080, OnRequest);
        }

        // NEW IN DAY 8: Read every line of the file and turn it back into GuestbookEntry objects
        static void LoadEntries()
        {
            if (!File.Exists(dataFile))
            {
                return; // No file yet. That's fine: the guestbook is just empty.
            }

            string[] lines = File.ReadAllLines(dataFile);
            foreach (string line in lines)
            {
                // Split into at most 3 parts: time | name | message
                string[] parts = line.Split(new char[] { '|' }, 3);
                if (parts.Length != 3)
                {
                    continue; // Skip broken or empty lines
                }

                GuestbookEntry entry = new GuestbookEntry
                {
                    Time = DateTime.Parse(parts[0]),
                    Name = parts[1],
                    Message = parts[2]
                };
                guestbookEntries.Add(entry);
            }
        }

        // NEW IN DAY 8: Add one entry as a new line at the end of the file
        static void SaveEntry(GuestbookEntry entry)
        {
            string time = entry.Time.ToString("yyyy-MM-dd HH:mm:ss");
            string line = time + "|" + entry.Name + "|" + entry.Message;
            File.AppendAllText(dataFile, line + Environment.NewLine);
        }

        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);
        }

        static void HandleGuestbook()
        {
            if (Request.HttpMethod == "POST")
            {
                string author = Request.Form["author"];
                string comment = Request.Form["comment"];

                if (!string.IsNullOrWhiteSpace(author) && !string.IsNullOrWhiteSpace(comment))
                {
                    GuestbookEntry entry = new GuestbookEntry
                    {
                        // NEW IN DAY 8: Clean the text so it fits on one line of our file
                        Name = author.Replace("|", "/").Replace("\r", " ").Replace("\n", " "),
                        Message = comment.Replace("\r", " ").Replace("\n", " "),
                        Time = DateTime.Now
                    };

                    guestbookEntries.Add(entry);
                    SaveEntry(entry); // NEW IN DAY 8: Also write it to the file
                }

                Response.Redirect("/guestbook");
                return;
            }

            StringBuilder listHtml = new StringBuilder();
            listHtml.Append("<ul>");
            foreach (GuestbookEntry entry in guestbookEntries)
            {
                string safeName = WebApp.HtmlEncode(entry.Name);
                string safeMessage = WebApp.HtmlEncode(entry.Message);
                string time = entry.Time.ToString("yyyy-MM-dd HH:mm");

                listHtml.Append($@"
                    <li style='margin-bottom: 12px;'>
                        <b>{safeName}</b> 
                        <small style='color: gray;'>({time})</small><br />
                        {safeMessage}
                    </li>");
            }
            listHtml.Append("</ul>");

            string content = $@"
                <h1>Community Guestbook</h1>
                <p>Leave a message for everyone visiting this site! Entries are now saved to a file.</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 safeName = WebApp.HtmlEncode(senderName);
                string safeEmail = WebApp.HtmlEncode(email);
                string safeMessage = WebApp.HtmlEncode(message);

                string thankYouContent = $@"
                    <h1>Thank You, {safeName}!</h1>
                    <p>We received your message and will reply to <b>{safeEmail}</b> soon.</p>
                    <div style='background: #f0f0f0; padding: 10px; border-radius: 4px;'>
                        <i>""{safeMessage}""</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
            {
                string safeName = WebApp.HtmlEncode(name);
                greetingMessage = $"<h2>Hello, {safeName}! 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>Our <a href='/guestbook'>Guestbook</a> now remembers every comment, even after a restart!</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. Look at the console terminal window: it prints the exact path to guestbook.txt on your computer.
  3. Open http://localhost:8080/guestbook and post two or three comments.
  4. Copy the guestbook.txt file path from your console window and open that file in Notepad—you will see your comments stored there, one per line!
  5. Now stop the server completely (Ctrl + C or press Stop in Visual Studio), and start it again.
  6. Refresh http://localhost:8080/guestbook in your browser—all of your comments are still there!

Good to Know — What if 2 Visitors Click Submit at the Exact Same Millisecond? Our file code works great for learning and personal sites! In a high-traffic website where two people might click Submit at the exact same millisecond, they could try to write to guestbook.txt at the same time. In C#, a keyword called lock (and later, real databases like SQLite or MySQL) prevents this collision. We will explore that in a future lesson once you have mastered the fundamentals!

In Day 9, we will learn how to organize CSS stylesheets, pictures, and JavaScript files inside an /assets/ folder, configure them to copy to the output bin directory, and serve them with Response.TransmitFile()!