EntityFrameworkCore.Crypto.DataEncryption
10.3.0
See the version list below for details.
dotnet add package EntityFrameworkCore.Crypto.DataEncryption --version 10.3.0
NuGet\Install-Package EntityFrameworkCore.Crypto.DataEncryption -Version 10.3.0
<PackageReference Include="EntityFrameworkCore.Crypto.DataEncryption" Version="10.3.0" />
<PackageVersion Include="EntityFrameworkCore.Crypto.DataEncryption" Version="10.3.0" />
<PackageReference Include="EntityFrameworkCore.Crypto.DataEncryption" />
paket add EntityFrameworkCore.Crypto.DataEncryption --version 10.3.0
#r "nuget: EntityFrameworkCore.Crypto.DataEncryption, 10.3.0"
#:package EntityFrameworkCore.Crypto.DataEncryption@10.3.0
#addin nuget:?package=EntityFrameworkCore.Crypto.DataEncryption&version=10.3.0
#tool nuget:?package=EntityFrameworkCore.Crypto.DataEncryption&version=10.3.0
EntityFrameworkCore.Crypto.DataEncryption
Transparent field-level encryption for Entity Framework Core 8, 9 and 10. Data is encrypted in your application process before it reaches the database, so the database, its administrators and its backups only ever see ciphertext. Keys never leave your process.
Website & licensing • Complete guide • Roadmap • Security
Install
dotnet add package EntityFrameworkCore.Crypto.DataEncryption
Quick start
Mark the properties to encrypt, then turn encryption on in OnModelCreating:
public class Customer
{
public int Id { get; set; }
[CryptoEncrypted]
public string Ssn { get; set; } = string.Empty;
[CryptoEncrypted]
public string Email { get; set; } = string.Empty;
[CryptoEncrypted]
[Column(TypeName = "TEXT")]
public decimal AnnualSalary { get; set; }
}
public class AppDbContext : DbContext
{
private readonly IEncryptionCryptoProvider _provider = new AesGcmCryptoProvider(LoadKeyFromSecureStore()); // 32-byte key
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.UseEncryption(_provider); // call last
}
EF Core encrypts on SaveChanges and decrypts when rows are read. Supported types: string, byte[], long, decimal, double, Dictionary<string, string>, JSON classes and owned entity types.
What you get
| Capability | Notes |
|---|---|
| AES-256-GCM authenticated encryption | Random nonce per value, tamper detection, key IDs and key rings for rotation. |
| Cloud KMS | Azure Key Vault, AWS KMS, HashiCorp Vault. |
| Searchable columns | Blind index for equality lookups (HasBlindIndex, WhereBlindEquals). |
| Substring search and sorting | Exact Contains / StartsWith / EndsWith and in-application sorting, plus WhereBlindIn for IN lists and per-column normalization (WithNormalization). The key is never sent to the database. |
| Query guard | Queries that cannot work on ciphertext throw instead of silently returning wrong results. |
| Batch re-encryption | Verified, resumable AES-CBC to AES-GCM migration and key rotation (EncryptionRotator). |
| Legacy migration bridge | MigrationCryptoProvider reads old AES-CBC data and writes AES-GCM. |
| Auto-encryption | UseAutoEncryption encrypts sensitive columns by naming convention. |
| Observability | Health checks, OpenTelemetry metrics. |
| Enterprise | PII scanner with ML classifier, Crypto Sentinel anomaly detection, email/webhook alerts, decryption audit trail, DPDP / GDPR compliance reports. |
Searching encrypted columns
// Equality: a keyed hash in an indexed shadow column
modelBuilder.Entity<Customer>().Property(c => c.Email).HasBlindIndex();
var customer = await context.Customers.WhereBlindEquals(nameof(Customer.Email), email, hasher).FirstOrDefaultAsync();
var customers = await context.Customers.WhereBlindIn(context, nameof(Customer.Email), emails, hasher).ToListAsync(); // IN list
// Substring search and sorting
modelBuilder.Entity<Customer>().Property(c => c.Ssn).HasSearchIndex();
var rows = await context.Customers.AsEncrypted(context, hasher)
.WhereContains(c => c.Ssn, "123")
.OrderBy(c => c.Ssn)
.ToListAsync(maxRows: 5_000);
Results are exact: the index narrows candidates in SQL and each one is confirmed against the decrypted value. Sorting runs in the application and needs a maxRows cap, so it throws instead of truncating. Contains needs a term of at least 3 characters. Existing rows become searchable after a one-time SearchIndexBackfill.
Measured on 300,000 rows (single runs, default Docker containers on one desktop, no tuning, no PostgreSQL trigram index):
| PostgreSQL 16 | SQL Server 2022 | MySQL 8.4 | |
|---|---|---|---|
Contains("marnia") → 69 rows |
77 ms | 111 ms | 358 ms |
StartsWith("MARIA") → 1,169 rows |
53 ms | 158 ms | 305 ms |
| SQL filter + sort, 30,000 rows | 168 ms | 468 ms | 495 ms |
Tested on all three databases and SQLite: results match the plaintext answer exactly (including %, _, accents and mixed case), sort order with nulls, upgrade of legacy CBC data, CBC to GCM and key rotation, and no plaintext or keys in the PostgreSQL and MySQL server logs. Not measured: concurrent load and tables over 300,000 rows.
Setup, limits and method: search and re-encryption guide. Everything else, including KMS, scanning, alerts and compliance: full reference.
Licensing
Free for evaluation, open source and projects under $2,000/yr revenue (Community). Above that, every paid plan unlocks the full platform, priced per organization with no per-seat counting:
| Plan | Price | Support |
|---|---|---|
| Community | Free, under $2,000 annual revenue | Community |
| Enterprise Annual | $2,999 / year, unlimited developers and projects | Priority, under 1 business day |
| Enterprise 3-Year | $5,999 prepaid, one payment | Priority, 1-hour response |
| Enterprise 5-Year | $6,999 prepaid, one payment | Priority, 1-hour response |
Field encryption, key rotation, cloud KMS, blind indexing, search and auto-encryption are in Community. The ML classifier, Crypto Sentinel, alerts, audit trail and compliance reports need an Enterprise key. Plans and contact form: efcore-encryption.com/#plans.
Local development is never blocked or nagged. On a developer machine every feature works with or without a key, and no license message or error is shown. The license is checked only if you call AddDataEncryptionML(...); modelBuilder.UseEncryption(...), search and re-encryption on their own never check it. When you deploy with AddDataEncryptionML and no valid key, the host throws LicenseValidationException at startup in Kubernetes, managed cloud, a container (unless the environment is Development), or when the environment is Production or Staging. Set options.Enforcement.Mode = LicenseEnforcementMode.WarnOnly (or the environment variable EFCRYPTOML_LICENSE_ENFORCEMENT=WarnOnly) to log a warning instead of failing; paid features then stay off until a key is added.
Support
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- MailKit (>= 4.18.2)
- Microsoft.EntityFrameworkCore (>= 10.0.14)
- Microsoft.Extensions.Caching.Memory (>= 10.0.14)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.14)
- Microsoft.Extensions.Http (>= 10.0.14)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.14)
- Microsoft.ML (>= 5.0.1)
- Microsoft.ML.TimeSeries (>= 5.0.1)
-
net8.0
- MailKit (>= 4.18.2)
- Microsoft.EntityFrameworkCore (>= 8.0.33)
- Microsoft.Extensions.Caching.Memory (>= 8.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.1)
- Microsoft.Extensions.Http (>= 8.0.1)
- Microsoft.Extensions.Options.DataAnnotations (>= 8.0.0)
- Microsoft.ML (>= 5.0.1)
- Microsoft.ML.TimeSeries (>= 5.0.1)
-
net9.0
- MailKit (>= 4.18.2)
- Microsoft.EntityFrameworkCore (>= 9.0.22)
- Microsoft.Extensions.Caching.Memory (>= 9.0.22)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.22)
- Microsoft.Extensions.Http (>= 9.0.22)
- Microsoft.Extensions.Options.DataAnnotations (>= 9.0.22)
- Microsoft.ML (>= 5.0.1)
- Microsoft.ML.TimeSeries (>= 5.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 10.6.5 | 162 | 10/5/2026 | |
| 10.6.0 | 242 | 9/10/2026 | |
| 10.5.0 | 145 | 9/8/2026 | |
| 10.3.0 | 0 | 11/5/2026 | |
| 10.2.0 | 0 | 11/5/2026 | |
| 10.1.0 | 0 | 11/5/2026 | |
| 10.0.1 | 4,889 | 11/12/2025 | |
| 10.0.0 | 4,697 | 11/12/2025 | |
| 9.0.0 | 1,015 | 6/23/2025 | |
| 8.0.3 | 69,384 | 5/29/2024 | |
| 8.0.2 | 456 | 5/27/2024 | |
| 8.0.1 | 430 | 5/27/2024 | |
| 8.0.0 | 453 | 5/24/2024 |
Version 10.3.0
WHAT CHANGED SINCE 10.2.0
- Licensing validation is back. 10.2.0 had it switched off, so it granted every feature with no key. 10.3.0 enforces it again, with one change from 10.1.0 and earlier: on a local development machine nothing is checked or logged, and every feature works with no key (before, a developer machine got a startup warning and the paid features were switched off). See LICENSE BEHAVIOR below.
CORRECTION: CHANGES IN 10.2.0 THAT ITS NOTES DID NOT LIST (the 10.2.0 notes repeated the 10.1.0 notes)
- New WhereBlindIn method for IN-list lookups on a blind-indexed column.
- Stricter query guard: with UseEncryptedQueryGuard, equality (==, !=) and IN-list lookups (list.Contains) on a randomized (AES-GCM) encrypted column now throw EncryptedQueryNotSupportedException, because they silently matched no rows. Null checks, and equality on a deterministic provider such as the legacy fixed-IV AesCryptoProvider, are still allowed.
- EncryptedQueryNotSupportedException constructor gained an optional fourth parameter, hint. Code that only catches the exception is unaffected. Code that constructs it must be recompiled (binary-incompatible change, source-compatible).
- Fixed: MigrationCryptoProvider could fail to read about 1 in 256 legacy AES-CBC values whose first bytes looked like a GCM envelope with an unregistered key ID. It now tries the legacy decryptor first, and still reports a genuinely missing key.
- Dependencies updated (see DEPENDENCIES).
BREAKING CHANGES
- Since 10.2.0: licensing enforcement returns (see LICENSE BEHAVIOR). An app that calls AddDataEncryptionML without a valid key will now fail at startup in production-like environments, as it did before 10.2.0.
- Since 10.1.0: LicenseEnforcer.Enforce returns a new value, Outcome.AllowedInDevelopment, for an unlicensed local development machine under Auto mode (it used to return AllowedWithWarning). Code that switched on that result needs a new case.
- Since 10.1.0: the guard change and the exception constructor change above. The net10.0 package requires Microsoft.EntityFrameworkCore 10.0.14 or later.
NEW AND REMOVED APIS
- 10.3.0 added: LicenseEnforcer.Outcome.AllowedInDevelopment, LicenseGate.IsDevelopmentMode, and an optional developmentMode parameter on the LicenseGate constructor. Removed: none.
- Added in 10.2.0: WhereBlindIn; optional hint parameter on EncryptedQueryNotSupportedException. Removed: none.
- Also present in the 10.1.0 and 10.2.0 line: HasSearchIndex, AsEncrypted (WhereContains, WhereStartsWith, WhereEndsWith, OrderBy, ThenBy), EncryptedSortOptions, SearchIndexBackfill, UseEncryptedQueryGuard, WithNormalization and NormalizeForQuery, EncryptionRotator with RotationOptions and checkpoint stores.
DEPENDENCIES (unchanged in 10.3.0; changed in 10.2.0 compared with 10.1.0)
- Microsoft.EntityFrameworkCore and Microsoft.Extensions.* (net10.0): 10.0.11 to 10.0.14. The net8.0 and net9.0 builds use the newest 8.0.* and 9.0.* patch releases.
- MailKit 4.17.0 to 4.18.2. Microsoft.ML and Microsoft.ML.TimeSeries 5.0.0 to 5.0.1.
- Security: MailKit 4.18.2 brings in System.Security.Cryptography.Pkcs 10.0.13, which fixes advisories GHSA-846f-f764-76mx (high severity) and GHSA-2fmv-rv2c-59x7 (moderate) that affected the Pkcs version used by earlier releases.
STORED DATA FORMAT
- Unchanged. Values written by earlier versions, AES-GCM or legacy AES-CBC, read exactly as before. Nothing needs re-encrypting to upgrade.
- HasSearchIndex adds an optional nullable column named after the property plus SearchIndex. It needs a database migration and a one-time SearchIndexBackfill for existing rows. WhereBlindIn uses the existing blind-index column and needs no schema change.
LICENSE BEHAVIOR
- The license is checked only when AddDataEncryptionML is called. UseEncryption, blind index, search, sorting and EncryptionRotator never check it.
- Community needs no key: field encryption, key rotation, cloud KMS, blind indexing, search and auto-encryption. Enterprise needs a key: ML classifier, Crypto Sentinel, email and webhook alerts, custom alert channels, decryption audit trail, compliance reports.
- Local development (default mode Auto): everything works with or without a key, every feature is available, and no license warning or error is shown.
- Deployed (Auto): without a valid key it throws LicenseValidationException at startup in Kubernetes, managed cloud, a container (unless the environment is Development), or when the environment is Production or Staging.
- To log a warning instead of failing, set Enforcement.Mode to WarnOnly in the options, or set the environment variable EFCRYPTOML_LICENSE_ENFORCEMENT to WarnOnly. Paid features then stay off until a key is added. Strict mode fails everywhere, including a developer machine.
- Plans: Community (free, under $2,000 annual revenue), Enterprise Annual ($2,999/yr), Enterprise 3-Year ($5,999 prepaid), Enterprise 5-Year ($6,999 prepaid), all Enterprise plans with unlimited developers and projects. See https://efcore-encryption.com/#plans
KNOWN LIMITS
- Sorting an encrypted column runs in the application and needs a maxRows cap. If more rows match, it throws and never truncates.
- Contains search needs a term of at least 3 characters. StartsWith and EndsWith accept shorter terms.
- The search index reveals which rows share short fragments of text, not the text itself. Use a dedicated blind-index key.
- Existing rows are not searchable until SearchIndexBackfill has run. On PostgreSQL a trigram GIN index on the search column is recommended; the published timings were measured without one.
- EncryptionRotator needs a single-column primary key and a concurrency token (or AllowWithoutConcurrencyToken), and stops on a row that cannot be decrypted.
- Unicode normalization of search terms is not performed. Not tested: concurrent load, tables over 300,000 rows, MariaDB, managed cloud databases. The real-database tests ran on .NET 10 against PostgreSQL 16, MySQL 8.4 and SQL Server 2022.
Roadmap: https://github.com/efcore-encryption/EntityFrameworkCore.Crypto.DataEncryption/blob/main/ROADMAP.md