Centralize Postgres connection string policy across all modules
Extract connection string building into PostgresConnectionStringPolicy so all services use consistent pooling, application_name, and timeout settings. Adopt the new policy in 20+ module DataSource/ServiceCollectionExtensions classes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
## Determinism & Guardrails
|
||||
- Target runtime: .NET 10, Npgsql 9.x; keep options defaults deterministic (UTC timezone, statement timeout, stable pagination ordering).
|
||||
- Tenant context must be set via `set_config('app.current_tenant', ...)` on every connection before use; never bypass DataSourceBase.
|
||||
- Runtime `NpgsqlDataSource` creation must carry a stable `application_name`; prefer `PostgresConnectionStringPolicy` or an explicit `NpgsqlConnectionStringBuilder.ApplicationName`.
|
||||
- Anonymous `NpgsqlDataSource.Create(...)` is forbidden in steady-state runtime code. Allow exceptions only for tests, migrations, CLI/setup paths, or sprint-documented one-shot diagnostics.
|
||||
- Migrations ship as embedded resources; MigrationRunner uses SHA256 checksums and `RunFromAssemblyAsync`???do not execute ad-hoc SQL outside tracked migrations.
|
||||
- Respect air-gap posture: no external downloads at runtime; pin Postgres/Testcontainers images (`postgres:16-alpine` or later) in tests.
|
||||
|
||||
@@ -30,4 +32,3 @@
|
||||
## Handoff Notes
|
||||
- Align configuration defaults with the provisioning values under `devops/database/postgres` (ports, pool sizes, SSL/TLS).
|
||||
- Update this AGENTS file whenever connection/session rules or provisioning defaults change; record updates in the sprint Execution Log.
|
||||
|
||||
|
||||
@@ -241,15 +241,8 @@ public abstract class DataSourceBase : IAsyncDisposable
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static string BuildConnectionString(PostgresOptions options)
|
||||
{
|
||||
var builder = new NpgsqlConnectionStringBuilder(options.ConnectionString)
|
||||
{
|
||||
Pooling = options.Pooling,
|
||||
MaxPoolSize = options.MaxPoolSize,
|
||||
MinPoolSize = options.MinPoolSize
|
||||
};
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
private string BuildConnectionString(PostgresOptions options)
|
||||
=> PostgresConnectionStringPolicy.Build(
|
||||
options,
|
||||
PostgresConnectionStringPolicy.BuildDefaultApplicationName(ModuleName));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using Npgsql;
|
||||
using StellaOps.Infrastructure.Postgres.Options;
|
||||
|
||||
namespace StellaOps.Infrastructure.Postgres.Connections;
|
||||
|
||||
/// <summary>
|
||||
/// Applies StellaOps runtime defaults to PostgreSQL connection strings.
|
||||
/// </summary>
|
||||
public static class PostgresConnectionStringPolicy
|
||||
{
|
||||
public static string Build(PostgresOptions options, string defaultApplicationName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(defaultApplicationName);
|
||||
|
||||
return Build(
|
||||
options.ConnectionString,
|
||||
string.IsNullOrWhiteSpace(options.ApplicationName)
|
||||
? defaultApplicationName.Trim()
|
||||
: options.ApplicationName.Trim(),
|
||||
options.Pooling,
|
||||
options.MinPoolSize,
|
||||
options.MaxPoolSize,
|
||||
options.ConnectionIdleLifetimeSeconds);
|
||||
}
|
||||
|
||||
public static string Build(
|
||||
string connectionString,
|
||||
string applicationName,
|
||||
bool pooling = true,
|
||||
int minPoolSize = 1,
|
||||
int maxPoolSize = 100,
|
||||
int? connectionIdleLifetimeSeconds = null)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(applicationName);
|
||||
|
||||
var normalizedMinPoolSize = Math.Max(minPoolSize, 0);
|
||||
var normalizedMaxPoolSize = Math.Max(maxPoolSize, normalizedMinPoolSize);
|
||||
|
||||
var builder = new NpgsqlConnectionStringBuilder(connectionString)
|
||||
{
|
||||
ApplicationName = applicationName.Trim(),
|
||||
Pooling = pooling,
|
||||
MinPoolSize = normalizedMinPoolSize,
|
||||
MaxPoolSize = normalizedMaxPoolSize,
|
||||
};
|
||||
|
||||
if (connectionIdleLifetimeSeconds.HasValue && connectionIdleLifetimeSeconds.Value > 0)
|
||||
{
|
||||
builder.ConnectionIdleLifetime = connectionIdleLifetimeSeconds.Value;
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static string BuildDefaultApplicationName(string moduleName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(moduleName);
|
||||
|
||||
return $"stellaops-{ToKebabCase(moduleName.Trim())}";
|
||||
}
|
||||
|
||||
private static string ToKebabCase(string value)
|
||||
{
|
||||
var chars = new List<char>(value.Length + 8);
|
||||
for (var i = 0; i < value.Length; i++)
|
||||
{
|
||||
var current = value[i];
|
||||
if (char.IsWhiteSpace(current) || current is '_' or '-')
|
||||
{
|
||||
if (chars.Count > 0 && chars[^1] != '-')
|
||||
{
|
||||
chars.Add('-');
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsUpper(current))
|
||||
{
|
||||
var hasPrevious = chars.Count > 0;
|
||||
var previous = hasPrevious ? value[i - 1] : '\0';
|
||||
var hasNext = i + 1 < value.Length;
|
||||
var next = hasNext ? value[i + 1] : '\0';
|
||||
|
||||
if (hasPrevious
|
||||
&& chars[^1] != '-'
|
||||
&& (!char.IsUpper(previous) || (hasNext && char.IsLower(next))))
|
||||
{
|
||||
chars.Add('-');
|
||||
}
|
||||
|
||||
chars.Add(char.ToLowerInvariant(current));
|
||||
continue;
|
||||
}
|
||||
|
||||
chars.Add(char.ToLowerInvariant(current));
|
||||
}
|
||||
|
||||
return new string(chars.ToArray()).Trim('-');
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ public sealed class PostgresOptions
|
||||
/// </summary>
|
||||
public int MaxPoolSize { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Stable PostgreSQL application name used for runtime attribution.
|
||||
/// </summary>
|
||||
public string? ApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Minimum number of connections in the pool. Default is 1.
|
||||
/// </summary>
|
||||
|
||||
@@ -5,6 +5,7 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| SPRINT_20260405_011-XPORT-STD | DONE | `docs/implplan/SPRINT_20260405_011___Libraries_transport_pooling_and_attribution_hardening.md`: shared transport hardening, PostgreSQL application-name policy, runtime caller remediation, and static guardrails. |
|
||||
| AUDIT-0089-M | DONE | Revalidated 2026-01-08; maintainability audit for Infrastructure.Postgres. |
|
||||
| AUDIT-0089-T | DONE | Revalidated 2026-01-08; test coverage audit for Infrastructure.Postgres. |
|
||||
| AUDIT-0089-A | TODO | Pending approval (non-test project; revalidated 2026-01-08). |
|
||||
|
||||
Reference in New Issue
Block a user