Normalize container image reference representation in Aspire 14
- Dominant language
- C#
- Stars
- 6.3k
- Forks
- 991
- Avg merge
- 2d 15h
- Merged PRs (30d)
- 196
Description
## Background and Motivation
`ContainerImageAnnotation` currently represents a container reference through independently mutable `Registry`, `Image`, `Tag`, and `SHA256` properties, but the repository does not consistently populate those properties.
In particular, `WithImage(...)` preserves Aspire 9.0 behavior by folding a registry-qualified reference into `Image` and leaving `Registry` unset:
```csharp
// mcr.microsoft.com/dotnet/aspnet:10.0
annotation.Registry = null;
annotation.Image = "mcr.microsoft.com/dotnet/aspnet";
annotation.Tag = "10.0";
```
Newer code, including the resource projection implementation in #19880, needs the structured form expected by registry-aware features:
```csharp
annotation.Registry = "mcr.microsoft.com";
annotation.Image = "dotnet/aspnet";
annotation.Tag = "10.0";
```
The compatibility behavior is called out in [this review discussion](https://github.com/microsoft/aspire/pull/19880#discussion_r3970961837). It prevents registry-aware behavior such as image mirroring and `WithContainerRegistry` from reliably understanding image references. It also means consumers must account for multiple valid internal representations and can disagree about whether the first or last annotation/property mutation is authoritative.
There is a related selector-mutation problem previously tracked by #5834: replacing only the image repository currently defaults the tag to `latest`, overwriting an existing explicit tag or digest even when the caller did not supply a replacement selector.
Aspire 14.0 is the appropriate opportunity to make a breaking change to the annotation contract, normalize the representation globally, and remove compatibility branches that perpetuate the ambiguity.
## Proposed API
The exact API shape should go through API review, but `ContainerImageAnnotation` should own a single canonical parsed image-reference representation rather than expose four independently mutable values whose invariants every caller must reproduce.
One possible shape is:
```diff
namespace Aspire.Hosting.ApplicationModel;
public sealed class ContainerImageAnnotation : IResourceAnnotation
{
- public required string Image { get; set; }
- public string? Registry { get; set; }
- public string? Tag { get; set; }
- public string? SHA256 { get; set; }
+ public ContainerImageAnnotation(string imageReference);
+
+ public string ImageReference { get; set; }
+ public string Image { get; }
+ public string? Registry { get; }
+ public string? Tag { get; }
+ public string? Digest { get; }
}
```
The final shape does not need to match this sketch, but it should enforce these invariants centrally:
- `Image` never contains the registry.
- `Registry` contains only the registry host/port when one was supplied.
- A tag and digest are mutually exclusive.
- A digest retains its algorithm-qualified form rather than requiring each caller to add or remove `sha256:`.
- Parsing and formatting round-trip through one implementation.
- Updating only the repository/image component does not silently replace an existing tag or digest with `latest`; callers that intend to replace the complete reference can do so explicitly.
Update `WithImage`, `WithImageRegistry`, `WithImageTag`, `WithImageSHA256` (or its 14.0 replacement), `AddContainer`, Dockerfile image generation, projections, publishers, deployment targets, manifest serialization, and registry/mirroring logic to use the canonical representation.
## Usage Examples
A complete image reference should be decomposed consistently everywhere:
```csharp
var api = builder.AddContainer("api", "mcr.microsoft.com/contoso/api:2.1");
// Canonical annotation state:
// Registry: mcr.microsoft.com
// Image: contoso/api
// Tag: 2.1
// Digest: null
```
Replacing only the repository should preserve the existing selector:
```csharp
var redis = builder.AddRedis("redis")
.WithImage("docker.io/library/redis");
// Preserve the integration's pinned Redis tag or digest unless the new
// reference explicitly contains a tag or digest.
```
Replacing the complete reference should update all components atomically:
```csharp
redis.WithImageReference("mirror.example.com/library/redis@sha256:...");
```
Registry-aware features should no longer need to detect whether the registry was embedded in `Image`:
```csharp
redis.WithContainerRegistry(registry);
```
## Alternative Designs
- **Change only `WithImage` while retaining the current annotation.** This fixes the primary writer but leaves public object initializers and other writers able to construct ambiguous states. Every reader would still need compatibility logic.
- **Add normalization helpers without breaking the annotation.** This is suitable for additive releases but cannot guarantee a canonical representation because the mutable public properties remain independently assignable.
- **Keep the current annotation and document conventions.** Existing behavior demonstrates that conventions alone are insufficient; different writers already produce different valid-looking states.
- **Add a new annotation and retain `ContainerImageAnnotation`.** This avoids an API break but creates another precedence problem and requires every reader to support both representations indefinitely.
## Risks
- This is a deliberate source and binary breaking change and must target Aspire 14.0.
- Community integrations that directly construct or mutate `ContainerImageAnnotation` will need to rebuild and may require small source changes. The preferred design should make those changes mechanical and produce compile-time guidance rather than silently changing behavior.
- Changing omitted-tag semantics can alter image selection for callers that currently rely on `WithImage("repository")` forcing `latest`. Migration guidance must show how to request `latest` explicitly.
- All container-reference readers and writers must move together. Partial migration would be worse than the current state, so focused tests should cover parsing, mutation ordering, manifests, DCP, Docker Compose, Kubernetes, Azure publishing, Radius, image mirroring/registry replacement, tags, and digests.
- Manifest and generated SDK compatibility should be reviewed separately from the CLR API break so serialized references remain intentional.
Contributor guide
Research direction
Start with ContainerImageAnnotation and the named WithImage, WithImageRegistry, WithImageTag, WithImageSHA256, and AddContainer entry points, then trace the listed Dockerfile, projection, publisher, deployment, manifest, and registry consumers. Review the focused tests described in the issue for parsing, mutation ordering, manifests, deployment targets, tags, and digests. Done means one canonical representation is used consistently, selector mutations preserve existing values, and the intentional Aspire 14.0 API break is covered across those consumers.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, docker
- Domain
- devops, infrastructure
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100