Skip to content

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]

PropertyValue
IDTAYRA001
SeverityWarning
CategoryTayra.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):

csharp
// 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:

csharp
public class Customer
{
    [DataSubjectId]                       // Added
    public Guid Id { get; set; }

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

TAYRA002: Unused [DataSubjectId]

PropertyValue
IDTAYRA002
SeverityInfo
CategoryTayra.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):

csharp
// 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

PropertyValue
IDTAYRA003
SeverityError
CategoryTayra.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):

csharp
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:

csharp
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

PropertyValue
IDTAYRA004
SeverityWarning
CategoryTayra.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):

csharp
// 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:

csharp
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]

PropertyValue
IDTAYRA005
SeverityWarning
CategoryTayra.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):

csharp
// 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:

csharp
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

PropertyValue
IDTAYRA006
SeverityWarning
CategoryTayra.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):

csharp
// 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:

csharp
#pragma warning disable TAYRA001 // Intentionally no DataSubjectId
public class LegacyEntity
{
    [PersonalData]
    public string Name { get; set; }
}
#pragma warning restore TAYRA001

SuppressMessage Attribute

Use [SuppressMessage] for a cleaner approach:

csharp
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:

ini
# 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 = warning

NoWarn in .csproj

Suppress in the project file to affect the entire project:

xml
<PropertyGroup>
  <NoWarn>$(NoWarn);TAYRA002</NoWarn>
</PropertyGroup>

Summary Table

Rule IDSeverityDescription
TAYRA001WarningEntity with [PersonalData] must have [DataSubjectId]
TAYRA002Info[DataSubjectId] without [PersonalData] fields is unused
TAYRA003Error[DeepPersonalData] must be on a class or record type
TAYRA004WarningMultiple [DataSubjectId] properties require Group
TAYRA005Warning[BlindIndex] without companion property
TAYRA006Warning[BlindIndex] on a non-string property

See Also