Skip to content

MassTransit

Tayra integrates with MassTransit through scoped filters that automatically encrypt and decrypt [PersonalData] fields in messages flowing through the pipeline. It also provides a built-in ErasePersonalDataConsumer for GDPR erasure workflows.

Prerequisites

Tayra.MassTransit requires MassTransit 8.x or later.

Install

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

Setup

Registration is two steps: UseTayra() on the bus configurator and UseTayraFilters() inside the transport callback:

csharp
services.AddTayra(opts => opts.LicenseKey = licenseKey); // defaults to InMemoryKeyStore; use PostgreSQL, Vault, etc. in production

services.AddMassTransit(x =>
{
    x.UseTayra(); // Registers consume filter + erasure consumer

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.UseTayraFilters(context); // Registers send + publish filters
        cfg.ConfigureEndpoints(context);
    });
});

UseTayra() does two things:

  1. Registers a consume filter callback that decrypts inbound messages before consumers
  2. Registers the built-in ErasePersonalDataConsumer for GDPR erasure commands

UseTayraFilters() registers send and publish filters that encrypt outbound messages.

Prerequisites

Tayra core services must be registered via services.AddTayra() (optionally chaining a production key store) before the filters resolve at runtime.

Options

csharp
services.Configure<TayraMassTransitOptions>(opts =>
{
    opts.EncryptOutbound = true;  // default
    opts.DecryptInbound = true;   // default
});
PropertyTypeDefaultDescription
EncryptOutboundbooltrueEncrypt PII fields on outbound messages (send and publish)
DecryptInboundbooltrueDecrypt PII fields on inbound messages before consumers

Messages

Annotate your MassTransit message classes with [DataSubjectId] and [PersonalData]:

csharp
public class CreateCustomerCommand
{
    [DataSubjectId]
    public string CustomerId { get; set; }

    [PersonalData]
    public string Name { get; set; }

    [PersonalData]
    public string Email { get; set; }

    public string AccountType { get; set; } // Not encrypted
}

How the Filters Work

MassTransit uses a pipes-and-filters architecture. Tayra registers three scoped filters:

  • TayraConsumeFilter<T> -- Runs before the consumer. Decrypts all [PersonalData] fields so your consumer receives cleartext.
  • TayraSendFilter<T> -- Runs before send. Encrypts all [PersonalData] fields before the message reaches the transport.
  • TayraPublishFilter<T> -- Runs before publish. Encrypts all [PersonalData] fields before the message reaches the transport.

Your consumers work with plaintext values and do not need to know about encryption:

csharp
public class CreateCustomerConsumer : IConsumer<CreateCustomerCommand>
{
    public async Task Consume(ConsumeContext<CreateCustomerCommand> context)
    {
        var cmd = context.Message;
        // cmd.Name and cmd.Email are already decrypted
        await SaveCustomer(cmd);
    }
}

Graceful Error Handling

If decryption or encryption fails (e.g., a key store is temporarily unavailable), the filter logs a warning and allows the message to proceed. This prevents transient key store failures from blocking message processing.

GDPR Erasure Consumer

Tayra includes a built-in consumer for GDPR Article 17 "right to erasure" workflows:

csharp
// Send the command through MassTransit
var endpoint = await bus.GetSendEndpoint(new Uri("queue:erase-personal-data"));
await endpoint.Send(new ErasePersonalDataCommand("cust-42", "GDPR Article 17 request"));

The built-in ErasePersonalDataConsumer:

  1. Calls cryptoEngine.DeleteAllKeysAsync(dataSubjectId) to delete all encryption keys
  2. Logs the erasure with the data subject ID and reason
  3. Publishes a PersonalDataErasedEvent for downstream consumers

Erasure Event

The PersonalDataErasedEvent is published by the built-in consumer and can be consumed by your own consumers for audit logging, notifications, or cascading operations:

csharp
public class AuditErasureConsumer : IConsumer<PersonalDataErasedEvent>
{
    public async Task Consume(ConsumeContext<PersonalDataErasedEvent> context)
    {
        var evt = context.Message;
        logger.LogInformation(
            "Personal data erased for {Subject} at {Time}. Reason: {Reason}",
            evt.DataSubjectId, evt.ErasedAt, evt.Reason);
    }
}

See Also