Skip to content

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

shell
dotnet add package Tayra.EFCore
powershell
Install-Package Tayra.EFCore

Setup

There are two ways to register Tayra with EF Core: through dependency injection or directly on the DbContextOptionsBuilder.

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.

cs
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();
anchor

Builder-Based Setup

If you prefer to pass ITayra directly, call UseTayra() on the DbContextOptionsBuilder:

cs
// 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;
        }));
anchor

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

cs
// 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
});
anchor
PropertyTypeDefaultDescription
EncryptOnSavebooltrueEncrypt [PersonalData] fields before SaveChanges
DecryptOnLoadbooltrueDecrypt [PersonalData] fields after entity materialization

Define Your Model

Annotate entity properties with [DataSubjectId] and [PersonalData] to enable automatic encryption:

cs
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; } = "";
}
anchor
  • [DataSubjectId] on CustomerId tells Tayra to derive the encryption key from this value.
  • [PersonalData] on Name and Email marks them for encryption.
  • The MaskValue parameter on Email specifies the value returned after crypto-shredding.
  • AccountType has 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:

cs
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);
    }
}
anchor

How It Works

Tayra uses two EF Core interceptors to provide transparent encryption:

  1. TayraSaveChangesInterceptor -- Hooks into SavingChanges. Before entities are persisted, it scans the change tracker for entities with [PersonalData] fields and encrypts them. After SaveChanges completes, it decrypts the in-memory entities back to cleartext so your application code continues working with plaintext values.

  2. 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:

cs
// 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.");
anchor

After shredding, any subsequent query that materializes this customer's data will return replacement values instead of the original plaintext:

  • Name returns "" (the default replacement for strings)
  • Email returns "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

cs
// 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();
anchor

Encrypt Existing Data

The migration service scans entities in batches, detects cleartext vs. already-encrypted fields, and encrypts only the cleartext rows:

cs
// 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");
anchor

The MigrationResult includes:

  • TotalScanned -- Total entities examined
  • Encrypted -- Entities that were encrypted during this run
  • Skipped -- Entities that were already encrypted
  • Errors -- 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:

cs
// 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}");
}
anchor

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