Skip to content

Commit fffba4b

Browse files
committed
Add connection retry, connection managamanet and command retry to passkey handling
1 parent 3c404d2 commit fffba4b

15 files changed

Lines changed: 129 additions & 29 deletions

NpgsqlRest/ConnectionHelper.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using Npgsql;
2+
3+
namespace NpgsqlRest;
4+
5+
/// <summary>
6+
/// Helper methods for creating and opening database connections.
7+
/// </summary>
8+
public static class ConnectionHelper
9+
{
10+
/// <summary>
11+
/// Creates and opens a connection using the standard resolution order:
12+
/// 1. Named DataSource (if connectionName provided)
13+
/// 2. Named ConnectionString (if connectionName provided)
14+
/// 3. Default DataSource
15+
/// 4. Default ConnectionString
16+
/// </summary>
17+
/// <param name="options">NpgsqlRest options containing connection configuration</param>
18+
/// <param name="connectionName">Optional connection name for named DataSource or ConnectionString lookup</param>
19+
/// <param name="loggingMode">Mode for logging PostgreSQL NOTICE events</param>
20+
/// <param name="cancellationToken">Cancellation token</param>
21+
/// <param name="logger">Optional logger for retry logging</param>
22+
/// <returns>An opened NpgsqlConnection</returns>
23+
/// <exception cref="InvalidOperationException">
24+
/// Thrown when connectionName is specified but not found, or when no default connection is configured
25+
/// </exception>
26+
public static async Task<NpgsqlConnection> OpenConnectionAsync(
27+
NpgsqlRestOptions options,
28+
string? connectionName,
29+
PostgresConnectionNoticeLoggingMode loggingMode,
30+
CancellationToken cancellationToken,
31+
ILogger? logger = null)
32+
{
33+
NpgsqlConnection connection;
34+
35+
if (connectionName is not null)
36+
{
37+
// Named DataSource lookup
38+
if (options.DataSources?.TryGetValue(connectionName, out var namedDataSource) is true)
39+
{
40+
connection = namedDataSource.CreateConnection();
41+
}
42+
// Named ConnectionString lookup
43+
else if (options.ConnectionStrings?.TryGetValue(connectionName, out var connString) is true)
44+
{
45+
connection = new NpgsqlConnection(connString);
46+
}
47+
else
48+
{
49+
throw new InvalidOperationException(
50+
$"Connection name '{connectionName}' not found in DataSources or ConnectionStrings");
51+
}
52+
}
53+
else
54+
{
55+
// Default DataSource
56+
if (options.DataSource is not null)
57+
{
58+
connection = options.DataSource.CreateConnection();
59+
}
60+
// Default ConnectionString
61+
else if (!string.IsNullOrEmpty(options.ConnectionString))
62+
{
63+
connection = new NpgsqlConnection(options.ConnectionString);
64+
}
65+
else
66+
{
67+
throw new InvalidOperationException(
68+
"No DataSource or ConnectionString configured");
69+
}
70+
}
71+
72+
// Setup notice logging
73+
if (options.LogConnectionNoticeEvents)
74+
{
75+
connection.Notice += (sender, args) =>
76+
{
77+
NpgsqlRestLogger.LogConnectionNotice(args.Notice, loggingMode);
78+
};
79+
}
80+
81+
// Open with retry
82+
await connection.OpenRetryAsync(options.ConnectionRetryOptions, cancellationToken, logger);
83+
84+
return connection;
85+
}
86+
}

NpgsqlRestClient/Builder.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,8 @@ public void BuildPasskeyAuthentication()
749749
Enabled = true,
750750
EnableRegister = _config.GetConfigBool("EnableRegister", passkeyCfg, false),
751751
RateLimiterPolicy = _config.GetConfigStr("RateLimiterPolicy", passkeyCfg),
752+
ConnectionName = _config.GetConfigStr("ConnectionName", passkeyCfg),
753+
CommandRetryStrategy = _config.GetConfigStr("CommandRetryStrategy", passkeyCfg) ?? "default",
752754
RelyingPartyId = _config.GetConfigStr("RelyingPartyId", passkeyCfg),
753755
RelyingPartyName = _config.GetConfigStr("RelyingPartyName", passkeyCfg) ?? Instance.Environment.ApplicationName,
754756
RelyingPartyOrigins = _config.GetConfigEnumerable("RelyingPartyOrigins", passkeyCfg)?.ToArray() ?? [],

NpgsqlRestClient/ConfigDefaults.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ private static JsonObject GetAuthDefaults()
217217
["Enabled"] = false,
218218
["EnableRegister"] = false,
219219
["RateLimiterPolicy"] = null,
220+
["ConnectionName"] = null,
221+
["CommandRetryStrategy"] = "default",
220222
["RelyingPartyId"] = null,
221223
["RelyingPartyName"] = null,
222224
["RelyingPartyOrigins"] = new JsonArray(),

NpgsqlRestClient/Fido2/Endpoints/AddPasskeyEndpoint.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest,
114114

115115
CommandLogger.LogCommand(verifyCommand, ctx.Logger, LogChallengeVerify);
116116

