Skip to content

Data Subject Access

Tayra provides built-in support for GDPR Article 15 (Right of Access) and Article 20 (Data Portability) through ITayraCompliance. This service collects, decrypts, and exports all personal data held for a given data subject.

GDPR Requirements

Article 15 — Right of Access requires that you provide a data subject with:

  • Confirmation of whether their data is being processed
  • A copy of all personal data held about them
  • Information about the processing purposes, categories, and recipients

Article 20 — Data Portability requires that you provide personal data in:

  • A structured, commonly used, machine-readable format
  • A format that can be transmitted to another controller

Setup

Package Requirement

This feature requires the Tayra.Compliance package and a Compliance edition license. Data protection itself is handled by Tayra.Core — this package provides the reporting tooling.

shell
dotnet add package Tayra.Compliance

Register the compliance services with entity providers for each type that contains PII:

cs
// Register the data subject access service and entity providers.
var accessServices = new ServiceCollection();
accessServices.AddTayra(opts => opts.LicenseKey = licenseKey)
    .AddCompliance(complianceOpts =>
    {
        complianceOpts.AddEntityProvider<Customer, CustomerEntityProvider>();
        complianceOpts.AddEntityProvider<Order, OrderEntityProvider>();
    });
anchor

.AddCompliance() registers all compliance services including data subject access. Each AddEntityProvider call tells the service where to find entities of a given type for a data subject.

Entity Providers

Implement IDataSubjectEntityProvider<T> for each entity type that contains personal data. The provider is responsible for loading all instances of that entity for a given subject ID.

cs
/// <summary>
/// Provides Customer entities for a given data subject.
/// Implement IDataSubjectEntityProvider&lt;T&gt; for each entity type that contains PII.
/// </summary>
public class CustomerEntityProvider : IDataSubjectEntityProvider<Customer>
{
    // In a real app, this would query your database
    public static readonly Dictionary<string, Customer> Customers = new();

    public Task<IReadOnlyList<Customer>> GetTypedEntitiesForSubjectAsync(
        string subjectId, CancellationToken ct = default)
    {
        if (Customers.TryGetValue(subjectId, out var customer))
        {
            return Task.FromResult<IReadOnlyList<Customer>>([customer]);
        }

        return Task.FromResult<IReadOnlyList<Customer>>([]);
    }
}

public class OrderEntityProvider : IDataSubjectEntityProvider<Order>
{
    public static readonly Dictionary<string, Order> Orders = new();

    public Task<IReadOnlyList<Order>> GetTypedEntitiesForSubjectAsync(
        string subjectId, CancellationToken ct = default)
    {
        if (Orders.TryGetValue(subjectId, out var order))
        {
            return Task.FromResult<IReadOnlyList<Order>>([order]);
        }

        return Task.FromResult<IReadOnlyList<Order>>([]);
    }
}
anchor

In a real application, your entity provider would query your database:

csharp
public class CustomerEntityProvider : IDataSubjectEntityProvider<Customer>
{
    private readonly ICustomerRepository _repo;

    public CustomerEntityProvider(ICustomerRepository repo) => _repo = repo;

    public async Task<IReadOnlyList<Customer>> GetTypedEntitiesForSubjectAsync(
        string subjectId, CancellationToken ct = default)
    {
        var customer = await _repo.FindBySubjectIdAsync(subjectId, ct);
        return customer is not null ? [customer] : [];
    }
}

Structured Export (Art. 15)

Use ExportAsync to generate a structured report of all personal data for a subject:

cs
// Export all personal data for a data subject (GDPR Art. 15 — Right of Access)
var report = await compliance.ExportAccessAsync("subject-access-001");

Console.WriteLine($"Subject: {report.SubjectId}");
Console.WriteLine($"Generated: {report.GeneratedAt}");
foreach (var entityReport in report.Entities)
{
    Console.WriteLine($"  Entity: {entityReport.EntityType}");
    foreach (var instance in entityReport.Instances)
    {
        foreach (var field in instance.Fields)
        {
            Console.WriteLine($"    {field.FieldName}: {field.Value} (redacted: {field.IsRedacted})");
        }
    }
}
anchor

The DataSubjectAccessReport contains:

PropertyTypeDescription
SubjectIdstringThe data subject identifier
GeneratedAtDateTimeOffsetWhen the report was generated
EntitiesIReadOnlyList<EntityDataReport>Reports grouped by entity type

Each EntityDataReport contains:

  • EntityType — The name of the entity class (e.g., "Customer")
  • Instances — A list of EntityInstanceReport, each containing the PII fields and their decrypted values

Each FieldDataReport contains:

  • FieldName — The property name
  • Value — The decrypted value (or replacement if the key was shredded)
  • IsRedacted — Whether the value is a replacement due to crypto-shredding

Portable JSON Export (Art. 20)

Use ExportPortableAsync to generate a machine-readable JSON document:

cs
// Export personal data as machine-readable JSON (GDPR Art. 20 — Data Portability)
var portableJson = await compliance.ExportPortableAsync("subject-access-001");
Console.WriteLine("Portable JSON export:");
Console.WriteLine(portableJson);
anchor

The JSON output uses camelCase property names and is formatted for readability. The structure matches the DataSubjectAccessReport record:

json
{
  "subjectId": "subject-access-001",
  "generatedAt": "2025-01-15T10:30:00+00:00",
  "entities": [
    {
      "entityType": "Customer",
      "instances": [
        {
          "fields": [
            { "fieldName": "FullName", "value": "Diana Prince", "isRedacted": false },
            { "fieldName": "Email", "value": "diana@example.com", "isRedacted": false }
          ]
        }
      ]
    }
  ]
}

Audit Events

Both export operations emit audit events:

  • DataSubjectAccessExported — Emitted by ExportAsync, includes the number of entities and types exported
  • DataSubjectPortableExported — Emitted by ExportPortableAsync

These events record that a data access request was fulfilled, supporting your GDPR Art. 30 records of processing.

Handling Shredded Data

If a data subject's key has been shredded (via crypto-shredding), the export will still work. Fields whose keys are missing will show replacement values, and IsRedacted will be true. This allows you to demonstrate that data was held but has been erased.

INFO

It is recommended to export data before processing an erasure request. This gives you a record of what was erased and allows you to confirm the erasure was complete.

See Also