`AddSqlServer` connection string ignores `TargetHost`: `EndpointProperty.IPV4Host` is hard-coded to `127.0.0.1` in the localhost network context
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
## Summary
Setting `TargetHost` on the SQL Server resource's `tcp` endpoint changes the DCP port binding and the allocated endpoint, but the connection string produced by `SqlServerServerResource` (and therefore the built-in `sql_check` / `_check` health checks and the `AddDatabase` creation step) still uses `127.0.0.1`. The cause is in `EndpointReferenceExpression.GetValueAsync`: `EndpointProperty.IPV4Host` returns the literal `"127.0.0.1"` whenever the network context is `LocalhostNetwork`, regardless of the endpoint's configured or allocated address.
This part of the report reproduces on any machine; no broken Docker is needed. The motivation (why anyone would set `TargetHost` to `[::1]`) is docker/desktop-feedback#622 (Windows IPv4-loopback defect, still present in Docker Desktop 4.90.0), but the bug itself is independent of it.
## Versions
- Aspire.Hosting 13.4.6 (+87fe259e), Aspire.Hosting.SqlServer 13.4.6, Aspire.Hosting.Testing 13.4.6, AppHost SDK 13.4.6 (also reproduced on 13.5.3 earlier)
- .NET SDK 10.0.400 / runtime 10.0.11, Microsoft.Data.SqlClient 7.0.2
- Windows 11 Pro 25H2 (build 26200), Docker Desktop 4.90.0 (238679), WSL 2 backend, Linux containers
## Reproduction (works on any OS)
```csharp
var builder = DistributedApplication.CreateBuilder(args);
var db = builder.AddSqlServer("sql")
.WithLifetime(ContainerLifetime.Session)
.WithEndpoint("tcp", e => { e.TargetHost = "[::1]"; e.IsProxied = false; })
.AddDatabase("bookstore-db");
builder.Build().Run();
```
From a test using `DistributedApplicationTestingBuilder`, after `StartAsync()` and waiting for `sql` to reach Running:
| What | Observed |
| --- | --- |
| `sql.GetEndpoint("tcp").Host` / `.Port` | `[::1]` / `1433` |
| `docker ps` port binding created by DCP | `[::1]:1433->1433/tcp` |
| `await app.GetConnectionStringAsync("bookstore-db")` data source | **`127.0.0.1,1433`** |
| `sql_check` health check | Unhealthy (connection refused, nothing listens on `127.0.0.1:1433`) |
| `AddDatabase` creation on `ResourceReadyEvent` | fails the same way; the failure surfaces from `WaitForResourceHealthyAsync("sql")` as the SqlException |
Expected: the connection string data source is `[::1],1433` (the endpoint's allocated address), so the health check and the database creation use the address that DCP actually published.
## Same result for every `TargetHost` value
Eight variants on the unchanged AppHost, all yielding a `127.0.0.1` data source:
| # | TargetHost | IsProxied | DCP binding | Connection-string host | Direct connection to the endpoint host |
| --- | --- | --- | --- | --- | --- |
| A | (default `localhost`) | false | `127.0.0.1:1433` | `127.0.0.1` | n/a |
| B | `[::1]` | false | `[::1]:1433` | `127.0.0.1` | login OK |
| C | `[::1]`, Port=51433 | false | `[::1]:51433` | `127.0.0.1` | login OK |
| D | `::1` | false | `[::1]:1433` | `127.0.0.1` | login OK |
| E | `localhost` | true | `127.0.0.1:` (proxy `localhost:`) | `127.0.0.1` | n/a |
| F | LAN IPv4 | true | `:` (proxy `:`) | `127.0.0.1` | login OK (through the proxy) |
| G | LAN IPv4 | false | `:1433` | `127.0.0.1` | login OK |
| H | `0.0.0.0` | false | `0.0.0.0:1433` | `127.0.0.1` | n/a |
"login OK" means PRELOGIN, TLS and SQL authentication all succeeded when connecting to `EndpointReference.Host:Port` directly (SqlException 4060 "Cannot open database" only because the database had not been created yet).
## Where it comes from
- `src/Aspire.Hosting.SqlServer/SqlServerServerResource.cs`: the private `ConnectionString` expression is `Server={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};…`.
- `src/Aspire.Hosting/ApplicationModel/EndpointReference.cs` (`EndpointReferenceExpression.GetValueAsync`):
```csharp
EndpointProperty.IPV4Host when networkContext == KnownNetworkIdentifiers.LocalhostNetwork => "127.0.0.1",
```
so for the localhost network the allocated address is never consulted.
## Proof that honouring `TargetHost` is sufficient
On a Windows machine where `127.0.0.1` to a published SQL Server port stalls in the TDS pre-login handshake (docker/desktop-feedback#622), a test-side experiment that (1) rewrote the connection string's data source to `EndpointReference.Host:Port`, (2) replaced `sql_check` / `bookstore-db_check` with a `SELECT 1` check over that rewritten string, and (3) waited with `WaitForResourceAsync("sql", e => e.Snapshot.HealthStatus == HealthStatus.Healthy)` instead of `WaitForResourceHealthyAsync` (to avoid awaiting the faulted `AddDatabase` creation task) made the full 44-test suite pass (28.6 s). Steps 2 and 3 exist only because the health check and the creation step reuse the same `127.0.0.1` string; if `IPV4Host` resolved to the allocated address, none of the three steps would be needed. I am not proposing that experiment as a workaround — it depends on internal registration names and on the `ResourceReadyEvent` wait semantics.
## Ask
Resolve `IPV4Host` from the allocated endpoint address (or fall back to `EndpointProperty.Host`) when the endpoint's `TargetHost` has been customised, so that `WithEndpoint("tcp", e => e.TargetHost = "[::1]")` produces a usable SQL Server connection string. Alternatively, let `SqlServerServerResource` use `Host` instead of `IPV4Host` when `TargetHost` is not the default.
A side observation while diagnosing: when the `AddDatabase` creation step fails, the SqlException surfaces from `WaitForResourceHealthyAsync` although the resource's own health check is Healthy; release/13.4 `SqlServerBuilderExtensions` looks like it only logs creation failures, so this may be worth a look on its own.
Related: #19803 (bind-address knob request), docker/desktop-feedback#622 (the Windows IPv4 loopback defect that makes this matter).
Contributor guide
Assessment
This issue has not been assessed yet.