From 361bddaa6b592bd8ae35f48cde0e65528a496cfc Mon Sep 17 00:00:00 2001 From: giammin Date: Thu, 22 Oct 2026 22:55:37 +0200 Subject: [PATCH] fix(postgresql): drop global Dapper underscore name mapping Alias the snake_case columns in the PostgreSQL SELECT instead of enabling Dapper.DefaultTypeMap.MatchNamesWithUnderscores process-wide. Closes #23 BREAKING CHANGE: the PostgreSQL provider no longer sets Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true. Applications that relied on it for their own Dapper queries must set it explicitly at startup. --- AGENTS.md | 2 +- README.md | 4 ---- .../DapperDataProtectionExtensions.cs | 3 --- .../PostgreSQLDataProtectionRepository.cs | 8 ++++---- .../PostgreSQL/PostgreSqlContainerFixture.cs | 2 -- ...tgreSqlDapperDataProtectionExtensionsTests.cs | 12 ++++++++++++ .../PostgreSqlDataProtectionRepositoryTests.cs | 16 ++++++++++++++++ 7 files changed, 33 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4733070..e87ed65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ Both repositories use raw SQL via Dapper. They differ in naming conventions: | Columns | PascalCase, via `nameof(DataProtectionKeyEntity.X)` | snake_case literals | | Unique index on friendly name | Filtered NONCLUSTERED (`WHERE ... IS NOT NULL`) | `CREATE UNIQUE INDEX IF NOT EXISTS` (NULLs allowed) | -- The PostgreSQL provider sets the global `Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true` so snake_case columns map to `DataProtectionKeyEntity`. This affects the whole host process (documented in the README). +- The PostgreSQL `SELECT` aliases each snake_case column to its `DataProtectionKeyEntity` property name (`AS "{nameof(...)}"`). Never set the process-wide `Dapper.DefaultTypeMap.MatchNamesWithUnderscores`: it changes column mapping for every Dapper query in the host application. - Schema and table names are interpolated directly into SQL strings, not parameterized or validated. - Defaults are in each provider's `Constants.cs`. Keep the table DDL in sync with the "Database schema" section of `README.md`. diff --git a/README.md b/README.md index 89c1204..bddca4a 100644 --- a/README.md +++ b/README.md @@ -117,10 +117,6 @@ 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: diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs index b33e488..88b442d 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs @@ -1,5 +1,4 @@ using System; -using Dapper; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Npgsql; @@ -40,8 +39,6 @@ public static IDataProtectionBuilder PersistKeysWithDapperInPostgreSQL(this IDat var npgsqlDataSource = new NpgsqlDataSourceBuilder(connectionString).Build(); - DefaultTypeMap.MatchNamesWithUnderscores = true; - builder.Services.AddSingleton(npgsqlDataSource); builder.Services.AddScoped(); diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs index 9f6d846..2180e05 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs @@ -15,10 +15,10 @@ public class PostgreSQLDataProtectionRepository : IDbDataProtectionStorage private string GetAllQuery() => $""" SELECT - id, - insert_date , - friendly_name, - xml + id AS "{nameof(DataProtectionKeyEntity.Id)}", + insert_date AS "{nameof(DataProtectionKeyEntity.InsertDate)}", + friendly_name AS "{nameof(DataProtectionKeyEntity.FriendlyName)}", + xml AS "{nameof(DataProtectionKeyEntity.Xml)}" FROM {_config.SchemaName}.{_config.TableName}; """; diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs index 6743028..f1a8f3e 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlContainerFixture.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using AspNetCore.DataProtection.CustomStorage.Dapper; using AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL; -using Dapper; using DotNet.Testcontainers.Builders; using Microsoft.Extensions.Options; using Npgsql; @@ -45,7 +44,6 @@ public async ValueTask InitializeAsync() } }; NpgsqlDataSource = npgsqlDataSourceBuilder.Build(); - DefaultTypeMap.MatchNamesWithUnderscores = true; SeedDatabase(); await using var dbConnection = await NpgsqlDataSource.OpenConnectionAsync(); diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs index 98c2d68..e05ded0 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs @@ -1,5 +1,6 @@ using System.Linq; using AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL; +using Dapper; using FluentAssertions; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; @@ -25,4 +26,15 @@ public void PersistKeysWithDapperInPostgreSQL_PreservesIncludeErrorDetail(string new NpgsqlConnectionStringBuilder(dataSource.ConnectionString).IncludeErrorDetail .Should().Be(expected); } + + [Fact] + public void PersistKeysWithDapperInPostgreSQL_DoesNotEnableMatchNamesWithUnderscores() + { + DefaultTypeMap.MatchNamesWithUnderscores = false; + var services = new ServiceCollection(); + + services.AddDataProtection().PersistKeysWithDapperInPostgreSQL("Host=localhost;Database=test"); + + DefaultTypeMap.MatchNamesWithUnderscores.Should().BeFalse(); + } } diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs index 81ab5d5..4ce1c31 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDataProtectionRepositoryTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL; +using Dapper; using FluentAssertions; using Npgsql; using Xunit; @@ -93,4 +94,19 @@ public async Task GetAllAsync_KeysInserted_ReturnsEntitiesWithIdAndInsertDate() .And.OnlyContain(x => x.Id > 0 && x.InsertDate != default); entities.Select(x => x.Id).Should().OnlyHaveUniqueItems(); } + [Fact] + public async Task GetAllAsync_MatchNamesWithUnderscoresDisabled_MapsAllColumns() + { + DefaultTypeMap.MatchNamesWithUnderscores = false; + await _fixture.ResetDatabaseAsync(); + var key = new DataProtectionKey { FriendlyName = "MappingFriendlyName", Xml = "MappingXml" }; + _sut.Insert(key); + + var entity = (await _sut.GetAllAsync()).Should().ContainSingle().Subject; + + entity.Id.Should().BePositive(); + entity.InsertDate.Should().NotBe(default); + entity.FriendlyName.Should().Be(key.FriendlyName); + entity.Xml.Should().Be(key.Xml); + } }