Skip to content

PII Data Map ​

Compliance edition

This feature ships with Tayra.Compliance and requires a Compliance edition license. See Licensing for the edition comparison.

Tayra provides a built-in PII inventory service that generates a comprehensive data map of every personal data field in your application. This is your primary tool for GDPR Article 30 compliance - the Records of Processing Activities that every data controller is required to maintain. The inventory reads from Tayra's metadata caches at runtime, producing a structured report that you can hand directly to your DPO, auditor, or supervisory authority.

Why This Matters ​

GDPR Article 30 requires every data controller to maintain a written record of processing activities. In practice, most organizations maintain these records in spreadsheets that go stale within weeks of being created. Tayra's PII inventory is generated from your actual code - the same [PersonalData], [DataSubjectId], and [BlindIndex] attributes that drive encryption at runtime also drive the inventory. When you add a new PII field, the inventory updates automatically. When you remove one, it disappears from the report. There is no separate document to maintain.

This is especially valuable during audits. Instead of assembling evidence from multiple systems, you generate a single report that shows exactly which entities contain personal data, which fields are encrypted, which have blind indexes for searchable encryption, and which integrations protect them.

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 after calling AddTayra() (the AddCompliance extension lives in the Tayra.Compliance namespace). List every entity type that contains personal data:

cs
services.AddTayra(opts => opts.LicenseKey = licenseKey)
    // InMemoryKeyStore is used by default for development.
    // For production, chain a secrets manager: .UseVaultKeyStore(...)
    .AddCompliance(complianceOpts =>
    {
        complianceOpts.ApplicationName = "MyApp";
        complianceOpts.AddEntityType<Customer>();
        complianceOpts.AddEntityType<Order>();
        complianceOpts.AddEntityType<Employee>();
        complianceOpts.AddEntityType<SupportTicket>();
    });
anchor

The AddEntityType<T>() method registers a type for scanning. Only registered types appear in the inventory. This is intentional - it prevents the inventory from surfacing internal types that are not part of your processing activities.

TayraComplianceOptions ​

PropertyTypeDefaultDescription
ApplicationNamestring"Application"Name used in report headers
MethodDescription
AddEntityType<T>()Registers an entity type for PII scanning (fluent, returns TayraComplianceOptions)
AddEntityType(Type type)Registers an entity type by Type reference
AddIntegrationProvider(IPiiIntegrationProvider provider)Registers an integration provider instance for the ProtectedBy column

Generating a Report ​

Use ITayraCompliance.GenerateInventory() to get a structured PiiInventoryReport:

cs
public class ComplianceController : ControllerBase
{
    private readonly ITayraCompliance _compliance;

    public ComplianceController(ITayraCompliance compliance)
    {
        _compliance = compliance;
    }

    [HttpGet("/compliance/pii-inventory")]
    public IActionResult GetInventory()
    {
        var report = _compliance.GenerateInventory();
        return Ok(report);
    }
}
anchor

GenerateInventory() reads from PersonalDataMetadataCache and BlindIndexMetadataCache to produce the report. These caches are populated automatically when Tayra scans your entity types at startup.

Report Structure ​

PiiInventoryReport ​

The top-level report object:

PropertyTypeDescription
GeneratedAtDateTimeOffsetTimestamp when the report was generated
ApplicationNamestringApplication name from TayraComplianceOptions
EntitiesIReadOnlyList<PiiEntityEntry>All entity types containing personal data
SummaryPiiInventorySummaryAggregate statistics

PiiEntityEntry ​

Each entity type that contains personal data:

PropertyTypeDescription
EntityTypestringThe CLR type name (e.g., "Customer")
FullTypeNamestringFully qualified type name including namespace
DataSubjectsIReadOnlyList<PiiDataSubjectEntry>Data subject identifier fields on this entity
FieldsIReadOnlyList<PiiFieldEntry>Personal data fields
BlindIndexesIReadOnlyList<PiiBlindIndexEntry>Blind indexes configured on this entity
ProtectedByIReadOnlyList<string>Integration names protecting this entity (e.g., "EF Core", "Marten")
IsFullyEncryptedbooltrue when all PII fields have encryption configured

