Mert Özen Her Satırda Daha İleri
Backend C# Temelleri — #20

C# Mini Proje: Konsolda Bookstore (Listele, Ekle, Ara, Dosyaya Kaydet)

Mert Özen 7 Kas 2026 13 dk 1 görüntülenme
C# Mini Proje: Konsolda Bookstore (Listele, Ekle, Ara, Dosyaya Kaydet)

C# Temelleri serisinin bitiş projesi: enum, DateOnly, nullable, TryParse, exception filtreleri ve dosya işlemleriyle konsolda çalışan bir Bookstore. İlk çalıştırmada yakalanan iki gerçek hata (14,00'ün 1400 olması ve sessizce silinen satır), düzeltmeleri ve OOP serisine köprü.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

Bu yazıda yazacağımız konsol uygulaması kullanıcıdan kitap fiyatı istiyor. Fiyatı okuyan fonksiyon şu. Kullanıcı Türkiye'de alıştığı gibi virgülle 14,00 yazarsa ne olur? Fonksiyon girdiyi reddedip tekrar mı sorar, 14 mü döner, başka bir şey mi?

decimal? ReadPrice()
{
    while (true)
    {
        string? text = ReadLine("Price (e.g. 12.50): ");
        if (text == null)
        {
            return null;
        }

        if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal price) && price > 0)
        {
            return price;
        }

        Console.WriteLine("Price must be a positive number like 12.50.");
    }
}

Ne Yazıyoruz

Cevap: fonksiyon 1400 döner. Uyarı yok, exception yok; 14 liralık kitap sessizce 1400 lira olur. NumberStyles.Number binlik ayırıcıya izin verir ve invariant culture'da binlik ayırıcı virgüldür; "14,00" böylece "bin dört yüz" olarak okunur. Bu satır, aşağıda yazacağımız uygulamanın ilk sürümünden. Hatayı tasarlayarak koymadım; uygulamayı ilk kez çalıştırdığımda yakaladım. Yazının ortasında onu nasıl fark ettiğimizi ve nasıl düzelttiğimizi göreceğiz.

Bu, C# Temelleri serisinin son yazısı. Şimdiye kadar gördüklerimizi tek bir programda birleştiriyoruz: kitapları listeleyen, yeni kitap ekleyen, başlığa ya da yazara göre arayan ve her şeyi bir dosyaya kaydeden küçük bir Bookstore. Proje tek bir console projesi ve üç dosyadan oluşuyor: Book.cs veriyi, BookFile.cs dosya işlerini, Program.cs kullanıcıyla konuşmayı tutuyor. csproj, serinin geri kalanıyla aynı: net6.0, LangVersion 10, nullable ve implicit using'ler açık.

Veri dosyası her satırda bir kitap tutuyor. Field'ları | ile ayırdım, çünkü kitap adlarında virgül çok sık geçer ("Love, Rosie" gibi) ve virgülle ayrılmış bir dosyada bu, satırı bozar. İlk çalıştırmadan önce dosya şöyleydi:

978-0441172719|Dune|Frank Herbert|Fiction|12.99|14|1965-08-01
978-0553380163|A Brief History of Time|Stephen Hawking|Science|18.50|6|1988-04-01
978-0062316097|Sapiens|Yuval Noah Harari|History|22.00|9|2014-09-04
978-0451524935|1984|George Orwell|Fiction|9.99|20|1949-06-08
978-0547928227|The Hobbit|J. R. R. Tolkien|Fantasy|14.00|5|1937-09-21

Son satırdaki "Fantasy" türü uygulamada tanımlı değil. Onu bilerek bıraktım; geçersiz bir satırın başına ne geldiğini birazdan göreceğiz.

Veri: Book ve Genre

namespace Bookstore.App;

public enum Genre
{
    Unknown = 0,
    Fiction = 1,
    Science = 2,
    History = 3,
    Children = 4
}

public record Book(
    string Isbn,
    string Title,
    string Author,
    Genre Genre,
    decimal Price,
    int Stock,
    DateOnly PublishedOn);

Genre, enum yazısındaki önerimize uyuyor: 0 değeri anlamlı bir tür değil, Unknown. Değerler elle verilmiş, çünkü ileride türü sayı olarak saklamaya karar verirsek numaraların kaymasını istemeyiz.

