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.EnumerateFilesis lazy and starts returning results immediately;GetFilesbuilds the whole array first.- Always specify
useAsync: trueon aFileStreamwhen you use the async methods, or the read is served from the thread pool and you gain nothing. FileShare.Readlets another process read while you hold the handle. Opening without a share mode blocks other readers, which is a common cause of an intermittentIOException.- A
usingdeclaration (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);
}
}| Need | Approach | Note |
|---|---|---|
| Serialise your own types | Source-generated JsonSerializerContext | Required for AOT and trimming |
| Unknown shapes or dynamic keys | JsonDocument / JsonNode | No allocation of your types, full control |
| A different wire format | Custom JsonConverter<T> | Read and Write must be symmetric |
| Stream a huge array | JsonSerializer.DeserializeAsyncEnumerable | Yields items as they arrive |
| Pretty output for humans | WriteIndented = true | Never 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.
Related
Exceptions and error handling Dependency injection and configuration
Last refreshed 2026-09-18.