Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to store data

Whole files, streams, CSV, JSON, binary, EF Core on a relational database, raw SQL, Dapper and MongoDB. Where to keep your data, and how to read it back.

Ask how much data there is, who else reads it, and how you'll look things up.

  • A small file your program owns (settings, a saved game, a to-do list) → JSON with System.Text.Json, read and written whole with File.ReadAllText and File.WriteAllText.
  • A big file, or one you process line by line (a log, an import) → a stream with a StreamReader or StreamWriter, so it never has to fit in memory.
  • A spreadsheet will open it, or one sends it to you → CSV, with quoting done properly, or a library such as CsvHelper once the format gets messy.
  • A format defined in bytes (an image header, a checksum) → binary reading and writing.
  • Many records, related to each other, changed by many users (customers, orders, stock) → a relational database through EF Core. SQLite for a single-file database, a server database for a shared one.
  • A report or read-heavy query where you want full control of the SQL → EF Core's raw SQL, or Dapper. Always with parameters.
  • Each record is read whole by its id and records vary in shape (a product catalogue) → a document database such as MongoDB.

Write down the three queries your app runs most before choosing a database. If they cut across entities, stay relational.

Files

Path.Combine, File.ReadAllText, File.WriteAllLines and friends, for files that fit in memory.

A line at a time, for files of any size.

Section

CSV files

Commas and quotes inside fields, and when to reach for CsvHelper.

Serialize an object to JSON and back, and handle names and types that don't match.

Section

Binary data

Bytes, encodings, BinaryWriter and checksums, and why JSON is usually the better first choice.

Relational databases

What EF Core is writing for you underneath.

Classes map to tables, and the DbContext is your session with the database.

One model, four providers, and where the connection string comes from.

The choosing paragraph for EF Core, EF Core raw SQL and Dapper, all parameterized.

Formal. A value concatenated into SQL text becomes part of the command and can change its meaning (SQL injection). A parameterized query sends the command text and the values separately; the database treats each value as data of its declared type, never as SQL. EF Core LINQ queries send captured variables as parameters; a constant written inside the lambda (p => p.Price > 5m) is embedded as a literal, which is safe because it is part of your code, not input. For raw SQL, FromSql/ SqlQuery/ExecuteSql take an interpolated string and turn each {value} into a parameter, while FromSqlRaw with string concatenation does not. Dapper parameterizes @name placeholders from an object.

In plain English. If you paste what a user typed into a database command, a crafted input can rewrite the command, for example to delete a table or read other people's data. Sending the values separately, as parameters, makes the database treat them only as values. EF Core does this for you as long as you use LINQ or its interpolated raw-SQL methods.

Example. db.Products.FromSql($"SELECT * FROM Products WHERE Name = {name}") sends @p0 with the name as a parameter. FromSqlRaw("... WHERE Name = '" + name + "'") lets x' OR '1'='1 return every row.

Why this rule exists. Keeping code and data apart is the only reliable defence; escaping by hand is easy to get wrong.

User input goes into SQL as a parameter, never pasted into the query text.

Documents

Embed what's read together, reference what's shared, and when documents fit at all.