Book bir record. Record'ları OOP serisinde ayrıntılı göreceğiz; şimdilik onu birkaç değeri tek bir isim altında taşıyan bir kutu olarak düşün. Parantez içindeki her parametre bir property olur: book.Title, book.Price. Fiyat decimal, çünkü para; yayın tarihi DateOnly, çünkü saati ve zaman dilimi yok.

Dosyaya Yazmak ve Okumak: BookFile

using System.Globalization;

namespace Bookstore.App;

public static class BookFile
{
    private const char Separator = '|';
    private static readonly CultureInfo FileCulture = CultureInfo.InvariantCulture;

    public static List<Book> Load(string path, out int skippedLines)
    {
        List<Book> books = new List<Book>();
        skippedLines = 0;

        if (!File.Exists(path))
        {
            return books;
        }

        foreach (string line in File.ReadLines(path))
        {
            Book? book = ParseLine(line);
            if (book != null)
            {
                books.Add(book);
            }
            else
            {
                skippedLines++;
            }
        }

        return books;
    }

    public static void Save(string path, List<Book> books)
    {
        List<string> lines = new List<string>();
        foreach (Book book in books)
        {
            lines.Add(string.Join(Separator,
                book.Isbn,
                book.Title,
                book.Author,
                book.Genre.ToString(),
                book.Price.ToString(FileCulture),
                book.Stock.ToString(FileCulture),
                book.PublishedOn.ToString("yyyy-MM-dd", FileCulture)));
        }

        string tempPath = path + ".tmp";
        File.WriteAllLines(tempPath, lines);
        File.Move(tempPath, path, overwrite: true);
    }

    private static Book? ParseLine(string line)
    {
        string[] parts = line.Split(Separator);
        if (parts.Length != 7)
        {
            return null;
        }

        if (!Enum.TryParse(parts[3], out Genre genre) || !Enum.IsDefined(genre)
            || !decimal.TryParse(parts[4], NumberStyles.Number, FileCulture, out decimal price)
            || !int.TryParse(parts[5], NumberStyles.Integer, FileCulture, out int stock)
            || !DateOnly.TryParseExact(parts[6], "yyyy-MM-dd", FileCulture, DateTimeStyles.None, out DateOnly publishedOn))
        {
            return null;
        }

        return new Book(parts[0], parts[1], parts[2], genre, price, stock, publishedOn);
    }
}

Load önce dosyanın var olup olmadığına bakıyor. İlk çalıştırmada dosya yoksa bu bir hata değil, sadece boş bir liste. Satırları File.ReadLines ile tek tek okuyor ve her birini ParseLine'a veriyor. ParseLine beklenmeyen bir şey görürse exception fırlatmak yerine null döndürüyor; Load da bozuk satırları sayıp skippedLines ile bildiriyor. Bir tek bozuk satır yüzünden bütün dükkânın açılmaması kötü bir kullanıcı deneyimi olurdu.

Dosyadaki sayılar ve tarihler her zaman invariant culture'la ve ISO biçimiyle yazılıp okunuyor. Tarih ve saat yazısındaki kural: makineden makineye giden veri culture'a bağlı olmamalı. Türü okurken TryParse'ı Enum.IsDefined ile birlikte kullanıyoruz; "Fantasy" gibi tanımsız bir isim ya da "42" gibi bir sayı geçemez.

Save'deki iki satıra dikkat et. Önce yeni içeriği geçici bir dosyaya yazıyoruz, sonra File.Move(..., overwrite: true) ile asıl dosyanın yerine koyuyoruz. Doğrudan books.txt'nin üzerine yazsaydık ve program tam o sırada çökseydi, elimizde yarısı yazılmış bir dosya kalırdı. Bu yöntemde en kötü ihtimalle geride bir .tmp dosyası kalır; eski veri sağlam durur.

Ana Döngü ve Menü

Program.cs uzun olduğu için dört parçada gösteriyorum. Parçaları sırayla aynı dosyaya koyduğunda tam programı elde edersin. İlk parça programın iskeleti:

using System.Globalization;
using Bookstore.App;

CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-US");

