Querying with Blind Indexes
A blind index is only useful if you can efficiently query by it. This page covers how to compute a blind index for a search term and use it in EF Core and Marten queries.
Computing a Blind Index
Use ITayra.ComputeBlindIndexAsync() to compute a blind index hash for a search term:
By property expression (recommended):
// Compute a blind index hash using a strongly-typed property expression
string emailHash = await biTayra.ComputeBlindIndexAsync(
"jane@example.com", "EmailHash", typeof(BlindIndexedCustomer));
Console.WriteLine($" Typed hash: {emailHash}");By hash name directly:
// Compute a blind index hash by index name and entity type
string hash = await biTayra.ComputeBlindIndexAsync(
"jane@example.com", "EmailHash", typeof(BlindIndexedCustomer));
Console.WriteLine($" Raw hash: {hash}");Both methods apply the configured transforms in order before computing the HMAC. The resulting hash string is URL-safe Base64.
EF Core
Save
When using the EF Core interceptor, the blind index is computed automatically during SaveChanges. You do not need to call EncryptAsync manually if transparent encryption is enabled.
// With transparent encryption enabled, blind indexes are computed automatically
// during SaveChanges — no manual call to EncryptAsync is required.
//
// var customer = new ProtectedCustomer
// {
// Id = Guid.NewGuid(),
// Name = "Jane Doe",
// Email = "jane@example.com",
// };
// dbContext.Customers.Add(customer);
// await dbContext.SaveChangesAsync();
// // customer.EmailHash is now populated with the HMAC
Console.WriteLine(" Blind index is computed automatically during SaveChanges.");Query by Blind Index and Decrypt
// Compute the hash for the search term
string searchHash = await blindIndexer.ComputeHashAsync(
"jane@example.com", "EmailHash", typeof(ProtectedCustomer));
// Query by the companion column
// var customer = await dbContext.Customers
// .Where(c => c.EmailHash == searchHash)
// .FirstOrDefaultAsync();
// The interceptor decrypts automatically on materialization
// Console.WriteLine($"Found: {customer?.Name}");
Console.WriteLine($" EF Core search hash: {searchHash}");Use AsNoTracking for read-only queries
When you only need to read and display data, AsNoTracking() avoids the overhead of change tracking and is safe to combine with Tayra's materialization interceptor.
Add a Database Index
Blind index queries are only efficient if the database has an index on the companion column. Add one in your migration or OnModelCreating:
// In your DbContext.OnModelCreating, add a database index on the companion column
// so blind-index queries are efficient (avoids full table scans):
modelBuilder.Entity<ProtectedCustomer>()
.HasIndex(c => c.EmailHash);Marten
Save
Marten stores documents as JSONB. The companion property is included in the document JSON and can be queried with LINQ.
// EncryptAsync computes the blind index before encryption.
// The EmailHash companion property is populated automatically.
// await tayra.EncryptAsync(customer);
// Store the document — EmailHash is included in the JSONB.
// await using var session = store.LightweightSession();
// session.Store(customer);
// await session.SaveChangesAsync();
Console.WriteLine("\nBlind index save: EmailHash is populated before the document is stored.");Query by Companion Property
// Compute the search hash for the lookup term (same transforms applied as during save)
// var blindIndexer = provider.GetRequiredService<IBlindIndexer>();
// string searchHash = await blindIndexer.ComputeHashAsync(
// "jane@example.com", "EmailHash", typeof(MartenCustomer));
// Query by the companion property in the JSONB document
// await using var querySession = store.QuerySession();
// var customer = await querySession.Query<MartenCustomer>()
// .Where(c => c.EmailHash == searchHash)
// .FirstOrDefaultAsync();
// Decrypt the loaded document to restore plaintext PII
// await tayra.DecryptAsync(customer);
Console.WriteLine("Blind index query: filter by EmailHash, then decrypt on load.");Create a Marten index on the companion property
Use Marten's Index<T>() API in StoreOptions to add a GIN or B-tree index on the companion JSONB field for efficient querying.
Compound Blind Index Queries
When you need to search by multiple encrypted fields simultaneously, compute a hash for each and combine them in a single query:
// For compound indexes, the hash is computed automatically by EncryptAsync
// when you call it on the entity — all fields are combined into one HMAC.
// Retrieve the stored hash from the entity to build your query:
// var entity = new IndexedCustomer { FirstName = "Jane", LastName = "Doe", ... };
// await tayra.EncryptAsync(entity);
// string fullNameHash = entity.FullNameIndex;
// Query the companion column:
// db.Query<IndexedCustomer>().Where(c => c.FullNameIndex == fullNameHash)
Console.WriteLine(" Compound blind index hash computed during EncryptAsync.");Compound indexes on multiple companion columns
If you frequently query by two companion columns together, consider creating a composite database index on both columns (FirstNameHash, LastNameHash) to avoid index intersection overhead.
Paginated Lookups
Blind indexes support exact-match lookups only. You cannot perform prefix searches, range queries, or LIKE comparisons on a blind index hash. For paginated result sets, use the blind index to identify matching rows, then apply additional ordering:
// Blind indexes support exact-match lookups only.
// Use the hash to filter, then apply ordering and paging.
string pagedEmailHash = await biTayra.ComputeBlindIndexAsync(
"jane@example.com", "EmailHash", typeof(BlindIndexedCustomer));
// var results = await dbContext.Customers
// .Where(c => c.EmailHash == pagedEmailHash)
// .OrderBy(c => c.Id)
// .Skip(page * pageSize)
// .Take(pageSize)
// .ToListAsync();
Console.WriteLine($" Paginated hash computed: {pagedEmailHash}");Performance Notes
- HMAC computation is fast. HMAC-SHA256 on a short string is sub-microsecond on modern hardware. It does not meaningfully impact write throughput.
- The HMAC key is cached. After the first call, the key is held in the
DefaultCryptoEnginememory cache. Subsequent calls toComputeBlindIndexAsyncare memory-only operations. - Add a database index. Without an index on the companion column, every query becomes a full table scan. This is by far the biggest performance factor.
- Companion columns add storage. A SHA-256 hash stored as URL-safe Base64 is 43 characters. Budget accordingly for large tables.
See Also
- Blind Indexes Overview — How HMAC blind indexes work
- Transforms — Normalisation options
- Key Management — HMAC key storage and rotation
- EF Core Integration — Transparent encryption setup
- Marten Integration — Marten document encryption
