Entity Framework Core
Tayra integrates with Entity Framework Core through interceptors that transparently encrypt and decrypt [PersonalData] fields during SaveChanges and entity materialization. No changes to your queries, migrations, or DbContext configuration are needed beyond initial setup.
Install
dotnet add package Tayra.EFCoreInstall-Package Tayra.EFCoreSetup
There are two ways to register Tayra with EF Core: through dependency injection or directly on the DbContextOptionsBuilder.
DI-Based Setup (Recommended)
Register Tayra core services, a key store, and the EF Core interceptors in your DI container. The interceptors are resolved automatically when the DbContext is created.
var services = new ServiceCollection();
// Register Tayra core services and an in-memory key store
services.AddTayra(opts => opts.LicenseKey = licenseKey);
// Register Tayra EF Core interceptors via DI
services.AddTayraEFCore();
// Register the DbContext — interceptors are resolved from DI automatically
services.AddDbContext<AppDbContext>(opts =>
opts.UseInMemoryDatabase("TayraSample"));
var provider = services.BuildServiceProvider();Builder-Based Setup
If you prefer to pass ITayra directly, call UseTayra() on the DbContextOptionsBuilder:
// Alternative: pass the Tayra instance directly to UseTayra()
var tayra = provider.GetRequiredService<ITayra>();
var builderServices = new ServiceCollection();
builderServices.AddDbContext<AppDbContext>(opts =>
opts.UseInMemoryDatabase("TayraSampleBuilder")
.UseTayra(tayra, configure: efOpts =>
{
efOpts.EncryptOnSave = true;
efOpts.DecryptOnLoad = true;
}));When to Use Builder vs DI
Use the DI-based setup when you have a standard ASP.NET Core or hosted service application. Use the builder-based setup when you need fine-grained control or are configuring the DbContext outside of DI (e.g., in a migration tool or console app).
Options
// TayraEFCoreOptions — controls automatic encryption/decryption behavior
//
// Property Type Default Description
// ────────────── ────── ──────── ─────────────────────────────────────────
// EncryptOnSave bool true Encrypt [PersonalData] fields before SaveChanges
// DecryptOnLoad bool true Decrypt [PersonalData] fields after materialization
services.AddTayraEFCore(opts =>
{
opts.EncryptOnSave = true; // Default: encrypt PII on SaveChanges
opts.DecryptOnLoad = true; // Default: decrypt PII on query materialization
});| Property | Type | Default | Description |
|---|---|---|---|
EncryptOnSave | bool | true | Encrypt [PersonalData] fields before SaveChanges |
DecryptOnLoad | bool | true | Decrypt [PersonalData] fields after entity materialization |
Define Your Model
Annotate entity properties with [DataSubjectId] and [PersonalData] to enable automatic encryption:
public class CustomerEntity
{
public int Id { get; set; }
[DataSubjectId]
public string CustomerId { get; set; } = "";
[PersonalData]
public string Name { get; set; } = "";
[PersonalData(MaskValue = "redacted@example.com")]
public string Email { get; set; } = "";
/// <summary>
/// Not annotated — never encrypted by Tayra.
/// </summary>
public string AccountType { get; set; } = "";
}[DataSubjectId]onCustomerIdtells Tayra to derive the encryption key from this value.[PersonalData]onNameandEmailmarks them for encryption.- The
MaskValueparameter onEmailspecifies the value returned after crypto-shredding. AccountTypehas no attribute and is never touched by Tayra.
DbContext
Your DbContext does not require any special configuration for Tayra. Define your DbSet<> properties and model configuration as usual:
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<CustomerEntity> Customers => Set<CustomerEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CustomerEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.CustomerId).IsRequired();
entity.Property(e => e.Name).IsRequired();
entity.Property(e => e.Email).IsRequired();
entity.Property(e => e.AccountType).IsRequired();
});
// In your DbContext.OnModelCreating, add a database index on the companion column
// so blind-index queries are efficient (avoids full table scans):
modelBuilder.Entity<ProtectedCustomer>()
.HasIndex(c => c.EmailHash);
}
}How It Works
Tayra uses two EF Core interceptors to provide transparent encryption:
TayraSaveChangesInterceptor-- Hooks intoSavingChanges. Before entities are persisted, it scans the change tracker for entities with[PersonalData]fields and encrypts them. AfterSaveChangescompletes, it decrypts the in-memory entities back to cleartext so your application code continues working with plaintext values.TayraMaterializationInterceptor-- Hooks into EF Core's entity materialization pipeline (EF Core 7+). After EF Core creates an entity instance from a database row, it immediately decrypts any[PersonalData]fields before the entity is returned to your code.
Performance Note
The materialization interceptor runs synchronously because the crypto engine caches keys in memory. After the initial key fetch from the key store, all subsequent encrypt/decrypt operations are memory-only lookups.
Crypto-Shredding
GDPR Article 17 "right to be forgotten" is implemented via crypto-shredding. Instead of finding and deleting every row containing a person's data, you delete their encryption key. All encrypted data becomes permanently unreadable:
// GDPR "right to be forgotten" — delete the encryption key
var cryptoEngine = provider.GetRequiredService<ICryptoEngine>();
await db.ShredDataSubjectAsync(cryptoEngine, "cust-42");
Console.WriteLine("\n=== After Crypto-Shredding ===");
Console.WriteLine("All data for cust-42 is now permanently unreadable.");After shredding, any subsequent query that materializes this customer's data will return replacement values instead of the original plaintext:
Namereturns""(the default replacement for strings)Emailreturns"redacted@example.com"(the custom replacement)
Irreversible
Crypto-shredding is permanent. The encryption key is deleted from the key store and cannot be recovered. The original data is lost forever.
Data Migration
When adding Tayra to an existing application with pre-existing cleartext data, use the migration service to bulk-encrypt existing rows.
Register Migration Services
// Register migration services (requires AddTayra() and AddTayraEFCore() first)
var migrationServices = new ServiceCollection();
migrationServices.AddTayra(opts => opts.LicenseKey = licenseKey);
migrationServices.AddTayraEFCore();
migrationServices.AddTayraEFCoreMigrations();
migrationServices.AddDbContext<AppDbContext>(opts =>
opts.UseInMemoryDatabase("TayraMigration"));
var migrationProvider = migrationServices.BuildServiceProvider();Encrypt Existing Data
The migration service scans entities in batches, detects cleartext vs. already-encrypted fields, and encrypts only the cleartext rows:
// Bulk-encrypt existing cleartext data in batches
var migrationService = migrationProvider.GetRequiredService<ITayraDataMigrationService>();
var result = await migrationService.EncryptExistingDataAsync<CustomerEntity>(
migrationDb,
batchSize: 50);
Console.WriteLine($"\n=== Migration Result ===");
Console.WriteLine($" Scanned: {result.TotalScanned}");
Console.WriteLine($" Encrypted: {result.Encrypted}");
Console.WriteLine($" Skipped: {result.Skipped}");
Console.WriteLine($" Errors: {result.Errors}");
Console.WriteLine($" Duration: {result.Duration.TotalMilliseconds:F0}ms");The MigrationResult includes:
TotalScanned-- Total entities examinedEncrypted-- Entities that were encrypted during this runSkipped-- Entities that were already encryptedErrors-- Entities with mixed encryption state (flagged for manual review)Duration-- How long the migration took
Verify Encryption
After migration, verify that all rows contain valid Tayra wire format encryption:
// Verify all rows are now properly encrypted
var verification = await migrationService.VerifyEncryptionAsync<CustomerEntity>(
migrationDb,
batchSize: 50);
Console.WriteLine($"\n=== Verification Result ===");
Console.WriteLine($" Verified: {verification.TotalVerified}");
Console.WriteLine($" Valid: {verification.Valid}");
Console.WriteLine($" Invalid: {verification.Invalid}");
Console.WriteLine($" Duration: {verification.Duration.TotalMilliseconds:F0}ms");
foreach (var invalid in verification.InvalidRows)
{
Console.WriteLine($" [INVALID] Entity {invalid.EntityId}, Property: {invalid.PropertyName} — {invalid.Reason}");
}Batch Size
Adjust batchSize based on your table size and available memory. The default is 100 rows per batch. For large tables, consider running migrations during off-peak hours.
See Also
- Getting Started -- End-to-end encryption tutorial
- Attributes -- All four PII annotation attributes
- Marten Integration -- Document database integration
- Key Stores -- Production key store options
