PII Data Map
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.
dotnet add package Tayra.ComplianceRegister the compliance services after calling AddTayra(). List every entity type that contains personal data:
using Tayra.Compliance;
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>();
});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.
PiiInventoryOptions
| Property | Type | Default | Description |
|---|---|---|---|
ApplicationName | string | "Application" | Name used in report headers |
| Method | Description |
|---|---|
AddEntityType<T>() | Registers an entity type for PII scanning (fluent, returns PiiInventoryOptions) |
AddEntityType(Type type) | Registers an entity type by Type reference |
Generating a Report
Use ITayraCompliance.GenerateInventory() to get a structured PiiInventoryReport:
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);
}
}The GenerateReport() method 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:
| Property | Type | Description |
|---|---|---|
GeneratedAt | DateTimeOffset | Timestamp when the report was generated |
ApplicationName | string | Application name from PiiInventoryOptions |
Entities | IReadOnlyList<PiiEntityEntry> | All entity types containing personal data |
Summary | PiiInventorySummary | Aggregate statistics |
PiiEntityEntry
Each entity type that contains personal data:
| Property | Type | Description |
|---|---|---|
EntityType | string | The CLR type name (e.g., "Customer") |
FullTypeName | string | Fully qualified type name including namespace |
DataSubjects | IReadOnlyList<PiiDataSubjectEntry> | Data subject identifier fields on this entity |
Fields | IReadOnlyList<PiiFieldEntry> | Personal data fields |
BlindIndexes | IReadOnlyList<PiiBlindIndexEntry> | Blind indexes configured on this entity |
ProtectedBy | IReadOnlyList<string> | Integration names protecting this entity (e.g., "EF Core", "Marten") |
IsFullyEncrypted | bool | true when all PII fields have encryption configured |
PiiFieldEntry
Each personal data field:
| Property | Type | Description |
|---|---|---|
PropertyName | string | The property name on the entity |
FieldKind | string | The kind of personal data: Text, Deep, Serialized |
Group | string? | The data subject group this field belongs to |
IsEncrypted | bool | Whether this field is encrypted by Tayra |
HasBlindIndex | bool | Whether this field has a blind index for searchable encryption |
Replacement | string? | The mask value used when crypto-shredding |
PiiDataSubjectEntry
Each data subject identifier on the entity:
| Property | Type | Description |
|---|---|---|
PropertyName | string | The property name marked with [DataSubjectId] |
Group | string? | The data subject group |
Prefix | string? | The key prefix for this data subject |
PiiBlindIndexEntry
Each blind index configured on the entity:
| Property | Type | Description |
|---|---|---|
IndexName | string | The name of the blind index |
SourceProperty | string | The source property (or +-joined names for compound indexes) |
IsCompound | bool | Whether this is a compound index spanning multiple fields |
Transforms | IReadOnlyList<string> | Transform names applied before hashing (e.g., Lowercase, Trim) |
PiiInventorySummary
Aggregate statistics across all registered entity types:
| Property | Type | Description |
|---|---|---|
TotalEntityTypes | int | Number of entity types with PII |
TotalPiiFields | int | Total personal data fields across all entities |
EncryptedFields | int | Number of fields with encryption |
BlindIndexedFields | int | Number of fields with blind indexes |
FullyEncryptedEntities | int | Entities where every PII field is encrypted |
PartiallyEncryptedEntities | int | Entities where some PII fields lack encryption |
IntegrationCount | int | Number of active integration providers |
Export Formats
JSON Export
Use ExportAsJson() to get a machine-readable JSON representation:
var json = _compliance.ExportInventoryAsJson();
await File.WriteAllTextAsync("pii-inventory.json", json);The JSON output uses camelCase property names and omits null values:
{
"generatedAt": "2026-03-10T14:30:00+00:00",
"applicationName": "MyApp",
"entities": [
{
"entityType": "Customer",
"fullTypeName": "MyApp.Models.Customer",
"dataSubjects": [
{ "propertyName": "CustomerId", "prefix": "cust-" }
],
"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 ExportAsMarkdown() to generate a human-readable Markdown document suitable for Art. 30 documentation:
var markdown = _compliance.ExportInventoryAsMarkdown();
await File.WriteAllTextAsync("pii-data-map.md", markdown);The Markdown output includes a summary table and detailed per-entity sections:
# 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 |
|----------|-------|--------|
| `CustomerId` | -- | cust- |
**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, MassTransit, etc.) can implement IPiiIntegrationProvider to report which entity types they protect. This information appears in the ProtectedBy field of each entity entry.
public interface IPiiIntegrationProvider
{
/// The integration name (e.g., "EF Core", "Marten", "MassTransit").
string IntegrationName { get; }
/// Returns the entity types protected by this integration.
IReadOnlyList<Type> GetProtectedTypes();
}Register your custom integration provider:
services.AddSingleton<IPiiIntegrationProvider, MyIntegrationProvider>();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 MassTransit 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:
var markdown = compliance.ExportInventoryAsMarkdown();
await File.WriteAllTextAsync(
$"art30-data-map-{DateTimeOffset.UtcNow:yyyy-MM-dd}.md",
markdown);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:
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)}");
}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:
dotnet tayra inventory --assembly bin/Release/net9.0/MyApp.dllSee the CLI Tool page for full command reference.
Automated Compliance Documentation
Generate the inventory on a schedule and commit it to your compliance repository:
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);
}
}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
