microsoft/aspire · error · InvalidOperationException
A ConfigureRadiusInfrastructure callback removed port
Error message
A ConfigureRadiusInfrastructure callback removed port '{portName}' from container '{mapKey}'. Aspire service discovery already emitted this port ({expected.Port}) into consumer 'services__*' variables, so removing it would break cross-container calls. Remove the port change to keep service discovery consistent. What it means
Service discovery variables already embed a container's port. A ConfigureRadiusInfrastructure callback removed the port (or set it to a non-port value), so consumers' services__* variables would point at a port that no longer exists; the publisher throws InvalidOperationException and asks the user to revert the port change.
Solutions
- Restore the port/endpoint on the container (same name and port value).
- If the port must change, change it in the app model before publishing so discovery variables are regenerated, not in a callback.
- Remove the port-mutating code from the callback entirely.
- Verify every pre-callback port in the snapshot still exists post-callback with an integer value.
Example fix
// before
infra.Callback(c => c.Resources.OfType<ContainerResource>().First(x => x.Name == "api").Ports.Remove("http"));
// after
infra.Callback(c => { /* keep ports intact */ }); Defensive patterns
Strategy: validation
Validate before calling
foreach (var (name, port) in preCallbackPorts)
{
var c = finalContainers.FirstOrDefault(x => x.Name == containerName);
if (c?.Ports.TryGetValue(name, out var v) != true || v is not int)
throw new InvalidOperationException($"Port '{name}' must remain an integer on '{containerName}'.");
} Try / catch
try { PublishAsync(model); }
catch (InvalidOperationException ex) when (ex.Message.Contains("callback removed port"))
{ log.LogError(ex, "Restore port '{Port}' on container '{Container}'.", ex.Data["port"], ex.Data["mapKey"]); } Prevention
- Never remove or retarget existing endpoints/ports inside publish callbacks.
- Change port topology in the AppHost before publishing so discovery variables regenerate.
- Keep endpoint names and port numbers stable once consumers reference them.
- Diff the pre/post-callback port snapshot in local publish runs.
When it happens
Trigger: A ConfigureRadiusInfrastructure callback removes an endpoint/port (or mutates container.Ports so the entry is missing or not a port value) that existed pre-callback with a known port number used in emitted discovery variables.
Common situations: Callback 'tightening' exposed ports; swapping WithEndpoint definitions inside a callback; changing endpoint names so the port lookup misses; setting the port value to a non-integer expression.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- A ConfigureRadiusInfrastructure callback removed or…
- A ConfigureRadiusInfrastructure callback changed port
- A ConfigureRadiusInfrastructure callback renamed container
- A ConfigureRadiusInfrastructure callback replaced container
- A ConfigureRadiusInfrastructure callback replaced port
AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16).
Data as JSON: /api/errors/889b8f3a09e84dbd.
Report an issue: GitHub.
Appendix: source
Thrown at src/Aspire.Hosting.Radius/Publishing/RadiusInfrastructureBuilder.cs:4969
{
throw new InvalidOperationException(
$"A ConfigureRadiusInfrastructure callback removed or replaced container '{mapKey}'. Aspire " +
$"service discovery already emitted 'services__*' variables that address it, so dropping the " +
$"workload would break cross-container calls. Keep the container to keep service discovery consistent.");
}
// Only containers that had service ports pre-callback have a Service (`{name}-{name}`)
// that `services__*` addresses, so the name/map-key equality is only required for them.
// A portless baseline container or one added entirely by the callback has no service-
// discovery contract — Radius permits its top-level name to differ from the map key — so
// gating this check on a non-empty snapshot keeps the customization escape hatch open.
ValidateContainerNameMatchesMapKey(container);
foreach (var (portName, expected) in snapshot)
{
if (!container.Ports.TryGetValue(portName, out var portValue) || portValue.Value is not { } port)
{
throw new InvalidOperationException(
$"A ConfigureRadiusInfrastructure callback removed port '{portName}' from container " +
$"'{mapKey}'. Aspire service discovery already emitted this port ({expected.Port}) into " +
$"consumer 'services__*' variables, so removing it would break cross-container calls. " +
$"Remove the port change to keep service discovery consistent.");
}
// Reject a non-literal port/protocol: service discovery is a fixed literal, so a
// callback that swaps in a Bicep expression could evaluate to a different value at
// deploy time, reintroducing exactly the mismatch this guard prevents. An
// expression-backed BicepValue<int> reports a default LiteralValue of 0 (not null),
// so a non-null Expression is the reliable "non-literal" signal, not the LiteralValue.
var portValueBicep = (IBicepValue)port.ContainerPort;
if (portValueBicep.Expression is not null || portValueBicep.LiteralValue is not int literalPort)
{
throw new InvalidOperationException(
$"A ConfigureRadiusInfrastructure callback replaced port '{portName}' on container " +
$"'{mapKey}' with a non-literal Bicep expression. Aspire service discovery already emitted " +
$"the literal port {expected.Port} into consumer 'services__*' variables and cannot follow a " +View on GitHub (pinned to 25830f84bd)