Comprehensive Project: Task Manager
Requirements Analysis
We will build a command-line task manager that supports the following features:
- Add, delete, and complete tasks
- Filter and aggregate by priority
- Persist data to a JSON file
- Save asynchronously without blocking the main flow
- Comprehensive exception handling
The functionality is broken down into seven steps, implemented incrementally.
Class Design: Enums and Data Models
First, define the priority enum and the task entity class.
Example
enum Priority
{
Low,
Medium,
High
}
class Task
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Priority Priority { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public Task(int id, string title, string description, Priority priority)
{
Id = id;
Title = title;
Description = description;
Priority = priority;
IsCompleted = false;
CreatedAt = DateTime.Now;
}
public override string ToString()
{
string status = IsCompleted ? "[x]" : "[ ]";
return $"{Id,-3} {status} {Priority,-6} {CreatedAt:yyyy-MM-dd} {Title}";
}
}
Collection Storage: TaskManager and CRUD Operations
Use List<Task> to store tasks and implement basic create, read, update, and delete operations.
Example
class TaskManager
{
private List<Task> tasks = new List<Task>();
private int nextId = 1;
public Task AddTask(string title, string description, Priority priority)
{
Task task = new Task(nextId++, title, description, priority);
tasks.Add(task);
return task;
}
public bool RemoveTask(int id)
{
Task task = tasks.Find(t => t.Id == id);
if (task != null)
{
tasks.Remove(task);
return true;
}
return false;
}
public bool CompleteTask(int id)
{
Task task = tasks.Find(t => t.Id == id);
if (task != null)
{
task.IsCompleted = true;
return true;
}
return false;
}
public List<Task> GetAllTasks()
{
return new List<Task>(tasks);
}
public void SortByPriority()
{
tasks.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
}
File Persistence: JSON Serialization and Deserialization
Use System.Text.Json to save the task list to a file and load it back.
Example
using System.Text.Json;
class TaskManager
{
private List<Task> tasks = new List<Task>();
private int nextId = 1;
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public string SaveToFile()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
File.WriteAllText(FilePath, json);
return "Save succeeded.";
}
catch (Exception ex)
{
return $"Save failed: {ex.Message}";
}
}
public string LoadFromFile()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = File.ReadAllText(FilePath);
List<Task>? loaded = JsonSerializer.Deserialize<List<Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Load failed: {ex.Message}";
}
}
}
LINQ Queries: Filtering and Aggregation
Use LINQ to implement filtering by priority, querying pending tasks, and generating statistics.
Example
class TaskManager
{
private List<Task> tasks = new List<Task>();
public List<Task> FilterByPriority(Priority priority)
{
return tasks.Where(t => t.Priority == priority).ToList();
}
public List<Task> GetPendingTasks()
{
return tasks.Where(t => !t.IsCompleted)
.OrderByDescending(t => t.Priority)
.ToList();
}
public Dictionary<Priority, int> GetStatistics()
{
return tasks.GroupBy(t => t.Priority)
.ToDictionary(g => g.Key, g => g.Count());
}
public int GetCompletedCount()
{
return tasks.Count(t => t.IsCompleted);
}
public int GetPendingCount()
{
return tasks.Count(t => !t.IsCompleted);
}
}
Sample statistics output:
High: 2, Medium: 3, Low: 1
Completed: 1, Pending: 5
Async File Saving
Use async/await to implement non-blocking file write operations.
Example
class TaskManager
{
private List<Task> tasks = new List<Task>();
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public async Task<string> SaveToFileAsync()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
await File.WriteAllTextAsync(FilePath, json);
return "Async save succeeded.";
}
catch (UnauthorizedAccessException ex)
{
return $"File access denied: {ex.Message}";
}
catch (IOException ex)
{
return $"IO error: {ex.Message}";
}
catch (Exception ex)
{
return $"Async save failed: {ex.Message}";
}
}
public async Task<string> LoadFromFileAsync()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = await File.ReadAllTextAsync(FilePath);
List<Task>? loaded = JsonSerializer.Deserialize<List<Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Async load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Async load failed: {ex.Message}";
}
}
}
Command-Line Interactive Interface
Use a switch statement to build a command-line menu loop.
Example
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
TaskManager manager = new TaskManager();
string loadResult = await manager.LoadFromFileAsync();
Console.WriteLine(loadResult);
while (true)
{
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add task");
Console.WriteLine("2. Delete task");
Console.WriteLine("3. Complete task");
Console.WriteLine("4. List all tasks");
Console.WriteLine("5. Filter by priority");
Console.WriteLine("6. View statistics");
Console.WriteLine("7. Save to file");
Console.WriteLine("0. Exit");
Console.Write("Select: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
Console.Write("Title: ");
string? title = Console.ReadLine() ?? "";
Console.Write("Description: ");
string? desc = Console.ReadLine() ?? "";
Console.Write("Priority (Low/Medium/High): ");
string? priStr = Console.ReadLine();
if (Enum.TryParse<Priority>(priStr, true, out Priority pri))
{
Task added = manager.AddTask(title, desc, pri);
Console.WriteLine($"Added: {added}");
}
else
{
Console.WriteLine("Invalid priority.");
}
break;
case "2":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int delId))
{
Console.WriteLine(manager.RemoveTask(delId) ? "Deleted." : "Not found.");
}
break;
case "3":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int compId))
{
Console.WriteLine(manager.CompleteTask(compId) ? "Completed." : "Not found.");
}
break;
case "4":
manager.SortByPriority();
foreach (Task t in manager.GetAllTasks())
{
Console.WriteLine(t);
}
break;
case "5":
Console.Write("Priority (Low/Medium/High): ");
if (Enum.TryParse<Priority>(Console.ReadLine(), true, out Priority filterPri))
{
foreach (Task t in manager.FilterByPriority(filterPri))
{
Console.WriteLine(t);
}
}
break;
case "6":
Dictionary<Priority, int> stats = manager.GetStatistics();
foreach (var kv in stats)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Console.WriteLine($"Completed: {manager.GetCompletedCount()}, Pending: {manager.GetPendingCount()}");
break;
case "7":
string saveResult = await manager.SaveToFileAsync();
Console.WriteLine(saveResult);
break;
case "0":
string finalSave = await manager.SaveToFileAsync();
Console.WriteLine(finalSave);
Console.WriteLine("Goodbye!");
return;
default:
Console.WriteLine("Invalid selection.");
break;
}
}
}
}
===== Task Manager =====
1. Add task
2. Delete task
3. Complete task
4. List all tasks
5. Filter by priority
6. View statistics
7. Save to file
0. Exit
Select: 1
Title: Learn C#
Description: Complete lesson 36
Priority (Low/Medium/High): High
Added: 1 [ ] High 2026-06-28 Learn C#
Project Structure Organization
Use namespaces and using to organize code, placing different responsibilities in separate files.
Example
Project file structure:
TaskManagerApp/
├── Models/
│ ├── Priority.cs
│ └── Task.cs
├── Services/
│ └── TaskManager.cs
└── Program.cs
Priority.cs:
namespace TaskManagerApp.Models
{
public enum Priority
{
Low,
Medium,
High
}
}
Task.cs:
using System;
namespace TaskManagerApp.Models
{
public class Task
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Priority Priority { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public Task(int id, string title, string description, Priority priority)
{
Id = id;
Title = title;
Description = description;
Priority = priority;
IsCompleted = false;
CreatedAt = DateTime.Now;
}
public override string ToString()
{
string status = IsCompleted ? "[x]" : "[ ]";
return $"{Id,-3} {status} {Priority,-6} {CreatedAt:yyyy-MM-dd} {Title}";
}
}
}
TaskManager.cs:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using TaskManagerApp.Models;
namespace TaskManagerApp.Services
{
public class TaskManager
{
private List<Models.Task> tasks = new List<Models.Task>();
private int nextId = 1;
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public Models.Task AddTask(string title, string description, Priority priority)
{
Models.Task task = new Models.Task(nextId++, title, description, priority);
tasks.Add(task);
return task;
}
public bool RemoveTask(int id)
{
Models.Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
tasks.Remove(task);
return true;
}
return false;
}
public bool CompleteTask(int id)
{
Models.Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
task.IsCompleted = true;
return true;
}
return false;
}
public List<Models.Task> GetAllTasks()
{
return new List<Models.Task>(tasks);
}
public void SortByPriority()
{
tasks.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
public List<Models.Task> FilterByPriority(Priority priority)
{
return tasks.Where(t => t.Priority == priority).ToList();
}
public List<Models.Task> GetPendingTasks()
{
return tasks.Where(t => !t.IsCompleted)
.OrderByDescending(t => t.Priority)
.ToList();
}
public Dictionary<Priority, int> GetStatistics()
{
return tasks.GroupBy(t => t.Priority)
.ToDictionary(g => g.Key, g => g.Count());
}
public int GetCompletedCount()
{
return tasks.Count(t => t.IsCompleted);
}
public int GetPendingCount()
{
return tasks.Count(t => !t.IsCompleted);
}
public async Task<string> SaveToFileAsync()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
await File.WriteAllTextAsync(FilePath, json);
return "Save succeeded.";
}
catch (UnauthorizedAccessException ex)
{
return $"Access denied: {ex.Message}";
}
catch (IOException ex)
{
return $"IO error: {ex.Message}";
}
catch (Exception ex)
{
return $"Save failed: {ex.Message}";
}
}
public async Task<string> LoadFromFileAsync()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = await File.ReadAllTextAsync(FilePath);
List<Models.Task>? loaded = JsonSerializer.Deserialize<List<Models.Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Load failed: {ex.Message}";
}
}
}
}
Program.cs:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TaskManagerApp.Models;
using TaskManagerApp.Services;
namespace TaskManagerApp
{
class Program
{
static async Task Main()
{
TaskManager manager = new TaskManager();
string loadResult = await manager.LoadFromFileAsync();
Console.WriteLine(loadResult);
while (true)
{
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add task");
Console.WriteLine("2. Delete task");
Console.WriteLine("3. Complete task");
Console.WriteLine("4. List all tasks");
Console.WriteLine("5. Filter by priority");
Console.WriteLine("6. View statistics");
Console.WriteLine("7. Save to file");
Console.WriteLine("0. Exit");
Console.Write("Select: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
Console.Write("Title: ");
string? title = Console.ReadLine() ?? "";
Console.Write("Description: ");
string? desc = Console.ReadLine() ?? "";
Console.Write("Priority (Low/Medium/High): ");
string? priStr = Console.ReadLine();
if (Enum.TryParse<Priority>(priStr, true, out Priority pri))
{
Models.Task added = manager.AddTask(title, desc, pri);
Console.WriteLine($"Added: {added}");
}
else
{
Console.WriteLine("Invalid priority.");
}
break;
case "2":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int delId))
{
Console.WriteLine(manager.RemoveTask(delId) ? "Deleted." : "Not found.");
}
break;
case "3":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int compId))
{
Console.WriteLine(manager.CompleteTask(compId) ? "Completed." : "Not found.");
}
break;
case "4":
manager.SortByPriority();
foreach (Models.Task t in manager.GetAllTasks())
{
Console.WriteLine(t);
}
break;
case "5":
Console.Write("Priority (Low/Medium/High): ");
if (Enum.TryParse<Priority>(Console.ReadLine(), true, out Priority filterPri))
{
foreach (Models.Task t in manager.FilterByPriority(filterPri))
{
Console.WriteLine(t);
}
}
break;
case "6":
Dictionary<Priority, int> stats = manager.GetStatistics();
foreach (var kv in stats)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Console.WriteLine($"Completed: {manager.GetCompletedCount()}, Pending: {manager.GetPendingCount()}");
break;
case "7":
string saveResult = await manager.SaveToFileAsync();
Console.WriteLine(saveResult);
break;
case "0":
string finalSave = await manager.SaveToFileAsync();
Console.WriteLine(finalSave);
Console.WriteLine("Goodbye!");
return;
default:
Console.WriteLine("Invalid selection.");
break;
}
}
}
}
}
Complete Single-File Version
Merge all code into a single compilable file for quick testing.
Example
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
enum Priority
{
Low,
Medium,
High
}
class Task
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public Priority Priority { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
public Task(int id, string title, string description, Priority priority)
{
Id = id;
Title = title;
Description = description;
Priority = priority;
IsCompleted = false;
CreatedAt = DateTime.Now;
}
public override string ToString()
{
string status = IsCompleted ? "[x]" : "[ ]";
return $"{Id,-3} {status} {Priority,-6} {CreatedAt:yyyy-MM-dd} {Title}";
}
}
class TaskManager
{
private List<Task> tasks = new List<Task>();
private int nextId = 1;
private const string FilePath = "tasks.json";
private static readonly JsonSerializerOptions jsonOptions = new JsonSerializerOptions
{
WriteIndented = true
};
public Task AddTask(string title, string description, Priority priority)
{
Task task = new Task(nextId++, title, description, priority);
tasks.Add(task);
return task;
}
public bool RemoveTask(int id)
{
Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
tasks.Remove(task);
return true;
}
return false;
}
public bool CompleteTask(int id)
{
Task? task = tasks.Find(t => t.Id == id);
if (task != null)
{
task.IsCompleted = true;
return true;
}
return false;
}
public List<Task> GetAllTasks()
{
return new List<Task>(tasks);
}
public void SortByPriority()
{
tasks.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
public List<Task> FilterByPriority(Priority priority)
{
return tasks.Where(t => t.Priority == priority).ToList();
}
public List<Task> GetPendingTasks()
{
return tasks.Where(t => !t.IsCompleted)
.OrderByDescending(t => t.Priority)
.ToList();
}
public Dictionary<Priority, int> GetStatistics()
{
return tasks.GroupBy(t => t.Priority)
.ToDictionary(g => g.Key, g => g.Count());
}
public int GetCompletedCount()
{
return tasks.Count(t => t.IsCompleted);
}
public int GetPendingCount()
{
return tasks.Count(t => !t.IsCompleted);
}
public async Task<string> SaveToFileAsync()
{
try
{
string json = JsonSerializer.Serialize(tasks, jsonOptions);
await File.WriteAllTextAsync(FilePath, json);
return "Save succeeded.";
}
catch (UnauthorizedAccessException ex)
{
return $"Access denied: {ex.Message}";
}
catch (IOException ex)
{
return $"IO error: {ex.Message}";
}
catch (Exception ex)
{
return $"Save failed: {ex.Message}";
}
}
public async Task<string> LoadFromFileAsync()
{
try
{
if (!File.Exists(FilePath))
{
return "File does not exist, skipping load.";
}
string json = await File.ReadAllTextAsync(FilePath);
List<Task>? loaded = JsonSerializer.Deserialize<List<Task>>(json);
if (loaded != null)
{
tasks = loaded;
nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;
}
return "Load succeeded.";
}
catch (JsonException ex)
{
return $"JSON format error: {ex.Message}";
}
catch (Exception ex)
{
return $"Load failed: {ex.Message}";
}
}
}
class Program
{
static async Task Main()
{
TaskManager manager = new TaskManager();
string loadResult = await manager.LoadFromFileAsync();
Console.WriteLine(loadResult);
while (true)
{
Console.WriteLine("\n===== Task Manager =====");
Console.WriteLine("1. Add task");
Console.WriteLine("2. Delete task");
Console.WriteLine("3. Complete task");
Console.WriteLine("4. List all tasks");
Console.WriteLine("5. Filter by priority");
Console.WriteLine("6. View statistics");
Console.WriteLine("7. Save to file");
Console.WriteLine("0. Exit");
Console.Write("Select: ");
string? input = Console.ReadLine();
switch (input)
{
case "1":
Console.Write("Title: ");
string? title = Console.ReadLine() ?? "";
Console.Write("Description: ");
string? desc = Console.ReadLine() ?? "";
Console.Write("Priority (Low/Medium/High): ");
string? priStr = Console.ReadLine();
if (Enum.TryParse<Priority>(priStr, true, out Priority pri))
{
Task added = manager.AddTask(title, desc, pri);
Console.WriteLine($"Added: {added}");
}
else
{
Console.WriteLine("Invalid priority.");
}
break;
case "2":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int delId))
{
Console.WriteLine(manager.RemoveTask(delId) ? "Deleted." : "Not found.");
}
break;
case "3":
Console.Write("Task ID: ");
if (int.TryParse(Console.ReadLine(), out int compId))
{
Console.WriteLine(manager.CompleteTask(compId) ? "Completed." : "Not found.");
}
break;
case "4":
manager.SortByPriority();
foreach (Task t in manager.GetAllTasks())
{
Console.WriteLine(t);
}
break;
case "5":
Console.Write("Priority (Low/Medium/High): ");
if (Enum.TryParse<Priority>(Console.ReadLine(), true, out Priority filterPri))
{
foreach (Task t in manager.FilterByPriority(filterPri))
{
Console.WriteLine(t);
}
}
break;
case "6":
Dictionary<Priority, int> stats = manager.GetStatistics();
foreach (var kv in stats)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Console.WriteLine($"Completed: {manager.GetCompletedCount()}, Pending: {manager.GetPendingCount()}");
break;
case "7":
string saveResult = await manager.SaveToFileAsync();
Console.WriteLine(saveResult);
break;
case "0":
string finalSave = await manager.SaveToFileAsync();
Console.WriteLine(finalSave);
Console.WriteLine("Goodbye!");
return;
default:
Console.WriteLine("Invalid selection.");
break;
}
}
}
}
Load succeeded.
===== Task Manager =====
1. Add task
2. Delete task
3. Complete task
4. List all tasks
5. Filter by priority
6. View statistics
7. Save to file
0. Exit
Select: 1
Title: Learn C#
Description: Complete lesson 36
Priority (Low/Medium/High): High
Added: 1 [ ] High 2026-06-28 Learn C#
Select: 1
Title: Write documentation
Description: Organize project docs
Priority (Low/Medium/High): Medium
Added: 2 [ ] Medium 2026-06-28 Write documentation
Select: 3
Task ID: 2
Completed.
Select: 4
2 [x] Medium 2026-06-28 Write documentation
1 [ ] High 2026-06-28 Learn C#
Select: 6
High: 1
Medium: 1
Completed: 1, Pending: 1
Select: 0
Save succeeded.
Goodbye!
❓ FAQ
📖 Summary
- Enum types define finite sets of states, improving code readability
- Entity classes encapsulate data and behavior; ToString provides formatted output
- List
combined with Find/Sort/Where implements collection operations - System.Text.Json provides lightweight serialization and deserialization
- LINQ's Where/OrderBy/GroupBy enables declarative queries
- async/await implements non-blocking file I/O
- try-catch catches specific exceptions first, then falls back to Exception
- switch statements build command-line menu loops
- namespace and using organize multi-file project structures
📝 Exercises
- Add a DueDate property to the Task class and implement a "show overdue tasks" feature
- Add an EditTask method to TaskManager that supports modifying the title and description
- Change the save format from JSON to CSV, implementing your own serialization logic
- Add sorting by creation date and searching by title
- Implement a command-line argument mode: the program executes an operation via args at startup and then exits