string dataPath = args.Length > 0 ? args[0] : "books.txt";
List<Book> books = BookFile.Load(dataPath, out int skipped);
Console.WriteLine($"Loaded {books.Count} book(s) from {Path.GetFileName(dataPath)}.");
if (skipped > 0)
{
    Console.WriteLine($"Warning: {skipped} invalid line(s) were skipped.");
}

bool running = true;
while (running)
{
    Console.WriteLine();
    Console.WriteLine("1) List  2) Add  3) Search  4) Save and exit");
    string? choice = ReadLine("Choice: ");

    switch (choice?.Trim())
    {
        case "1":
            PrintBooks(books);
            break;
        case "2":
            AddBook();
            break;
        case "3":
            Search();
            break;
        case "4":
        case null:
            running = false;
            break;
        default:
            Console.WriteLine("Please choose 1, 2, 3 or 4.");
            break;
    }
}

try
{
    BookFile.Save(dataPath, books);
    Console.WriteLine($"Saved {books.Count} book(s). Bye!");
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
    Console.WriteLine($"Could not save the books: {ex.Message}");
}

Arayüzdeki metinler İngilizce olduğu için ekrandaki sayı ve tarihler de en-US culture'ıyla biçimleniyor; bunu ilk satırda açıkça ayarladım. Dosya yolu komut satırından verilebiliyor, verilmezse çalışma klasöründeki books.txt kullanılıyor. Dosya ve konsol yazısındaki göreli yol uyarısını hatırla: programı hangi klasörden çalıştırırsan dosya orada aranır.

Menü bir while döngüsü ve bir switch. case null kolu önemli: girdi bittiğinde, yani kullanıcı Ctrl+D'ye bastığında ya da pipe'tan gelen satırlar tükendiğinde, ReadLine null döner ve program sonsuz döngüye girmek yerine kaydedip çıkar. Kaydetme kısmı exception handling yazısındaki when filtresini kullanıyor: sadece dosya ile ilgili iki exception'ı yakalıyoruz, beklemediğimiz bir hatayı yutmuyoruz.

Listeleme ve Arama

void PrintBooks(List<Book> list)
{
    if (list.Count == 0)
    {
        Console.WriteLine("No books.");
        return;
    }

    foreach (Book book in list)
    {
        Console.WriteLine($"{book.Isbn,-15} {book.Title,-24} {book.Author,-18} {book.Genre,-9} {book.Price,7:0.00} {book.Stock,3} {book.PublishedOn:yyyy}");
    }
}

void Search()
{
    string? term = ReadLine("Search title or author: ");
    if (string.IsNullOrWhiteSpace(term))
    {
        return;
    }

    List<Book> found = new List<Book>();
    foreach (Book book in books)
    {
        if (book.Title.Contains(term.Trim(), StringComparison.OrdinalIgnoreCase)
            || book.Author.Contains(term.Trim(), StringComparison.OrdinalIgnoreCase))
        {
            found.Add(book);
        }
    }

    Console.WriteLine($"{found.Count} result(s):");
    PrintBooks(found);
}

PrintBooks, string interpolation'ın hizalama özelliğiyle düz bir tablo çiziyor: {book.Title,-24} sola dayalı 24 karakter, {book.Price,7:0.00} sağa dayalı 7 karakter ve iki ondalık. Aynı fonksiyonu hem bütün liste hem arama sonuçları için kullanıyoruz.

Arama, başlıkta ya da yazar adında geçen metni büyük/küçük harfe bakmadan buluyor. StringComparison.OrdinalIgnoreCase seçimi bilinçli: string'ler yazısındaki Türkçe "I/ı/İ" tuzağına düşmeden, culture'dan bağımsız bir karşılaştırma yapıyor. Eşleşenleri toplamak için düz bir foreach yazdık; LINQ serisinde bunun tek satırlık hâlini göreceğiz.

Kitap Eklemek ve Girdiyi Doğrulamak

