microsoft / microsoft/aspire

Design Proposal: Radius Compute Environment for Aspire

Open
#16,844 0 comments 3 reactions 0 assignees View on GitHub
area-deployment area-integrations
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 15h
Merged PRs (30d)
196

Description

# Aspire.Hosting.Radius: Radius as a Compute Environment for Aspire

## Summary

This issue proposes adding [Radius](https://radapp.io) as a first-class compute environment for Aspire via a new `Aspire.Hosting.Radius` NuGet package. This enables developers to deploy Aspire applications to Kubernetes (and potentially other platforms in the future) through Radius using the standard `aspire run` / `aspire publish` / `aspire deploy` workflow — without modifying how resources are declared in the app host.

Radius is an open-source, cloud-native application platform that runs on Kubernetes and provides resource abstractions (Redis, SQL, MongoDB, RabbitMQ, etc.) backed by swappable "recipes." This maps naturally to Aspire's resource model, where `AddRedis("cache")` or `AddSqlServer("sql")` are declared once and materialized differently depending on the target compute environment.

> **Important context**: Radius is actively transitioning from built-in "portable resource types" (e.g., `Applications.Datastores/redisCaches`) to a fully extensible **user-defined resource type (UDT)** model (e.g., `Radius.Data/redisCaches`). Portable types will be removed from Radius in a future release and re-implemented as UDTs in the [resource-types-contrib](https://github.com/radius-project/resource-types-contrib) repository. This proposal is designed with that transition in mind — see [User-Defined Resource Types](#user-defined-resource-types-udt-transition) for details.

A working implementation has been built and tested against the Aspire source tree — the source code can be found [here](https://github.com/nellshamrell/aspire/commit/ca0266f10fb15adc781bb52e76d8aa991ae6d8b0). This proposal describes the design, API surface, and architecture based on that implementation.

NOTE — A previous prototype included a preview version of the Radius graph (through the Radius dashboard) as part of `aspire run`. On the Radius side, we are currently designing those changes — a preview version of the Radius app graph — as part of a larger effort. I will open a new design issue when those are complete and propose adding a preview of the Radius graph to `aspire run` at that time.

## Motivation

### Why Radius + Aspire?

Aspire already supports multiple compute environments — Azure Container Apps, Docker Compose, Kubernetes, and Azure App Service. Radius adds a distinct value:

1. **Cloud-agnostic Kubernetes deployment** — Radius deploys to any Kubernetes cluster (AKS, EKS, GKE, on-prem), not just Azure. Aspire's existing Kubernetes support generates manifests but lacks an `aspire deploy` story; Radius closes that gap.

2. **Infrastructure abstraction via recipes** — Platform teams author Radius recipes (Bicep/Terraform templates) that enforce organizational standards. App developers declare resources in Aspire; Radius handles provisioning per the platform team's policies. Recipe packs bundle these recipes into versioned, shareable units.

### Alignment with Aspire Roadmap

The [deployment roadmap](https://github.com/microsoft/aspire/pull/15594) (PR [#15594](https://github.com/microsoft/aspire/pull/15594), authored by [@mitchdenny](https://github.com/mitchdenny)) explicitly calls out Radius integration under Pillar 3 ("Make Azure Awesome"):

> _"Radius support is progressing in the background. Radius provides a cloud-agnostic application platform that aligns well with Aspire's model. As this integration matures, we need to ensure it composes well with the rest of the deployment story — particularly compute environments and pipeline generation."_

This proposal defines what that integration looks like concretely.

## Proposed API

### Registration

```csharp
var builder = DistributedApplication.CreateBuilder(args);

// Add Radius as the compute environment
builder.AddRadiusEnvironment("radius")
.WithNamespace("my-namespace");

// Declare resources as usual — Radius handles deployment
var cache = builder.AddRedis("cache");
var sql = builder.AddSqlServer("sql").AddDatabase("appdb");

var api = builder.AddProject("api")
.WithReference(cache)
.WithReference(sql)
.WithHttpEndpoint(port: 8080, name: "http");

builder.Build().Run();
```

This mirrors the existing patterns:

* `builder.AddDockerComposeEnvironment("compose")`
* `builder.AddAzureContainerAppEnvironment("aca")`

### Legacy Container Opt-In

Radius is transitioning container workloads to the `Radius.Compute/containers` UDT, which requires a recipe to be registered in the target environment. For Radius installs that do not yet have that recipe (e.g., older clusters, or environments where the platform team hasn't installed the UDT yet), the environment can opt back into the legacy container type via `WithLegacyContainers()`:

```csharp
builder.AddRadiusEnvironment("radius")
.WithNamespace("my-namespace")
.WithLegacyContainers();
```

When enabled, the publisher emits container workloads as `Applications.Core/containers@2023-10-01-preview` (parented to the legacy application), which ships with built-in Kubernetes deployment behavior and does not require a recipe. When this flag is set **and** there are no UDT resource-type instances in the app, the entire UDT environment / application / recipe pack chain is skipped, producing pure-legacy Bicep that older Radius installs can deploy without modification. This is intended as a transitional escape hatch; new apps should target the UDT container by default.

### Per-Resource Customization

```csharp
// Override the Radius recipe for a specific resource
cache.PublishAsRadiusResource(r =>
{
r.Recipe = new RadiusRecipe
{
Name = "azure-redis-premium",
RecipeLocation = "br:myregistry.azurecr.io/recipes/azure-redis:latest",
Parameters = new Dictionary
{
["sku"] = "Premium",
["capacity"] = 2,
}
};
});

// Use manual provisioning (pre-existing infrastructure, no recipe)
// Note: Manual provisioning is expected to be retired in a future Radius release.
postgres.PublishAsRadiusResource(r =>
{
r.Provisioning = ResourceProvisioning.Manual;
r.ConnectionStringOverrides["host"] = "postgres.default.svc.cluster.local";
r.ConnectionStringOverrides["port"] = "5432";
});

// Override the Radius type for early UDT adoption or custom types
cache.PublishAsRadiusResource(r =>
{
r.RadiusType = "MyOrg.Custom/myRedis";
r.RadiusApiVersion = "2025-01-01";
});
```

Setting both `Recipe` and `Provisioning = Manual` is mutually exclusive and throws `InvalidOperationException` at configuration time.

> **⚠️ Property rename (pre-GA)**: The property previously named `TemplatePath` on `RadiusRecipe` was renamed to **`RecipeLocation`** on 2026-04-20 to align with the shipped Radius UDT schema. This is a breaking pre-GA change. `RecipeLocation` is emitted as `recipeLocation` on the new `Radius.Core/recipePacks` UDT, and as `templatePath` under the legacy `Applications.Core/environments@2023-10-01-preview` inline `properties.recipes` shape — the same value drives both output shapes.

> **⚠️ UDT transition note**: Radius's UDT model will retire the ability to specify named recipes (`Recipe.Name`). Under UDTs, each resource type has one recipe per environment — there is no "named recipe" override. The `RadiusRecipe.Name` property carries a deprecation doc-comment and will continue to work with legacy portable types but should be treated as deprecated for forward compatibility. `RadiusRecipe.RecipeLocation` and `RadiusRecipe.Parameters` remain valid under UDTs. Similarly, `ResourceProvisioning.Manual` carries a deprecation doc-comment — it remains functional as the current escape hatch but will be replaced by passthrough recipes in the UDT model.

### Infrastructure Customization (AST Hook)

```csharp
// Fine-grained control over the generated Radius Bicep AST
builder.AddRadiusEnvironment("radius")
.ConfigureRadiusInfrastructure(options =>
{
// Mutate typed Azure.Provisioning constructs before Bicep compilation.
// Access: options.Environments, options.Applications, options.RecipePacks,
// options.ResourceTypeInstances, options.Containers
// Last-write-wins semantics when multiple callbacks are registered.
});
```

`ConfigureRadiusInfrastructure` is a C#-only escape hatch (marked `[AspireExportIgnore]`) because the callback parameter exposes the Azure.Provisioning typed AST, which is not ATS-compatible and cannot be marshalled over RPC to polyglot AppHost runtimes (TypeScript, Python, …). Polyglot AppHosts customize per-resource via `PublishAsRadiusResource` instead. When a callback renames a construct's `BicepIdentifier`, the builder automatically re-resolves only the `.id` cross-references it originally created that targeted the renamed construct; direct edits a callback makes to any reference value are preserved (last-write-wins).

### Multi-Environment Support

```csharp
var dev = builder.AddRadiusEnvironment("dev")
.WithNamespace("dev-ns");

var staging = builder.AddRadiusEnvironment("staging")
.WithNamespace("staging-ns");

// Resources without an explicit deployment target default to the
// first registered Radius environment ("dev" in this case).
// Each environment gets its own publish output directory and deploy step.
```

## Architecture

The package follows the same compute environment pattern used by Docker Compose, Azure Container Apps, and Kubernetes.

Component | Type | Purpose
---------------------------------- | --------------------------------------------- | -------
`RadiusEnvironmentResource` | `IComputeEnvironmentResource` | Registered in the app model; provides Kubernetes DNS address expressions (`.svc.cluster.local`)
`RadiusInfrastructure` | Internal | Owns `PrepareDeploymentTargetsAsync`, which materializes `DeploymentTargetAnnotation` instances on compute resources scoped to a given Radius environment
`RadiusInfrastructureBuilder` | Internal | Walks the `DistributedApplicationModel`, classifies resources, and builds a typed Azure.Provisioning AST
`BicepPostProcessor` | Internal | Compiles the AST via `Infrastructure.Build().Compile()`, prepends `extension radius`, sanitizes identifiers
`RadiusBicepPublishingContext` | Internal | Orchestrates publish: builds AST, compiles Bicep, writes `app.bicep` + `bicepconfig.json`
`RadiusDeploymentPipelineStep` | Internal | Detects `rad` CLI, executes `rad deploy app.bicep`, streams output, handles errors
`ResourceTypeMapper` | Internal | Maps Aspire resource CLR types to Radius resource types; walks inheritance chain; falls back to `Radius.Compute/containers` for unmapped types
`RadiusInfrastructureOptions` | Public | Exposes mutable AST construct collections for `ConfigureRadiusInfrastructure` callbacks

### Run-Mode vs Publish-Mode Wiring

`AddRadiusEnvironment` behaves differently depending on `ExecutionContext`, matching how `AddKubernetesEnvironment` and `AddDockerComposeEnvironment` behave today:

* **Run mode (`aspire run`)** — The Radius integration returns an *unregistered* resource builder, so:
* The environment does not surface as a resource in the dashboard.
* No pipeline steps are wired up.
* No `DeploymentTargetAnnotation` is attached to compute resources.
* No Kubernetes cluster or `rad` CLI is required.
* Resources continue to use Aspire's standard inner-loop provisioning (Docker containers, etc.).

This is the "graceful degradation" property the design intends: a developer can add `AddRadiusEnvironment()` to their app host and run `aspire run` with zero Radius prerequisites.

* **Publish mode (`aspire publish` / `aspire deploy`)** — The environment is fully registered and three `PipelineStepAnnotation`s are attached:
1. A per-environment **prepare** step that materializes `DeploymentTargetAnnotation`s on compute resources scoped to this environment.
2. A **Bicep publish** step that builds the AST, compiles to Bicep, and writes `app.bicep` + `bicepconfig.json`.
3. A **deploy** step that detects the `rad` CLI and runs `rad deploy app.bicep`.

### Pipeline Step Dependencies

```
prepare-deployment-targets-{name} ──DependsOn──▶ ValidateComputeEnvironments
──RequiredBy─▶ BeforeStart

publish-radius-{name} ──RequiredBy─▶ WellKnownPipelineSteps.Publish

deploy-radius-{name} ──DependsOn──▶ WellKnownPipelineSteps.DeployPrereq
──RequiredBy─▶ WellKnownPipelineSteps.Deploy
```

The prepare step's `DependsOn(ValidateComputeEnvironments)` ensures multi-environment ambiguity fails fast before deployment-target wiring runs, and `RequiredBy(BeforeStart)` ties it to the standard synchronization point downstream code observes.

Note: There is **no Push step dependency**. Radius deploy consumes Bicep output directly, supporting local kind clusters without a container registry.

### Connection Model

Container workloads reference dependent Radius resources via resource ID-based connections, not hardcoded connection strings:

```bicep
resource api 'Radius.Compute/containers@2025-08-01-preview' = {
name: 'api'
properties: {
// ...
connections: {
cache: { source: cache.id }
sqlserver: { source: sqlserver.id }
}
}
}
```

Radius resolves connection details at deploy time via its runtime, enabling environment-specific secret injection without app-level changes.

## Resource Type Mapping

Aspire resources map to Radius resource types. The mapper walks the CLR inheritance chain to find the most specific match and falls back to `Radius.Compute/containers` for unmapped types.

> **UDT context**: The Radius team is actively replacing all built-in portable types (`Applications.*`) with user-defined types (`Radius.*`). Types marked with ¹ below use legacy portable types that will be removed from future Radius releases and re-implemented as UDTs in the [resource-types-contrib](https://github.com/radius-project/resource-types-contrib) repository. See [User-Defined Resource Types](#user-defined-resource-types-udt-transition) for the full migration plan.

Aspire Resource | Radius Type (Target) | Currently Emitted | API Version | Notes
------------------ | ---------------------------------- | ---------------------------------------------- | --------------------- | -----
`AddRedis()` | `Radius.Data/redisCaches` | `Applications.Datastores/redisCaches` ¹ | `2023-10-01-preview` | Legacy fallback with warning
`AddSqlServer()` | `Radius.Data/sqlDatabases` | `Radius.Data/sqlDatabases` | `2025-08-01-preview` | Full support
`AddPostgres()` | `Radius.Data/postgreSqlDatabases` | `Radius.Data/postgreSqlDatabases` | `2025-08-01-preview` | Full support
`AddMongoDB()` | `Radius.Data/mongoDatabases` | `Applications.Datastores/mongoDatabases` ¹ | `2023-10-01-preview` | Legacy fallback with warning
`AddRabbitMQ()` | `Radius.Messaging/rabbitMQQueues` | `Applications.Messaging/rabbitMQQueues` ¹ | `2023-10-01-preview` | Legacy fallback with warning
`AddDaprStateStore()` | `Radius.Dapr/stateStores` | `Applications.Dapr/stateStores` ¹ | `2023-10-01-preview` | Legacy fallback with warning
`AddDaprPubSub()` | `Radius.Dapr/pubSubBrokers` | `Applications.Dapr/pubSubBrokers` ¹ | `2023-10-01-preview` | Legacy fallback with warning
`AddProject()` | `Radius.Compute/containers` | `Radius.Compute/containers` (or legacy `Applications.Core/containers` when `WithLegacyContainers()` is set) | `2025-08-01-preview` / `2023-10-01-preview` | Workload container
`AddContainer()` | `Radius.Compute/containers` | `Radius.Compute/containers` (or legacy `Applications.Core/containers` when `WithLegacyContainers()` is set) | `2025-08-01-preview` / `2023-10-01-preview` | Workload container
(unmapped) | — | `Radius.Compute/containers` | `2025-08-01-preview` | Fallback with diagnostic warning

¹ These types use legacy `Applications.*` portable resource types because the `Radius.*` UDT equivalents are not yet available in the current Radius release. **The Radius team has confirmed that these portable types will be removed from Radius and re-implemented as user-defined types** (see [feature spec](https://github.com/radius-project/radius/blob/main/eng/design-notes/extensibility/2025-02-user-defined-resource-type-feature-spec.md)). When the UDT equivalents are available, the mapper will emit the new type automatically with no code changes required.

## Generated Bicep Output

For a typical Aspire app, `aspire publish` generates two files per environment in the output directory:

### `app.bicep`

```bicep
extension radius

resource recipepack 'Radius.Core/recipePacks@2025-08-01-preview' = {
name: 'default'
properties: {
recipes: {
'Applications.Datastores/redisCaches': {
templateKind: 'bicep'
recipeLocation: 'ghcr.io/radius-project/recipes/local-dev/rediscaches:latest'
}
'Radius.Data/sqlDatabases': {
templateKind: 'bicep'
recipeLocation: 'ghcr.io/radius-project/recipes/local-dev/sqldatabases:latest'
}
}
}
}

resource radiusenv 'Radius.Core/environments@2025-08-01-preview' = {
name: 'radius'
properties: {
recipePacks: [
recipepack.id
]
}
}

resource app 'Radius.Core/applications@2025-08-01-preview' = {
name: 'app'
properties: {
environment: radiusenv.id
}
}

resource cache 'Applications.Datastores/redisCaches@2023-10-01-preview' = {
name: 'cache'
properties: {
application: app.id
environment: radiusenv.id
}
}

resource sqlserver 'Radius.Data/sqlDatabases@2025-08-01-preview' = {
name: 'sqlserver'
properties: {
application: app.id
environment: radiusenv.id
}
}

resource api 'Radius.Compute/containers@2025-08-01-preview' = {
name: 'api'
properties: {
application: app.id
container: {
image: 'api:latest'
}
connections: {
cache: { source: cache.id }
sqlserver: { source: sqlserver.id }
}
}
}
```

When `WithLegacyContainers()` is enabled, the container block instead emits as `Applications.Core/containers@2023-10-01-preview` parented to the legacy `Applications.Core/application` instead of the UDT application.

### `bicepconfig.json`

```json
{
"experimentalFeaturesEnabled": {
"extensibility": true
},
"extensions": {
"radius": "br:biceptypes.azurecr.io/radius:latest",
"aws": "br:biceptypes.azurecr.io/aws:latest"
}
}
```

The `bicepconfig.json` registers the Radius Bicep extension, which is required by Radius v0.55+ for `extension radius` support.

## User-Defined Resource Types (UDT) Transition

### Background

The Radius team is executing a major architectural shift: **all built-in portable resource types are being replaced by user-defined resource types (UDTs)**. This is documented in the [2025-02 UDT feature spec](https://github.com/radius-project/radius/blob/main/eng/design-notes/extensibility/2025-02-user-defined-resource-type-feature-spec.md), which states:

> _"The portable resource types will be removed from Radius builds and no longer shipped with Radius releases. Instead, these resource types will be implemented as user-defined resource types and published in a Radius-maintained samples repository. All associated functionality with portable resource types will be removed from Radius including manual resource provisioning."_

Additionally, core resource types like containers, gateways, and secret stores are being converted to recipe-backed UDTs ([2025-04 compute extensibility spec](https://github.com/radius-project/radius/blob/main/eng/design-notes/extensibility/2025-04-compute-extensibility.md)). Only `environments` and `applications` will remain as true built-in types.

### What are UDTs?

User-defined resource types allow platform engineers to define custom resource types without modifying the Radius codebase:

1. **Define a type** via YAML manifest (or TypeSpec) with an OpenAPI schema:

```yaml
namespace: Radius.Data
types:
postgreSqlDatabases:
apiVersions:
'2025-08-01-preview':
schema:
type: object
properties:
environment: { type: string }
application: { type: string }
size: { type: string, enum: ['S', 'M', 'L'] }
host: { type: string, readOnly: true }
port: { type: string, readOnly: true }
required: [environment, secretName]
```

2. **Register the type**: `rad resource-type create --from-file postgreSqlDatabases.yaml`

3. **Author a recipe** (Bicep or Terraform) that provisions the underlying infrastructure and returns the schema's read-only output properties.

UDT definitions and their recipes are maintained in the [radius-project/resource-types-contrib](https://github.com/radius-project/resource-types-contrib) repository, which already includes types for PostgreSQL, MySQL, Neo4j, containers, routes, persistent volumes, and secrets.

### Key changes from the portable model

Aspect | Portable Resources (current) | User-Defined Types (future)
----------------------- | --------------------------------------------------------- | ---------------------------
**Where defined** | Hardcoded in Radius Go codebase | YAML/TypeSpec manifests in `resource-types-contrib` repo
**Namespace** | `Applications.Datastores/*`, `Applications.Messaging/*` | `Radius.Data/*`, `Radius.Messaging/*`, `Radius.Compute/*`
**Named recipes** | Developers can choose a recipe by name | **Retired** — one recipe per type per environment
**Manual provisioning** | `resourceProvisioning: manual` | **Retired** — replaced by recipes that connect to existing infra
**Extensibility** | `Applications.Core/extenders` escape hatch | **Retired** — define a proper UDT instead
**Schema** | Fixed in code per type | Platform-engineer-defined OpenAPI schema
**Distribution** | Shipped with Radius binary | Community-maintained `resource-types-contrib` repo with maturity model (Alpha → Beta → Stable)

### Impact on this integration

This integration is designed to be forward-compatible with the UDT transition:

1. **Type mapper already targets `Radius.*` namespace**: Where UDTs are available (SQL, Postgres, containers), the mapper already emits the new `Radius.*` types. Legacy `Applications.*` types are used only as temporary fallbacks with diagnostic warnings.

2. **Recipe packs align with UDT model**: The integration generates `Radius.Core/recipePacks` resources, which is the UDT-era mechanism for bundling recipes with environments.

3. **`RecipeLocation` matches the UDT schema**: The `RadiusRecipe.RecipeLocation` property (renamed from `TemplatePath` on 2026-04-20) maps directly to the `recipeLocation` field on the new `Radius.Core/recipePacks` UDT, and is also emitted as `templatePath` under the legacy inline shape — so a single API drives both output forms.

4. **`WithLegacyContainers()` provides a transition escape hatch**: For Radius installs that do not yet have the `Radius.Compute/containers` UDT recipe registered, environments can opt back into legacy `Applications.Core/containers`, which ships with built-in deployment behavior and requires no recipe.

5. **Adaptations for the UDT transition** (current status):

* ✅ **Custom type override**: `RadiusResourceCustomization.RadiusType` and `RadiusApiVersion` properties allow users to override any resource's Radius type — useful for early UDT adoption or organization-specific types. The builder's `ResolveResourceType()` checks for a custom override before consulting the `ResourceTypeMapper`.
* ✅ **Legacy type deprecation signals**: All `Legacy*` constants in `RadiusResourceTypes` are marked `[Obsolete]` with migration guidance. `RadiusRecipe.Name` and `ResourceProvisioning.Manual` carry deprecation doc-comments explaining the upstream changes.
* ✅ **Container v2 alignment**: `RadiusContainerConstruct` no longer emits `imagePullPolicy` (removed from the v2 schema). The builder emits warnings when container images use `:latest` tags or lack a registry prefix, since users of kind clusters will need to pre-load images and use explicit tags.
* 🔜 **Type mapper update** (when portable types are removed): Switch remaining `Applications.*` entries (Redis, MongoDB, RabbitMQ, Dapr) to their `Radius.*` UDT equivalents. The mapper's inheritance-walking design means this is a dictionary update, not a restructuring.
* 🔜 **Named recipe deprecation**: `RadiusRecipe.Name` carries a deprecation doc-comment but remains `required` — making it optional would create invalid states without unnamed-recipe semantics being defined upstream first.
* 🔜 **Manual provisioning alternative**: `ResourceProvisioning.Manual` carries a deprecation doc-comment but remains fully functional as the only escape hatch for unsupported resources. The likely replacement is a "passthrough" recipe that connects to pre-existing infrastructure.
* 🔜 **UDT registration in publish output**: The integration may need to generate `rad resource-type create` commands or embed type manifests in the publish output to ensure required UDTs are registered before deployment.
* 🔜 **Retire `WithLegacyContainers()`**: Once `Radius.Compute/containers` is broadly available across Radius installs, this escape hatch can be marked `[Obsolete]` and eventually removed.

6. **Container v2 alignment**: The integration already targets `Radius.Compute/containers@2025-08-01-preview` and is aligned with the v2 schema:

* `imagePullPolicy` has been removed — the builder warns when images use `:latest` or lack a registry prefix, guiding users of kind clusters to pre-load images with explicit tags
* Single-container definitions remain compatible with the v2 schema but don't yet leverage multi-container, init container, resource limits, or autoscaling capabilities
* `iam` on connections and Kubernetes metadata extension are not emitted (removed in v2)

## Implementation Status

A working implementation has been built and tested:

* **Repository**: [radius-aspire-integration](https://github.com/nellshamrell/radius-aspire-integration) (under `aspire/src/Aspire.Hosting.Radius/`)
* **Package structure**: Self-contained — no modifications to existing Aspire source files
* **Dependencies**: `Aspire.Hosting` and `Azure.Provisioning`
* **Samples**: Two sample apps (basic and advanced) demonstrating the full workflow

### Capability Status

Capability | Status
------------------------------------------------------------------- | ------
`AddRadiusEnvironment()` registration | ✅ Working
`WithNamespace()` with RFC 1123 validation | ✅ Working
`WithLegacyContainers()` legacy-container opt-in | ✅ Working
Run mode returns unregistered builder (no resource, no pipeline) | ✅ Working
Publish-mode pipeline step wiring (prepare/publish/deploy) | ✅ Working
`DeploymentTargetAnnotation` materialization in prepare step | ✅ Working
Multi-environment support with default-to-first targeting | ✅ Working
Typed AST generation via Azure.Provisioning SDK | ✅ Working
Bicep compilation via `Infrastructure.Build().Compile()` | ✅ Working
Resource type mapping (9 types + inheritance walk + fallback) | ✅ Working
Legacy `Applications.*` fallback with diagnostic warnings | ✅ Working
Legacy constants marked `[Obsolete]` with migration guidance | ✅ Working
Custom type override via `RadiusType` / `RadiusApiVersion` | ✅ Working
Recipe pack generation with default recipe templates | ✅ Working
Per-resource customization (`PublishAsRadiusResource`) | ✅ Working
`RadiusRecipe.RecipeLocation` (renamed from `TemplatePath`) | ✅ Working
Custom recipe with parameters | ✅ Working
Manual provisioning with host/port | ✅ Working
Recipe + Manual mutual exclusion validation | ✅ Working
`ConfigureRadiusInfrastructure()` AST hook (last-write-wins) | ✅ Working
Identifier-rename propagation in `ConfigureRadiusInfrastructure` | ✅ Working
Connection propagation via `WithReference()` → Bicep `connections` | ✅ Working
Child-to-parent resource resolution (e.g., `SqlServerDatabaseResource` → `SqlServerServerResource`) | ✅ Working
Pipeline step registration with `PipelineStepAnnotation` | ✅ Working
`rad deploy` execution with stdout/stderr streaming | ✅ Working
`rad` CLI detection with actionable error messaging | ✅ Working
Identifier sanitization (hyphens, digit-prefixed, `radius` collision) | ✅ Working
Graceful inner-loop behavior (no Kubernetes/rad required for `aspire run`) | ✅ Working
Container v2 schema alignment (no `imagePullPolicy`) | ✅ Working
Image pull warnings for `:latest` and unregistered images | ✅ Working

### Test Coverage

The implementation includes comprehensive tests organized by concern:

Test Area | Test Count | Covers
-------------------------------------------------- | ---------- | ------
Core (`RadiusExtensions`, `RadiusEnvironmentResource`) | 14 | Registration, namespace validation, `WithLegacyContainers`, run-mode unregistered behavior, DI, resource model
Models (`RadiusRecipe`, `RadiusResourceCustomization`) | 12 | Defaults, shape, provisioning enum, `RecipeLocation`
Resource Mapping | 15 | All mapped types, inheritance walk, legacy fallback, unmapped fallback
Publishing — Bicep Generation | 20+ | Simple/multi-resource output, identifier sanitization, syntax validation
Publishing — Customization | 12 | Custom recipes, parameters, manual provisioning, recipe/manual conflict
Publishing — Connections | 8 | `WithReference()` propagation, child-to-parent resolution
Publishing — Multi-Environment | 8 | Per-env output, untargeted defaulting, isolation
Publishing — `ConfigureRadiusInfrastructure` | 8 | AST mutation, add/remove resources, last-write-wins
Publishing — Legacy fallback emission | 6 | `WithLegacyContainers` Bicep shape, pure-legacy output when no UDT instances exist
Inner-Loop | 12 | Graceful degradation, multi-env coexistence, annotation initialization
Deployment | 12 | `rad` CLI detection, pipeline step dependencies, E2E smoke tests

E2E deployment tests are explicitly gated on `rad` CLI availability and are designed for manual validation with a live Kubernetes cluster.

## Open Design Questions

We'd like the Aspire team's input on several questions:

### 1. Sustainability Model

Per the [integration contribution guidelines](https://github.com/microsoft/aspire/blob/main/src/Components/README.md), integrations require a plan to sustain them. The Radius team is prepared to maintain the package, respond to issues, and keep it current with Aspire releases. We'd like to discuss the right ownership model.

### 2. `rad` CLI Dependency

Deploy currently shells out to the `rad` CLI via `System.Diagnostics.Process`, detecting it on PATH before execution and providing an actionable error with install link if missing. Should we:

* **Require `rad` on PATH** (simplest, current approach)
* **Auto-download `rad`** as a tool dependency (like how Aspire manages DCP)
* **Embed Radius deploy logic** as a native dependency (eliminates CLI requirement but adds complexity)

### 3. UDT Migration Strategy

Radius is replacing all built-in portable resource types with user-defined types (UDTs). Several resource types in this integration (Redis, MongoDB, RabbitMQ, Dapr) currently emit legacy `Applications.*` portable types that will be removed in a future Radius release. The integration already supports:

* **Legacy fallback with `[Obsolete]` markers**: Legacy constants are marked obsolete with migration guidance. The mapper automatically falls back to `Applications.*` types with diagnostic warnings.
* **Custom type override**: Users can set `RadiusType` and `RadiusApiVersion` per resource via `PublishAsRadiusResource()` to adopt new UDTs before the integration adds built-in mappings.
* **`WithLegacyContainers()` escape hatch**: Environments can opt back into the legacy container type for installs that lack the UDT container recipe.

Remaining question: How should we handle the transition timeline?

* **Ship with legacy fallback and migrate incrementally** (current approach) — emit `Applications.*` types with warnings now, switch to `Radius.*` UDTs as they become available. Allows shipping sooner but risks breaking if users upgrade Radius before the integration is updated.
* **Block on full UDT availability** — wait until all required `Radius.*` UDTs are available and tested before shipping. Ensures a clean experience but delays the integration.
* **Support both simultaneously** — detect the Radius version or installed UDTs and emit the appropriate type strings. More complex but most resilient.

### 4. Named Recipe Deprecation

The UDT model retires named recipes — each resource type will have exactly one recipe per environment. The current `RadiusRecipe.Name` property enables developers to select a specific recipe (e.g., `"azure-redis-premium"`). Should we:

* **Deprecate `RadiusRecipe.Name` now** with an `[Obsolete]` attribute and documentation
* **Keep it functional** until portable types are actually removed, then deprecate
* **Remove it** from the public API before initial ship to avoid embedding a soon-to-be-obsolete pattern

### 5. Manual Provisioning Replacement

`ResourceProvisioning.Manual` allows pointing to pre-existing infrastructure without a recipe. The UDT model removes manual provisioning entirely. Should we:

* **Ship with manual provisioning** and plan to replace it when the UDT equivalent (likely a "passthrough" recipe) is available
* **Design a forward-compatible API** that works today with manual provisioning but naturally maps to the UDT approach (e.g., connection string overrides that can be expressed as recipe parameters)

### 6. Container Image Resolution

Container workloads resolve images from `ContainerImageAnnotation` (falling back to `{name}:latest`). The integration no longer sets `imagePullPolicy` (removed in the Radius container v2 schema) but emits warnings when images use `:latest` tags or lack a registry prefix — since these patterns can cause pull failures on Kubernetes clusters without pre-loaded images.

For kind cluster workflows, developers must pre-load images with `kind load docker-image` and use explicit tags. For production, should we integrate with Aspire's container registry configuration to automatically construct fully-qualified image references?

### 7. Lifetime of `WithLegacyContainers()`

`WithLegacyContainers()` is a transitional escape hatch for Radius installs that do not yet have the `Radius.Compute/containers` UDT recipe registered. Should we:

* **Ship it as-is** and document it as a transitional API that will be marked `[Obsolete]` once the UDT container is broadly available
* **Gate it behind an experimental diagnostic** (`[Experimental]`) so users explicitly opt in to a non-GA surface
* **Omit it from the initial ship** and rely on the per-resource `RadiusType` override for the rare cases that need legacy emission

## References

* **Implementation**: [radius-aspire-integration](https://github.com/nellshamrell/radius-aspire-integration)
* **Radius**: [radapp.io](https://radapp.io) — Open-source cloud-native application platform
* **Aspire Deployment Roadmap**: [PR #15594](https://github.com/microsoft/aspire/pull/15594)

EDIT: Slightly updated based on initial code review of the prototype.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.