From b1c69c5771221921739f3a0b625ae1dd32dbb797 Mon Sep 17 00:00:00 2001 From: giammin Date: Fri, 23 Oct 2026 11:03:53 +0200 Subject: [PATCH 1/3] fix(dapper)!: reject a second dapper provider registration PersistKeysWithDapperInSqlServer and PersistKeysWithDapperInPostgreSQL share one unnamed DapperDataProtectionConfig. A second call, to either provider, silently replaced the storage of the first one, overrode its configuration and ran both validators on it. Both methods now throw an InvalidOperationException when an IDbDataProtectionStorage is already registered. BREAKING CHANGE: calling PersistKeysWithDapperInSqlServer or PersistKeysWithDapperInPostgreSQL more than once now throws an InvalidOperationException. Keep a single call. --- AGENTS.md | 2 +- README.md | 10 ++++++++++ .../DapperDataProtectionExtensions.cs | 9 +++++++++ .../DapperDataProtectionExtensions.cs | 9 +++++++++ ...eSqlDapperDataProtectionExtensionsTests.cs | 20 +++++++++++++++++++ ...rverDapperDataProtectionExtensionsTests.cs | 20 +++++++++++++++++++ 6 files changed, 69 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index c60b036..c6e0cc2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ ASP.NET Core Data Protection (IXmlRepository) - `DeletableStorageWrapper.DeleteElements` offers the parsed keys (malformed rows skipped), then passes the chosen `GetAll()` instances, ordered by `DeletionOrder`, to a single `Delete()` call. A storage exception is logged (`ErrorDeletingKeysFromStorage`) and returns `false`. Note: in ASP.NET Core 10.0, `XmlKeyManager` exposes `DeleteKeys` but does not implement `IDeletableKeyManager`. - `StorageWrapper` (internal) creates a new DI scope and resolves `TStorage` on every `GetAllElements` / `StoreElement` call, so scoped storage implementations work. It trims `FriendlyName`, normalizes empty or whitespace-only names to `null`, and wraps any `Insert` exception in `KeyInsertException`. `GetAllElements` logs and skips rows with malformed or `null` XML (`SkippingMalformedKey`) instead of failing the whole key ring. - Never log or expose key XML (it may contain unencrypted key material); this includes `XmlException` messages, which can quote the input. -- **DB providers:** `PersistKeysWithDapperInSqlServer(...)` / `PersistKeysWithDapperInPostgreSQL(...)` configure `IOptions` with an internal per-provider `DapperDataProtectionConfigValidator` and `ValidateOnStart()`, register the connection source as a keyed singleton, so it never collides with one registered by the application (`SqlConnectionFactory` under the internal `Constants.ConnectionFactoryServiceKey`; `NpgsqlDataSource` under the internal `Constants.DataSourceServiceKey`, created by a factory so the container disposes it), register the repository as scoped `IDbDataProtectionStorage`, then call `PersistKeysToStorage()`. +- **DB providers:** `PersistKeysWithDapperInSqlServer(...)` / `PersistKeysWithDapperInPostgreSQL(...)` throw `InvalidOperationException` when an `IDbDataProtectionStorage` is already registered (both providers share one unnamed `DapperDataProtectionConfig`), then configure `IOptions` with an internal per-provider `DapperDataProtectionConfigValidator` and `ValidateOnStart()`, register the connection source as a keyed singleton, so it never collides with one registered by the application (`SqlConnectionFactory` under the internal `Constants.ConnectionFactoryServiceKey`; `NpgsqlDataSource` under the internal `Constants.DataSourceServiceKey`, created by a factory so the container disposes it), register the repository as scoped `IDbDataProtectionStorage`, then call `PersistKeysToStorage()`. - `IServiceProvider.UseDapperDataProtection()` (in the shared Dapper project) must be called at startup. It resolves the registrations and runs `InitializeDb()` when `DapperDataProtectionConfig.InitializeTable` is `true` (the default). Reading the options there also triggers validation when no host runs `ValidateOnStart`. ### Database Implementations diff --git a/README.md b/README.md index 76c46db..5765748 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,9 @@ builder.Services.AddDataProtection() }); ``` +Call `PersistKeysWithDapperInSqlServer()` or `PersistKeysWithDapperInPostgreSQL()` only once: a +second call, to either provider, throws an `InvalidOperationException`. + | Option | Type | Default (SQL Server) | Default (PostgreSQL) | Description | |--------|------|----------------------|----------------------|-------------| | `SchemaName` | `string` | `dbo` | `public` | Schema that contains the keys table. See [Schema and table names](#schema-and-table-names). | @@ -451,6 +454,13 @@ v1 registered its `SqlConnectionFactory` as an unkeyed singleton. v2 registers i sp.GetRequiredService>())); ``` +#### The Dapper providers can be registered only once + +v1 accepted several calls to `PersistKeysWithDapperInSqlServer()` and +`PersistKeysWithDapperInPostgreSQL()`: the last call silently replaced the storage of the previous +ones, which shared a single configuration. v2 throws an `InvalidOperationException` when an +`IDbDataProtectionStorage` is already registered. Keep a single call. + #### `StorageWrapper` is internal Affects code that created `StorageWrapper` directly, for example to assign diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs index 2918c95..50af5af 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/DapperDataProtectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -20,11 +21,19 @@ public static class DapperDataProtectionExtensions /// /// /// The value . + /// an is already registered public static IDataProtectionBuilder PersistKeysWithDapperInPostgreSQL(this IDataProtectionBuilder builder, string connectionString, Action? configAction = null) { ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); + // A second registration would silently replace the first one and share its options. + if (builder.Services.Any(d => d.ServiceType == typeof(IDbDataProtectionStorage))) + { + throw new InvalidOperationException( + $"An {nameof(IDbDataProtectionStorage)} is already registered: call {nameof(PersistKeysWithDapperInPostgreSQL)} " + + "or PersistKeysWithDapperInSqlServer only once."); + } var config = new DapperDataProtectionConfig { diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionExtensions.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionExtensions.cs index b8d14e7..1c56799 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionExtensions.cs +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/DapperDataProtectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -18,11 +19,19 @@ public static class DapperDataProtectionExtensions /// /// /// The value . + /// an is already registered public static IDataProtectionBuilder PersistKeysWithDapperInSqlServer(this IDataProtectionBuilder builder, string connectionString, Action? configAction = null) { ArgumentException.ThrowIfNullOrWhiteSpace(connectionString); + // A second registration would silently replace the first one and share its options. + if (builder.Services.Any(d => d.ServiceType == typeof(IDbDataProtectionStorage))) + { + throw new InvalidOperationException( + $"An {nameof(IDbDataProtectionStorage)} is already registered: call {nameof(PersistKeysWithDapperInSqlServer)} " + + "or PersistKeysWithDapperInPostgreSQL only once."); + } var config = new DapperDataProtectionConfig { diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs index 31792de..17c7181 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/PostgreSQL/PostgreSqlDapperDataProtectionExtensionsTests.cs @@ -63,6 +63,26 @@ public void PersistKeysWithDapperInPostgreSQL_DoesNotEnableMatchNamesWithUndersc DefaultTypeMap.MatchNamesWithUnderscores.Should().BeFalse(); } + [Fact] + public void PersistKeysWithDapperInPostgreSQL_CalledTwice_ThrowsInvalidOperationException() + { + var builder = new ServiceCollection().AddDataProtection().PersistKeysWithDapperInPostgreSQL("Host=localhost;Database=test"); + + builder.Invoking(x => x.PersistKeysWithDapperInPostgreSQL("Host=localhost;Database=test")) + .Should().Throw() + .WithMessage($"*{nameof(IDbDataProtectionStorage)}*"); + } + + [Fact] + public void PersistKeysWithDapperInPostgreSQL_AfterSqlServerProvider_ThrowsInvalidOperationException() + { + var builder = new ServiceCollection().AddDataProtection(); + Dapper.SQLServer.DapperDataProtectionExtensions.PersistKeysWithDapperInSqlServer(builder, "Server=localhost;Database=test"); + + builder.Invoking(x => x.PersistKeysWithDapperInPostgreSQL("Host=localhost;Database=test")) + .Should().Throw(); + } + [Fact] public void PersistKeysWithDapperInPostgreSQL_DefaultConfig_PassesStartupValidation() { diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDapperDataProtectionExtensionsTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDapperDataProtectionExtensionsTests.cs index c6e3c16..d37c757 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDapperDataProtectionExtensionsTests.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/SQLServer/SqlServerDapperDataProtectionExtensionsTests.cs @@ -40,6 +40,26 @@ public void PersistKeysWithDapperInSqlServer_ApplicationConnectionFactoryRegiste .Should().BeOfType(); } + [Fact] + public void PersistKeysWithDapperInSqlServer_CalledTwice_ThrowsInvalidOperationException() + { + var builder = new ServiceCollection().AddDataProtection().PersistKeysWithDapperInSqlServer(ConnectionString); + + builder.Invoking(x => x.PersistKeysWithDapperInSqlServer(ConnectionString)) + .Should().Throw() + .WithMessage($"*{nameof(IDbDataProtectionStorage)}*"); + } + + [Fact] + public void PersistKeysWithDapperInSqlServer_AfterPostgreSqlProvider_ThrowsInvalidOperationException() + { + var builder = new ServiceCollection().AddDataProtection(); + Dapper.PostgreSQL.DapperDataProtectionExtensions.PersistKeysWithDapperInPostgreSQL(builder, "Host=localhost;Database=test"); + + builder.Invoking(x => x.PersistKeysWithDapperInSqlServer(ConnectionString)) + .Should().Throw(); + } + [Fact] public void PersistKeysWithDapperInSqlServer_DefaultConfig_PassesStartupValidation() { From 168b3eb745a9e1d963b8ac87e06340bc03a15973 Mon Sep 17 00:00:00 2001 From: giammin Date: Fri, 23 Oct 2026 11:03:53 +0200 Subject: [PATCH 2/3] fix(core): log deleted keys only after a successful delete Each key was logged as being deleted before Delete() ran, so a failed, rolled-back deletion still showed every key as deleted. The keys are now logged after Delete() returns, as DeletedKeyFromStorage (event id 6), which replaces DeletingKeyFromStorage; both events are new in v2. --- AGENTS.md | 2 +- README.md | 4 ++-- .../DeletableStorageWrapper.cs | 13 +++++++------ .../LoggingExtensions.cs | 6 +++--- .../DeletableStorageWrapperTests.cs | 3 ++- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c6e0cc2..7ca8ca9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ ASP.NET Core Data Protection (IXmlRepository) ### Wiring - **Core:** `PersistKeysToStorage()` registers a singleton `IConfigureOptions` that sets `XmlRepository` to a `StorageWrapper`, or to a `DeletableStorageWrapper` when `TStorage` is assignable to `IDeletableDataProtectionStorage` (so `XmlKeyManager.CanDeleteKeys` reflects the storage). It does **not** register `TStorage`; the caller must. -- `DeletableStorageWrapper.DeleteElements` offers the parsed keys (malformed rows skipped), then passes the chosen `GetAll()` instances, ordered by `DeletionOrder`, to a single `Delete()` call. A storage exception is logged (`ErrorDeletingKeysFromStorage`) and returns `false`. Note: in ASP.NET Core 10.0, `XmlKeyManager` exposes `DeleteKeys` but does not implement `IDeletableKeyManager`. +- `DeletableStorageWrapper.DeleteElements` offers the parsed keys (malformed rows skipped), then passes the chosen `GetAll()` instances, ordered by `DeletionOrder`, to a single `Delete()` call. Each key is logged (`DeletedKeyFromStorage`) only after `Delete()` succeeds; a storage exception is logged (`ErrorDeletingKeysFromStorage`) and returns `false`. Note: in ASP.NET Core 10.0, `XmlKeyManager` exposes `DeleteKeys` but does not implement `IDeletableKeyManager`. - `StorageWrapper` (internal) creates a new DI scope and resolves `TStorage` on every `GetAllElements` / `StoreElement` call, so scoped storage implementations work. It trims `FriendlyName`, normalizes empty or whitespace-only names to `null`, and wraps any `Insert` exception in `KeyInsertException`. `GetAllElements` logs and skips rows with malformed or `null` XML (`SkippingMalformedKey`) instead of failing the whole key ring. - Never log or expose key XML (it may contain unencrypted key material); this includes `XmlException` messages, which can quote the input. - **DB providers:** `PersistKeysWithDapperInSqlServer(...)` / `PersistKeysWithDapperInPostgreSQL(...)` throw `InvalidOperationException` when an `IDbDataProtectionStorage` is already registered (both providers share one unnamed `DapperDataProtectionConfig`), then configure `IOptions` with an internal per-provider `DapperDataProtectionConfigValidator` and `ValidateOnStart()`, register the connection source as a keyed singleton, so it never collides with one registered by the application (`SqlConnectionFactory` under the internal `Constants.ConnectionFactoryServiceKey`; `NpgsqlDataSource` under the internal `Constants.DataSourceServiceKey`, created by a factory so the container disposes it), register the repository as scoped `IDbDataProtectionStorage`, then call `PersistKeysToStorage()`. diff --git a/README.md b/README.md index 5765748..bfbf6ad 100644 --- a/README.md +++ b/README.md @@ -342,8 +342,8 @@ if (keyManager is XmlKeyManager { CanDeleteKeys: true } xmlKeyManager) The selected keys are deleted with a single `Delete` call. The database providers delete the rows by `Id`, in deletion order, in one transaction: either every selected key is deleted or none is. If the storage throws, the error is logged (event `ErrorDeletingKeysFromStorage`) and `DeleteKeys` -returns `false`. Each deleted key is logged by friendly name (event `DeletingKeyFromStorage`); the -key XML is never logged. A stored key whose XML is malformed is not offered for deletion. +returns `false`. Once the deletion has succeeded, each deleted key is logged by friendly name (event +`DeletedKeyFromStorage`); the key XML is never logged. A stored key whose XML is malformed is not offered for deletion. ## How it works diff --git a/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs b/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs index 66f6ea5..2215266 100644 --- a/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs +++ b/src/AspNetCore.DataProtection.CustomStorage/DeletableStorageWrapper.cs @@ -53,21 +53,22 @@ public bool DeleteElements(Action> choose return true; } - foreach (var key in keysToDelete) - { - _logger.LogDeletingKeyFromStorage(key.FriendlyName, typeof(TStorage).Name); - } - try { storage.Delete(keysToDelete); - return true; } catch (Exception e) { _logger.LogErrorDeletingKeysFromStorage(e, keysToDelete.Count, typeof(TStorage).Name); return false; } + + foreach (var key in keysToDelete) + { + _logger.LogDeletedKeyFromStorage(key.FriendlyName, typeof(TStorage).Name); + } + + return true; } private sealed class DeletableElement(DataProtectionKey key, XElement element) : IDeletableElement diff --git a/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs b/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs index 127dd8c..a427605 100644 --- a/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs +++ b/src/AspNetCore.DataProtection.CustomStorage/LoggingExtensions.cs @@ -37,11 +37,11 @@ internal static partial class LoggingExtensions public static partial void LogSkippingMalformedKey(this ILogger logger, string? friendlyName, int lineNumber, int linePosition); /// - /// Logs an information message for each key about to be deleted from storage. + /// Logs an information message for each key deleted from storage, once the deletion has succeeded. /// The key XML is never logged because it may contain unencrypted key material. /// - [LoggerMessage(6, LogLevel.Information, "Deleting key '{FriendlyName}' with '{storage}'.", EventName = "DeletingKeyFromStorage")] - public static partial void LogDeletingKeyFromStorage(this ILogger logger, string? friendlyName, string storage); + [LoggerMessage(6, LogLevel.Information, "Deleted key '{FriendlyName}' with '{storage}'.", EventName = "DeletedKeyFromStorage")] + public static partial void LogDeletedKeyFromStorage(this ILogger logger, string? friendlyName, string storage); /// /// Logs an error message when deleting keys from storage fails. diff --git a/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs b/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs index 5173704..a5e4d32 100644 --- a/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs +++ b/tests/AspNetCore.DataProtection.CustomStorage.Tests/DeletableStorageWrapperTests.cs @@ -103,7 +103,7 @@ public void DeleteElements_ElementsChosen_LogsDeletedKeysWithoutKeyXml() { _sut.DeleteElements(elements => elements.First().DeletionOrder = 1); - _loggerFactory.Entries.Should().ContainSingle(e => e.EventId.Name == "DeletingKeyFromStorage") + _loggerFactory.Entries.Should().ContainSingle(e => e.EventId.Name == "DeletedKeyFromStorage") .Which.Contains(_first.FriendlyName!).Should().BeTrue(); _loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial)); } @@ -119,6 +119,7 @@ public void DeleteElements_StorageFails_ReturnsFalseAndLogsErrorWithoutKeyXml() result.Should().BeFalse(); _loggerFactory.Entries.Should().ContainSingle(e => e.Level == LogLevel.Error) .Which.Exception.Should().BeSameAs(storageException); + _loggerFactory.Entries.Should().NotContain(e => e.EventId.Name == "DeletedKeyFromStorage"); _loggerFactory.Entries.Should().NotContain(e => e.Contains(KeyMaterial)); } From 3e014d415a8abb1b974214537f16da2d0f63d397 Mon Sep 17 00:00:00 2001 From: giammin Date: Fri, 23 Oct 2026 11:03:54 +0200 Subject: [PATCH 3/3] refactor(dapper): quote insert columns and build select query once The SQL Server INSERT now brackets its column names like every other statement, and both repositories build the SELECT statement once in the constructor instead of on every read. --- .../PostgreSQLDataProtectionRepository.cs | 23 ++++++++-------- .../SQLServerDataProtectionRepository.cs | 27 ++++++++++--------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs index 1b0cb5f..d74fd95 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.PostgreSQL/PostgreSQLDataProtectionRepository.cs @@ -17,15 +17,7 @@ public class PostgreSQLDataProtectionRepository : IDbDataProtectionStorage private readonly NpgsqlDataSource _dataSource; private readonly DapperDataProtectionConfig _config; private readonly string _tableName; - private string GetAllQuery() => - $""" - SELECT - id AS "{nameof(DataProtectionKeyEntity.Id)}", - insert_date AS "{nameof(DataProtectionKeyEntity.InsertDate)}", - friendly_name AS "{nameof(DataProtectionKeyEntity.FriendlyName)}", - xml AS "{nameof(DataProtectionKeyEntity.Xml)}" - FROM {_tableName}; - """; + private readonly string _getAllQuery; /// /// create an instance of @@ -38,20 +30,29 @@ public PostgreSQLDataProtectionRepository([FromKeyedServices(Constants.DataSourc _dataSource = dataSource; _config = options.Value; _tableName = $"{QuoteIdentifier(_config.SchemaName)}.{QuoteIdentifier(_config.TableName)}"; + _getAllQuery = + $""" + SELECT + id AS "{nameof(DataProtectionKeyEntity.Id)}", + insert_date AS "{nameof(DataProtectionKeyEntity.InsertDate)}", + friendly_name AS "{nameof(DataProtectionKeyEntity.FriendlyName)}", + xml AS "{nameof(DataProtectionKeyEntity.Xml)}" + FROM {_tableName}; + """; } /// public IEnumerable GetAll() { using var connection = _dataSource.CreateConnection(); - return connection.Query(GetAllQuery()); + return connection.Query(_getAllQuery); } /// public async Task> GetAllAsync() { await using var connection = _dataSource.CreateConnection(); - return await connection.QueryAsync(GetAllQuery()); + return await connection.QueryAsync(_getAllQuery); } /// diff --git a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs index 577f954..4b18234 100644 --- a/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs +++ b/src/AspNetCore.DataProtection.CustomStorage.Dapper.SQLServer/SQLServerDataProtectionRepository.cs @@ -16,16 +16,8 @@ public class SQLServerDataProtectionRepository : IDbDataProtectionStorage private readonly SqlConnectionFactory _connectionFactory; private readonly DapperDataProtectionConfig _config; private readonly string _tableName; + private readonly string _getAllQuery; - private string GetAllQuery() => - $""" - SELECT - [{nameof(DataProtectionKeyEntity.Id)}], - [{nameof(DataProtectionKeyEntity.InsertDate)}], - [{nameof(DataProtectionKeyEntity.FriendlyName)}], - [{nameof(DataProtectionKeyEntity.Xml)}] - FROM {_tableName}; - """; /// /// create an instance of /// @@ -38,20 +30,29 @@ public SQLServerDataProtectionRepository( _connectionFactory = connectionFactory; _config = options.Value; _tableName = $"{QuoteIdentifier(_config.SchemaName)}.{QuoteIdentifier(_config.TableName)}"; + _getAllQuery = + $""" + SELECT + [{nameof(DataProtectionKeyEntity.Id)}], + [{nameof(DataProtectionKeyEntity.InsertDate)}], + [{nameof(DataProtectionKeyEntity.FriendlyName)}], + [{nameof(DataProtectionKeyEntity.Xml)}] + FROM {_tableName}; + """; } /// public IEnumerable GetAll() { using var connection = _connectionFactory.CreateConnection(); - return connection.Query(GetAllQuery()); + return connection.Query(_getAllQuery); } /// public async Task> GetAllAsync() { await using var connection = _connectionFactory.CreateConnection(); - return await connection.QueryAsync(GetAllQuery()); + return await connection.QueryAsync(_getAllQuery); } /// @@ -63,8 +64,8 @@ public void Insert(DataProtectionKey key) connection.Execute( $""" INSERT INTO {_tableName}( - {nameof(DataProtectionKeyEntity.FriendlyName)}, - {nameof(DataProtectionKeyEntity.Xml)}) + [{nameof(DataProtectionKeyEntity.FriendlyName)}], + [{nameof(DataProtectionKeyEntity.Xml)}]) VALUES ( @{nameof(DataProtectionKeyEntity.FriendlyName)}, @{nameof(DataProtectionKeyEntity.Xml)})