DKNet

DKNet.EfCore.Encryption

Transparent, column-level encryption for EF Core string properties, applied at the database boundary via a standard ValueConverter.

Scope note: this page covers src/EfCore/DKNet.EfCore.Encryption only — column-level encryption for EF Core entities. It is unrelated to DKNet.Svc.Encryption / DKNet.Svc.BlobStorage.Encryption under src/Services, which are general-purpose application/blob cryptography packages with no EF Core dependency.

✨ Why use it?

Do not reach for it when you need to filter, sort, or LIKE-search on the encrypted value in SQL — a random IV per write makes encrypted columns opaque to the database. See Gotchas & limits.

🚀 Quick Start

dotnet add package DKNet.EfCore.Encryption

Three steps get a property encrypted end-to-end:

a) Supply a key. Implement IEncryptionKeyProvider:

public sealed class AppEncryptionKeyProvider : IEncryptionKeyProvider
{
    private readonly byte[] _key = Convert.FromBase64String(
        Environment.GetEnvironmentVariable("APP_ENCRYPTION_KEY")!); // 16, 24, or 32 bytes

    public byte[] GetKey(Type entityType) => _key;
}

b) Register it in DI, via EfCoreEncryptionSetup.AddEfCoreEncryption<TKeyServiceImplementation>:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEfCoreEncryption<AppEncryptionKeyProvider>(); // registers IEncryptionKeyProvider as a singleton

The extension takes and returns IServiceCollection, so it is callable on builder.Services and chains with any other registration call.

c) Apply it in OnModelCreating via ModelBuilderExtensions.UseColumnEncryption, and mark the property:

public class Customer
{
    public int Id { get; set; }

    [Encrypted]
    public string? Ssn { get; set; }
}

public class AppDbContext(DbContextOptions<AppDbContext> options, IEncryptionKeyProvider keyProvider)
    : DbContext(options)
{
    public DbSet<Customer> Customers => Set<Customer>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.UseColumnEncryption(keyProvider);
    }
}

From here, context.Customers.Add(new Customer { Ssn = "123-45-6789" }) and SaveChangesAsync() stores ciphertext; reading customer.Ssn back gives the plaintext.

🧩 Features

[Encrypted] attribute (DKNet.EfCore.Encryption.Attributes.EncryptedAttribute)

A property-only marker attribute with no data — [AttributeUsage(AttributeTargets.Property)]. UseColumnEncryption scans every string property in the model and only touches ones decorated with it:

public class Employee
{
    public int Id { get; set; }

    [Encrypted]
    public string? TaxId { get; set; }

    public string Name { get; set; } = string.Empty; // untouched
}

It is only inspected on properties whose CLR type is string; applying it to a non-string property has no effect (silently ignored, not an error).

ColumnEncryptionConverter (DKNet.EfCore.Encryption.Converters)

The actual EF Core value converter doing the work — a thin ValueConverter<string?, string?> that delegates to an IColumnEncryptionProvider:

public sealed class ColumnEncryptionConverter(IColumnEncryptionProvider encryptionProvider)
    : ValueConverter<string?, string?>(
        v => encryptionProvider.Encrypt(v),
        v => encryptionProvider.Decrypt(v));

You normally never construct this yourself — UseColumnEncryption creates and applies one per matching property. You could still apply it manually to a single property for finer control:

modelBuilder.Entity<Customer>()
    .Property(c => c.Ssn)
    .HasConversion(new ColumnEncryptionConverter(new AesGcmColumnEncryptionProvider(key)));

IColumnEncryptionProvider (DKNet.EfCore.Encryption.Interfaces)

The encryption algorithm abstraction consumed by ColumnEncryptionConverter:

public interface IColumnEncryptionProvider
{
    string? Decrypt(string? ciphertext);
    string? Encrypt(string? plaintext);
}

The package ships exactly one implementation (AesGcmColumnEncryptionProvider) and UseColumnEncryption hardcodes it — there’s no DI slot to swap the algorithm for [Encrypted]-driven properties. To use a different provider you’d write your own model-building extension that builds ColumnEncryptionConverter with your IColumnEncryptionProvider instead of calling UseColumnEncryption.