PiiFieldEntry ​

Each personal data field:

PropertyTypeDescription
PropertyNamestringThe property name on the entity
FieldKindstringThe kind of personal data: Text, Deep, Serialized
Groupstring?The data subject group this field belongs to
IsEncryptedboolWhether this field is encrypted by Tayra
HasBlindIndexboolWhether this field has a blind index for searchable encryption
Replacementstring?The mask value used when crypto-shredding

PiiDataSubjectEntry ​

Each data subject identifier on the entity:

PropertyTypeDescription
PropertyNamestringThe property name marked with [DataSubjectId]
Groupstring?The data subject group
Prefixstring?The key prefix for this data subject

PiiBlindIndexEntry ​

Each blind index configured on the entity:

PropertyTypeDescription
IndexNamestringThe name of the blind index
SourcePropertystringThe source property (or +-joined names for compound indexes)
IsCompoundboolWhether this is a compound index spanning multiple fields
TransformsIReadOnlyList<string>Transform names applied before hashing (e.g., Lowercase, Trim)

PiiInventorySummary ​

Aggregate statistics across all registered entity types:

PropertyTypeDescription
TotalEntityTypesintNumber of entity types with PII
TotalPiiFieldsintTotal personal data fields across all entities
EncryptedFieldsintNumber of fields with encryption
BlindIndexedFieldsintNumber of fields with blind indexes
FullyEncryptedEntitiesintEntities where every PII field is encrypted
PartiallyEncryptedEntitiesintEntities where some PII fields lack encryption
IntegrationCountintNumber of active integration providers

Export Formats ​

JSON Export ​

Use ExportInventoryAsJson() to get a machine-readable JSON representation:

cs
var json = _compliance.ExportInventoryAsJson();
await File.WriteAllTextAsync("pii-inventory.json", json);
anchor

The JSON output uses camelCase property names and omits null values:

json
{
  "generatedAt": "2026-03-10T14:30:00+00:00",
  "applicationName": "MyApp",
  "entities": [
    {
      "entityType": "Customer",
      "fullTypeName": "MyApp.Models.Customer",
      "dataSubjects": [
        { "propertyName": "CustomerId", "prefix": "cust-", "allowsMissing": false }
      ],
      "fields": [
        {
          "propertyName": "FullName",
          "fieldKind": "Text",
          "isEncrypted": true,
          "hasBlindIndex": true,
          "replacement": "[REDACTED]"
        },
        {
          "propertyName": "Email",
          "fieldKind": "Text",
          "isEncrypted": true,
          "hasBlindIndex": true,
          "replacement": "redacted@example.com"
        },
        {
          "propertyName": "PhoneNumber",
          "fieldKind": "Text",
          "isEncrypted": true,
          "hasBlindIndex": false,
          "replacement": "[REDACTED]"
        }
      ],
      "blindIndexes": [
        {
          "indexName": "idx_customer_email",
          "sourceProperty": "Email",
          "isCompound": false,
          "transforms": ["Lowercase", "Trim"]
        },
        {
          "indexName": "idx_customer_name",
          "sourceProperty": "FullName",
          "isCompound": false,
          "transforms": ["Lowercase"]
        }
      ],
      "protectedBy": ["EF Core"],
      "isFullyEncrypted": true
    }
  ],
  "summary": {
    "totalEntityTypes": 4,
    "totalPiiFields": 12,
    "encryptedFields": 12,
    "blindIndexedFields": 5,
    "fullyEncryptedEntities": 4,
    "partiallyEncryptedEntities": 0,
    "integrationCount": 1
  }
}

Markdown Export ​

Use ExportInventoryAsMarkdown() to generate a human-readable Markdown document suitable for Art. 30 documentation:

cs
var markdown = _compliance.ExportInventoryAsMarkdown();
await File.WriteAllTextAsync("pii-data-map.md", markdown);
anchor

The Markdown output includes a summary table and detailed per-entity sections:

markdown
# PII Data Map -- MyApp

*Generated: 2026-03-10 14:30:00 UTC*

## Summary

