Skip to content

Changelog

All notable changes to Tayra are recorded here, newest first. Tayra follows Semantic Versioning: major versions may break the public API, minor versions add backward-compatible features, and patch versions are bug fixes only.

The framework major lives in the package name (Tayra.Marten8 / Tayra.Marten9, Tayra.Wolverine5 / Tayra.Wolverine6); the version below is Tayra's own suite version, shared across every package. See Installation.

2.6.0 - 2026-08-10

Upgrading from 2.5.0

See the upgrade guide. Licensing only - nothing to migrate, and every change removes a way to be locked out of your own data.

Fixed

  • Tayra.Marten9 threw MissingMethodException on every document read against Marten 9.20 and newer. Marten moved most of ISerializer onto Weasel.Storage.IStorageSerializer (which Marten.ISerializer now inherits) during the 9.x line. Source compiles against either version, so nothing warned at build or restore time, but TayraSerializer was compiled against the older assembly and its calls named members that no longer existed where it looked for them. Writes appeared to succeed - Marten's write path happens to use one of the four members that did not move - so the failure showed up on the first query.

    TayraSerializer now resolves every member it delegates to at runtime, against whichever interface the running Marten declares it on, so one package serves the whole 9.x line. It also implements WriteToParameter(DbParameter, object), a member the newer contract added and Tayra did not have, which was a hole a write could have passed through unencrypted. If a future Marten changes the contract again, Tayra now fails at startup naming the member instead of at first read.

    Reported as issue #15 against 2.5.0 on Marten 9.22.5. Tayra.Marten8 is unaffected: Marten 8's serializer contract is identical from 8.35.0 through 8.37.4.

Changed

  • Decryption is no longer gated by the license under any circumstances. The runtime licensing gate now sits on the encryption path only. DecryptAsync performs no license check of any kind, and there is no longer a code path anywhere in Tayra that can refuse a decryption for licensing reasons - not a lapsed term, not an expired trial, not a clock. Previously an expired trial key stopped decryption as well as encryption; production keys were already unconditional.

  • An expired trial key now starts the host and keeps decrypting. Startup validation now fails only when the key is absent, tampered with, unverifiable, or malformed - the cases where the key is not genuine. A genuine key always starts, however far past its term, because a process that refuses to boot cannot decrypt either. An expired trial is refused per-call on EncryptAsync instead, with an error that says decryption is unaffected. Anything you encrypted while evaluating stays readable.

    If you relied on an expired trial halting an application at startup, that no longer happens; check LicenseChecker.IsLicensed explicitly if you want that behaviour.

Added

  • LicenseChecker.IsAuthentic and LicenseValidationResult.IsAuthentic - whether the key itself is genuine (signature verified, payload well-formed), as distinct from IsLicensed, which additionally means "usable for encrypting new data". IsAuthentic is the startup gate; IsLicensed governs encryption and the edition-gated Compliance features.

  • LicenseValidationResult.Expired(license, error) - the result for a genuine key whose term has run out, carrying the parsed LicenseInfo so callers can report which key and which date.

Removed

  • LicenseChecker.EnsureCanDecrypt(), added during the 2.x licensing work and never part of a release, is gone rather than deprecated: a method that answers "may I decrypt?" is exactly the thing that must not exist. EnsureCanEncrypt() is unchanged and remains the only runtime gate.

