Roslyn Analyzers
Tayra ships with 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.
Properties and fields
The Tayra attributes apply to properties and public instance fields alike, and the analyzers flag both. Wherever a rule below talks about a "property", the same diagnostic fires for an annotated public instance field.
Analyzer Rules
TAYRA001: Missing [DataSubjectId]
| Property | Value |
|---|---|
| ID | TAYRA001 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A class or struct has properties marked with [PersonalData], but no property is marked with [DataSubjectId].
Not reported for nested payloads. A type reached through a [DeepPersonalData] member in the same project is encrypted under its holder's subject and must not have its own, so TAYRA001 skips it. That covers the member's type, the element type of a collection member, types derived from either, and generic payloads. A payload declared in a different project from all of its holders still warns there, because the analyzer cannot see the holder; suppress it on that type (see [DeepPersonalData]).
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 EncryptAsync fails closed with an InvalidOperationException at runtime. The analyzer catches this at compile time instead.
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 members are marked with [PersonalData], [SerializedPersonalData], or [DeepPersonalData]. A [DeepPersonalData] member counts, because its nested personal data is encrypted under this type's subject.
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 the member (a string encrypts in place; a non-string value type serializes into a companion byte[]?). Use [DeepPersonalData] only on members 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; } = new(); // 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 does not exist on the type. The companion is IndexPropertyName if set, otherwise the member named by IndexName if the type has one, otherwise {PropertyName}Index (see Index name and companion).
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 a Low-Cardinality Member
| Property | Value |
|---|---|
| ID | TAYRA006 |
| Severity | Warning |
| Category | Tayra.Security |
Trigger: [BlindIndex] is applied to a bool, or to an enum with 8 or fewer members.
Why it matters: A blind index maps each distinct plaintext to one fixed hash. That is safe for a high-entropy value like an email address or a national ID, where an attacker who can read the column learns very little. Over a bool there are exactly two distinct hashes, so an attacker counts how often each appears and labels them from known population distributions. The "encrypted" field is then a plaintext column with extra steps.
Example (triggers TAYRA006):
// Warning TAYRA006: Member 'HivPositive' has [BlindIndex] but its type 'bool'
// has very few possible values.
public class Patient
{
[DataSubjectId]
public Guid Id { get; set; }
[PersonalData, BlindIndex]
public bool HivPositive { get; set; }
public byte[]? HivPositiveEncrypted { get; set; }
public string? HivPositiveIndex { get; set; }
}Fix: Filter on a non-PII discriminator instead, or accept that the member is effectively public and do not encrypt it at all. See Frequency Analysis for the truncation lever, which trades false positives for reduced leakage.
This rule changed in 2.4.0
It previously reported [BlindIndex] on any non-string member as unsupported. Blind indexes now work on any type Tayra can canonicalize (int, DateOnly, Guid, enums, ...), so the type gate is gone and the rule was retargeted at the hazard that replaced it. A [BlindIndex] on an int BSN is now correct and silent.
TAYRA007: [PersonalData] Member Mapped Into a Flat-Table Column
| Property | Value |
|---|---|
| ID | TAYRA007 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A [PersonalData] member is mapped into a Marten FlatTableProjection column via StatementMap<T>.Map, Increment, or Decrement.
Why it matters: Flat-table columns are written via raw SQL and never pass through the Tayra serializer, so the value is stored in cleartext. Unlike binary events, flat tables have no encryption seam - this cannot be fixed at runtime, only avoided by design.
Example (triggers TAYRA007):
flat.Project<CustomerRegistered>(map =>
{
map.Map(x => x.CustomerId);
// Warning TAYRA007: 'Email' is [PersonalData] but is mapped into a
// flat-table column, which is written in cleartext.
map.Map(x => x.Email);
});Fix: Map a blind-index companion instead of the plaintext member, so the column holds a one-way HMAC for equality search rather than the value:
flat.Project<CustomerRegistered>(map =>
{
map.Map(x => x.CustomerId);
map.Map(x => x.EmailIndex); // HMAC companion, not x.Email
});Keep the actual personal data in the encrypted JSONB document projection and look it up by id when you need the value. See Marten integration → Flat-table projections.
TAYRA008: [PersonalData] Member Configured as a Duplicated Field
| Property | Value |
|---|---|
| ID | TAYRA008 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A [PersonalData] member is configured as a Marten duplicated field - via the [DuplicateField] attribute or a Schema.For<T>().Duplicate(x => x.Member) call.
Why it matters: Marten populates a duplicated column by reading the value off the .NET object, outside the Tayra serializer. The serializer restores the object to plaintext once the JSON is written, so the column would hold the personal data in cleartext. The analyzer is a warning, but the runtime is not lenient: UseTayra()'s startup guard throws for a duplicated personal-data member, and the serializer throws on the first write of a type that duplicates one by attribute.
Example (triggers TAYRA008):
public class Customer
{
[DataSubjectId] public string Id { get; set; }
// Warning TAYRA008: 'Email' is [PersonalData] but is configured as a
// Marten duplicated field.
[PersonalData, DuplicateField]
public string Email { get; set; }
}
// also flagged:
opts.Schema.For<Customer>().Duplicate(x => x.Email);Fix: Duplicate the blind-index companion instead - it is a deterministic HMAC that is safe to store and actually queryable:
opts.Schema.For<Customer>().Duplicate(x => x.EmailIndex); // companion, not x.EmailFor an [ArrayBlindIndex] companion the same safety rule applies (duplicate the hash companion, never the [PersonalData] source), but a duplicated array column does not speed up Contains lookups in Marten - prefer the JSONB GIN index. See Array blind indexes - duplicated fields.
See Marten integration -> Duplicated fields.
TAYRA009: [PersonalData] Member on a Wolverine Saga
| Property | Value |
|---|---|
| ID | TAYRA009 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A type deriving from Wolverine.Saga has a [PersonalData] or [DeepPersonalData] member.
Why it matters: Wolverine persists and correlates saga state through its own saga storage - the saga's identity is the storage key and the state is written outside the Tayra serializer. Tayra's Wolverine message serializer only encrypts message bodies, not saga state, so personal data on a saga (including any PII used as the saga identity) is stored in cleartext. Like flat-table columns, saga storage has no Tayra encryption seam.
Example (triggers TAYRA009):
public class AccountReviewSaga : Wolverine.Saga
{
public string Id { get; set; }
// Warning TAYRA009: 'AccountReviewSaga' derives from Wolverine.Saga and
// has [PersonalData] members. Saga storage writes this in cleartext.
[PersonalData]
public string ApplicantEmail { get; set; }
}Fix: Keep only an opaque correlation id and non-PII status on the saga; carry the personal data in the encrypted messages the saga consumes and produces. Tayra's message serializer decrypts inbound PII before saga handlers run and re-encrypts it on outbound messages.
public class AccountReviewSaga : Saga
{
public string Id { get; set; } = ""; // opaque review id
public string Status { get; set; } = "Pending"; // non-PII state
// PII arrives decrypted in the message and is re-encrypted on the way out,
// so it never lands in saga storage.
public AccountReviewCompleted Handle(SubmitReviewDecision command)
{
Status = command.Approved ? "Approved" : "Rejected";
MarkCompleted();
return new AccountReviewCompleted
{
ApplicantId = command.ApplicantId,
ReviewId = Id,
Approved = command.Approved,
ApplicantEmail = command.ApplicantEmail,
};
}
}See Wolverine integration → What Tayra does not protect.
TAYRA010: [ArrayBlindIndex] Companion Shape Mismatch
| Property | Value |
|---|---|
| ID | TAYRA010 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: One of the following is true for a property annotated with [ArrayBlindIndex]:
- The source property type is not a supported string collection (allowed:
string[],List<string>,IList<string>,IReadOnlyList<string>,HashSet<string>, and their nullable-element variants).IEnumerable<string>and non-string element types are rejected. - The companion property has a different collection kind than the source (for example
string[]source with aList<string>companion). - The companion property has a different element nullability than the source (for example
string[]source with astring?[]companion, or vice versa). - The companion property is missing entirely.
- Both
[BlindIndex]and[ArrayBlindIndex]are applied to the same property.
Why it matters: Array blind indexes write one HMAC per element into a companion collection of the same shape and length as the source. If the companion kind or element nullability does not match, the indexer cannot align hashes to elements, and the runtime guard throws. Catching it at compile time keeps you from shipping a broken model.
Example (triggers TAYRA010):
public class User
{
[DataSubjectId]
public Guid Id { get; set; }
// Warning TAYRA010: 'Emails' has [ArrayBlindIndex] but the companion
// 'EmailsIndex' is List<string> while the source is string[].
// The companion must be string[] to match the source shape.
[PersonalData, ArrayBlindIndex]
public string[] Emails { get; set; } = [];
public List<string> EmailsIndex { get; set; } = []; // wrong kind
}Fix: Declare the companion with the same collection kind and element nullability as the source:
public class User
{
[DataSubjectId]
public Guid Id { get; set; }
[PersonalData, ArrayBlindIndex]
public string[] Emails { get; set; } = [];
public string[] EmailsIndex { get; set; } = []; // matches source shape
}See Array Blind Indexes for the full shape-rules table.
TAYRA011: [BinaryEvent] Type Has [PersonalData] Fields Tayra.Marten9
| Property | Value |
|---|---|
| ID | TAYRA011 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A type marked with Marten's [BinaryEvent] also has [PersonalData] fields.
Why it matters: Marten's binary event path serializes to mt_events.bdata through a separate IEventBinarySerializer, bypassing the Tayra document serializer. Unless that binary serializer is wrapped with TayraBinaryEventSerializer, the personal data is stored in cleartext.
Example (triggers TAYRA011):
// Warning TAYRA011: Type 'CustomerRegistered' is a [BinaryEvent] with
// [PersonalData] fields. Marten's binary serialization bypasses the Tayra
// serializer; wrap the binary serializer with TayraBinaryEventSerializer.
[BinaryEvent]
public class CustomerRegistered
{
[DataSubjectId]
public string CustomerId { get; set; }
[PersonalData]
public string Name { get; set; }
}Fix: Wrap the binary serializer with TayraBinaryEventSerializer (set opts.Events.DefaultBinarySerializer before UseTayra() for automatic wrapping, or pass it to opts.Events.UseBinarySerializer<T>()). See Marten integration → Binary events. If the event genuinely holds no personal data, remove the [PersonalData] annotation.
TAYRA013: Missing Companion Member for a Non-String [PersonalData]
| Property | Value |
|---|---|
| ID | TAYRA013 |
| Severity | Error |
| Category | Tayra.Usage |
Trigger: A non-string member has [PersonalData] (for example an int, DateOnly, Guid, or an enum) but the companion member that holds the ciphertext (default: {MemberName}Encrypted, or the name given by EncryptedFieldName) does not exist on the type.
Why it matters: A value type cannot hold ciphertext in place - an int has nowhere to put a Base64 string - so [PersonalData] on a non-string member writes the ciphertext to a companion byte[]? member instead. Without the companion there is nowhere for it to go. Before 2.4.0 Tayra logged a warning and skipped the field, which silently persisted the value as plaintext. It now fails closed at runtime, and this analyzer catches it at compile time.
Example (triggers TAYRA013):
public class Patient
{
[DataSubjectId]
public string PatientId { get; set; }
// Error TAYRA013: Member 'Bsn' has [PersonalData] but companion
// member 'BsnEncrypted' was not found on type 'Patient'.
[PersonalData]
public int Bsn { get; set; }
}A typo is enough to trigger it, which is the point - BsnEncryped is not BsnEncrypted, and before this rule that typo shipped PII in the clear.
Fix: Add the companion, or point EncryptedFieldName at an existing member:
public class Patient
{
[DataSubjectId]
public string PatientId { get; set; } = "";
[PersonalData]
public int Bsn { get; set; }
public byte[]? BsnEncrypted { get; set; } // Added
}TAYRA014: Masking on a Non-String [PersonalData] Member
| Property | Value |
|---|---|
| ID | TAYRA014 |
| Severity | Error |
| Category | Tayra.Usage |
Trigger: [PersonalData] sets a Masking strategy (MaskAfter, MaskBefore, MaskEmailDomain, ...) on a member that is not a string.
Why it matters: Masking retains a partial cleartext view (e.g. Ja******, jane@***.***) that survives crypto-shredding. It is only meaningful over a string, and it is a deliberate partial-cleartext leak. There is no sensible masked form of an int or a DateOnly, so masking a non-string member is always a mistake. At runtime Tayra throws an InvalidOperationException at metadata-build time; this analyzer surfaces the same problem at compile time. Use ReplacementValue for non-string members - it leaks nothing.
Example (triggers TAYRA014):
public class Patient
{
[DataSubjectId]
public string PatientId { get; set; }
// Error TAYRA014: Member 'Bsn' sets Masking but its type 'int' is not a string.
// Masking is string-only; use ReplacementValue instead.
[PersonalData(Masking = MaskingStrategies.MaskAfter, MaskingParameter = 2)]
public int Bsn { get; set; }
public byte[]? BsnEncrypted { get; set; }
}Fix: Drop Masking and use ReplacementValue for the value written back after shredding:
[PersonalData(ReplacementValue = "-1")]
public int Bsn { get; set; }
public byte[]? BsnEncrypted { get; set; }In the fluent API this is enforced at compile time: the WithMask* methods only exist on a string member, so masking a non-string member does not compile at all.
TAYRA015: Encrypted Member Compared as Cleartext in a Query
| Property | Value |
|---|---|
| ID | TAYRA015 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A [PersonalData] or [BlindIndex] member is compared (==, !=, .Equals(...)) against a value inside a query predicate - a lambda that converts to Expression<Func<T, bool>>, or a LINQ query-syntax clause bound to a method taking one.
Why it matters: This is the failure mode with no symptom. At rest the member holds non-deterministic ciphertext (or, for a non-string member, nothing at all or a zeroed default, with the real value in its companion), so the predicate is translated to SQL and run against what is stored. Nothing throws. The query returns zero rows, every time, which reads as "no such patient" rather than as a bug, and a != filter matches everything instead. Comparing against the default value can match every row.
Example (triggers TAYRA015):
// Warning TAYRA015: Member 'Bsn' is encrypted at rest, so this query comparison
// runs against the stored ciphertext and silently matches zero rows.
var patient = await session.Query<Patient>()
.Where(x => x.Bsn == 123456789)
.FirstOrDefaultAsync();Fix: Compare the deterministic blind-index companion. BuildPredicateAsync builds the predicate so the query site never names the companion member or the index:
var match = await blindIndexer.BuildPredicateAsync((Patient x) => x.Bsn, 123456789);
var patient = await session.Query<Patient>()
.Where(match)
.FirstOrDefaultAsync();If the member has no blind index yet, add [BlindIndex] and its companion first - see Blind Indexes. The diagnostic message tells you which of the two fixes applies.
What it does not report: a comparison in a lambda that converts to a plain Func<,> runs in memory over materialized objects, which Tayra has already decrypted, so it is correct and is deliberately left alone:
// No diagnostic: these objects are decrypted, this comparison is fine.
var matches = patients.Where(x => x.Bsn == 123456789).ToList();Comparisons against null are also not reported: checking whether a field is populated reads correctly against ciphertext.
Equality only
A blind index is an HMAC, so it answers equality and nothing else. Contains, StartsWith and range comparisons on an encrypted member cannot be rewritten and are not reported by this rule. See Blind Index Security.
TAYRA016: More Than One [BlindIndexTenant] Member
| Property | Value |
|---|---|
| ID | TAYRA016 |
| Severity | Error |
| Category | Tayra.Usage |
Trigger: A type has [BlindIndexTenant] on more than one property or field, counting members it inherits from its base types.
Why it matters: A type's tenant-partitioned blind indexes are keyed by a single tenant, so only one member can name it. At runtime Tayra throws an InvalidOperationException the first time it builds the type's metadata, which is the type's first write; this analyzer surfaces the same problem at compile time.
Example (triggers TAYRA016):
// Error TAYRA016: Type 'Patient' has more than one [BlindIndexTenant] member
// (PracticeCode, Region). Its blind indexes are keyed by one tenant.
public class Patient
{
[DataSubjectId]
public string PatientId { get; set; }
[BlindIndexTenant]
public string PracticeCode { get; set; }
[BlindIndexTenant]
public string Region { get; set; }
}Fix: Keep the attribute on the member that identifies the tenant:
public class Patient
{
[DataSubjectId]
public string PatientId { get; set; } = "";
[BlindIndexTenant]
public string PracticeCode { get; set; } = "";
// Not a tenant: an ordinary member.
public string Region { get; set; } = "";
[PersonalData, BlindIndex]
public string Surname { get; set; } = "";
public string? SurnameIndex { get; set; }
}The rule sees attributes only. A tenant member named in the fluent API (BlindIndexTenant(x => x.Member)) replaces the attribute and is not checked here.
TAYRA017: [BlindIndexTenant] on an Unsupported Member Type
| Property | Value |
|---|---|
| ID | TAYRA017 |
| Severity | Error |
| Category | Tayra.Usage |
Trigger: [BlindIndexTenant] is on a member that is not a string, a Guid, or an integer type (int, long, short, byte and their unsigned forms), or a nullable of one.
Why it matters: The tenant becomes part of the id of the tenant's HMAC key, so it has to format the same way for as long as the data exists. An enum ties the key id to the name of an enum member, a strongly typed id formats as TenantId { Value = ... }, and a class without its own ToString() formats as its type name, which would key every record of every tenant under one "tenant". At runtime Tayra throws an InvalidOperationException the first time it builds the type's metadata; this analyzer surfaces the same problem at compile time.
Example (triggers TAYRA017):
public class Patient
{
[DataSubjectId]
public string PatientId { get; set; }
// Error TAYRA017: Member 'Region' is a [BlindIndexTenant] but its type 'Region'
// is not a string, a Guid, or an integer type.
[BlindIndexTenant]
public Region Region { get; set; }
}Fix: Remove the attribute and return the tenant as a string you control from BlindIndexTenantResolver (t.BlindIndexTenantResolver = TenantOf):
public class Patient
{
[DataSubjectId]
public string PatientId { get; set; } = "";
// No [BlindIndexTenant]: the resolver below turns the enum into the tenant string.
public Region Region { get; set; }
[PersonalData, BlindIndex]
public string Surname { get; set; } = "";
public string? SurnameIndex { get; set; }
}
public static string? TenantOf(object record) => record switch
{
Patient { Region: Region.North } => "north",
Patient { Region: Region.South } => "south",
_ => null,
};TAYRA018: Blind Index Name and Companion Default From One Another
| Property | Value |
|---|---|
| ID | TAYRA018 |
| Severity | Warning |
| Category | Tayra.Usage |
Trigger: A blind index sets only one of IndexName and IndexPropertyName, in a way that will mean something else in Tayra 3.0:
[BlindIndex]withIndexNamebut noIndexPropertyName, where the type has a member with that name.[BlindIndex]or[ArrayBlindIndex]withIndexPropertyNamebut noIndexName.
It is not reported when the value set is the conventional {PropertyName}Index, when IndexName names no member (the companion is {PropertyName}Index already), or for [ArrayBlindIndex] with only IndexName: those mean the same before and after.
Why it matters: Today the two default from one another: IndexName alone also chooses the companion of a [BlindIndex] when a member of that name exists, and IndexPropertyName alone also names the index. From Tayra 3.0 they are independent and each defaults to {PropertyName}Index. A model that relies on the current default would then store its hash in a different member, or be looked up under a different index name, and its queries would match nothing, with no error. Naming both now makes the model mean the same in 2.x and in 3.0.
Example (triggers TAYRA018):
public class Customer
{
[DataSubjectId]
public Guid Id { get; set; }
// Warning TAYRA018: Member 'Email' sets IndexName but not IndexPropertyName, so its
// companion is 'Login' today and will be 'EmailIndex' from Tayra 3.0.
// Set IndexPropertyName = "Login" to keep the current behaviour.
[PersonalData, BlindIndex(IndexName = "Login")]
public string Email { get; set; }
public string? Login { get; set; }
}Fix: Set the other property to the value the message names:
public class Customer
{
[DataSubjectId]
public Guid Id { get; set; }
// Both named: the index is "Login" and the hash is stored in Login, in 2.x and in 3.0.
[PersonalData, BlindIndex(IndexName = "Login", IndexPropertyName = "Login")]
public string Email { get; set; } = "";
public string? Login { get; set; }
}The rule sees attributes only. For the fluent API, a BlindIndex(...) whose companion is the member named by WithIndexName(...) logs a warning at startup instead; add .StoredIn(...) to it.
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] (from System.Diagnostics.CodeAnalysis) for a cleaner approach:
[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] (nested [DeepPersonalData] payloads excepted) |
| 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 low-cardinality member (bool, small enum) |
| TAYRA007 | Warning | [PersonalData] member mapped into a flat-table column |
| TAYRA008 | Warning | [PersonalData] member configured as a duplicated field |
| TAYRA009 | Warning | [PersonalData] member on a Wolverine saga |
| TAYRA010 | Warning | [ArrayBlindIndex] companion shape mismatch or missing companion |
| TAYRA011 | Warning | [BinaryEvent] type has [PersonalData] fields |
| TAYRA013 | Error | Non-string [PersonalData] member has no companion byte[] to hold the ciphertext |
| TAYRA014 | Error | Masking set on a non-string [PersonalData] member (masking is string-only) |
| TAYRA015 | Warning | Encrypted member compared as cleartext in a query predicate (silently matches zero rows) |
| TAYRA016 | Error | More than one [BlindIndexTenant] member on a type |
| TAYRA017 | Error | [BlindIndexTenant] on a member that is not a string, a Guid, or an integer type |
| TAYRA018 | Warning | Blind index sets only one of IndexName and IndexPropertyName, which will mean something else in Tayra 3.0 |
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
- Array Blind Indexes - Collection blind indexes validated by TAYRA010
