◐ Shell
clean mode source ↗

Add DNS SRV service discovery support (RFC 2782) by x4m · Pull Request #6554 · npgsql/npgsql

@x4m

Introduce a new `SrvHost` connection string property that enables a single
DNS name to represent an entire high-availability PostgreSQL cluster.  When
set, Npgsql resolves `_postgresql._tcp.<SrvHost>` SRV records at
`NpgsqlDataSource` build time and uses the returned host/port pairs as the
multi-host list, sorted by priority (ascending) then weight (descending) per
RFC 2782.

### Changes

**`NpgsqlConnectionStringBuilder`**
- New `SrvHost` string property, exposed as the `SrvHost` keyword in
  connection strings (e.g. `SrvHost=cluster.example.com`).
- `PostProcessAndValidate` updated to allow a null/empty `Host` when `SrvHost`
  is set, and to enforce mutual exclusivity: supplying both throws
  `ArgumentException`.

**`SrvLookup.cs`** (new)
- Static helper class that queries `_postgresql._tcp.<srvHost>` via the
  `DnsClient` NuGet package, sorts SRV records by priority / weight, strips
  trailing FQDN dots, and returns a comma-separated `host:port,host:port,...`
  string ready for use as `NpgsqlConnectionStringBuilder.Host`.
- `SortAndBuild(IEnumerable<SrvRecord>)` is `internal` so unit tests can
  exercise sorting logic without a live DNS server.

**`NpgsqlSlimDataSourceBuilder` / `NpgsqlDataSourceBuilder`**
- New `SrvLookupClient` property (`DnsClient.ILookupClient?`).  When `null`
  (the default), the OS resolver is used.  Inject a custom client in tests to
  return deterministic SRV records without hitting DNS.
- `Build()` and `BuildMultiHost()` call `ResolveSrvIfNeeded()` before
  `PostProcessAndValidate()`, expanding SRV results into the `Host` property
  so the existing multi-host path handles all subsequent connection logic.

**Dependency**
- `DnsClient 1.8.0` added to `Directory.Packages.props` and `Npgsql.csproj`.

### New test project — `Npgsql.SrvTests`

Isolated from the main `Npgsql.Tests` assembly (which requires a live
PostgreSQL server) so SRV unit tests can run on any machine without a
database.

Unit tests cover:
- Connection-string roundtrip and keyword parsing.
- `SrvHost` / `Host` mutual exclusivity.
- RFC 2782 sort order: priority ascending, weight descending.
- Trailing-dot stripping from FQDNs returned by DnsClient.
- Priority/weight ordering mirroring the real records at `mmatvei.ru`.
- Empty result set throws `NpgsqlException`.

`ResolveSrvLive` performs an end-to-end DNS lookup against
`_postgresql._tcp.mmatvei.ru` (four real SRV records, priorities 96–100)
and verifies ordering.  The test skips automatically if the records are
unreachable.  Set `NPGSQL_TEST_SRV_DNS=<ip>` to force a specific nameserver
(useful when the system resolver has a stale negative cache).

### Usage

```csharp
// Connection string keyword
var ds = NpgsqlDataSource.Create(
    "SrvHost=cluster.example.com;Database=app;Username=app_user");

// Builder API
var builder = new NpgsqlDataSourceBuilder();
builder.ConnectionStringBuilder.SrvHost = "cluster.example.com";
builder.ConnectionStringBuilder.Database = "app";
var ds = builder.Build();
```

### Connection string format
```
SrvHost=cluster.example.com;Database=mydb;Username=myuser;Password=...
```

### Notes
- SRV resolution happens once at `Build()` time.  Re-build the data source to
  re-query DNS (matches how `NpgsqlMultiHostDataSource` works today).
- `TargetSessionAttributes` (e.g. `read-write`, `primary`, `standby`) work
  unchanged with the resolved host list.
- `SrvHost` and `Host` are mutually exclusive; mixing them throws
  `ArgumentException` at build time.

Made-with: Cursor