void AddBook()
{
    string? isbn = ReadLine("ISBN-13: ")?.Trim();
    if (isbn == null || !IsValidIsbn(isbn))
    {
        Console.WriteLine("Invalid ISBN.");
        return;
    }

    foreach (Book existing in books)
    {
        if (existing.Isbn == isbn)
        {
            Console.WriteLine($"'{existing.Title}' already has this ISBN.");
            return;
        }
    }

    string? title = ReadLine("Title: ")?.Trim();
    string? author = ReadLine("Author: ")?.Trim();
    if (string.IsNullOrEmpty(title) || string.IsNullOrEmpty(author)
        || title.Contains('|') || author.Contains('|'))
    {
        Console.WriteLine("Title and author are required and cannot contain '|'.");
        return;
    }

    Genre? genre = ReadGenre();
    decimal? price = ReadPrice();
    int? stock = ReadStock();
    DateOnly? publishedOn = ReadDate();
    if (genre == null || price == null || stock == null || publishedOn == null)
    {
        Console.WriteLine("Input ended, the book was not added.");
        return;
    }

    books.Add(new Book(isbn, title, author, genre.Value, price.Value, stock.Value, publishedOn.Value));
    Console.WriteLine($"Added '{title}'.");
}

AddBook baştan sona guard clause'larla yazıldı: bir şey yanlışsa mesajı yaz ve hemen dön. ISBN'i serbest metin olarak değil, kod düzenleme yazısındaki ISBN-13 kontrolüyle doğruluyoruz; aynı ISBN listede varsa ekleme yapmıyoruz. Başlıkta ya da yazar adında | varsa da reddediyoruz, çünkü o karakter dosyamızın ayırıcısı. Kullanıcı girdisinin dosya formatını bozmasına izin vermek, sessiz veri bozulmasının en klasik yoludur.

Tür, fiyat, stok ve tarih ayrı fonksiyonlardan geliyor ve hepsi nullable dönüyor: Genre?, decimal?, int?, DateOnly?. Null, "girdi bitti" demek. Nullable yazısında gördüğümüz gibi, dört değerin de null olmadığından emin olduktan sonra .Value ile asıl değerlere ulaşıyoruz.

Son parça, girdi okuyan yardımcı fonksiyonlar. ReadPrice'ı yazının başında gördün; geri kalanlar aynı pattern'de:

Genre? ReadGenre()
{
    while (true)
    {
        string? text = ReadLine("Genre (Fiction, Science, History, Children): ");
        if (text == null)
        {
            return null;
        }

        if (!int.TryParse(text, out _)
            && Enum.TryParse(text, ignoreCase: true, out Genre genre)
            && Enum.IsDefined(genre) && genre != Genre.Unknown)
        {
            return genre;
        }

        Console.WriteLine("Unknown genre, try again.");
    }
}

int? ReadStock()
{
    while (true)
    {
        string? text = ReadLine("Stock: ");
        if (text == null)
        {
            return null;
        }

        if (int.TryParse(text, out int stock) && stock >= 0)
        {
            return stock;
        }

        Console.WriteLine("Stock must be zero or a positive whole number.");
    }
}

DateOnly? ReadDate()
{
    while (true)
    {
        string? text = ReadLine("Published on (yyyy-MM-dd): ");
        if (text == null)
        {
            return null;
        }

        if (DateOnly.TryParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly date)
            && date <= DateOnly.FromDateTime(DateTime.Today))
        {
            return date;
        }

        Console.WriteLine("Use the yyyy-MM-dd format and a date that is not in the future.");
    }
}

string? ReadLine(string prompt)
{
    Console.Write(prompt);
    string? line = Console.ReadLine();
    if (Console.IsInputRedirected)
    {
        Console.WriteLine(line);
    }
    return line;
}

static bool IsValidIsbn(string isbn)
{
    string digits = isbn.Replace("-", "");
    if (digits.Length != 13 || !digits.All(char.IsDigit))
    {
        return false;
    }

    int sum = 0;
    for (int i = 0; i < 12; i++)
    {
        int digit = digits[i] - '0';
        sum += i % 2 == 0 ? digit : digit * 3;
    }

    int check = (10 - sum % 10) % 10;
    return check == digits[12] - '0';
}

Her yardımcı, geçerli bir değer gelene ya da girdi bitene kadar soruyor. ReadGenre, enum yazısındaki sayı tuzağını kapatmak için önce girdinin bir sayı olmadığını kontrol ediyor; "1" yazan kullanıcı Fiction seçmiş olmuyor. ReadDate gelecekteki bir yayın tarihini kabul etmiyor. ReadLine ise küçük bir kolaylık: girdi pipe'tan geliyorsa (Console.IsInputRedirected) okunan satırı ekrana da yazıyor. Böylece bir test girdisiyle çalıştırdığında oturum, klavyeden yazılmış gibi okunuyor.