117-
var challengeResult = await verifyCommand.ExecuteScalarAsync(context.RequestAborted);
117+
var challengeResult = await verifyCommand.ExecuteScalarWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger);
118118
if (challengeResult == null || challengeResult == DBNull.Value)
119119
{
120120
await ExecuteTransactionCommandAsync(connection, "ROLLBACK", context.RequestAborted);
@@ -255,7 +255,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest,
255255
int storeStatus = 200;
256256
string storeMessage = "Passkey added successfully";
257257

258-
await using (var storeReader = await storeCommand.ExecuteReaderAsync(context.RequestAborted))
258+
await using (var storeReader = await storeCommand.ExecuteReaderWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger))
259259
{
260260
if (await storeReader.ReadAsync(context.RequestAborted))
261261
{

NpgsqlRestClient/Fido2/Endpoints/AddPasskeyOptionsEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.Unauthorized,
118118
string? excludeCredentialsJson = null;
119119
string? userContextJson = null;
120120

121-
await using (var reader = await command.ExecuteReaderAsync(context.RequestAborted))
121+
await using (var reader = await command.ExecuteReaderWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger))
122122
{
123123
if (!await reader.ReadAsync(context.RequestAborted))
124124
{

NpgsqlRestClient/Fido2/Endpoints/LoginEndpoint.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest,
131131

132132
CommandLogger.LogCommand(verifyCommand, ctx.Logger, LogChallengeVerify);
133133

134-
var challengeResult = await verifyCommand.ExecuteScalarAsync(context.RequestAborted);
134+
var challengeResult = await verifyCommand.ExecuteScalarWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger);
135135
if (challengeResult == null || challengeResult == DBNull.Value)
136136
{
137137
await ExecuteTransactionCommandAsync(connection, "ROLLBACK", context.RequestAborted);
@@ -155,7 +155,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest,
155155
long storedSignCount = 0;
156156
string? userContext = null;
157157

158-
await using (var dataReader = await dataCommand.ExecuteReaderAsync(context.RequestAborted))
158+
await using (var dataReader = await dataCommand.ExecuteReaderWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger))
159159
{
160160
if (!await dataReader.ReadAsync(context.RequestAborted))
161161
{

NpgsqlRestClient/Fido2/Endpoints/LoginOptionsEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public async Task InvokeAsync(HttpContext context)
9797
string? challengeId = null;
9898
string? allowCredentialsJson = null;
9999

100-
await using (var reader = await command.ExecuteReaderAsync(context.RequestAborted))
100+
await using (var reader = await command.ExecuteReaderWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger))
101101
{
102102
if (!await reader.ReadAsync(context.RequestAborted))
103103
{

NpgsqlRestClient/Fido2/Endpoints/RegistrationEndpoint.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest,
106106

107107
CommandLogger.LogCommand(verifyCommand, ctx.Logger, LogChallengeVerify);
108108

109-
var challengeResult = await verifyCommand.ExecuteScalarAsync(context.RequestAborted);
109+
var challengeResult = await verifyCommand.ExecuteScalarWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger);
110110
if (challengeResult == null || challengeResult == DBNull.Value)
111111
{
112112
await ExecuteTransactionCommandAsync(connection, "ROLLBACK", context.RequestAborted);
@@ -247,7 +247,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest,
247247
int storeStatus = 200;
248248
string storeMessage = "Passkey registered successfully";
249249

250-
await using (var storeReader = await storeCommand.ExecuteReaderAsync(context.RequestAborted))
250+
await using (var storeReader = await storeCommand.ExecuteReaderWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger))
251251
{
252252
if (await storeReader.ReadAsync(context.RequestAborted))
253253
{

NpgsqlRestClient/Fido2/Endpoints/RegistrationOptionsEndpoint.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ await WriteErrorResponseAsync(context, HttpStatusCode.BadRequest,
127127
string? excludeCredentialsJson = null;
128128
string? userContextJson = null;
129129

130-
await using (var reader = await command.ExecuteReaderAsync(context.RequestAborted))
130+
await using (var reader = await command.ExecuteReaderWithRetryAsync(ctx.RetryStrategy, context.RequestAborted, ctx.Logger))
131131
{
132132
if (!await reader.ReadAsync(context.RequestAborted))
133133
{

NpgsqlRestClient/Fido2/PasskeyAuth.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ public static class PasskeyAuth
99
public static void UsePasskeyAuth(
1010
this WebApplication app,
1111
PasskeyConfig? config,
12-
string connectionString,
1312
NpgsqlRestOptions options,
14-
RetryStrategy? retryStrategy,
13+
CommandRetryOptions commandRetryOptions,
1514
PostgresConnectionNoticeLoggingMode loggingMode)
1615
{
1716
if (config?.Enabled != true)
@@ -21,11 +20,17 @@ public static void UsePasskeyAuth(
2120

2221
Logger = app.Services.GetService<ILoggerFactory>()?.CreateLogger("PasskeyAuth");
2322

23+
// Resolve command retry strategy from config
24+
RetryStrategy? commandRetryStrategy = null;
25+
if (commandRetryOptions.Enabled && !string.IsNullOrEmpty(config.CommandRetryStrategy))
26+
{
27+
commandRetryOptions.Strategies.TryGetValue(config.CommandRetryStrategy, out commandRetryStrategy);
28+
}
29+
2430
var ctx = new PasskeyEndpointContext(
2531
config,
26-
connectionString,
2732
options,
28-
retryStrategy,
33+
commandRetryStrategy,
2934
loggingMode,
3035
Logger);
3136

0 commit comments

Comments
 (0)