From 4f4be3dd8db2315f513385f9d6974cdc3a1a432f Mon Sep 17 00:00:00 2001 From: giammin Date: Thu, 23 Jul 2026 11:40:24 +0200 Subject: [PATCH 1/3] improve readme #1 --- README.md | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 219 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b6c0c10..a2f891f 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,225 @@ [![GitHub](https://img.shields.io/github/license/giammin/AspNetCore.DataProtection.CustomStorage)](https://github.com/giammin/AspNetCore.DataProtection.CustomStorage/blob/main/LICENSE) # AspNetCore.DataProtection.CustomStorage -Support for storing ASP.NET Core Data Protection keys using a custom storage -# AspNetCore.DataProtection.CustomStorage.Dapper -A storage implementation using Dapper +Persist [ASP.NET Core Data Protection](https://learn.microsoft.com/aspnet/core/security/data-protection/introduction) keys in a storage backend of your choice. -# AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer -Implementation of a Dapper custom storage in SQLServer +The base package lets you plug any backend into the Data Protection key ring by implementing a single, small interface. Ready-to-use SQL Server and PostgreSQL implementations (built on [Dapper](https://github.com/DapperLib/Dapper)) are provided as separate packages. -# AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL -Implementation of a Dapper custom storage in PostgreSQL +## Contents + +- [Why](#why) +- [Packages](#packages) +- [Requirements](#requirements) +- [Installation](#installation) +- [Quick start](#quick-start) + - [SQL Server](#sql-server) + - [PostgreSQL](#postgresql) +- [Configuration](#configuration) +- [Database schema](#database-schema) +- [Custom storage backend](#custom-storage-backend) +- [How it works](#how-it-works) +- [Contributing](#contributing) +- [License](#license) + +## Why + +When an ASP.NET Core app runs on more than one node (containers, a web farm, autoscaling), every instance must share the same Data Protection keys; otherwise cookies, antiforgery tokens and anything else protected on one node cannot be read on another. This library keeps that key ring in a shared, durable store instead of the local filesystem. + +## Packages + +| Package | Description | +|---------|-------------| +| `AspNetCore.DataProtection.CustomStorage` | Core abstractions. Bring your own backend by implementing `IDataProtectionStorage`. | +| `AspNetCore.DataProtection.CustomStorage.Dapper` | Shared Dapper-based building blocks used by the database providers. | +| `AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer` | Ready-to-use SQL Server provider. | +| `AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL` | Ready-to-use PostgreSQL provider. | + +The database provider packages transitively depend on `.Dapper` and the core package, so installing a provider is all you need for the SQL Server or PostgreSQL scenario. + +## Requirements + +- .NET 10.0 or later + +## Installation + +For SQL Server: + +```bash +dotnet add package AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer +``` + +For PostgreSQL: + +```bash +dotnet add package AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL +``` + +For a custom backend, install only the core package: + +```bash +dotnet add package AspNetCore.DataProtection.CustomStorage +``` + +## Quick start + +### SQL Server + +```csharp +using AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDataProtection() + .PersistKeysWithDapperInSqlServer( + builder.Configuration.GetConnectionString("DataProtection")!); + +var app = builder.Build(); + +// Creates the keys table if it does not exist yet +// (InitializeTable defaults to true; see Configuration below). +app.Services.UseDapperDataProtection(); + +app.Run(); +``` + +### PostgreSQL + +```csharp +using AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDataProtection() + .PersistKeysWithDapperInPostgreSQL( + builder.Configuration.GetConnectionString("DataProtection")!); + +var app = builder.Build(); + +app.Services.UseDapperDataProtection(); + +app.Run(); +``` + +> **Note:** the PostgreSQL provider enables Dapper's snake_case column mapping globally +> (`Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true`). Keep this in mind if the same +> application uses Dapper elsewhere. + +## Configuration + +Both providers accept an optional `Action` to override the defaults: + +```csharp +builder.Services.AddDataProtection() + .PersistKeysWithDapperInSqlServer(connectionString, config => + { + config.SchemaName = "security"; + config.TableName = "DpKeys"; + config.InitializeTable = false; // you manage the schema yourself + }); +``` + +| Option | Type | Default (SQL Server) | Default (PostgreSQL) | Description | +|--------|------|----------------------|----------------------|-------------| +| `SchemaName` | `string` | `dbo` | `public` | Schema that contains the keys table. | +| `TableName` | `string` | `DataProtectionKeys` | `data_protection_keys` | Name of the keys table. | +| `InitializeTable` | `bool` | `true` | `true` | When `true`, `UseDapperDataProtection()` creates the table (and its unique index) if it is missing. | + +`UseDapperDataProtection()` is an extension on `IServiceProvider`. Call it once at startup +(`app.Services.UseDapperDataProtection()`): it validates the service registrations and, when +`InitializeTable` is `true`, creates the table. If you set `InitializeTable = false` you are +responsible for creating the schema (see below) before the app stores keys. + +## Database schema + +When `InitializeTable` is enabled, the provider creates a table equivalent to the following. + +SQL Server (`dbo.DataProtectionKeys` by default): + +```sql +CREATE TABLE [dbo].[DataProtectionKeys]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [InsertDate] [datetime] NOT NULL DEFAULT getdate(), + [FriendlyName] [nvarchar](256) NULL, + [Xml] [nvarchar](max) NOT NULL, + CONSTRAINT [PK_DataProtectionKeys] PRIMARY KEY CLUSTERED ([Id] ASC) +); +CREATE UNIQUE NONCLUSTERED INDEX [IX_DataProtectionKeys_FriendlyName] + ON [dbo].[DataProtectionKeys]([FriendlyName] ASC) + WHERE [FriendlyName] IS NOT NULL; +``` + +PostgreSQL (`public.data_protection_keys` by default): + +```sql +CREATE TABLE IF NOT EXISTS public.data_protection_keys ( + id INTEGER GENERATED ALWAYS AS IDENTITY, + insert_date timestamp with time zone NOT NULL DEFAULT NOW(), + friendly_name character varying(256) NULL, + xml text NOT NULL, + CONSTRAINT pk_public_data_protection_keys PRIMARY KEY (id) +); +CREATE UNIQUE INDEX IF NOT EXISTS ix_public_data_protection_keys_friendly_name + ON public.data_protection_keys (friendly_name ASC NULLS LAST); +``` + +The unique index on `FriendlyName` / `friendly_name` allows `NULL` values. + +## Custom storage backend + +Reference `AspNetCore.DataProtection.CustomStorage` and implement `IDataProtectionStorage`: + +```csharp +using AspNetCore.DataProtection.CustomStorage; + +public class MyDataProtectionStorage : IDataProtectionStorage +{ + public IEnumerable GetAll() + { + // load every stored key from your backend + } + + public void Insert(DataProtectionKey key) + { + // persist a new key; key.FriendlyName must be unique when it is not null + } +} +``` + +`DataProtectionKey` carries the two values the key ring needs: + +- `FriendlyName` (`string?`) — a name that must be unique when it is not `null`. +- `Xml` (`string`, required) — the serialized key. + +Register your implementation in DI and wire it up: + +```csharp +builder.Services.AddScoped(); + +builder.Services.AddDataProtection() + .PersistKeysToStorage(); +``` + +`PersistKeysToStorage()` resolves `TStorage` from a fresh dependency-injection scope +on each read/write, so you must register it yourself (any lifetime works). If an `Insert` throws, +it is wrapped in a `KeyInsertException`. + +## How it works + +`PersistKeysToStorage()` registers a `StorageWrapper` as the Data Protection +`IXmlRepository`. The wrapper adapts the framework's XML-element calls to the simpler +`IDataProtectionStorage` contract (`GetAll()` / `Insert()`), resolving `TStorage` from a scoped +service provider for every operation. + +The database providers add a Dapper layer (`IDbDataProtectionStorage`) on top of that contract, +implementing the raw SQL for SQL Server and PostgreSQL, plus schema initialization +(`InitializeDb()`) and an async read path (`GetAllAsync()`). + +## Contributing + +Issues and pull requests are welcome at the +[GitHub repository](https://github.com/giammin/AspNetCore.DataProtection.CustomStorage). + +## License + +Licensed under the [Apache License 2.0](LICENSE). From 5197bf5e95e813ae8388ac04757ed3dce28936d8 Mon Sep 17 00:00:00 2001 From: giammin Date: Thu, 23 Jul 2026 11:42:15 +0200 Subject: [PATCH 2/3] - update references for security warnings on transitive packages. - unified package definition in directory.paclages.props --- AspNetCore.DataProtection.CustomStorage.slnx | 1 + Directory.Packages.props | 25 +++++++++++++++++++ NuGet.Config | 6 +++++ ...ion.CustomStorage.Dapper.PostgreSQL.csproj | 6 +---- ...tion.CustomStorage.Dapper.SQLServer.csproj | 5 +--- ...DataProtection.CustomStorage.Dapper.csproj | 6 +---- .../DapperDataProtectionExtensions.cs | 1 - ...etCore.DataProtection.CustomStorage.csproj | 6 +---- .../LoggingExtensions.cs | 1 - src/Directory.Build.props | 2 +- ....DataProtection.CustomStorage.Tests.csproj | 6 ++--- .../SQLServer/SqlServerContainerFixture.cs | 1 - .../SqlServerDataProtectionRepositoryTests.cs | 1 - .../StorageWrapperTests.cs | 2 +- tests/Directory.Build.props | 12 ++++----- 15 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 Directory.Packages.props diff --git a/AspNetCore.DataProtection.CustomStorage.slnx b/AspNetCore.DataProtection.CustomStorage.slnx index ebd5732..164ae57 100644 --- a/AspNetCore.DataProtection.CustomStorage.slnx +++ b/AspNetCore.DataProtection.CustomStorage.slnx @@ -1,5 +1,6 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..6dfa2e4 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,25 @@ + + + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/NuGet.Config b/NuGet.Config index 504a20c..3a65a61 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,8 +1,14 @@  + + + + + + diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL.csproj b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL.csproj index e114fc6..429f1fc 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL.csproj +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL.csproj @@ -5,15 +5,11 @@ Support for storing AspNetCore data protection keys using Dapper in PostgreSQL. - + - - - - diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer.csproj b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer.csproj index 73398b2..e913c81 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer.csproj +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer.csproj @@ -5,12 +5,9 @@ Support for storing AspNetCore data protection keys using Dapper in SQLServer. - + - - - diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper/AspNetCore.DataProtection.CustomStorage.Dapper.csproj b/src/AspNetCore.DataProtection.CustomStorage.Dapper/AspNetCore.DataProtection.CustomStorage.Dapper.csproj index 691350c..e7dcf67 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper/AspNetCore.DataProtection.CustomStorage.Dapper.csproj +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper/AspNetCore.DataProtection.CustomStorage.Dapper.csproj @@ -6,15 +6,11 @@ - + - - - - diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper/DapperDataProtectionExtensions.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper/DapperDataProtectionExtensions.cs index 5a3a682..5b03884 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper/DapperDataProtectionExtensions.cs +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper/DapperDataProtectionExtensions.cs @@ -1,5 +1,4 @@ using System; -using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; diff --git a/src/AspNetCore.DataProtection.CustomStorage/AspNetCore.DataProtection.CustomStorage.csproj b/src/AspNetCore.DataProtection.CustomStorage/AspNetCore.DataProtection.CustomStorage.csproj index 3a7ae9d..acc58be 100644 --- a/src/AspNetCore.DataProtection.CustomStorage/AspNetCore.DataProtection.CustomStorage.csproj +++ b/src/AspNetCore.DataProtection.CustomStorage/AspNetCore.DataProtection.CustomStorage.csproj @@ -7,11 +7,7 @@ - - - - - + diff --git a/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs b/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs index c585c70..1d94651 100644 --- a/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs +++ b/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.Extensions.Logging; namespace AspNetCore.DataProtection.CustomStorage; diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 63d32ac..9c21473 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -41,7 +41,7 @@ - + diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj b/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj index 38fff92..a891505 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/AspNetCore.DataProtection.CustomStorage.Tests.csproj @@ -19,9 +19,9 @@ - - - + + + diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs index 58e4455..acfad3d 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerContainerFixture.cs @@ -1,7 +1,6 @@ using System; using System.Threading.Tasks; using AspNetCore.DataProtection.CustomStorage.Dapper; -using AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL; using AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Options; diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs index 85adf71..1c529b5 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDataProtectionRepositoryTests.cs @@ -3,7 +3,6 @@ using AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer; using FluentAssertions; using Microsoft.Data.SqlClient; -using Npgsql; using Xunit; namespace AspNetCore.DataProtection.CustomStorage.Tests.SQLServer; diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs index 53bef74..efeb87d 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/StorageWrapperTests.cs @@ -102,6 +102,6 @@ public void StoreElement_ShouldCallInsertOnStorage() // Assert _storage.Received(1).Insert(Arg.Is(key => - key.FriendlyName == "TestName" && key.Xml == element.ToString(SaveOptions.DisableFormatting))); + key != null && key.FriendlyName == "TestName" && key.Xml == element.ToString(SaveOptions.DisableFormatting))); } } \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index d1a252f..0ef9877 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -18,12 +18,12 @@ - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 031c90290ce03d2ba520d227acfd65097d2a2ab3 Mon Sep 17 00:00:00 2001 From: giammin Date: Thu, 23 Jul 2026 11:45:59 +0200 Subject: [PATCH 3/3] library version increase --- src/Directory.Build.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 779fbc3..3c2d550 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,7 +1,7 @@ - 3 + 4 0 $([System.DateTime]::Now.ToString(yyMM)).$([System.DateTime]::Now.ToString(dd)) $(VersionPrefix).$(buildNumber)$(versionBuild)$(versionRevision)