Documentation

  • Wire Format Specification - the normative, byte-exact description of everything Tayra writes, published so that encrypted data can be recovered with nothing but your key store and any AES-256-GCM implementation: no Tayra assemblies, no license key, no involvement from Radarleaf. Covers the 0x03 and 0x04 payload layouts, the associated-data construction (the part a hand-written decryptor cannot guess), the TAYRA_M: masking prefix, key id derivation and rotation suffixes, the key store table layout, and the 0xE1 envelope format for wrapped DEKs. Decryption only; it is not a specification for producing ciphertext.

  • Reference decryptors in .NET and Python, neither of which references any Tayra package. The .NET one uses only the base class library, the Python one only the cryptography package, and both are maintained as single runnable files that decrypt the published test vectors and report. The spec page also publishes those vectors inline: known-good ciphertext with its keys and expected plaintexts, so a reimplementation in any language can be validated against the same cases. Both implementations are wired into the docs as snippets rather than copied, and the snippet plugin now scans .py sources as well as .cs.

  • WireFormatSpecTests guards the specification itself: the documented byte offsets, the associated-data string, the 0xE1 envelope layout, the published vectors, and a run of the .NET recovery script as a customer would invoke it. CI runs the Python script the same way, so both published recovery paths are enforced. A change that would invalidate the published document now fails the build naming the part that moved.

  • The wire format diagram on Encryption showed the 0x01 and 0x02 layout while the text described 0x03 as what field encryption writes, and the stated 31-byte overhead was given as 29. Both corrected.

  • LICENSE §1(a), §1(d), §1(e) and the contract templates now state that no license check runs on decryption at all, and that only an inauthentic key can fail startup.

2.5.0 - 2026-08-08

Upgrading from 2.4.0

See the upgrade guide. One thing changes, and it changes your build rather than your data.

Security

  • The analyzers were packaged so that they never ran. No TAYRA rule fired in any consumer build up to and including 2.4.0. Tayra.Analyzers shipped its assembly in the package's lib/ folder, where NuGet treats it as an ordinary library reference; analyzers are only loaded from analyzers/dotnet/cs. No package depended on it either, so the documented experience - install Tayra.Core, get the rules - delivered nothing, and neither did installing Tayra.Analyzers explicitly. Tayra.Core now carries the analyzers itself and they load correctly.

    Every compile-time guard described in the 2.4.0 notes and throughout the docs was inert. In particular, TAYRA013 could not have warned you about a non-string [PersonalData] member whose companion was missing, which is the case where the value was persisted in cleartext. On your first build after upgrading, expect diagnostics you have never seen before; each one describes a condition that already existed in your code. Read them before suppressing any.

Removed

  • The standalone Tayra.Analyzers package is no longer published. The analyzers now ship inside Tayra.Core, so there is nothing left for a separate package to deliver, and installing both would load two copies of the same assembly and report every diagnostic twice. Every rule is about Tayra attributes and is useless without Tayra.Core, so nothing needs it on its own. If you reference Tayra.Analyzers explicitly, remove the PackageReference; the rules arrive through Tayra.Core with no configuration.

2.4.0 - 2026-08-08

Superseded by 2.5.0

Install 2.5.0. Everything below shipped in 2.4.0 and is accurate, but the analyzers it describes were packaged so that they never loaded, so none of the compile-time guards worked until 2.5.0.

Upgrading from 2.3.x

See the upgrade guide for the ordered steps: what breaks the build, what changes behavior with no compile signal, and the one-way wire format change. Coming from 2.3.x you will land on 2.5.0, so read the 2.4.0 to 2.5.0 section as well.

Security

  • A missing companion member no longer persists PII as plaintext. A non-string [PersonalData] member (int, DateOnly, an enum, a record, ...) is serialized and its ciphertext written to a companion byte[]? member, because a value type cannot hold ciphertext in place. If that companion was absent or misnamed (a typo like BsnEncryped was enough), Tayra logged a warning, skipped the field, and left the value in cleartext for the caller to persist. Encryption and decryption now throw an InvalidOperationException naming the member, and analyzer TAYRA013 reports it as a compile-time error.

    If you hit TAYRA013 or this exception when upgrading, that field was never being encrypted. Treat the data already written to your database as compromised plaintext.