AesGcmColumnEncryptionProvider (DKNet.EfCore.Encryption.Encryption) — the default provider

AES-256/192/128-GCM (authenticated encryption). Constructor takes the raw key:

public AesGcmColumnEncryptionProvider(byte[] key) // key.Length must be 16, 24, or 32

Because the IV is random per call, encrypting the same plaintext twice produces different ciphertext — this is by design (semantic security) but has query implications, see Gotchas.

IEncryptionKeyProvider (DKNet.EfCore.Encryption.Encryption) — where key material comes from

public interface IEncryptionKeyProvider
{
    byte[] GetKey(Type entityType);
}

The package supplies no concrete key source — no config binding, no Key Vault client, nothing that reads a connection string or secret store for you. You always write the implementation and decide where the bytes come from (environment variable, IConfiguration, Azure Key Vault SDK, a secrets file, etc.), by implementing this interface directly. There is no abstract base class to derive from.

GetKey receives the entity’s CLR type (the property’s DeclaringType), not the property name — so you can vary keys per entity type, but every [Encrypted] property on the same entity shares one key.

ModelBuilderExtensions.UseColumnEncryption (DKNet.EfCore.Encryption.Extensions) — the wiring hook

public static void UseColumnEncryption(this ModelBuilder modelBuilder, IEncryptionKeyProvider encryptionKeyProvider)

Called once from OnModelCreating. For every string property across every entity type in the model that carries [Encrypted]:

  1. Throws InvalidOperationException if the property is a primary key or foreign key column (encrypting join/identity columns is unsupported).
  2. Otherwise calls encryptionKeyProvider.GetKey(propertyInfo.DeclaringType), builds new ColumnEncryptionConverter(new AesGcmColumnEncryptionProvider(key)), and calls property.SetValueConverter(converter).

Throws ArgumentNullException if either argument is null.

EfCoreEncryptionSetup.AddEfCoreEncryption<TKeyServiceImplementation> — DI registration

public static IServiceCollection AddEfCoreEncryption<TKeyServiceImplementation>(this IServiceCollection services)
    where TKeyServiceImplementation : class, IEncryptionKeyProvider

Registers TKeyServiceImplementation as the singleton IEncryptionKeyProvider, but only if one isn’t already registered (idempotent — safe to call more than once, or alongside a manual registration you added yourself).

⚙️ Configuration reference

There is no options/settings class and no appsettings.json binding shipped by this package — the only “configuration” surface is the IEncryptionKeyProvider implementation you write, and the constructor argument to AesGcmColumnEncryptionProvider.

Knob Type Default Effect
IEncryptionKeyProvider.GetKey(Type entityType) byte[] none — you must implement it Supplies the AES key for every [Encrypted] property on that entity type. Evaluated once per property at model-build time.
AesGcmColumnEncryptionProvider(byte[] key) byte[] none — required Key material. Must be exactly 16, 24, or 32 bytes; anything else throws ArgumentException, null throws ArgumentNullException.
AddEfCoreEncryption<TKeyServiceImplementation>() IServiceCollection extension Registers TKeyServiceImplementation as the singleton IEncryptionKeyProvider, only if one is not already registered.
ModelBuilderExtensions.UseColumnEncryption(...) ModelBuilder extension Must be called from OnModelCreating; without it, [Encrypted] has no effect.

Behaviour worth knowing beyond the table:

🧱 Where it fits

The whole package is one value converter wired up at model-build time — which is exactly why a key change needs a model rebuild and why the database never sees plaintext:

Data-flow diagram of column encryption: at model build, UseColumnEncryption attaches a ColumnEncryptionConverter to each [Encrypted] string property, rejecting key and foreign-key columns outright. The converter calls the AES-GCM provider, keyed by IEncryptionKeyProvider.GetKey for that entity type, and the database column stores Base64 of the IV, tag and ciphertext.

⚠️ Gotchas & limits