Skip to content

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

shell
dotnet add package Tayra.Wolverine
powershell
Install-Package Tayra.Wolverine

Setup

Call UseTayraMiddleware() on WolverineOptions to register the encryption middleware globally:

cs
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();
});
anchor

UseTayraMiddleware() does three things:

  1. Registers TayraEncryptionMiddleware as a global middleware policy
  2. Registers TayraWolverineOptions in DI for configuration
  3. Discovers the built-in ErasePersonalDataHandler in 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

cs
// 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
    });
});
anchor
PropertyTypeDefaultDescription
EncryptOutboundbooltrueEncrypt PII fields on outbound messages after handlers
DecryptInboundbooltrueDecrypt PII fields on inbound messages before handlers
RedactDeadLetterQueuesboolfalseAttempts to encrypt known PII fields before dead-letter storage/publish operations.
RedactExceptionMessagesboolfalseReplaces 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]:

cs
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; }
}
anchor

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:

csharp
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:

cs
// 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}");
anchor

Send the command through the Wolverine bus:

csharp
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) to delete all encryption keys
  2. Logs the erasure with the data subject ID and reason
  3. Returns a PersonalDataErasedEvent for 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:

cs
// 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}");
anchor

Example subscriber:

csharp
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