İlk Çalıştırma ve İki Hata

Uygulamayı bir girdi dosyasıyla çalıştırdım: listele, "history" ara, var olan bir ISBN'i eklemeye çalış, The Hobbit'i ekle, tekrar listele, kaydet. dotnet run'ın en başta yazdığı NETSDK1138 build uyarısı dışında oturumun tamamı şu:

$ dotnet run < input.txt
Loaded 4 book(s) from books.txt.
Warning: 1 invalid line(s) were skipped.

1) List  2) Add  3) Search  4) Save and exit
Choice: 1
978-0441172719  Dune                     Frank Herbert      Fiction     12.99  14 1965
978-0553380163  A Brief History of Time  Stephen Hawking    Science     18.50   6 1988
978-0062316097  Sapiens                  Yuval Noah Harari  History     22.00   9 2014
978-0451524935  1984                     George Orwell      Fiction      9.99  20 1949

1) List  2) Add  3) Search  4) Save and exit
Choice: 3
Search title or author: history
1 result(s):
978-0553380163  A Brief History of Time  Stephen Hawking    Science     18.50   6 1988

1) List  2) Add  3) Search  4) Save and exit
Choice: 2
ISBN-13: 978-0441172719
'Dune' already has this ISBN.

1) List  2) Add  3) Search  4) Save and exit
Choice: 2
ISBN-13: 978-0547928227
Title: The Hobbit
Author: J. R. R. Tolkien
Genre (Fiction, Science, History, Children): Fantasy
Unknown genre, try again.
Genre (Fiction, Science, History, Children): Children
Price (e.g. 12.50): 14,00
Stock: 14.00
Stock must be zero or a positive whole number.
Stock: 5
Published on (yyyy-MM-dd): 1937-09-21
Added 'The Hobbit'.

1) List  2) Add  3) Search  4) Save and exit
Choice: 1
978-0441172719  Dune                     Frank Herbert      Fiction     12.99  14 1965
978-0553380163  A Brief History of Time  Stephen Hawking    Science     18.50   6 1988
978-0062316097  Sapiens                  Yuval Noah Harari  History     22.00   9 2014
978-0451524935  1984                     George Orwell      Fiction      9.99  20 1949
978-0547928227  The Hobbit               J. R. R. Tolkien   Children  1400.00   5 1937

1) List  2) Add  3) Search  4) Save and exit
Choice: 4
Saved 5 book(s). Bye!

İlk bakışta her şey yolunda: geçersiz satır atlandı ve uyarısı verildi, arama doğru kitabı buldu, tekrar eden ISBN reddedildi, "Fantasy" türü kabul edilmedi. Ama son listede The Hobbit 1400.00 görünüyor. Girdiye bakınca sebep ortada: fiyat sorusuna "14,00" yazılmış ve kabul edilmiş. Bir sonraki "14.00" satırı da stok sorusuna gitmiş, orada reddedilmiş. Hata ayıklama yazısındaki gibi ReadPrice'ın return satırına bir breakpoint koysaydın, Watch penceresinde price'ın 1400 olduğunu ilk duruşta görürdün.

Düzeltme tek satır. Binlik ayırıcıya izin vermeyen bir stil kullanıyoruz:

if (decimal.TryParse(text, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal price) && price > 0)

Aynı değişikliği BookFile.ParseLine'daki decimal.TryParse'a da yaptım; dosyada da binlik ayırıcı olmamalı.

İkinci hata ekranda görünmüyor, dosyada saklı. Kaydedilen dosya şu:

978-0441172719|Dune|Frank Herbert|Fiction|12.99|14|1965-08-01
978-0553380163|A Brief History of Time|Stephen Hawking|Science|18.50|6|1988-04-01
978-0062316097|Sapiens|Yuval Noah Harari|History|22.00|9|2014-09-04
978-0451524935|1984|George Orwell|Fiction|9.99|20|1949-06-08
978-0547928227|The Hobbit|J. R. R. Tolkien|Children|1400|5|1937-09-21

