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 / <db>_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)
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:<random> (proxy localhost:<port>) |
127.0.0.1 |
n/a |
| F |
LAN IPv4 |
true |
<LAN>:<random> (proxy <LAN>:<port>) |
127.0.0.1 |
login OK (through the proxy) |
| G |
LAN IPv4 |
false |
<LAN>: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):
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).
Summary
Setting
TargetHoston the SQL Server resource'stcpendpoint changes the DCP port binding and the allocated endpoint, but the connection string produced bySqlServerServerResource(and therefore the built-insql_check/<db>_checkhealth checks and theAddDatabasecreation step) still uses127.0.0.1. The cause is inEndpointReferenceExpression.GetValueAsync:EndpointProperty.IPV4Hostreturns the literal"127.0.0.1"whenever the network context isLocalhostNetwork, 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
TargetHostto[::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
Reproduction (works on any OS)
From a test using
DistributedApplicationTestingBuilder, afterStartAsync()and waiting forsqlto reach Running:sql.GetEndpoint("tcp").Host/.Port[::1]/1433docker psport binding created by DCP[::1]:1433->1433/tcpawait app.GetConnectionStringAsync("bookstore-db")data source127.0.0.1,1433sql_checkhealth check127.0.0.1:1433)AddDatabasecreation onResourceReadyEventWaitForResourceHealthyAsync("sql")as the SqlExceptionExpected: 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
TargetHostvalueEight variants on the unchanged AppHost, all yielding a
127.0.0.1data source:localhost)127.0.0.1:1433127.0.0.1[::1][::1]:1433127.0.0.1[::1], Port=51433[::1]:51433127.0.0.1::1[::1]:1433127.0.0.1localhost127.0.0.1:<random>(proxylocalhost:<port>)127.0.0.1<LAN>:<random>(proxy<LAN>:<port>)127.0.0.1<LAN>:1433127.0.0.10.0.0.00.0.0.0:1433127.0.0.1"login OK" means PRELOGIN, TLS and SQL authentication all succeeded when connecting to
EndpointReference.Host:Portdirectly (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 privateConnectionStringexpression isServer={PrimaryEndpoint.Property(EndpointProperty.IPV4Host)},{PrimaryEndpoint.Property(EndpointProperty.Port)};….src/Aspire.Hosting/ApplicationModel/EndpointReference.cs(EndpointReferenceExpression.GetValueAsync):so for the localhost network the allocated address is never consulted.
Proof that honouring
TargetHostis sufficientOn a Windows machine where
127.0.0.1to 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 toEndpointReference.Host:Port, (2) replacedsql_check/bookstore-db_checkwith aSELECT 1check over that rewritten string, and (3) waited withWaitForResourceAsync("sql", e => e.Snapshot.HealthStatus == HealthStatus.Healthy)instead ofWaitForResourceHealthyAsync(to avoid awaiting the faultedAddDatabasecreation 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 same127.0.0.1string; ifIPV4Hostresolved 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 theResourceReadyEventwait semantics.Ask
Resolve
IPV4Hostfrom the allocated endpoint address (or fall back toEndpointProperty.Host) when the endpoint'sTargetHosthas been customised, so thatWithEndpoint("tcp", e => e.TargetHost = "[::1]")produces a usable SQL Server connection string. Alternatively, letSqlServerServerResourceuseHostinstead ofIPV4HostwhenTargetHostis not the default.A side observation while diagnosing: when the
AddDatabasecreation step fails, the SqlException surfaces fromWaitForResourceHealthyAsyncalthough the resource's own health check is Healthy; release/13.4SqlServerBuilderExtensionslooks 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).