Added

  • Event-store encryption migration for Marten. A new ITayraMartenEventMigrationService (registered by AddTayraMartenMigrations(), in Tayra.Marten8 / Tayra.Marten9) bulk-encrypts historical events that were written as cleartext before their type was marked [PersonalData] - the counterpart of the existing document migration. EncryptExistingEventsAsync<TEvent> rewrites the immutable mt_events log in place using raw SQL (never through Marten's read pipeline, which would crypto-shred a never-encrypted event on read), encrypting personal data out-of-band, computing blind-index companions, and writing back only the data column - version, seq_id, stream_id, id and timestamp are never touched. It runs against a raw store (no UseTayra(); a Tayra-enabled store is rejected), is idempotent/resumable (already-encrypted events are skipped), is tenant-correct (each row is encrypted under its own tenant_id), and needs no projection rebuild afterward. VerifyEventEncryptionAsync<TEvent> reports any event still holding cleartext PII. v1 limitations: upcast-target event types and binary (bdata) events are counted as skipped and logged, not migrated. See Marten: event migration.

  • Bulk key prefetch for large projection rebuilds. A new IKeyStore.GetManyAsync(keyIds) retrieves many keys in one batched round trip (returning a map of found key id to bytes; absent or shredded ids are simply omitted), and ICryptoEngine.PrefetchKeysAsync(keyIds) warms the in-memory key cache from it so a subsequent per-document decrypt hits the cache instead of the store. This collapses the millions of single-row key lookups a Marten rebuild would otherwise issue - one per subject - into a handful of bulk reads. Fast backends override with a single query (= ANY(...) on PostgreSQL/Aurora, chunked IN (...) on SQLite, a dictionary scan in-memory); the envelope and tenant-aware decorators pass the batch through; stores with no native multi-get (Vault, cloud secret stores) fall back to a per-key loop. Both new members ship with default implementations, so existing custom stores and engines keep working unchanged. Prefetch is best-effort and never throws. See Marten: bulk key prefetch for large rebuilds.

  • Rolling-window prefetch pacer for very large Marten rebuilds. An opt-in overload RebuildProjectionWithPrefetchAsync<TProjection>(PrefetchPacing pacing, ...) (in Tayra.Marten8 / Tayra.Marten9) warms a bounded window of subject keys just ahead of the async daemon and evicts windows it has already passed, so the warmed working set stays bounded for rebuilds spanning tens of millions of subjects where warming everything up front is too much memory. It follows the daemon via its progress tracker and paces windowed SELECT DISTINCT subject scans against the observed seq_id. Best-effort: the rebuild is authoritative and any pacer failure is caught and logged without breaking it. v1 targets the single-shard case (conjoined multi-tenancy or single-tenant); for per-tenant-partitioned or database-per-tenant projections it logs a warning and falls back to warm-all-then-rebuild. Two new cache-only Core primitives back it: ICryptoEngine.EvictCachedKeys(keyIds) and IFieldEncrypter.EvictCachedKeysForSubjects<TEntity>(subjectIds) (also on ITayra), the inverse of the prefetch members, which remove keys from the in-memory cache without deleting them from the store (not crypto-shredding). Both Core members ship with default/no-op behavior so existing custom engines keep working. The simple warm-all overload is unchanged. See Marten: rolling-window pacer.

  • Turnkey projection prefetch for Marten rebuilds. New IServiceProvider extensions RebuildProjectionWithPrefetchAsync<TProjection>() and PrefetchKeysForProjectionAsync<TProjection>() (in Tayra.Marten8 / Tayra.Marten9) discover a projection's handled event types, scan mt_events for the distinct [DataSubjectId] values those events carry, warm the keys in chunks, and (for the first) rebuild the projection - all in one call, no hand-built key id strings. A new subject-id-based Core surface, IFieldEncrypter.PrefetchForSubjectsAsync<TEntity>(subjectIds) (also on ITayra), takes domain ids and derives the base key ids from the entity's [DataSubjectId] metadata (Prefix and Group). This is turnkey (one call), not transparent per-page batching; it is best for full rebuilds where the subject set is knowable, targets the DataEmbedded / single-tenant model, and warms base (version 0) keys only. See Marten: turnkey projection prefetch.

  • One-call multi-tenancy setup: .WithMultiTenancy(...) on the Tayra builder. A single fluent entry point (chained after .UseXxxKeyStore() and .WithXxxMasterKey(...)) picks a tenancy model and wires everything coherently: the tenant provider, the TenantAwareKeyStore decorator, envelope master-key resolution, strict mode, and integration auto-flow, with the correct decorator ordering. Two strategies: TenantStrategy.Ambient (default, request/handler-scoped, settable AsyncLocalTenantProvider or a custom ITenantProvider via the generic overload) and TenantStrategy.DataEmbedded (background-safe, master key derived from the subject-id prefix, requires envelope mode). Fail-closed by default: RequireTenant is seeded to true, and for Ambient + envelope it is propagated to the envelope resolver in the same call, fixing the footgun of setting it in two places. The lower-level AddTayraMultiTenancy() / DeriveMasterKeyIdFromSubjectPrefix() building blocks remain for advanced use. See Multi-tenancy: configure once.

  • Multi-tenancy now works in background contexts (Marten async daemon, projection rebuilds, Wolverine handlers). A new data-derived master-key resolver, opts.DeriveMasterKeyIdFromSubjectPrefix(), takes the tenant/org from the DEK key id itself (e.g. orgA from orgA:patient1), so per-tenant envelope isolation holds with no ambient tenant. The shipped AsyncLocalTenantProvider (registered via the new parameterless AddTayraMultiTenancy()) lets background code set the tenant per unit of work with BeginScope(tenantId), and opt-in strict mode (RequireTenant, on both TayraMultiTenancyOptions and EnvelopeOptions) throws TayraTenantRequiredException instead of silently sharing one master key. Breaking (EA minor): IMasterKeyIdResolver.Resolve() is now Resolve(MasterKeyContext context) - update any custom resolver. See Multi-tenancy.

  • Wolverine handlers now flow the tenant automatically. When the settable AsyncLocalTenantProvider is registered (parameterless AddTayraMultiTenancy()) alongside UseTayra(), the tenant flows from Envelope.TenantId into message-body encryption/decryption and into the handler's ambient scope for its whole duration, so Marten session work inside a handler (outbox and tenant sessions included) keys off the message tenant with no BeginScope call. No behavior change when the settable provider is not registered. See Wolverine multi-tenancy.

  • Marten startup advisory for multi-tenant async projections. When a TenantAwareKeyStore is combined with a store that has async-lifecycle projections, Tayra now logs a startup warning: the async projection daemon serializes on a background consumer with no ambient tenant, so ambient tenancy cannot reach it. The advisory points to data-embedded tenancy (DeriveMasterKeyIdFromSubjectPrefix()) and RequireTenant. See Marten multi-tenancy.

  • UseTayra() now covers Marten ancillary stores automatically. Previously it decorated only the default AddMarten() store, so a store registered via AddMartenStore<TStore> kept its own serializer and [PersonalData] on its types was silently ignored. Tayra's serializer wrapper is now registered as a global Marten configuration, so a single UseTayra() protects the default store and every ancillary store, with no per-store opt-in. See Marten integration.

  • AWS Secrets Manager keystore supports a custom endpoint and DI-supplied client. A new ServiceUrl option targets a custom endpoint (e.g. LocalStack) for dev/CI parity, and the envelope master keystore now prefers an IAmazonSecretsManager registered in DI (for example via AddAWSService<IAmazonSecretsManager>()) - matching the data keystore - so the whole key topology can run against LocalStack without a real AWS account. See AWS Secrets Manager.

  • [PersonalData] is now the single field-level PII attribute and dispatches on member type. A string (or string collection) is encrypted in place as before; any other type (int, DateOnly, decimal, Guid, enums, records, ...) is JSON-serialized and its ciphertext is stored in a companion byte[]? member automatically (named {Member}Encrypted, or set via the attribute's EncryptedFieldName / the fluent .StoredIn(...)). You no longer choose the mechanism - Tayra infers it from the type - and you do not need to migrate strongly-typed members to strings. [DeepPersonalData] is unchanged.

  • Any type System.Text.Json can serialize now works on a non-string member. Previously a hand-rolled binary serializer supported exactly eleven types and threw NotSupportedException at first encrypt for anything else, with no compile-time warning - so a plausible member like short Age or TimeSpan compiled clean and failed in production. short, byte, uint, ulong, char, TimeOnly, TimeSpan, Uri, records, classes, and collections now all work, alongside the types that already did. DateTime keeps its Kind and DateTimeOffset keeps its offset. An exotic type System.Text.Json cannot handle now fails with a clear message naming the member and pointing at a custom JsonConverter.

  • Per-field crypto-shred replacement values. [PersonalData(ReplacementValue = "-1")] writes a specific value back after shredding instead of the type default; the string is parsed into the member's type (invariant culture; enum name or number) once at metadata-build time, so an unparseable value fails fast. The fluent form is strongly typed to the member - e.PersonalData(x => x.Bsn).WithReplacement(-1) takes a value of the member's own type, with no string parsing and no invariant-culture ambiguity.

  • A single generic fluent overload. PersonalData<TProperty>(Expression<Func<T, TProperty>>) binds by inference for any member type (string, string collection, or a value type with its byte[]? companion configured via .StoredIn(x => x.FooEncrypted)).

  • Blind indexes work on any canonicalizable type. [BlindIndex] now supports int, long, DateOnly, DateTime, Guid, enums, and their nullable forms, alongside string. Strongly-typed PII is now queryable without migrating it to strings. Existing string indexes are byte-identical - strings are still hashed as themselves, so no reindex is needed for them.

  • IBlindIndexer.BuildPredicateAsync() builds the query predicate for you, so a query site never has to name the companion member or the index:

    csharp
    var match = await blindIndexer.BuildPredicateAsync((Patient x) => x.Bsn, 123456789);
    var results = await session.Query<Patient>().Where(match).Where(x => x.Active).ToListAsync();

    It returns a plain Expression<Func<T,bool>>, so it composes with other clauses and works on Marten, EF Core, and MongoDB alike. Note that a blind index is an HMAC: equality only, never ranges.

  • Analyzer TAYRA015 catches cleartext comparisons in query predicates. Comparing a [PersonalData] or [BlindIndex] member against a value inside a query predicate (==, !=, .Equals(...) in a lambda that converts to Expression<Func<T, bool>>, or the equivalent LINQ query syntax) is now a compile-time warning. At rest the member holds ciphertext, or a zeroed default for a non-string member, so the predicate translates to SQL, runs against the ciphertext, and silently matches zero rows - which reads as "no such record" rather than as a bug. The diagnostic names the fix that applies: compare the blind index via IBlindIndexer.BuildPredicateAsync, or add [BlindIndex] first if the member has none. A comparison in a lambda that converts to a plain Func<,> runs in memory over objects Tayra has already decrypted and is not reported, and neither is a comparison against null. See TAYRA015.

  • Analyzer TAYRA014 reports a compile-time error when a Masking strategy is set on a non-string [PersonalData] member. Masking is string-only (a deliberate partial-cleartext leak); use ReplacementValue for non-string members. In the fluent API the WithMask* methods only exist on a string member, so masking a non-string member does not compile at all.

Changed

  • Licensing is now a single-term model (breaking). One signed expiresAt replaces MaintenanceUntil; production decryption is perpetual, encryption is term + 60-day grace, and pre-term keys must be re-issued. See Licensing.

  • Non-string [PersonalData] payloads are now UTF-8 JSON in a v4 wire format (previously a binary encoding in v3). Both formats remain readable - the decoder is chosen by the version byte on the data - so existing ciphertext keeps decrypting after you upgrade, and a field migrates to v4 lazily the next time it is encrypted. No migration job is required.

    Upgrade is one-way

    A v4 payload cannot be read by Tayra 2.3.x or earlier. Once a node running 2.4.0 encrypts a field, a node still on 2.3.x cannot decrypt it, so rolling the library version back after writing is not supported. Upgrade the whole fleet together, and take a backup first if you need a rollback path.

    This affects serialized (non-string) members only. [PersonalData] strings and string collections are unchanged, and so are envelope-wrapped DEKs.

  • [SerializedPersonalData] and the SerializedPersonalData(...) fluent method are now [Obsolete] aliases of [PersonalData]. The distinction between the two attributes was purely mechanical (non-string PII needs a companion byte[]?), and Tayra now infers it from the member type, so the separate attribute is redundant. The alias behaves identically and existing code keeps compiling; move to [PersonalData] at your convenience. Tayra is in early access, so this lands as a minor rather than going through the deprecation window.

  • .WithMaskValue(...) is renamed to .WithReplacement(...) (the old name is an [Obsolete] alias), and the attribute's MaskValue property is renamed to ReplacementValue (the old name is an [Obsolete] alias). A replacement value is universal (every member type) and leaks nothing; it is kept distinct from Masking, which is string-only and a deliberate partial-cleartext leak.

  • Analyzer TAYRA006 retargeted. It used to report [BlindIndex] on any non-string member as unsupported, which is no longer true. It now warns when [BlindIndex] is applied to a bool or a small enum, where a deterministic index is close to useless: with only two or three distinct hashes in the column, frequency analysis recovers the plaintext immediately. [BlindIndex] on an int BSN is now correct and silent; [BlindIndex] on bool HivPositive now warns.

Removed

  • The standalone SerializedPersonalDataBuilder<T> is removed, folded into the generic PersonalDataBuilder<T, TProp>. Its WithGroup, StoredIn, and WithReplacement are now methods on the unified builder. The SerializedPersonalData(...) entry point remains as an [Obsolete] shim returning the unified builder.

  • BinaryFieldSerializer.Serialize is removed. Nothing writes the v3 binary payload any more, so the write half of the frozen encoder is gone. Tayra is in early access, so this lands as a minor rather than going through the deprecation window.

    BinaryFieldSerializer.Deserialize is deliberately kept, and is not deprecated. It is the only thing that can decode a v3 payload, and every serialized (non-string) field encrypted by 2.3.x is v3. Removing it would make that PII permanently undecryptable, so it stays indefinitely. Writing v3 ended; reading v3 has not, and no re-encrypt migration is forced on you.

Fixed

  • A key-store outage during encryption now fails closed with a clear TayraKeyStoreUnavailableException. Previously, when the key store was unreachable while resolving a key (for example a missing schema under Marten, in envelope mode), the raw store error (a PostgresException) propagated with nothing in the exception chain naming Tayra or the key store, and could surface downstream as a misleading error (e.g. Marten's 42883: operator does not exist: text = integer). Key resolution runs before any field is touched, so this exception is raised with the object unmodified - no half-encrypted or corrupt data reaches the database. The underlying store failure is preserved as the inner exception.

  • [BlindIndex] on a non-string member silently indexed nothing. The indexer read the member as a string, which is null for an int or a DateOnly, and skipped it. The companion stayed null on every row, so every query on that index matched zero rows and nothing errored. TAYRA006 was only a warning, so it compiled and shipped. Non-string members are now indexed properly (see below), and a value that cannot be canonicalized throws instead of quietly producing an unusable index.

    If you had [BlindIndex] on a non-string member, its companion column is empty. Recompute the indexes for those documents after upgrading; see Recomputing Indexes.

  • PostgreSQL key store AutoMigrate now creates the configured schema. When Schema was set to a non-default value (e.g. "tayra_keys") on a fresh database, AutoMigrate issued CREATE TABLE without first creating the schema, so every key operation failed with 3F000: schema "..." does not exist. Tayra now issues CREATE SCHEMA IF NOT EXISTS before the table. This also covers the master-key store configured via WithPostgreSqlMasterKey. The runtime role needs CREATE on the database for first-run schema creation; set AutoMigrate = false and provision the schema through your own pipeline if it cannot.

2.3.0 - 2026-06-10

Core hardening: encryption now fails closed, tampering is surfaced, and crypto-shredding stays complete after key rotation. Annotations now work on public instance fields as well as properties.

Changed (breaking)

  • Encryption fails closed. EncryptAsync now throws an InvalidOperationException when a [DataSubjectId] value is null, or when no encryption key can be resolved for a field's group. Previously the affected fields were silently persisted as plaintext with only a warning log. Decryption tolerance is unchanged: a missing (shredded) key still follows the replacement-value path.
  • The metadata API no longer exposes a PropertyInfo. The reflection model now describes annotated members through a PersonalDataMember accessor (with Name, MemberType, CanWrite, GetValue, SetValue, and Underlying) that backs both properties and fields. PersonalDataFieldInfo.Property becomes PersonalDataFieldInfo.Member, DataSubjectInfo exposes Member, and the blind-index metadata exposes SourceMember / IndexMember. Code that read .Property off the public metadata types must move to the new Member accessor.
  • Non-writable annotated members now fail with a clear error at first use instead of being silently skipped. A member Tayra writes back to (Text and Serialized kinds, plus 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 field throws an InvalidOperationException at first encrypt. Similarly, read-only string collection instances (e.g. ImmutableList<string>) now throw instead of being silently skipped.
  • The AAD decrypt path rejects legacy v1 ciphertexts unconditionally. Version 0x01 payloads predate associated-data binding, so they cannot be context-verified - a database-level attacker could splice a v1 ciphertext into a different field of the same subject. AesGcmEncryptor.DecryptWithAssociatedData / DecryptStringWithAssociatedData now throw a CryptographicException on v1 payloads; at the field-encrypter level a v1 value is left untouched and surfaces as an IntegrityCheckFailed audit event. The v1 format remains valid only for the no-AAD AesGcmEncryptor.Encrypt / Decrypt primitive pairing.

Added

  • All seven attributes now annotate properties or public instance fields.[PersonalData], [DeepPersonalData], [SerializedPersonalData], [DataSubjectId], [BlindIndex], [CompoundBlindIndex], and [ArrayBlindIndex] are recognized on public instance fields in addition to properties; field usage compiles and works. Only public instance members are scanned (private and static members are ignored), mirroring the existing property scan rule. The fluent API expression selectors now accept field expressions (x => x.SomeField) as well as property expressions.
  • Tamper detection is surfaced. A CryptographicException during decryption (tampered ciphertext or context mismatch) is now logged at Warning and emits a new TayraAuditEventType.IntegrityCheckFailed audit event; the field value is left untouched. Legacy-plaintext tolerance (FormatException) stays quiet at Debug.
  • HashSet<string> and other mutable ICollection<string> properties are now encrypted (previously only IList<string> / string[] worked; other collection types were silently skipped). A new fluent overload EntityTypeBuilder<T>.PersonalData(Expression<Func<T, IEnumerable<string>>>) configures string-collection properties directly.

Fixed

  • ShredAsync(subjectId) now deletes the subject's base key and every {subjectId}:-prefixed key - rotated versions (:vN) and group keys - so crypto-shredding remains complete after key rotation. Previously only the exact base key was deleted, leaving data encrypted under rotated keys readable. Neighboring subjects that merely share an ID prefix are unaffected.
  • [DeepPersonalData] object graphs with cycles no longer cause a StackOverflowException, and shared (diamond) references are processed exactly once instead of being double-encrypted.
  • DeepPersonalData configured on a collection property via the fluent API now encrypts the elements (previously silently skipped).
  • Eliminated a key-creation race that could encrypt data with a never-persisted key: IKeyStore.StoreAsync is now documented as first-writer-wins, and the crypto engine and blind-index key provider read the key back after storing and serialize per-key-ID creation in-process.
  • Blind-index HMAC key rotation now actually replaces the stored key (delete, store, then read-back verify); previously the first-writer-wins store made rotation a silent no-op. The HMAC key cache also gained a TTL (default 5 minutes) so rotations and deletions performed elsewhere are observed.

2.2.0 - 2026-06-10

Changed

  • Updated the integration targets to Marten 9.7.1 and Wolverine 6.6.0. The dependency ranges still cap the next framework major, so a consumer that pulls Marten 10 or Wolverine 7 fails fast at restore instead of at runtime.
  • Wolverine 6 dead-letter exception redaction now uses a supported hook. When RedactExceptionMessages is enabled, Tayra.Wolverine6 scrubs the exception text on Wolverine's IDeadLetterInterceptor (added in WolverineFx 6.6.0) instead of reaching into private exception internals. The redacted dead letter now preserves the original exception type, so you can still filter and triage dead letters by type while the message is replaced with Exception details redacted by Tayra. The behavior of the option is otherwise unchanged. Tayra.Wolverine5 keeps its existing redaction mechanism.

No public API changes.

2.1.0 - 2026-06-09

Added

  • Per-major integration packages. Pick the package that matches your framework version: Tayra.Marten8 / Tayra.Marten9 and Tayra.Wolverine5 / Tayra.Wolverine6. Both lines ship from a single trunk at the same suite version, so you are never forced onto a new framework major to stay current with Tayra. See Installation.

Fixed

  • HashiCorp Vault key store: keys are now read back correctly from Vault KV v2.
  • Wolverine 5: the integration's source generator is kept enabled so its handlers (such as the built-in GDPR erasure handler) are discovered at runtime.

2.0.0 - 2026-06-07

Tayra 2.0 moves the default integrations onto the current framework majors.

Changed (breaking)

  • The Marten integration now targets Marten 9 and the Wolverine integration targets Wolverine 6. Marten 8 / Wolverine 5 support continues through the per-major packages introduced in 2.1.0.
  • The Wolverine integration now encrypts at the message serializer rather than via handler middleware. PII is therefore ciphertext at rest in the durable outbox/inbox and in dead-letter storage (the durable outbox could previously hold cleartext). UseTayraMiddleware() is now an [Obsolete] alias for UseTayra(), and the RedactDeadLetterQueues option was removed because dead-letter bodies are encrypted automatically.

Added

  • Searchable encrypted collections via [ArrayBlindIndex], with fluent configuration.
  • New Roslyn analyzers: TAYRA007 (PII on Marten binary events), TAYRA008 (flat-table PII guard), TAYRA009 (duplicated-field PII guard), and TAYRA010 (PII on a Wolverine saga).
  • Marten: fail fast at startup if a custom serializer drops the Tayra wrapper.

Performance

  • Core encrypt/decrypt dispatch is cached as compiled delegates.

1.4.0 - 2026-05-05

Added

  • Envelope encryption: two-tier encryption with pluggable master and data key stores and pluggable master-key id resolution.
  • AWS Aurora (PostgreSQL with IAM authentication) key store.
  • Persistent, hash-chained audit trail (Marten-backed), plus signed (ECDSA P-256) and scheduled compliance reports with a pluggable archive.
  • Date-only perpetual-fallback licensing model (the major-version gate was dropped).

Changed

  • [DeepPersonalData] now recurses transitively through nested annotations.

Removed

  • The Tayra.MediatR, Tayra.MassTransit, and Tayra.NServiceBus integrations were dropped because their upstreams moved to commercial or copyleft licensing. Tayra ships no copyleft or commercial-by-default dependencies.

1.3.0 - 2026-05-01

Changed

  • Compliance features were extracted into a dedicated Tayra.Compliance package.

1.2.0 and earlier - 2026-03 to 2026-04

Initial public releases established the core library and ecosystem:

  • AES-256-GCM field-level encryption with a versioned wire format and embedded key version for rotation, plus GDPR crypto-shredding.
  • The attribute model ([PersonalData], [DataSubjectId], [DeepPersonalData], [SerializedPersonalData]) and Roslyn analyzers TAYRA001 to TAYRA006.
  • Key stores: In-Memory, SQLite, PostgreSQL, HashiCorp Vault, Azure Key Vault, AWS Parameter Store, and AWS Secrets Manager.
  • Integrations for Entity Framework Core, Marten, MongoDB, Serilog, System.Text.Json, and ASP.NET Core, plus the dotnet tayra CLI.