"Fantasy" türlü orijinal The Hobbit satırı yok. Uygulama onu yüklerken atladı, kaydederken de dosyanın üzerine yazdı. Yani kullanıcının elle düzeltebileceği bir satır, bir uyarı mesajıyla birlikte sessizce silindi. Bozuk veriyi atlamak iyi bir karar, ama onu yok etmek değil. En basit çözüm, atlanan satır varsa kaydetmeden önce orijinal dosyanın bir yedeğini almak:

try
{
    if (skipped > 0 && File.Exists(dataPath))
    {
        File.Copy(dataPath, dataPath + ".bak", overwrite: true);
        Console.WriteLine($"Backed up the original file to {Path.GetFileName(dataPath)}.bak.");
    }

    BookFile.Save(dataPath, books);
    // kaydetmenin geri kalanı aynı

İki düzeltmeden sonra aynı girdiyle tekrar çalıştırdım. Oturumun ilgili kısımları:

Price (e.g. 12.50): 14,00
Price must be a positive number like 12.50.
Price (e.g. 12.50): 14.00
...
978-0451524935  1984                     George Orwell      Fiction      9.99  20 1949
978-0547928227  The Hobbit               J. R. R. Tolkien   Children    14.00   5 1937

1) List  2) Add  3) Search  4) Save and exit
Choice: 4
Backed up the original file to books.txt.bak.
Saved 5 book(s). Bye!

"14,00" artık reddediliyor, fiyat 14.00 olarak kaydediliyor ve orijinal satır books.txt.bak'ta duruyor. İki hata da derleyicinin göremeyeceği türdendi; ikisini de programı gerçek bir girdiyle çalıştırıp çıktıyı dikkatle okuyarak bulduk.

Sık Düşülen Tuzaklar

Sayı ve tarih parse ederken stilin ve culture'ın ne kabul ettiğini bilmemek. NumberStyles.Number binlik ayırıcı da kabul eder. Kullanıcıdan ne beklediğini net söyle ("12.50 gibi") ve tam olarak onu kabul eden bir stil seç.

Okuyamadığın veriyi atlayıp sonra dosyanın üzerine yazmak. Atlamak geçici bir karardır, silmek kalıcı. Yedek al, bozuk satırları aynen geri yaz ya da kaydetmeyi reddet.

Dosyanın üzerine doğrudan yazmak. Yazma yarıda kalırsa hem yeni hem eski veri gider. Geçici dosyaya yaz, sonra taşı.

Kullanıcı girdisinin dosya formatına sızmasına izin vermek. Ayırıcı karakter, satır sonu, tırnak: hepsi formatı bozabilir. Girdiyi doğrula ya da formatı bunlara dayanıklı bir yapıya (ileride JSON) taşı.

Bu küçük programda bile her şeyin top-level kodda birbirine dokunduğunu fark etmişsindir: books listesine her fonksiyon erişiyor, ekran ve iş kuralları iç içe, bir Book negatif fiyatla oluşturulmaya karşı kendini koruyamıyor. Bu kodu test etmek de zor. Bunlar hata değil, temel araçların sınırı.

Küçük Bir Deneme

★☆☆ Menüye "5) Delete" ekle: kullanıcıdan bir ISBN al, listede varsa kitabı sil, yoksa "Not found." yaz.

★★☆ "6) Sell" ekle: ISBN ve adet al, stok yeterliyse stoğu azalt, değilse satışı reddet. Record değiştirilemez olduğu için books[i] = book with { Stock = book.Stock - quantity }; yazımını kullanman gerekecek; with'i OOP serisinde ayrıntılı göreceğiz.

★★★ Yedek almak yerine bozuk satırları koru: Load geçersiz satırları ayrı bir List<string> olarak da döndürsün, Save bunları dosyanın sonuna aynen geri yazsın. Sonra "Fantasy" satırının birkaç kaydetmeden sonra hâlâ yerinde olduğunu kontrol et.

C# Temelleri serisini burada bitiriyoruz. Değişkenlerden dosyalara kadar bütün temel araçlar artık elinde; son bölümde saydığımız eksikler ise bir sonraki adımın konusu. C# ile Nesne Yönelimli Programlama (OOP) serisinde class ve object'lerle başlıyor, bu Bookstore'u veriyi ve davranışı bir arada tutan, kendini doğrulayan ve test edilebilen parçalara ayırmayı öğreniyoruz.