Every number here was measured with BenchmarkDotNet on .NET 10, and the benchmark ships in the repository so you can rerun it on your own hardware before you believe any of it. A row with five encrypted fields costs about a microsecond to write.
The common approach constructs a fresh Aes instance, a MemoryStream and a
CryptoStream for every single value. This package keeps one keyed AES-GCM instance for the
process and writes into a single output buffer. Same hardware-accelerated AES underneath — the difference is
everything around it.
| Payload | Encrypt | Decrypt | Allocation | ||
|---|---|---|---|---|---|
| 64 B
Aadhaar, phone, postcode
|
899 → 244 ns | 3.7× | 4,619 → 129 ns | 36× | 11–16× less |
| 1 KB
address, notes field
|
1,687 → 372 ns | 4.5× | 4,755 → 274 ns | 17× | 5× less |
| 16 KB
document blob
|
14,365 → 2,476 ns | 5.8× | 8,375 → 2,538 ns | 3.3× | 4× less |
The value converter is the code that runs on every read and every write. These are its real costs — and the case for choosing your storage format deliberately.
| Field | Base64 write | Binary write | Base64 read | Binary read | Read gain |
|---|---|---|---|---|---|
| 16 chars | 240 ns | 184 ns | 233 ns | 159 ns | 1.5× |
| 128 chars | 291 ns | 212 ns | 467 ns | 207 ns | 2.3× |
| 4 KB | 2,129 ns | 1,587 ns | 6,429 ns | 1,294 ns | 5.0× |
Base64 is the default because it stores in an existing nvarchar column. Switching to binary
drops the encode on write, the decode on read, and 33% of the bytes on the wire and in every backup.
// column becomes varbinary [CryptoEncrypted(CryptoStorageFormat.Binary)] public string? Aadhaar { get; set; }
Five encrypted short fields — Aadhaar, phone, email, name, address — measured end to end through the converter:
| Operation | Cost | Why |
|---|---|---|
| Queries not touching encrypted columns | Zero | No converter is involved. |
| Projections that skip encrypted columns | Zero | Select(o => new { o.Id, o.Status }) never materializes them. |
| Sentinel telemetry per operation | tens of ns | One lock-free bounded-channel write, drop-on-full — it can never block a query. |
| Startup scan, per column | O(1) | Take(100) — the same cost on a thousand rows or five hundred million. |
| Storage overhead | +30 B, ×1.33 | Envelope plus Base64 expansion. Binary format drops the 1.33×. |
The keyed AesGcm is created once per key ring and reused for the life of the process, instead
of being constructed and torn down per value.
Version, key id, nonce, tag and ciphertext are written into a single allocation with spans — no intermediate streams, no copies.
96-bit nonces are drawn from the system CSPRNG in 4 KiB blocks rather than one call per value, which alone made short-field writes 1.75× faster. Same randomness source, same collision bounds.
It's in the repository, it takes a couple of minutes, and it reports on your hardware rather than ours.
dotnet run -c Release --project benchmarks/EntityFrameworkCore.Crypto.DataEncryption.Benchmarks
AES-CBC against AES-GCM at 64 B / 1 KiB / 16 KiB — the comparison at the top of this page.
The Base64 and binary converter paths at 16 / 128 / 4096 characters, with allocation counts.
Whole-row push and pull for a realistic five-field entity, plus a blind-index hash.
No key, no sign-up, no telemetry. Install it, call UseEncryption,
and measure it against whatever you're using today.