File I/O, streams and JSON serialization

Read and write files asynchronously, stream instead of buffering, and serialise JSON fast with source generation and converters.

Files, directories and streams

using System.Text;

// whole-file shortcuts for small files
string text = await File.ReadAllTextAsync("config.json");
await File.WriteAllTextAsync("out.txt", "hello", Encoding.UTF8);

string[] lines = await File.ReadAllLinesAsync("data.csv");

// streaming for large files: never load the whole thing into memory
await using (FileStream fs = new("big.log", FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true))
using (StreamReader reader = new(fs, Encoding.UTF8))
{
    string? line;
    while ((line = await reader.ReadLineAsync()) is not null)
    {
        if (line.Contains("ERROR", StringComparison.Ordinal)) Console.WriteLine(line);
    }
}

// write through a stream, flushing in the right order
await using (FileStream outFs = File.Create("report.txt"))
await using (StreamWriter writer = new(outFs, Encoding.UTF8))
{
    await writer.WriteLineAsync("header");
    await writer.FlushAsync();
}

// paths: build them with Path, never string concatenation
string path = Path.Combine("data", "2026", "report.csv");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
bool exists = File.Exists(path);

foreach (string file in Directory.EnumerateFiles("data", "*.csv", SearchOption.AllDirectories))
    Console.WriteLine(Path.GetFileName(file));
  • Directory.EnumerateFiles is lazy and starts returning results immediately; GetFiles builds the whole array first.
  • Always specify useAsync: true on a FileStream when you use the async methods, or the read is served from the thread pool and you gain nothing.
  • FileShare.Read lets another process read while you hold the handle. Opening without a share mode blocks other readers, which is a common cause of an intermittent IOException.
  • A using declaration (using var stream = ...;) disposes at the end of the enclosing scope, which is usually what you want in a method.

System.Text.Json and source generation

using System.Text.Json;
using System.Text.Json.Serialization;

public sealed record Order(int Id, string Sku, int Quantity, DateTimeOffset CreatedAt);

// source-generated context: no reflection at run time, trims and AOT friendly
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = false,
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
public partial class AppJsonContext : JsonSerializerContext { }

public static class JsonDemo
{
    public static async Task RunAsync()
    {
        var order = new Order(1, "ABC-1", 3, DateTimeOffset.UtcNow);

        // use the generated context, not the reflection-based default
        string json = JsonSerializer.Serialize(order, AppJsonContext.Default.Order);
        Order? back = JsonSerializer.Deserialize(json, AppJsonContext.Default.Order);

        await using FileStream fs = File.Create("order.json");
        await JsonSerializer.SerializeAsync(fs, order, AppJsonContext.Default.Order);

        // tolerant reading: options decide whether an unknown property throws
        var options = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            AllowTrailingCommas = true,
            ReadCommentHandling = JsonCommentHandling.Skip,
        };
        var parsed = JsonSerializer.Deserialize<Order>("{\"id\":2,\"sku\":\"X\",\"quantity\":1,\"createdAt\":\"2026-09-18T00:00:00Z\"}", options);
        Console.WriteLine(parsed?.Sku);
    }
}
NeedApproachNote
Serialise your own typesSource-generated JsonSerializerContextRequired for AOT and trimming
Unknown shapes or dynamic keysJsonDocument / JsonNodeNo allocation of your types, full control
A different wire formatCustom JsonConverter<T>Read and Write must be symmetric
Stream a huge arrayJsonSerializer.DeserializeAsyncEnumerableYields items as they arrive
Pretty output for humansWriteIndented = trueNever for a hot path
// a converter for a type the serialiser cannot guess
public sealed class MoneyConverter : JsonConverter<decimal>
{
    public override decimal Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions o)
        => decimal.Parse(reader.GetString()!, System.Globalization.CultureInfo.InvariantCulture);

    public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions o)
        => writer.WriteStringValue(value.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture));
}

Configuration from JSON

using Microsoft.Extensions.Configuration;

IConfigurationRoot config = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
    .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production"}.json", optional: true)
    .AddEnvironmentVariables()          // environment wins over JSON
    .AddCommandLine(args)
    .Build();

// read a value, or bind a section to a strongly typed object
string? connection = config.GetConnectionString("Default");
int timeout = config.GetValue("Http:TimeoutSeconds", 30);

var options = new HttpOptions();
config.GetSection("Http").Bind(options);
Console.WriteLine(options.BaseUrl);
💡
Configuration is layered, and the last provider that supplies a key wins. Put the environment variable provider after the JSON files so a container can override anything without rebuilding the image — that is the whole point of the layering.

FAQ

Why did serialisation stop working after trimming?
Trimming removed properties the reflection-based serialiser needed. Switch to a source-generated JsonSerializerContext, which makes the required properties visible to the linker.
File.WriteAllText or a FileStream?
Use the shortcut for small, one-shot files. Use a stream when the content is large, produced incrementally, or written while other work continues, because the shortcut allocates the whole thing first.

Exceptions and error handling Dependency injection and configuration

Last refreshed 2026-09-18.