โ† All C# lessons

C# / Chapter 06

Files, Exceptions, Async & Testing

Persist structured data, handle failures intentionally, perform asynchronous I/O, and verify behavior with automated tests.

Chapter roadmap

Persist structured data, handle failures intentionally, perform asynchronous I/O, and verify behavior with automated tests. Work through the concepts in order, type every example yourself, and keep a short debugging log that records predictions, errors, fixes, and what each fix taught you.

Learning objectives

  • Read and write files safely
  • Design exception boundaries
  • Use async and await
  • Write unit tests with controlled dependencies

06.1

Files and JSON serialization

File methods read and write text, while System.Text.Json converts objects to and from JSON. Async file methods avoid blocking during I/O. Validate deserialized data because syntactically valid JSON may still violate application rules.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
using System.Text.Json;

var settings = new AppSettings("dark", 14);
string json = JsonSerializer.Serialize(settings);
await File.WriteAllTextAsync("settings.json", json);

public record AppSettings(string Theme, int FontSize);
Result
settings.json contains a JSON representation of the record.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

06.2

Exception handling and cleanup

Catch exceptions only where the program can add context, recover, retry, or translate them for a user. Avoid empty catch blocks. using declarations dispose resources even when an exception occurs, and finally is reserved for cleanup not already handled by IDisposable.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
try
{
    string text = await File.ReadAllTextAsync(path);
    return JsonSerializer.Deserialize<AppSettings>(text)
        ?? throw new InvalidDataException("Settings were empty");
}
catch (FileNotFoundException)
{
    return new AppSettings("light", 16);
}
Result
A missing file receives safe defaults; malformed content remains a visible error.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

06.3

async, await, and cancellation

Task represents asynchronous work. An async method should usually return Task or Task<T>, not void. CancellationToken cooperatively communicates that work is no longer needed; pass it through every asynchronous operation that supports cancellation.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
static async Task<string> DownloadAsync(HttpClient client, Uri uri, CancellationToken token)
{
    using HttpResponseMessage response = await client.GetAsync(uri, token);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync(token);
}
Result
The caller can await the text or cancel the request.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

06.4

Unit testing and fakes

A unit test arranges inputs, acts once, and asserts observable behavior. Pure methods need direct tests; classes with external dependencies should receive interfaces so tests can supply fakes. Test normal cases, boundaries, invalid input, and failure behavior without depending on real files or networks.

Before running the example, trace each expression and write down the expected state or result. Then run it, compare the actual result, and change one input or rule to confirm that you understand which part controls the behavior.

Example
[Fact]
public void Deposit_AddsPositiveAmount()
{
    var account = new BankAccount("Ana", 50m);
    account.Deposit(25m);
    Assert.Equal(75m, account.Balance);
}
Result
The test passes when Deposit preserves the account contract.
Check your understanding

Explain the example in plain language, identify one boundary or failure case, and revise it so the output changes in a predictable way.

Chapter assignments

Complete these in order. Later assignments assume that earlier skills are working. Do not only test the sample values; design tests that challenge boundaries, missing values, incorrect types, empty collections, and other likely failures.

  1. A6.1

    Save and load a versioned JSON settings file with defaults for missing files and validation for malformed values.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  2. A6.2

    Create custom domain exceptions and decide which layer catches, translates, logs, or rethrows each.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  3. A6.3

    Implement an asynchronous data service that supports cancellation and reports progress or loading state.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

  4. A6.4

    Write a test suite for a domain class using normal, boundary, invalid, and dependency-failure cases.

    Submit readable source code, a brief design note, and evidence from at least three tests including one boundary or invalid case.

Chapter project

Resilient Study Tracker

Resilient Study Tracker โ€” Build a layered console application that stores courses and sessions as JSON, uses asynchronous file operations, supports cancellation for long reports, validates deserialized data, and includes automated tests with an in-memory repository.

Required process

  1. Write a short specification listing inputs, outputs, rules, and failure cases.
  2. Break the work into small functions, queries, modules, or classes appropriate to the language.
  3. Build the smallest working version before adding optional features.
  4. Test normal, boundary, empty, and invalid cases and record the results.
  5. Refactor names and duplication, then write a concise user guide.

Key terms

  • serialization
  • JSON
  • exception boundary
  • IDisposable
  • Task
  • async
  • await
  • CancellationToken
  • unit test
  • fake