Marten
Tayra integrates with Marten by wrapping the configured serializer with TayraSerializer. This provides transparent encryption and decryption of [PersonalData] fields in both documents and events stored in PostgreSQL's JSONB columns.
Prerequisites
Tayra.Marten requires Marten 8.x or later and PostgreSQL 13+. If upgrading from Marten 7.x, see the Marten migration guide for breaking changes.
Install
dotnet add package Tayra.MartenInstall-Package Tayra.MartenSetup
Call UseTayra() on Marten's StoreOptions to enable PII encryption. This wraps Marten's default serializer with TayraSerializer:
// Configure Marten with Tayra encryption.
// UseTayra() wraps the serializer so PII fields are encrypted in JSONB storage.
var connectionString = "Host=localhost;Port=5432;Database=tayra_sample;Username=postgres;Password=postgres";
services.AddMarten(opts =>
{
opts.Connection(connectionString);
// Enable Tayra encryption for documents and events
opts.UseTayra(tayra);
});How TayraSerializer Works
TayraSerializer is a decorator around Marten's existing ISerializer. When serializing (e.g., ToJson()), it encrypts [PersonalData] fields before passing the object to the inner serializer. When deserializing (e.g., FromJson<T>()), it decrypts the fields after the inner serializer reconstructs the object. All non-PII fields pass through untouched.
How It Works
Tayra wraps Marten's ISerializer to transparently encrypt and decrypt PII fields during serialization. Encryption is selective by type — only entities with [PersonalData] annotations are encrypted. Types without PII attributes pass through unmodified.
Documents
Annotate your Marten document classes with [DataSubjectId] and [PersonalData]:
public class CustomerDocument
{
public Guid Id { get; set; }
[DataSubjectId]
public string SubjectId { get; set; } = "";
[PersonalData]
public string Name { get; set; } = "";
[PersonalData(MaskValue = "redacted@example.com")]
public string Email { get; set; } = "";
/// <summary>
/// Not annotated — stored as plaintext in JSONB.
/// </summary>
public string AccountType { get; set; } = "";
}[DataSubjectId]onSubjectIdidentifies the data owner.[PersonalData]onNameandEmailmarks them for encryption.AccountTypeis not annotated and stored as plaintext in JSONB.
When you store this document with session.Store(customer), the Name and Email values in the JSONB column will contain AES-256-GCM ciphertext. When you load it with session.Load<CustomerDocument>(id), the fields are decrypted back to plaintext automatically.
Events
Event payloads are encrypted the same way as documents:
public class CustomerRegisteredEvent
{
[DataSubjectId]
public string SubjectId { get; set; } = "";
[PersonalData]
public string CustomerName { get; set; } = "";
[PersonalData(MaskValue = "redacted@example.com")]
public string CustomerEmail { get; set; } = "";
public DateTime RegisteredAt { get; set; }
}When you append this event to a stream, the CustomerName and CustomerEmail values are encrypted in the event store. Events are immutable in Marten, so the encrypted values are permanent -- which is exactly what makes crypto-shredding work.
Event Sourcing and GDPR
Crypto-shredding is the recommended approach for GDPR compliance in event-sourced systems. Instead of rewriting event history (which violates event sourcing principles), you delete the encryption key. The events remain intact, but the PII fields become permanently unreadable.
Crypto-Shredding
Delete a data subject's encryption keys to make all their PII permanently unreadable:
// GDPR "right to be forgotten" — destroy encryption keys for a data subject.
// After this, any attempt to decrypt the subject's data returns replacement values.
//
// Usage with Marten IDocumentSession:
// await session.ShredDataSubjectAsync(cryptoEngine, subjectId);
// await session.SaveChangesAsync();
// Example (standalone, without a live Marten session):
var subjectId = "cust-42";
await cryptoEngine.DeleteAllKeysAsync(subjectId);
Console.WriteLine($"Crypto-shredded all keys for subject '{subjectId}'.");The ShredDataSubjectAsync extension method on IDocumentSession calls cryptoEngine.DeleteAllKeysAsync() to destroy all encryption keys for the specified subject.
Check if Shredded
Verify whether a data subject has been crypto-shredded:
// Check if a data subject's keys have been shredded.
//
// Usage with Marten IDocumentSession:
// bool shredded = await session.IsDataSubjectShreddedAsync(cryptoEngine, subjectId);
//
// Standalone equivalent:
var keyExists = await cryptoEngine.KeyExistsAsync(subjectId);
var isShredded = !keyExists;
Console.WriteLine($"Subject '{subjectId}' is shredded: {isShredded}");The IsDataSubjectShreddedAsync extension checks whether the subject's encryption key still exists in the key store. If the key is gone, the subject has been shredded.
Projections
When using Marten projections with encrypted events, the event data needs to be decrypted before being applied to projected documents. Use AddTayraProjection() to wrap your projection with automatic decryption:
// Register a projection with automatic PII decryption.
// Events are decrypted before being applied to the projection.
services.AddMarten(opts =>
{
opts.Connection(connectionString);
opts.UseTayra(tayra);
// Wrap your projection so events are decrypted before Apply/ApplyAsync
// opts.AddTayraProjection(myProjection, ProjectionLifecycle.Inline, tayra);
});
// The TayraProjectionWrapper decrypts event.Data for each event in the stream
// before delegating to your inner IProjection's Apply/ApplyAsync method.
Console.WriteLine("\nProjection decryption wraps your IProjection with TayraProjectionWrapper.");
Console.WriteLine("Events have their PII fields decrypted before being applied to projections.");How Projection Decryption Works
AddTayraProjection() wraps your IProjection in a TayraProjectionWrapper. Before the wrapper delegates to your projection's Apply or ApplyAsync method, it iterates through all events in the stream and decrypts their PII fields using TayraEventDecryptor.
This means your projection handlers receive cleartext values and do not need to know about encryption at all.
Shredded Events in Projections
If a data subject has been crypto-shredded, their events will contain replacement values instead of the original data. Your projections should handle these gracefully -- for example, by checking for known replacement values or empty strings.
Blind Indexes
Tayra's blind index support works transparently with Marten. Blind index services are registered automatically by AddTayra() when [BlindIndex] attributes are present, so TayraSerializer automatically computes HMAC blind indexes on companion properties before encrypting PII fields during serialization.
Define a model with [BlindIndex] on encrypted fields:
public class CustomerDocument
{
public Guid Id { get; set; }
[DataSubjectId]
public string SubjectId { get; set; } = "";
[PersonalData, BlindIndex(Transforms = ["lowercase", "trim"])]
public string Email { get; set; } = "";
public string? EmailIndex { get; set; } // auto-populated
}Query by blind index using LINQ:
var hash = await blindIndexer.ComputeHashAsync(
"alice@example.com", "EmailIndex", typeof(CustomerDocument));
var customer = await session.Query<CustomerDocument>()
.Where(c => c.EmailIndex == hash)
.FirstOrDefaultAsync();For efficient queries, add a Marten computed index on the companion property:
opts.Schema.For<CustomerDocument>().Index(x => x.EmailIndex);See Blind Indexes for the full guide including transforms, compound indexes, and security considerations.
Data Migration
If you're adding Tayra to an existing application with pre-existing cleartext documents, use the Marten migration service to bulk-encrypt them. See the Brownfield Adoption guide for step-by-step instructions.
Register the migration service and set up a raw store:
// Register Tayra Marten migration services (requires AddTayra() first)
var migrationServices = new ServiceCollection();
migrationServices.AddTayra(opts => opts.LicenseKey = licenseKey);
migrationServices.AddTayraMartenMigrations();
var migrationProvider = migrationServices.BuildServiceProvider();// Create a "raw" store WITHOUT UseTayra() — reads cleartext field values as-is.
// This is required so the migration service can detect which documents need encrypting.
var rawStore = DocumentStore.For(opts =>
{
opts.Connection(connectionString);
// Do NOT call opts.UseTayra() here — we need raw access to read cleartext
});Then encrypt existing documents and verify:
// Bulk-encrypt existing cleartext documents in batches
var migrationService = migrationProvider.GetRequiredService<ITayraMartenMigrationService>();
var result = await migrationService.EncryptExistingDocumentsAsync<CustomerDocument>(
rawStore,
batchSize: 100);
Console.WriteLine($"\n=== Marten Migration Result ===");
Console.WriteLine($" Scanned: {result.TotalScanned}");
Console.WriteLine($" Encrypted: {result.Encrypted}");
Console.WriteLine($" Skipped: {result.Skipped}");
Console.WriteLine($" Errors: {result.Errors}");
Console.WriteLine($" Duration: {result.Duration.TotalMilliseconds:F0}ms");// Verify all documents are now properly encrypted
var verification = await migrationService.VerifyDocumentEncryptionAsync<CustomerDocument>(
rawStore,
batchSize: 100);
Console.WriteLine($"\n=== Marten Verification Result ===");
Console.WriteLine($" Verified: {verification.TotalVerified}");
Console.WriteLine($" Valid: {verification.Valid}");
Console.WriteLine($" Invalid: {verification.Invalid}");
Console.WriteLine($" Duration: {verification.Duration.TotalMilliseconds:F0}ms");
foreach (var invalid in verification.InvalidRows)
{
Console.WriteLine($" [INVALID] Document {invalid.EntityId}, Property: {invalid.PropertyName} — {invalid.Reason}");
}See Also
- Getting Started -- End-to-end encryption tutorial
- Wolverine Integration -- Message pipeline encryption
- EF Core Integration -- Entity Framework Core integration
- Key Stores -- Production key store options