| Metric | Value |
|--------|-------|
| Entity types with PII | 4 |
| Total PII fields | 12 |
| Encrypted fields | 12 |
| Blind-indexed fields | 5 |
| Fully encrypted entities | 4 |
| Active integrations | 1 |

## Entity Details

### Customer

**Full type:** `MyApp.Models.Customer`
**Protected by:** EF Core
**Encryption status:** Fully encrypted

**Data Subjects:**

| Property | Group | Prefix | When missing |
|----------|-------|--------|--------------|
| `CustomerId` | -- | cust- | Refused |

**PII Fields:**

| Field | Kind | Encrypted | Blind Index | Replacement |
|-------|------|-----------|-------------|-------------|
| `FullName` | Text | Yes | Yes | [REDACTED] |
| `Email` | Text | Yes | Yes | redacted@example.com |
| `PhoneNumber` | Text | Yes | No | [REDACTED] |

Integration Providers ​

Integration packages (EF Core, Marten, Wolverine, etc.) can implement IPiiIntegrationProvider to report which entity types they protect. This information appears in the ProtectedBy field of each entity entry.

cs
public interface IPiiIntegrationProvider
{
    /// <summary>The integration name (e.g., "EF Core", "Marten", "Wolverine").</summary>
    string IntegrationName { get; }

    /// <summary>Returns the entity types protected by this integration.</summary>
    IReadOnlyList<Type> GetProtectedTypes();
}
anchor

Register your custom integration provider in the container (or pass an instance to complianceOpts.AddIntegrationProvider(...) inside AddCompliance):

cs
services.AddSingleton<IPiiIntegrationProvider, MyIntegrationProvider>();
anchor

When the inventory service generates a report, it queries all registered IPiiIntegrationProvider implementations and maps their protected types to entity entries. If an entity is protected by multiple integrations (e.g., encrypted by EF Core for storage and scrubbed by Wolverine for messaging), all integration names appear in the ProtectedBy list.

Use Cases ​

Hand to Your DPO or Auditor ​

Generate the Markdown report and include it in your compliance documentation package. The report is self-contained and requires no additional context to understand:

cs
var markdown = compliance.ExportInventoryAsMarkdown();
await File.WriteAllTextAsync(
    $"art30-data-map-{DateTimeOffset.UtcNow:yyyy-MM-dd}.md",
    markdown);
anchor

CI/CD Encryption Coverage Gate ​

Run the inventory during your build pipeline to verify that all PII fields are encrypted. Fail the build if any entity is only partially encrypted:

cs
var report = compliance.GenerateInventory();

if (report.Summary.PartiallyEncryptedEntities > 0)
{
    var partial = report.Entities
        .Where(e => !e.IsFullyEncrypted)
        .Select(e => e.EntityType);

    throw new InvalidOperationException(
        $"Partial encryption detected on: {string.Join(", ", partial)}");
}
anchor

CLI Assembly Scanning ​

Use the dotnet tayra inventory command to scan a compiled assembly without running the application. This performs reflection-based scanning of [PersonalData], [DataSubjectId], and [BlindIndex] attributes:

bash
dotnet tayra inventory --assembly bin/Release/net9.0/MyApp.dll

See the CLI Tool page for full command reference.

Automated Compliance Documentation ​

Generate the inventory on a schedule and commit it to your compliance repository:

cs
public class WeeklyComplianceReportJob
{
    private readonly ITayraCompliance _compliance;

    public WeeklyComplianceReportJob(ITayraCompliance compliance)
    {
        _compliance = compliance;
    }

    public async Task ExecuteAsync()
    {
        var json = _compliance.ExportInventoryAsJson();
        await File.WriteAllTextAsync("compliance/pii-inventory.json", json);

        var markdown = _compliance.ExportInventoryAsMarkdown();
        await File.WriteAllTextAsync("compliance/pii-data-map.md", markdown);
    }
}
anchor

See Also ​

  • Compliance Reports - Generate formatted Art. 30, Art. 15, breach, and coverage reports
  • GDPR Overview - Full GDPR article mapping
  • CLI Tool - Assembly scanning and report generation from the command line
  • Observability - OpenTelemetry metrics and distributed tracing