Persistord.Protection
1.0.0-beta.3
dotnet add package Persistord.Protection --version 1.0.0-beta.3
NuGet\Install-Package Persistord.Protection -Version 1.0.0-beta.3
<PackageReference Include="Persistord.Protection" Version="1.0.0-beta.3" />
<PackageVersion Include="Persistord.Protection" Version="1.0.0-beta.3" />
<PackageReference Include="Persistord.Protection" />
paket add Persistord.Protection --version 1.0.0-beta.3
#r "nuget: Persistord.Protection, 1.0.0-beta.3"
#:package Persistord.Protection@1.0.0-beta.3
#addin nuget:?package=Persistord.Protection&version=1.0.0-beta.3&prerelease
#tool nuget:?package=Persistord.Protection&version=1.0.0-beta.3&prerelease
Persistord.Protection
<div align="center">
← Persistord docs · Documentation site
</div>
Encrypts [Protected] string columns of a
Persistord context at rest, using
ASP.NET Core Data Protection.
It replaces hand-rolled calls to protector.Protect(...) scattered across
every write path — miss one, and that value silently lands in the database as
plaintext — with a single model-wide declaration: annotate the property, call
one extension method, and every write to it goes through the same converter.
Setup
Reference this package.
Annotate the secret with
[Protected](Persistord.Core.Abstractions.ProtectedAttribute):public sealed class Webhook { [Protected] public string Token { get; set; } = string.Empty; }Register
ProtectedStringConventionfromConfigureConventions— the recommended entry point. It runs at model finalization, after every module and entity configuration has had a chance to add properties, so unlike theApplyProtectionalternative below it cannot be called too early:public sealed class MyBotContext( DbContextOptions<MyBotContext> options, IDataProtectionProvider dataProtectionProvider) : DiscordDbContext(options) { public DbSet<Webhook> Webhooks => Set<Webhook>(); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { base.ConfigureConventions(configurationBuilder); configurationBuilder.Conventions.Add(_ => new ProtectedStringConvention(dataProtectionProvider)); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Webhook>(); } }The explicit alternative,
ApplyProtection, is still shipped for call sites that configure protection inline inOnModelCreating. Call it last there, after the module and entity configurations that create the annotated properties — it walks the model as already built, so a property configured afterwards is not seen:protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Webhook>(); modelBuilder.ApplyProtection(dataProtectionProvider); // last }Both routes cover a
[Protected]property nested inside an EF complex type (ComplexProperty), not only one declared directly on the entity. They disagree on one edge case, though: an explicit fluentHasConversion(...)the consumer configures on the same[Protected]property wins over the convention (EF'sExplicitprecedence beats the convention'sDataAnnotationprecedence), leaving that property unprotected —ApplyProtection, called last, overwrites it unconditionally instead. See Precedence on the documentation site.
[Protected] is inert without this package. Persistord.Managed's
ManagedWebhook.Token, for example, is annotated [Protected], but without a
reference to Persistord.Protection and either registering
ProtectedStringConvention or calling ApplyProtection, it is stored as
plaintext. The attribute alone changes nothing.
Every write is non-deterministic ciphertext. IDataProtector.Protect
randomises its output, so the same plaintext produces a different stored value
every time it is written. A [Protected] column cannot be queried by
equality, cannot back a useful unique index, and its ciphertext is
substantially longer than the plaintext (over 130 characters for a short
secret) — account for that in any HasMaxLength you set on the property.
The purpose string is fixed
Every protector this package creates is derived from ProtectionPurposes.V1
("Persistord.Protection.v1"). This is deliberate: a protector derived from a
different purpose cannot decrypt what this one wrote, so the string can never
change without orphaning every ciphertext already in the database.
One provider per application
EF Core caches the compiled model per context type. The protector baked into
that cached model is the one derived from whichever IDataProtectionProvider
built it first, so the first IDataProtectionProvider supplied to a given
context type is the one every subsequent instance of that context type
uses, for the lifetime of the process — passing a different provider on a
later call has no effect. The supported arrangement is one
IDataProtectionProvider per application, registered as a singleton in DI and
constructor-injected into the context.
Tests that need different key rings per instance are the exception: they must
also replace EF's IModelCacheKeyFactory so each configuration gets its own
cache entry. Persistord.Testing's fixtures (SqliteTestDatabase) already do
this for you.
Key ring
Data Protection encrypts with keys held in a key ring. This package does
not configure where that key ring lives — that is
IDataProtectionBuilder
configuration, done once when you register Data Protection in your host.
Persistord ships only Microsoft.AspNetCore.DataProtection.Abstractions; the
implementation package (Microsoft.AspNetCore.DataProtection) is your
dependency to add and keep patched.
Losing the key ring means losing every protected value. There is no recovery: without the keys that encrypted a value,
Unprotectcannot produce it back.The default key ring location is a per-user profile folder, which does not travel with your database and is easy to lose on redeploy, container recreation, or a new host. Persist it explicitly, next to the database, with
PersistKeysToFileSystem:services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo("/var/my-bot/keys"));Rotation alone does not break decryption. Data Protection keeps every key it has ever used in the ring, and each one keeps decrypting values it encrypted, even after a newer key becomes the default for new writes. A read only fails when the specific key that encrypted a value is actually gone from the ring — deleted or explicitly revoked — which surfaces as
System.Security.Cryptography.CryptographicExceptionwhile EF materializes the entity. See Protection on the documentation site for the deleted-versus-revoked distinction and what recovers.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- Microsoft.AspNetCore.DataProtection.Abstractions (>= 10.0.0 && < 11.0.0)
- Persistord.Core (>= 1.0.0-beta.3)
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 |
|---|---|---|
| 1.0.0-beta.3 | 70 | 9/10/2026 |