Skip to content

Attributes Overview

Tayra uses attributes to declaratively mark personal data on your model classes. At runtime, the metadata cache scans your types via reflection and drives the encryption engine automatically.

Fluent API Alternative

All attributes have fluent API equivalents that can be used instead of (or alongside) attributes. The fluent API is useful when you cannot modify the model classes directly, or when you prefer centralized configuration. The expression selectors accept either a property expression (x => x.SomeProperty) or a field expression (x => x.SomeField).

AttributeFluent EquivalentBuilder Options
[DataSubjectId]e.DataSubjectId(x => x.Prop).WithGroup(), .WithPrefix()
[PersonalData]e.PersonalData(x => x.Prop).WithGroup(), .WithMaskValue(), .WithMaskAfter(), .WithMaskBefore(), .WithMaskEmailDomain(), .WithMask()
[DeepPersonalData]e.DeepPersonalData(x => x.Prop)-
[SerializedPersonalData]e.SerializedPersonalData(x => x.Prop).WithGroup(), .StoredIn()
[BlindIndex]e.BlindIndex(x => x.Prop).WithLowercase(), .WithTrim(), .WithTransform(), .StoredIn(), .WithScope(), .WithBitLength()
[CompoundBlindIndex]e.CompoundBlindIndex("name").Field(), .StoredIn(), .WithScope(), .WithBitLength()
[ArrayBlindIndex]-(attribute mirrors [BlindIndex]: IndexName, IndexPropertyName, Scope, BitLength, Transforms)

See Fluent API for complete documentation.

Attribute Summary

AttributeTargetPurpose
[PersonalData]string properties or fieldsMarks a member for AES-256-GCM encryption
[DataSubjectId]Guid or string properties or fieldsIdentifies the data owner; derives the encryption key ID
[DeepPersonalData]Class-type properties or fieldsRecurses into nested objects to encrypt their [PersonalData] members
[SerializedPersonalData]Non-string properties or fields (int, DateTime, etc.)Serializes to binary, encrypts, and stores in a companion byte[] member
[BlindIndex]string properties or fields with [PersonalData]Computes an HMAC hash for equality queries on encrypted fields
[CompoundBlindIndex]ClassesCombines multiple members into a single HMAC hash for multi-field lookups
[ArrayBlindIndex]String collection properties or fields (string[], List<string>, HashSet<string>, ...) with [PersonalData]Computes one HMAC hash per element into a same-shape companion collection for Contains / Any queries

Example

Here is a typical annotated model:

cs
public class Customer
{
    [DataSubjectId]
    public Guid Id { get; set; }

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

    [PersonalData(MaskValue = "redacted@example.com")]
    public string Email { get; set; } = "";

    /// <summary>
    /// Not annotated - stored and retrieved as plaintext.
    /// </summary>
    public string AccountType { get; set; } = "";
}
anchor

In this example:

  • Id is the data subject identifier. Tayra derives the encryption key ID from this property's value.
  • Name and Email are personal data fields that will be encrypted in-place.
  • Email specifies a custom replacement value for after crypto-shredding.
  • AccountType has no annotation and is never encrypted.

How Metadata Discovery Works

When Tayra first encounters a type (via EncryptAsync<T> or DecryptAsync<T>), the PersonalDataMetadataCache scans the type using reflection:

  1. Member scan - All public instance properties and public instance fields are inspected for attributes.
  2. Subject resolution - Members with [DataSubjectId] are recorded, including their group and prefix.
  3. Field classification - Members with [PersonalData], [DeepPersonalData], or [SerializedPersonalData] are classified by kind: Text, Deep, Serialized, TextCollection, or DeepCollection.
  4. Caching - The result is stored in a ConcurrentDictionary keyed by Type. Subsequent calls for the same type return the cached metadata with no reflection overhead.

The metadata cache is thread-safe and is registered as a singleton. The reflection cost is paid only once per type for the lifetime of the application.

Properties or Public Instance Fields

All seven attributes ([PersonalData], [DataSubjectId], [DeepPersonalData], [SerializedPersonalData], [BlindIndex], [CompoundBlindIndex], [ArrayBlindIndex]) can be applied to a property or a public instance field. Only public instance members are scanned - private and static members are ignored, exactly like the property scan rule.

Members that Tayra writes back to ([PersonalData] strings, [SerializedPersonalData] values, and their companions) must be writable: a settable property (init-only is fine) or a non-readonly, non-const field. A get-only property or a readonly/const annotated field fails fast with a clear InvalidOperationException the first time the type is encrypted or decrypted. Deep objects and collections are mutated in place and do not need a setter.

Properties are the default idiom in the examples throughout these docs, but a field works identically:

csharp
public class Customer
{
    [DataSubjectId]
    public Guid Id;                 // public instance field

    [PersonalData]
    public string Email = "";       // encrypted in place, just like a property
}

Roslyn Analyzers

The Tayra.Core package includes Roslyn analyzers that validate attribute usage at compile time:

RuleSeverityDescription
TAYRA001WarningEntity with [PersonalData] must have a [DataSubjectId] property
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 (e.g. EmailIndex)
TAYRA006Warning[BlindIndex] on a non-string property
TAYRA011Warning[BinaryEvent] type has [PersonalData] fields
TAYRA007Warning[PersonalData] member mapped into a flat-table column
TAYRA008Warning[PersonalData] member configured as a duplicated field
TAYRA009Warning[PersonalData] member on a Wolverine saga
TAYRA010Warning[ArrayBlindIndex] companion shape mismatch or missing companion

These analyzers catch common mistakes before your code runs. No separate package installation is needed.

See Also