C# Study Projects
Start from the top: implement each snippet in your own project, run it, then change constants and inputs to test your understanding. Two open-source repositories are linked at the end for deeper study.
Algorithms
Algorithms are the core of computer science. Study sorting and searching first, then recursion. Each example grows from simple to efficient so you can see why the efficient version matters.
Sorting
Bubble sort is the simplest algorithm to understand — it repeatedly swaps neighbouring elements that are out of order. It is slow (O(n²)), but it is the perfect starting exercise.
// Bubble sort — simplest to understand; O(n^2), fine for small inputs
static void BubbleSort(int[] items)
{
for (int i = 0; i < items.Length - 1; i++) // pass over the array
{
for (int j = 0; j < items.Length - 1 - i; j++) // sorted tail shrinks
{
if (items[j] > items[j + 1]) // out of order?
{
// swap using tuple assignment — no temporary variable needed
(items[j], items[j + 1]) = (items[j + 1], items[j]);
}
}
}
}
int[] data = { 5, 2, 9, 1 };
BubbleSort(data);
Console.WriteLine(string.Join(", ", data)); // 1, 2, 5, 9
Now the faster, production-style version: quicksort splits the array around a pivot and sorts each half recursively — average O(n log n).
// Quicksort — divide and conquer; averages O(n log n)
static void QuickSort(int[] items, int lo, int hi)
{
if (lo >= hi) return; // base case: 0 or 1 element
int p = Partition(items, lo, hi); // put the pivot in its final position
QuickSort(items, lo, p - 1); // sort the left side
QuickSort(items, p + 1, hi); // sort the right side
}
static int Partition(int[] items, int lo, int hi)
{
int pivot = items[hi]; // choose the last element as pivot
int i = lo - 1;
for (int j = lo; j < hi; j++)
{
if (items[j] < pivot) // smaller elements move left of pivot
{
i++;
(items[i], items[j]) = (items[j], items[i]);
}
}
(items[i + 1], items[hi]) = (items[hi], items[i + 1]); // pivot in place
return i + 1; // index where the pivot ended
}
int[] bigger = { 9, 3, 7, 1, 5 };
QuickSort(bigger, 0, bigger.Length - 1);
Console.WriteLine(string.Join(", ", bigger)); // 1, 3, 5, 7, 9
Searching
Binary search halves the search space at every step, reaching O(log n). It requires a sorted array — a precondition worth stating in a comment.
// Binary search — halves the search space each step; O(log n)
// REQUIRES a sorted array.
static int BinarySearch(int[] sorted, int target)
{
int lo = 0, hi = sorted.Length - 1;
while (lo <= hi)
{
int mid = lo + (hi - lo) / 2; // avoids overflow of lo + hi
if (sorted[mid] == target) return mid;
if (sorted[mid] < target) lo = mid + 1; // target lies on the right
else hi = mid - 1; // target lies on the left
}
return -1; // not found
}
int[] sorted = { 1, 3, 5, 7, 9 };
Console.WriteLine(BinarySearch(sorted, 7)); // 3
Recursion & Memoization
Recursion solves a problem by solving smaller versions of itself. The naive Fibonacci is elegant but exponential; memoization stores results so every n is computed once.
// Naive recursion — elegant but exponential: Fib(40) makes millions of calls
static long FibNaive(int n) =>
n <= 1 ? n : FibNaive(n - 1) + FibNaive(n - 2);
// Memoized version — caches results; each n computed once, O(n)
static long FibMemo(int n, Dictionary<int, long> cache)
{
if (cache.TryGetValue(n, out long cached)) return cached; // cache hit
long value = n <= 1 ? n : FibMemo(n - 1, cache) + FibMemo(n - 2, cache);
cache[n] = value; // remember the result before returning
return value;
}
var cache = new Dictionary<int, long>();
Console.WriteLine(FibMemo(50, cache)); // 12586269025 — instant
Database Interaction
SQLite is the simplest database for study: one file, no server, standard SQL. The Microsoft.Data.Sqlite package is the official lightweight ADO.NET provider. Never build SQL by string concatenation — always use parameters.
SQLite Setup
Create a console project and add the SQLite package with the .NET CLI.
dotnet new console -o DatabaseDemo # new console project
cd DatabaseDemo
dotnet add package Microsoft.Data.Sqlite # official SQLite provider
SQLite CRUD
The example below is the full Program.cs: it opens a file database, creates a table, inserts a row with a parameter, and reads the rows back.
using Microsoft.Data.Sqlite;
// 1. open (or create) the database file
using var connection = new SqliteConnection("Data Source=app.db");
connection.Open();
// 2. create the table when it does not exist yet
using var create = connection.CreateCommand();
create.CommandText =
"CREATE TABLE IF NOT EXISTS tasks (" +
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0);";
create.ExecuteNonQuery();
// 3. insert with a parameter — never concatenate user input into SQL
using var insert = connection.CreateCommand();
insert.CommandText = "INSERT INTO tasks (title) VALUES ($title);";
insert.Parameters.AddWithValue("$title", "Learn C#");
insert.ExecuteNonQuery();
// 4. query the rows back
using var select = connection.CreateCommand();
select.CommandText = "SELECT id, title, done FROM tasks;";
using var reader = select.ExecuteReader();
while (reader.Read())
{
int id = reader.GetInt32(0);
string title = reader.GetString(1);
int done = reader.GetInt32(2);
Console.WriteLine($"{id}: {title} (done={done})");
}
Web with Minimal API
ASP.NET Core Minimal APIs turn a single Program.cs into a small HTTP server. This is the fastest way to expose C# logic over the web and pairs naturally with the database code above.
Project Setup
dotnet new web -o WebApiDemo # ASP.NET Core empty web template
cd WebApiDemo
dotnet run # starts the server on http://localhost:5xxx
Hello Endpoint
MapGet registers a route. The simplest endpoint returns plain text.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello, World!"); // GET / -> plain text
// route parameters bind URL segments to arguments
app.MapGet("/hello/{name}", (string name) =>
$"Hello, {name}!"); // GET /hello/Ada
app.Run(); // never reached — blocks here
JSON API
Returning an anonymous object sends JSON automatically — the browser and tools like curl can read it directly.
app.MapGet("/tasks", () => new object[]
{
new { id = 1, title = "Learn C#", done = false },
new { id = 2, title = "Try Minimal APIs", done = false }
});
// GET /tasks -> [{ "id": 1, "title": "Learn C#", "done": false }, ... ]