Project · I/O & HTTP
Verify an offline bounded HTTP importer
Reuse one HttpClient, inject real in-memory HTTP responses, and check nine status, byte-budget and model boundaries.
All C# source files below are loaded together in the playground. Files written by a run live in its virtual directory.
Program.cs
await ImportChecks.RunAsync();
Import/ImportSummary.cs
public sealed record ImportSummary(int Count, decimal Total);
Import/ImportService.cs
using System.Net;
using System.Net.Http;
using System.Text.Json;
public sealed class ImportService {
private readonly HttpClient client;
public ImportService(HttpClient client) { this.client = client; }
public async Task<ImportSummary> ReadAsync(string route, CancellationToken token) {
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri("https://offline.invalid/" + route));
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
response.EnsureSuccessStatusCode();
if (response.StatusCode == HttpStatusCode.NoContent) throw new InvalidDataException("No document");
if (!string.Equals(response.Content.Headers.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("Media type");
using Stream body = await response.Content.ReadAsStreamAsync(token);
using var bytes = new MemoryStream(); byte[] buffer = new byte[16]; int read;
while ((read = await body.ReadAsync(buffer.AsMemory(), token)) != 0) {
if (bytes.Length + read > 256) throw new InvalidDataException("Byte limit");
bytes.Write(buffer, 0, read);
}
using var document = JsonDocument.Parse(bytes.ToArray()); JsonElement root = document.RootElement;
if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() is < 1 or > 4) throw new InvalidDataException("Array shape");
var names = new HashSet<string>(StringComparer.Ordinal); decimal total = 0; int count = 0;
foreach (JsonElement row in root.EnumerateArray()) {
if (row.ValueKind != JsonValueKind.Object || !row.TryGetProperty("Name", out var name) || name.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(name.GetString()) || !row.TryGetProperty("Amount", out var amount) || amount.ValueKind != JsonValueKind.Number || !amount.TryGetDecimal(out decimal value) || value < 0 || !names.Add(name.GetString()!)) throw new InvalidDataException("Invalid row");
total = checked(total + value); count++;
}
return new ImportSummary(count, total);
}
}
Testing/ScenarioHandler.cs
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
public sealed class ScenarioHandler : HttpMessageHandler {
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token) {
token.ThrowIfCancellationRequested();
string route = request.RequestUri!.AbsolutePath.TrimStart('/');
string body = route switch {
"large" => new string(' ', 257),
"duplicate" => """[{"Name":"A","Amount":1},{"Name":"A","Amount":2}]""",
"negative" => """[{"Name":"A","Amount":-1}]""",
"shape" => """{"Name":"A","Amount":1}""",
_ => """[{"Name":"A","Amount":12.5},{"Name":"B","Amount":4.25}]"""
};
var content = new StreamContent(new MemoryStream(Encoding.UTF8.GetBytes(body), writable: false));
content.Headers.ContentType = new MediaTypeHeaderValue(route == "media" ? "text/plain" : "application/json");
HttpStatusCode status = route == "missing" ? HttpStatusCode.NotFound : route == "empty" ? HttpStatusCode.NoContent : HttpStatusCode.OK;
if (route == "empty") { content.Dispose(); content = new StreamContent(new MemoryStream(Array.Empty<byte>())); }
return Task.FromResult(new HttpResponseMessage(status) { Content = content });
}
}
Testing/ImportChecks.cs
using System.Globalization;
using System.Net.Http;
public static class ImportChecks {
public static async Task RunAsync() {
using var handler = new ScenarioHandler(); using var client = new HttpClient(handler, disposeHandler: false);
var service = new ImportService(client); int checks = 0;
ImportSummary summary = await service.ReadAsync("valid", CancellationToken.None);
if (summary != new ImportSummary(2, 16.75m)) throw new Exception("Valid summary failed"); checks++;
async Task Reject<T>(string route, CancellationToken token) where T : Exception {
try { await service.ReadAsync(route, token); } catch (T) { checks++; return; }
throw new Exception("Expected " + typeof(T).Name + " for " + route);
}
await Reject<HttpRequestException>("missing", CancellationToken.None);
await Reject<InvalidDataException>("empty", CancellationToken.None);
await Reject<InvalidDataException>("media", CancellationToken.None);
await Reject<InvalidDataException>("large", CancellationToken.None);
await Reject<InvalidDataException>("duplicate", CancellationToken.None);
await Reject<InvalidDataException>("negative", CancellationToken.None);
await Reject<InvalidDataException>("shape", CancellationToken.None);
using var cancellation = new CancellationTokenSource(); cancellation.Cancel();
await Reject<OperationCanceledException>("valid", cancellation.Token);
Console.WriteLine("import checks:" + checks);
Console.WriteLine("validated total:" + summary.Total.ToString(CultureInfo.InvariantCulture));
Console.WriteLine("transport:injected handler");
}
}
BoundedImport.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>