Roslyn Analyzers
Tayra ships with six Roslyn analyzers that catch common PII attribute misconfigurations at compile time. These run in your IDE and during dotnet build, providing immediate feedback when entity models are configured incorrectly.
Auto-Installation
The analyzers are bundled with the Tayra.Core NuGet package. When you reference Tayra.Core, the analyzers are automatically loaded by your IDE (Visual Studio, Rider, VS Code with C# Dev Kit) and the build system. No additional package installation is required.
Analyzer Rules
TAYRA001: Missing [DataSubjectId]
| Property | Value |
|---|---|
| ID | TAYRA001 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A class or struct has properties marked with [PersonalData] or [SerializedPersonalData], but no property is marked with [DataSubjectId].
Why it matters: Tayra derives encryption keys from the data subject identifier. Without a [DataSubjectId], the field encrypter cannot determine which key to use, and encryption will silently skip the entity.
Example (triggers TAYRA001):
// Warning TAYRA001: Type 'Customer' has [PersonalData] fields
// but no [DataSubjectId] property
public class Customer
{
public Guid Id { get; set; } // Missing [DataSubjectId]
[PersonalData]
public string Name { get; set; }
}Fix:
public class Customer
{
[DataSubjectId] // Added
public Guid Id { get; set; }
[PersonalData]
public string Name { get; set; }
}TAYRA002: Unused [DataSubjectId]
| Property | Value |
|---|---|
| ID | TAYRA002 |
| Severity | Info |
| Category | Tayra.Usage |
Trigger: A class or struct has a property marked with [DataSubjectId], but no properties are marked with [PersonalData] or [SerializedPersonalData].
Why it matters: A [DataSubjectId] without any PII fields to encrypt has no effect. This usually indicates a missing [PersonalData] attribute or a leftover [DataSubjectId] from a refactoring.
Example (triggers TAYRA002):
// Info TAYRA002: Type 'AuditLog' has [DataSubjectId]
// but no [PersonalData] fields
public class AuditLog
{
[DataSubjectId]
public Guid UserId { get; set; }
public string Action { get; set; } // Not marked [PersonalData]
public DateTime Timestamp { get; set; }
}Fix: Either add [PersonalData] to fields that contain personal data, or remove the unused [DataSubjectId].
TAYRA003: [DeepPersonalData] on Non-Class Type
| Property | Value |
|---|---|
| ID | TAYRA003 |
| Severity | Error |
| Category | Tayra.Usage |
Trigger: [DeepPersonalData] is applied to a property whose type is not a class or record (e.g., string, int, DateTime, a struct, or an enum).
Why it matters: [DeepPersonalData] tells Tayra to recursively process a nested object for PII fields. This only makes sense for class or record types that can contain their own [PersonalData] properties. Applying it to a primitive or value type is always a mistake.
Example (triggers TAYRA003):
public class Order
{
[DataSubjectId]
public Guid CustomerId { get; set; }
// Error TAYRA003: [DeepPersonalData] on property 'Total' is invalid.
// It must be applied to a class or record type, not 'decimal'.
[DeepPersonalData]
public decimal Total { get; set; }
}Fix: Use [PersonalData] for string fields or [SerializedPersonalData] for non-string value types. Use [DeepPersonalData] only on properties whose type is a class containing its own PII annotations:
public class Order
{
[DataSubjectId]
public Guid CustomerId { get; set; }
[DeepPersonalData]
public ShippingAddress Address { get; set; } // Class with [PersonalData] fields
}
public class ShippingAddress
{
[PersonalData]
public string Street { get; set; }
[PersonalData]
public string City { get; set; }
}TAYRA004: Multiple [DataSubjectId] Without Group
| Property | Value |
|---|---|
| ID | TAYRA004 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A class has two or more [DataSubjectId] properties, and at least one of them does not specify a Group.
Why it matters: When multiple data subject identifiers exist on the same type, Tayra needs to know which [PersonalData] fields belong to which subject. Without Group, the key derivation is ambiguous. Each [DataSubjectId] and its corresponding [PersonalData] fields must be linked via a shared Group name.
Example (triggers TAYRA004):
// Warning TAYRA004: Type 'Transfer' has multiple [DataSubjectId]
// properties without Group specified
public class Transfer
{
[DataSubjectId] // No Group
public Guid SenderId { get; set; }
[DataSubjectId] // No Group
public Guid ReceiverId { get; set; }
[PersonalData]
public string SenderName { get; set; }
[PersonalData]
public string ReceiverName { get; set; }
}Fix: Assign a Group to each [DataSubjectId] and its corresponding [PersonalData] fields:
public class Transfer
{
[DataSubjectId(Group = "sender")]
public Guid SenderId { get; set; }
[DataSubjectId(Group = "receiver")]
public Guid ReceiverId { get; set; }
[PersonalData(Group = "sender")]
public string SenderName { get; set; }
[PersonalData(Group = "receiver")]
public string ReceiverName { get; set; }
}TAYRA005: Missing Companion Property for [BlindIndex]
| Property | Value |
|---|---|
| ID | TAYRA005 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A property has [BlindIndex] but the expected companion property (default: {PropertyName}Index) does not exist on the type.
Why it matters: Blind indexes compute an HMAC hash and store it in a companion property. Without the companion, the hash has nowhere to go and queries cannot work.
Example (triggers TAYRA005):
// Warning TAYRA005: Property 'Email' has [BlindIndex] but companion
// property 'EmailIndex' was not found on type 'Customer'
public class Customer
{
[DataSubjectId]
public Guid Id { get; set; }
[PersonalData, BlindIndex]
public string Email { get; set; }
// Missing: public string? EmailIndex { get; set; }
}Fix: Add the companion property, or set IndexPropertyName to point to an existing one:
public class Customer
{
[DataSubjectId]
public Guid Id { get; set; }
[PersonalData, BlindIndex]
public string Email { get; set; }
public string? EmailIndex { get; set; } // Added
}TAYRA006: [BlindIndex] on Non-String Property
| Property | Value |
|---|---|
| ID | TAYRA006 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: [BlindIndex] is applied to a property whose type is not string.
Why it matters: Blind indexes use text normalization transforms (lowercase, trim, etc.) and HMAC-SHA256 hashing, which only work on string values.
Example (triggers TAYRA006):
// Warning TAYRA006: Property 'Age' has [BlindIndex] but its type is 'int'.
// Blind indexes only support string properties.
public class Customer
{
[DataSubjectId]
public Guid Id { get; set; }
[PersonalData, BlindIndex]
public int Age { get; set; }
}Fix: Only use [BlindIndex] on string properties. For non-string fields, convert to string before storing if you need searchability.
Suppressing Analyzers
If you need to suppress a specific analyzer rule, you have several options:
Inline Suppression
Use #pragma warning disable to suppress a rule for a specific block of code:
#pragma warning disable TAYRA001 // Intentionally no DataSubjectId
public class LegacyEntity
{
[PersonalData]
public string Name { get; set; }
}
#pragma warning restore TAYRA001SuppressMessage Attribute
Use [SuppressMessage] for a cleaner approach:
using System.Diagnostics.CodeAnalysis;
[SuppressMessage("Tayra.Usage", "TAYRA001",
Justification = "Encryption handled externally")]
public class ExternalEntity
{
[PersonalData]
public string Name { get; set; }
}.editorconfig Configuration
Suppress or change severity for an entire project using .editorconfig:
# Disable TAYRA001 entirely
dotnet_diagnostic.TAYRA001.severity = none
# Downgrade TAYRA004 to a suggestion
dotnet_diagnostic.TAYRA004.severity = suggestion
# Upgrade TAYRA002 to a warning
dotnet_diagnostic.TAYRA002.severity = warningNoWarn in .csproj
Suppress in the project file to affect the entire project:
<PropertyGroup>
<NoWarn>$(NoWarn);TAYRA002</NoWarn>
</PropertyGroup>Summary Table
| Rule ID | Severity | Description |
|---|---|---|
| TAYRA001 | Warning | Entity with [PersonalData] must have [DataSubjectId] |
| TAYRA002 | Info | [DataSubjectId] without [PersonalData] fields is unused |
| TAYRA003 | Error | [DeepPersonalData] must be on a class or record type |
| TAYRA004 | Warning | Multiple [DataSubjectId] properties require Group |
| TAYRA005 | Warning | [BlindIndex] without companion property |
| TAYRA006 | Warning | [BlindIndex] on a non-string property |
See Also
- Getting Started -- Core attribute usage
- Configuration -- Service registration
- Collection Encryption -- Encrypting list and array properties
- Blind Indexes -- Searchable encryption with HMAC blind indexes
