Wolverine
Tayra integrates with Wolverine through middleware that automatically encrypts and decrypts [PersonalData] fields in messages flowing through the pipeline. It also provides a built-in ErasePersonalDataCommand handler for GDPR erasure workflows.
Prerequisites
Tayra.Wolverine requires WolverineFx 5.x or later. If upgrading from an earlier version, see the Wolverine migration guide for breaking changes.
Install
dotnet add package Tayra.WolverineInstall-Package Tayra.WolverineSetup
Call UseTayraMiddleware() on WolverineOptions to register the encryption middleware globally:
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices(services =>
{
// Register Tayra core services and key store
services.AddTayra(opts => opts.LicenseKey = licenseKey);
});
builder.UseWolverine(opts =>
{
// Add Tayra encryption/decryption middleware to the Wolverine pipeline.
// This automatically:
// - Decrypts inbound messages before handlers receive them
// - Encrypts outbound messages after handlers produce them
// - Discovers the built-in ErasePersonalDataHandler
opts.UseTayraMiddleware();
});UseTayraMiddleware() does three things:
- Registers
TayraEncryptionMiddlewareas a global middleware policy - Registers
TayraWolverineOptionsin DI for configuration - Discovers the built-in
ErasePersonalDataHandlerin the Tayra.Wolverine assembly
Prerequisites
Tayra core services must be registered via services.AddTayra() (optionally chaining a production key store) before UseTayraMiddleware() is called. The middleware resolves ITayra from DI at runtime.
Options
// TayraWolverineOptions — controls message pipeline encryption behavior
//
// Property Type Default Description
// ─────────────────────── ────── ──────── ─────────────────────────────────────────
// EncryptOutbound bool true Encrypt PII on outbound messages
// DecryptInbound bool true Decrypt PII on inbound messages
// RedactDeadLetterQueues bool false Encrypt PII before dead-letter persistence/publish
// RedactExceptionMessages bool false Replace exception messages with a redacted constant
builder.UseWolverine(opts =>
{
opts.UseTayraMiddleware(tayraOpts =>
{
tayraOpts.EncryptOutbound = true; // Default: encrypt outbound
tayraOpts.DecryptInbound = true; // Default: decrypt inbound
tayraOpts.RedactDeadLetterQueues = false; // Opt-in: encrypt PII before dead-lettering
tayraOpts.RedactExceptionMessages = false; // Opt-in: redact exception messages
});
});| Property | Type | Default | Description |
|---|---|---|---|
EncryptOutbound | bool | true | Encrypt PII fields on outbound messages after handlers |
DecryptInbound | bool | true | Decrypt PII fields on inbound messages before handlers |
RedactDeadLetterQueues | bool | false | Attempts to encrypt known PII fields before dead-letter storage/publish operations. |
RedactExceptionMessages | bool | false | Replaces exception messages with Exception details redacted by Tayra. before dead-letter persistence/publish operations. |
Redaction Scope
Redaction is best-effort and transport-dependent. Database-backed dead-letter storage and transports that republish dead-letter payloads can apply message redaction before persistence. Broker-native dead-letter mechanics that simply nack/reject the original broker message may preserve the original payload bytes.
Messages
Annotate your Wolverine message classes with [DataSubjectId] and [PersonalData]:
public class CreateCustomerCommand
{
[DataSubjectId]
public string CustomerId { get; set; } = "";
[PersonalData]
public string Name { get; set; } = "";
[PersonalData(MaskValue = "redacted@example.com")]
public string Email { get; set; } = "";
public string AccountType { get; set; } = "";
}
public class CustomerCreatedEvent
{
[DataSubjectId]
public string CustomerId { get; set; } = "";
[PersonalData]
public string Name { get; set; } = "";
[PersonalData(MaskValue = "redacted@example.com")]
public string Email { get; set; } = "";
public DateTime CreatedAt { get; set; }
}How the Middleware Works
Wolverine middleware uses a convention-based pattern:
BeforeAsync(object message, ...)-- Runs before your handler. Decrypts all[PersonalData]fields on the inbound message so your handler receives cleartext.AfterAsync(object message, ...)-- Runs after your handler. Encrypts all[PersonalData]fields on the outbound message so personal data is protected in transit and at rest.
Your handlers work with plaintext values and do not need to know about encryption:
public static class CreateCustomerHandler
{
public static CustomerCreatedEvent Handle(CreateCustomerCommand cmd)
{
// cmd.Name and cmd.Email are already decrypted
return new CustomerCreatedEvent
{
CustomerId = cmd.CustomerId,
Name = cmd.Name, // Will be encrypted by AfterAsync
Email = cmd.Email, // Will be encrypted by AfterAsync
CreatedAt = DateTime.UtcNow,
};
}
}Graceful Error Handling
If decryption or encryption fails (e.g., a key store is temporarily unavailable), the middleware logs a warning and allows the message to proceed. This prevents transient key store failures from blocking message processing.
GDPR Erasure Command
Tayra includes a built-in command and handler for GDPR Article 17 "right to erasure" workflows:
// ErasePersonalDataCommand is a built-in Wolverine command for GDPR erasure.
// Send it through the Wolverine bus to crypto-shred a data subject's keys.
//
// Usage:
// var bus = host.Services.GetRequiredService<IMessageBus>();
// await bus.InvokeAsync(new ErasePersonalDataCommand("cust-42", "GDPR Article 17 request"));
//
// The built-in ErasePersonalDataHandler:
// 1. Calls cryptoEngine.DeleteAllKeysAsync(dataSubjectId)
// 2. Logs the erasure
// 3. Returns a PersonalDataErasedEvent
var eraseCommand = new ErasePersonalDataCommand(
DataSubjectId: "cust-42",
Reason: "GDPR Article 17 request");
Console.WriteLine($"Erase command ready for subject: {eraseCommand.DataSubjectId}");
Console.WriteLine($"Reason: {eraseCommand.Reason}");Send the command through the Wolverine bus:
var bus = host.Services.GetRequiredService<IMessageBus>();
await bus.InvokeAsync(new ErasePersonalDataCommand("cust-42", "GDPR Article 17 request"));The built-in ErasePersonalDataHandler:
- Calls
cryptoEngine.DeleteAllKeysAsync(dataSubjectId)to delete all encryption keys - Logs the erasure with the data subject ID and reason
- Returns a
PersonalDataErasedEventfor downstream handlers
Erasure Event
The PersonalDataErasedEvent is returned by the built-in handler and can be consumed by your own handlers for audit logging, notifications, or cascading operations:
// PersonalDataErasedEvent is returned by the built-in ErasePersonalDataHandler.
// Subscribe to this event for audit logging, notifications, or cascading cleanup.
//
// public record PersonalDataErasedEvent(
// string DataSubjectId,
// DateTimeOffset ErasedAt,
// string? Reason);
//
// Example handler:
// public static class PersonalDataErasedHandler
// {
// public static void Handle(PersonalDataErasedEvent evt, ILogger logger)
// {
// logger.LogInformation(
// "Personal data erased for {Subject} at {Time}. Reason: {Reason}",
// evt.DataSubjectId, evt.ErasedAt, evt.Reason);
// }
// }
var erasedEvent = new PersonalDataErasedEvent(
DataSubjectId: "cust-42",
ErasedAt: DateTimeOffset.UtcNow,
Reason: "GDPR Article 17 request");
Console.WriteLine($"\nErased event: subject={erasedEvent.DataSubjectId}, at={erasedEvent.ErasedAt}");Example subscriber:
public static class AuditErasureHandler
{
public static void Handle(PersonalDataErasedEvent evt, ILogger logger)
{
logger.LogInformation(
"Personal data erased for {Subject} at {Time}. Reason: {Reason}",
evt.DataSubjectId, evt.ErasedAt, evt.Reason);
}
}See Also
- Getting Started -- End-to-end encryption tutorial
- Marten Integration -- Document database encryption with Marten
- EF Core Integration -- Entity Framework Core integration
- Key Stores -- Production key store options
