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.Encryptiononly — column-level encryption for EF Core entities. It is unrelated toDKNet.Svc.Encryption/DKNet.Svc.BlobStorage.Encryptionundersrc/Services, which are general-purpose application/blob cryptography packages with no EF Core dependency.
string property [Encrypted] and EF Core encrypts on the way
to the database and decrypts on the way back. No repository, service, or query call site changes.AesGcmColumnEncryptionProvider uses AES-GCM with a random IV per call,
so tampered ciphertext fails loudly instead of decrypting to garbage.IEncryptionKeyProvider (Key Vault, KMS, config,
whatever) and register it once with AddEfCoreEncryption<T>(); the model-build hook does the rest.Microsoft.EntityFrameworkCore, so it drops
into any EF Core project independent of the DKNet hook pipeline.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.
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.
[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 providerAES-256/192/128-GCM (authenticated encryption). Constructor takes the raw key:
public AesGcmColumnEncryptionProvider(byte[] key) // key.Length must be 16, 24, or 32
Encrypt(string? plaintext) — generates a random 12-byte IV per call, encrypts with AES-GCM, and returns Base64 of IV (12 bytes) + Tag (16 bytes) + ciphertext. Null/empty input passes through unchanged (never encrypted).Decrypt(string? ciphertext) — reverses the packing; throws ArgumentException if the Base64 payload is shorter than IV + Tag (invalid format), or InvalidOperationException if AES-GCM authentication fails (wrong key or corrupted/tampered data).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 frompublic 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 hookpublic 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]:
InvalidOperationException if the property is a primary key or foreign key column (encrypting join/identity columns is unsupported).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 registrationpublic 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).
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:
IEncryptionKeyProvider; nothing is read from configuration automatically.AesGcmColumnEncryptionProvider’s constructor throws ArgumentException otherwise, and ArgumentNullException for a null key.GetKey(Type)), applied to all of that entity’s [Encrypted] properties — no built-in per-property key.GetKey is evaluated once per property, at model-building time (effectively once per DbContext type per application lifetime, since EF Core caches the compiled model). Rotating a key means: change what your IEncryptionKeyProvider returns, restart the app (or otherwise force EF Core to rebuild the model), and run a data migration that decrypts existing rows with the old key and re-encrypts with the new one — there’s no dual-key/versioned-ciphertext support built in.Encrypt and Decrypt short-circuit on null/empty string and return the input unchanged — empty/null values are never turned into ciphertext.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:
Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<TModel,TProvider> — the package’s .csproj has exactly one PackageReference, Microsoft.EntityFrameworkCore. It does not depend on DKNet.EfCore.Abstractions, DKNet.EfCore.Extensions, or DKNet.EfCore.Hooks.ValueConverter applied in OnModelCreating), not a SaveChanges interceptor/hook — it does not participate in the DKNet.EfCore.Hooks pipeline and has no interaction with IHook/SaveChangesAsync interception.[Encrypted] and every other type documented above live entirely inside DKNet.EfCore.Encryption — there’s no dependency on (or hook into) DKNet.EfCore.Abstractions for model-building.SetValueConverter call, it composes with any other EF Core model configuration on the same property as long as nothing else also calls SetValueConverter/HasConversion on that property afterward (last write wins — don’t double-convert the same column).DKNet.EfCore.AuditLogs and DKNet.EfCore.DataAuthorization on the same DbContext — they operate on different model/pipeline surfaces (auditing/authorization are SaveChanges/query-filter concerns; this is a value-conversion concern) and don’t need explicit ordering relative to each other.AesGcmColumnEncryptionProvider uses a random IV per encryption call, so the same plaintext produces different ciphertext every time. context.Customers.Where(c => c.Ssn == "123-45-6789") translates to a SQL comparison against ciphertext and will not match — equality, LIKE, ORDER BY, and indexes on encrypted columns are all unusable. Decrypt-and-compare in memory, or maintain a separate deterministic value (e.g., a HMAC hash column) if you need to search by an encrypted field.UseColumnEncryption throws InvalidOperationException at model-build time if [Encrypted] is applied to a PK or FK property.string properties are considered. The scan filters on ClrType == typeof(string); [Encrypted] on any other type is silently ignored — no compile or runtime error tells you it wasn’t applied.GetKey(Type entityType) gives you the declaring entity type only; two [Encrypted] properties on the same entity always share the same key.12-byte IV + 16-byte tag + ciphertext, so a nvarchar/varchar column must be sized with headroom (roughly plaintext-bytes + 28, then ×~1.33 for Base64) or migrations/writes can truncate.Decrypt throws ArgumentException for a malformed payload and InvalidOperationException when AES-GCM authentication fails (tampered data or wrong key) — handle these at the boundary if a bad key/rotation could reach production data.DKNet.Svc.Encryption / DKNet.Svc.BlobStorage.Encryption. Those are general-purpose cryptography utilities under src/Services with no EF Core dependency; use this package specifically for EF Core column-level encryption.[SensitiveData], which controls whether a
value is shown in an audit trail. Reach for it to hide a value from logs; reach for this package to protect it at
rest.UseColumnEncryption is an independent
OnModelCreating call.