microsoft / microsoft/aspire

[API Proposal] Executable resource projections

Open
#20,055 1 comment 0 reactions 0 assignees View on GitHub
api-suggestion area-app-model triage:bot-seen
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 15h
Merged PRs (30d)
196

Description

**Related:** [microsoft/aspire#8984](https://github.com/microsoft/aspire/issues/8984),
[microsoft/aspire#19836](https://github.com/microsoft/aspire/issues/19836),
[microsoft/aspire#19880](https://github.com/microsoft/aspire/pull/19880),
[microsoft/aspire#18052](https://github.com/microsoft/aspire/pull/18052)

## Background and Motivation

### Summary

Aspire resource projections currently let a project, executable, or managed
resource acquire a container shape for one operation while its original
resource remains the sole logical member of the application model.

There is demonstrated demand for the reverse scenario: a container or
shape-neutral resource may need to run locally through a language-specific
executable implementation without replacing the resource that callers already
reference.

Supporting that scenario is substantially harder than projecting to a
container. `AddViteApp`, `AddPythonApp`, `AddDotnetProject`, and similar methods
are registration pipelines rather than simple factories. In addition to
constructing a resource, they apply defaults, register deferred callbacks,
configure debugging and launch behavior, and may create setup or companion
resources.

This proposal explores reusable, self-typed executable projection contracts
that separate:

1. Construction of a typed resource.
2. Selection of a specific implementation such as Vite, Next.js, or a generic
JavaScript application.
3. Application of that implementation's standard annotations and fluent
configuration.
4. Registration of owned auxiliary resources.
5. Selection of the effective resource while retaining one canonical owner.

The framework, rather than each `AddFoo` method, would invoke the same
projection contract for both direct and projected resources. `AddFoo` and
`RunAsFoo` would construct a projection-capable resource; neither would
independently reproduce its configuration logic.

The projection contract is an authoring-time hook, even when the resource
itself implements it. It is not a runtime behavior dispatch mechanism. The
resource type determines which fluent APIs and capabilities are available.
Structural resource state and annotations remain the model consumed by
orchestration, publishing, and deployment behavior.

This proposal does not choose an annotation inheritance policy. Inheritance
across the container/executable boundary requires research using representative
integrations and user scenarios.

### Current direction

The current design direction is:

- Use an invariant, self-typed configuration contract such as
`IExecutableProjection`.
- Let the projected resource itself implement the contract.
- Pass the exact typed resource builder to `ApplyDefaultConfiguration`.
- Invoke the same contract from direct `AddFoo` registration and projected
`RunAsFoo` registration.
- Represent resulting behavior through resource state and annotations; runtime
realization does not dispatch back through the projection contract.
- Compose shared family defaults explicitly through builder helpers rather than
through virtual override ordering.
- Apply defaults before the caller's typed customization callback.
- Keep the owner as the only logical model member.
- Keep operation selection outside the resource contract. `RunAs` registration
and its source strategy decide when a projection is eligible and selected.
- Treat language-specific source-project resources such as
`DotnetProjectResource` as executable implementations. Legacy
`ProjectResource` is not a target shape for this proposal.

The following areas remain deliberately unresolved:

- Annotation inheritance and transformation between the owner and effective
resource.
- Auxiliary-resource activation, replacement, and cleanup.
- The preferred balance between named `RunAsFoo` methods and a generic
`RunAs` API.
- Generated SDK representation.

### Terminology

| Term | Meaning in this proposal |
| --- | --- |
| Owner | The original resource added to `IResourceCollection`. It remains the only model member representing the primary logical workload and is the canonical identity used by references, waits, events, and notifications. |
| Projection | An alternate resource instance selected to provide the owner's effective shape for one operation. It is not added as a second logical model member. |
| Effective resource | The owner when no projection is selected; otherwise, the selected projection resource used by shape-sensitive behavior. |
| Projection contract | An authoring-time interface implemented by a resource that knows how to apply its standard model configuration to a typed builder. |
| Specific implementation | A projection-capable resource type and configuration contract representing a concrete registration profile, such as Vite rather than generic JavaScript. |
| Operation | The AppHost invocation mode for which a projection is selected, such as run or publish. Selection is scoped to one operation. |
| Source strategy | The registration policy that decides for which operations a projection is eligible. It remains separate from the target resource's configuration contract. |
| Shape-neutral resource | A logical or managed owner that does not itself define executable or container realization. |
| Direct registration | Creating a projection-capable resource as a normal top-level model member through an `AddFoo` API rather than as another owner's projection. |
| Owned auxiliary resource | A setup or companion resource associated with an owner and effective resource. For a projection, its activation and lifetime are tied to the selected projection. |

Owned auxiliary resources may be distinct members of the model. The owner-only
invariant prohibits adding the projection as a second representation of the
same logical workload; it does not prohibit explicitly modeled installers,
rebuilders, or companions with their own identities.

### Goals

- Preserve the original resource as the canonical model owner.
- Keep builders and references obtained before projection valid.
- Let containers and shape-neutral resources project to executable shapes,
including language-specific resources representing source projects.
- Let an integration define reusable projection configuration hooks that the
framework applies identically for direct registration and projection.
- Let specific resources such as `ViteAppResource` explicitly compose or
replace shared defaults from a common resource family.
- Keep realization behavior out of resource contracts; projection hooks
configure the resource model rather than becoming an alternate behavior
dispatch mechanism.
- Support required construction arguments without assuming a parameterless or
owner-only constructor.
- Give projection callbacks a strongly typed resource builder.
- Model installers, rebuilders, build helpers, and other owned auxiliary
resources explicitly rather than inferring their role from call order.
- Reuse the projection selection and identity behavior established for
container projections.
- Evaluate both per-resource `RunAsFoo` APIs and a generic `RunAs` primitive.
- Keep the design usable by third-party hosting integrations.

### Non-goals

- Implement this proposal as part of microsoft/aspire#19880.
- Project between the legacy `ProjectResource` shape and an executable shape in
either direction.
- Make arbitrary existing `AddFoo` methods automatically usable as projection
factories.
- Define a universal annotation inheritance rule without scenario research.
- Remove or replace the owner resource in the application model.
- Treat temporary donor resources or annotation copying as a supported
registration mechanism.
- Redesign executable, debugging, or launch recipe APIs in this proposal.

### Compatibility posture

This is an additive design. Existing `AddFoo` signatures and resource builder
return types should remain source and binary compatible. An integration opts
into projection by implementing a new contract and routing its existing
registration method through the shared materialization helper.

An existing `AddFoo` method must not change from
`IResourceBuilder` to `IResourceBuilder`.
Where a distinct implementation subtype is useful, the method may construct
that subtype internally and return it through the existing covariant builder
type. Exposing subtype-specific APIs from an existing base-typed method would
require a new additive entry point rather than a return-type change.

Existing third-party resource types that do not implement a projection contract
continue to work as direct resources. They are not automatically eligible as
executable projection targets. An external adapter may be considered for
resource types that cannot be changed, but the primary design should not require
one.

The proposal does not assume that ordinary calls to `AddResource` automatically
invoke projection defaults. The existing `AddFoo` method remains responsible for
choosing the projection-capable resource and delegating to the framework helper
that invokes its contract.

When migrating an existing `AddFoo`, its current fluent default chain must move
behind the projection contract rather than remain alongside it. Otherwise direct
registration would apply defaults twice while projected registration applied
them once.

Argument validation, path normalization, and construction of required resource
state may remain in `AddFoo` or an integration-owned factory. The projection
contract owns the standard builder configuration applied after construction.

Low-level projection authoring contracts and generic factories should initially
be considered experimental until inheritance, annotation, generated SDK, and
third-party integration behavior has been validated.

### Projection eligibility

The initial eligibility model under consideration is:

| Owner's intrinsic shape | Candidate projection targets |
| --- | --- |
| Legacy `ProjectResource` | Container |
| Executable, including language apps | Container |
| Container | Executable |
| Shape-neutral or managed resource | Container or executable |

The project-to-container and executable-to-container rows describe the
container projection capability established by microsoft/aspire#19880. This
proposal focuses on adding the container-to-executable and
shape-neutral-to-executable directions without weakening the existing rules.

Projection between legacy `ProjectResource` and executable shapes is explicitly
excluded in either direction.

The legacy `ProjectResource` and executable resources already represent
complete host-process strategies. Converting between them would require
reconciling competing launch recipes, debugger behavior, working directories,
tool requirements, setup resources, and build behavior. That is resource
redefinition rather than an alternate realization.

Language integrations may still represent source projects. For example,
`DotnetProjectResource` is an `ExecutableResource`, like the resources in other
language packages. Projecting a container to `DotnetProjectResource` is
container-to-executable projection; it does not require support for the legacy
`ProjectResource` shape.

Container-to-executable projections remain valid scenarios to investigate.
They cross an execution boundary and therefore require deliberate configuration
inheritance and adaptation, but there is direct evidence of users attempting
these substitutions.

### Required invariants

Any executable projection design must preserve the invariants already
established by container projections:

1. The owner remains the sole logical model member representing the resource.
2. A projection is created with `CreateResourceBuilder`, not `AddResource`.
3. A projection API returns the original owner builder.
4. References, waits, relationships, notifications, events, and resource-keyed
state use the canonical owner.
5. Shape-sensitive runtime and publishing behavior use the selected effective
resource.
6. A selected projection is authoritative for effective shape; when no
projection is selected, the owner retains its intrinsic behavior.
7. A projection cannot introduce a second effective shape for the same
operation.
8. Projection-owned auxiliary resources are explicitly associated with the
canonical owner and selected projection resource.
9. Removing or replacing the owner is never used as a conversion mechanism.

The ninth invariant is important because callers can retain a builder or
resource before applying a projection:

```csharp
var database = builder.AddContainer("database", "postgres");

database.RunAsDotnetProject("../database/Database.csproj");

builder.AddDotnetProject("api", "../api/Api.csproj")
.WithReference(database);
```

If `RunAsDotnetProject` removed the container and inserted an executable in its
place, `database` would refer to an object that is no longer in the model.
References, waits, events, endpoint references, and resource-keyed dictionaries
could then fail later and far away from the conversion call.

### Why existing `AddFoo` methods cannot be invoked as-is

An API that runs arbitrary registration code inside a projection scope appears
attractive:

```csharp
container.RunAs(app =>
{
app.AddViteApp("frontend", "../frontend");
app.AddDotnetProject("backend", "../backend/backend.csproj");
});
```

The framework cannot determine which resource is the projection and which
resources are dependencies, companions, or unrelated top-level resources.

Requiring the callback to return one builder would identify the primary result:

```csharp
container.RunAs(app =>
app.AddViteApp("frontend", "../frontend"));
```

It would not classify other effects of `AddViteApp`. A registration may:

- Add an installer or virtual environment creator.
- Add a hidden build helper.
- Register global or resource-filtered lifecycle callbacks.
- Register services.
- Add deferred waits and relationships.
- Capture the newly constructed resource in argument, environment, debugger,
or Dockerfile callbacks.
- Add another resource later during final model processing.

A transactional application-builder interceptor would need to capture and roll
back all those effects while distinguishing intended helpers from unrelated
resources. Integrations would still need a way to explicitly claim their
auxiliary resources. At that point the design has become an explicit
projection contract with additional interception complexity.

## Proposed API

### Proposed projection configuration contract

The unit of reuse cannot remain only the body of an `AddFoo` method. A specific
resource type, such as `ViteAppResource`, can formalize the implementation that
the method previously implied. The selected implementation is then represented
by the resource instance and its exact self-typed projection contract rather
than only by the name of the method that happened to construct it.

The contract also must not leave `AddFoo` responsible for reproducing part of
the default configuration. Both direct and projected materialization should be
framework operations that always invoke the selected resource's
`ApplyDefaultConfiguration` method.

The exact public shape is open. The snippets below illustrate the contracts and
are not proposed as final public API. One possible form is:

```csharp
public interface IExecutableProjection
where TResource : ExecutableResource
{
void ApplyDefaultConfiguration(
IResourceBuilder builder);
}
```

The resource itself can be the projection implementation. Required arguments
are supplied through its normal constructor, and the framework invokes
`ApplyDefaultConfiguration` after creating either a direct resource builder or
a projection builder for that instance.

Resource types should normally implement the method explicitly so it does not
appear as an ordinary runtime operation in resource IntelliSense. Framework
materialization invokes it through the generic interface constraint.

The hook is synchronous, matching existing `AddFoo` registration. Work that
must run asynchronously remains represented by lifecycle callbacks, pipeline
steps, or setup resources registered during configuration.

The contract intentionally has no permanent run/publish flag or separate
`IsProjection` parameter. Operation-specific configuration reads the execution
context from the builder, while projection-specific configuration compares
`builder.Resource` with `GetOwnerOrSelf()`. Projection eligibility remains the
responsibility of the registration source strategy.

Naming the contract `IExecutableProjection` remains reasonable even
though `AddFoo` also invokes it. The interface identifies that the resource is a
valid, self-configuring executable projection target. Reusing that
configuration path for ordinary direct registration prevents `AddFoo` and
`RunAsFoo` semantics from drifting; it does not require broadening the concept
into a general resource implementation abstraction.

This is related to, but not identical to, the existing
`IContainerProjection` contract. Container projections use a
static construction contract and apply caller configuration through the
projection builder. Executable integrations additionally need to share
substantial `AddFoo` default-registration pipelines. The final naming should
make that difference understandable without implying that runtime behavior
lives on the resource.

The projection contract is not consulted by runtime realization. Its
responsibility ends after it has applied the annotations, callbacks, and
relationships that describe the resource. Runtime and publishing components
continue to interpret that model in the same way they do for resources
registered through existing APIs.

The self-typed interface is intentionally invariant. A specific implementation
such as `ViteAppResource` implements
`IExecutableProjection` and receives
`IResourceBuilder` directly. This avoids builder retyping and
lets the resource type control the exact fluent API surface available to its
configuration method.

The builder already exposes `ApplicationBuilder` and the effective resource.
Other projection-specific data can be derived or exposed through focused
extensions:

- `builder.Resource.GetOwnerOrSelf()` returns canonical identity.
- `builder.ApplicationBuilder.ExecutionContext.Operation` returns the current
operation.
- Comparing the owner and effective resource identifies whether configuration
is occurring for a projection.
- `AddOwnedSetupResource` and `AddOwnedCompanionResource` extensions can
explicitly associate auxiliary resources with the canonical owner and
selected projection. These names also make sense when the same hook runs for
direct registration.

This keeps the public contract small. A dedicated configuration context should
only be introduced if prototyping identifies privileged or scoped state that
cannot be represented safely through the builder and projection annotations.

#### The resource can implement projection without owning runtime behavior

Implementing `IExecutableProjection` does not make the resource a
runtime strategy object. Resource types should continue to define structural
shape and API exposure:

- `ContainerResource`, `ExecutableResource`, or `ProjectResource` determines
the fundamental realization shape.
- Marker interfaces and resource subtypes determine which builder extension
methods are available.
- Resource properties hold structural state that cannot be represented as an
annotation.
- Annotations and annotation callbacks describe environment, arguments,
endpoints, health checks, launch recipes, publish behavior, pipeline steps,
and other model behavior.

The projection configuration hook is therefore closer to a reusable, typed
`AddFoo` definition than runtime behavior on the resource. It may call existing
builder extensions, but those extensions must leave the resulting behavior in
the resource model. Runtime components should never call the hook to decide how
to launch, publish, or monitor the resource.

A projection-capable resource may retain constructor inputs long enough for the
hook to translate them into annotations. Those inputs should not become a
second mutable or runtime-readable source of truth after configuration.

#### Specific implementations compose shared defaults explicitly

Override dispatch is not required if each concrete projection implementation
owns its complete registration path. Shared behavior can be extracted into
reusable builder helpers:

```csharp
public class JavaScriptAppResource
: ExecutableResource,
IExecutableProjection
{
void IExecutableProjection.ApplyDefaultConfiguration(
IResourceBuilder builder)
{
builder.WithJavaScriptAppDefaults();
}
}

public class ViteAppResource
: JavaScriptAppResource,
IExecutableProjection
{
void IExecutableProjection.ApplyDefaultConfiguration(
IResourceBuilder builder)
{
builder
.WithJavaScriptAppDefaults()
.WithViteDefaults();
}
}
```

`ViteAppResource` still inherits
`IExecutableProjection` from its base, but the generic
materialization path requires the exact self-typed contract:

```csharp
where TResource : ExecutableResource, IExecutableProjection
```

For `TResource == ViteAppResource`, the framework invokes
`IExecutableProjection`. It should not search the runtime type
for an apparently "most specific" interface or invoke both contracts.

The inherited base contract alone does not make an arbitrary derived resource
projection-capable as its derived type. A derived integration must explicitly
implement `IExecutableProjection` or use an eventual adapter. This is
intentional: inheriting a resource shape should not silently opt a third-party
type into a default-configuration contract it did not select.

This makes the dependency on JavaScript defaults explicit in the Vite
configuration and avoids hidden virtual ordering. A specific implementation can
reuse all base-family defaults, select individual helpers, or replace them
entirely. Annotation precedence still must make replacement deterministic.
Shared helpers must also compose without accidentally adding conflicting
defaults within one configuration pass.

#### JavaScript implementation resources

JavaScript demonstrates why a single base implementation is insufficient.
Specific resource implementations can formalize registrations that otherwise
exist only as `AddFooApp` method conventions:

| Resource implementation | Representative unique configuration |
| --- | --- |
| `JavaScriptAppResource` | Package-manager run script and generic generated Dockerfile |
| `ViteAppResource` | Base JavaScript defaults plus Vite port arguments, optional config path, TLS handling, and frontend endpoint behavior |
| `NextJsAppResource` | Base JavaScript defaults plus Next.js port arguments, standalone publish mode, config validation, and specialized generated Dockerfile |
| `NodeAppResource`, `BunAppResource`, or `DenoAppResource` | Runtime-specific command composition, required tools, arguments, and publish behavior |

This lets `ViteAppResource` simultaneously identify the registration
implementation and control the Vite-specific builder APIs exposed to callers.
The type still does not become the source of runtime behavior: its configuration
hook adds or replaces annotations, after which existing model consumers operate
only on resource shape, state, and annotations.

#### Source projects can use executable implementations

A source project does not require a distinct projection shape. The experimental
[`Aspire.Hosting.Dotnet` integration](https://github.com/microsoft/aspire/blob/main/docs/plans/project-v2-csharpprogram-watch.md)
already models a .NET source project as
`DotnetProjectResource : ExecutableResource`, alongside the executable resource
types used by other language integrations. Legacy `ProjectResource` and
`AddProject` remain unchanged.

`DotnetProjectResource` is therefore another candidate implementation of
`IExecutableProjection`, not evidence for a separate project-shaped
projection contract. Its constructor or integration-owned factory supplies the
project path and launch options, while
`ApplyDefaultConfiguration` translates those inputs into executable
annotations, debugging configuration, build callbacks, and owned build
resources.

This is a valuable second prototype after Vite because it exercises more than
command and endpoint defaults. `AddDotnetProject` participates in coordinated
build infrastructure and can create hidden build resources. Those effects must
follow the same explicit owner, effective-resource, activation, and cleanup
rules as JavaScript installers.

The legacy `ProjectResource` shape is intentionally excluded from this
proposal. Existing project-to-container projection remains supported by the
container projection work, but this proposal neither projects a container to
`ProjectResource` nor converts between `ProjectResource` and executable shapes.

### Consistent direct and projected registration

The framework needs one direct-materialization helper and one
projection-materialization helper. They differ only in whether the resource is
added to the model or associated with an existing owner. Both always invoke the
projection contract.

Conceptually, direct materialization is:

```csharp
private static IResourceBuilder AddConfiguredExecutable(
this IDistributedApplicationBuilder builder,
TResource resource)
where TResource : ExecutableResource, IExecutableProjection
{
var resourceBuilder = builder.AddResource(resource);

resource.ApplyDefaultConfiguration(resourceBuilder);

return resourceBuilder;
}
```

The projection materializer invokes the same method after using
`CreateResourceBuilder`. A small internal helper may still centralize
validation and exactly-once invocation, but `ApplyDefaultConfiguration` receives
the actual resource builder rather than a parallel configuration context.

The existing direct API supplies arguments and selects the implementation, but
does not apply any defaults itself:

```csharp
public static IResourceBuilder AddViteApp(
this IDistributedApplicationBuilder builder,
string name,
string appDirectory,
string runScriptName = "dev")
{
var resource = new ViteAppResource(
name,
command: "npm",
workingDirectory: builder.ResolvePath(appDirectory),
runScriptName: runScriptName);

return builder.AddConfiguredExecutable(resource);
}
```

A projection path consumes the same implementation. It creates a resource
builder without adding another logical model member, associates the effective
resource with the owner, and invokes the same method:

```csharp
var projection = createProjection(
new ResourceProjectionFactoryContext(
builder.Resource,
builder.ApplicationBuilder.ExecutionContext.Operation));

var projectionBuilder =
builder.ApplicationBuilder.CreateResourceBuilder(projection);

projection.ApplyDefaultConfiguration(projectionBuilder);

return builder;
```

This requires an integration to extract its existing fluent registration chain
into reusable configuration logic. It does not require every integration to be
rewritten at once. The extraction can be limited to integrations selected for
executable projection support.

### Configuration ordering and lifetime

The framework should invoke `ApplyDefaultConfiguration` exactly once for each
direct or projected resource instance that it materializes.

Direct registration follows this order:

1. `AddFoo` constructs the resource with required inputs.
2. The framework adds it as the owner resource.
3. The framework invokes `ApplyDefaultConfiguration` with its typed builder.
4. `AddFoo` returns the typed builder.
5. Subsequent fluent calls from the AppHost author customize or replace
defaults.

Projected registration follows this order:

1. The framework selects the projection for the current operation.
2. The selected factory constructs a resource using the owner's canonical name.
3. The framework creates a builder without adding the projection as a logical
model member.
4. The framework establishes projection ownership and the selected annotation
storage policy.
5. The framework invokes `ApplyDefaultConfiguration` exactly once.
6. The `RunAs` customization callback runs against the same typed builder.
7. The projection API returns the original owner builder.

The builder passed to `ApplyDefaultConfiguration` represents the effective
projection resource, not the owner builder. Configuration that needs canonical
identity must resolve it through `GetOwnerOrSelf()`. Target-shape defaults must
be written through the effective builder using the selected annotation-storage
policy.

An unselected projection factory should not create resources, register
auxiliary resources, or subscribe callbacks. Defaults run before caller
customization so ordinary append/replace annotation semantics remain available
to the caller.

Because `ApplyDefaultConfiguration` is publicly reachable through an interface,
the API must specify whether direct caller invocation is unsupported, guarded,
or idempotent. The framework should not depend on repeated invocation to update
a projection.

Construction or default-configuration failures propagate synchronously from the
registration call. The framework should not silently fall back to the owner's
intrinsic shape after a selected projection fails to configure.

Projection hooks may register services or lifecycle callbacks through
`ApplicationBuilder`. App-wide service registration must be safe when several
resources use the same implementation, and resource-specific callbacks must be
filtered by canonical owner identity. The hook must not perform side effects for
an unselected projection.

## Usage Examples

### Candidate API shape 1: per-resource `RunAsFoo`

The examples use `RunAs` because executable projections are primarily local
run-mode scenarios. The projection configuration contract itself does not
encode run mode; a different source strategy could reuse it if another
operation has a valid executable-shaped realization.

Each projection-capable integration can expose a strongly typed convenience
method:

```csharp
container.RunAsViteApp(
appDirectory: "../frontend",
runScriptName: "dev",
configure: app => app.WithRunScript("start"));
```

The method selects the same implementation as `AddViteApp` and forwards it to
the common projection materializer:

```csharp
public static IResourceBuilder RunAsViteApp(
this IResourceBuilder builder,
string appDirectory,
string runScriptName = "dev",
Action>? configure = null)
where TOwner : IResource
{
return builder.RunAs(
context => new ViteAppResource(
context.Owner.Name,
command: "npm",
workingDirectory: context.ResolvePath(appDirectory),
runScriptName: runScriptName),
configure);
}
```

#### Advantages

- Most discoverable and approachable API.
- Required arguments are visible and documented.
- The integration can validate source paths and options.
- The integration can provide a clean polyglot API.
- The integration controls which owner shapes it supports.
- Error messages can use integration terminology.
- Common API behavior cannot drift if both named methods delegate to the same
framework materializer and resource implementation.

#### Disadvantages

- Every executable integration adds another extension method.
- `RunAsViteApp`, `RunAsPythonApp`, `RunAsUvicornApp`, `RunAsGoApp`,
`RunAsJavaApp`, and similar methods could create substantial IntelliSense
noise.
- C# extension method availability cannot express negative constraints such as
"owner does not already have an executable shape."

### Candidate API shape 2: generic `RunAs` with a resource factory

A generic API accepts a factory that constructs a resource implementing the
projection contract. Required arguments are supplied through normal resource
construction:

```csharp
container.RunAs(
context => new ViteAppResource(
context.Owner.Name,
command: "npm",
workingDirectory: context.ResolvePath("../frontend"),
runScriptName: "dev"),
vite => vite.WithRunScript("start"));
```

A possible signature is:

```csharp
public static IResourceBuilder RunAs(
this IResourceBuilder builder,
Func, TResource> createProjection,
Action>? configure = null)
where TOwner : IResource
where TResource : ExecutableResource, IExecutableProjection;
```

The factory solves the required-argument problem without reflection or a
parameterless constructor. The constructed resource identifies its
exact self-typed projection contract, and the caller's customization callback
receives the same typed builder.

The generic constraint establishes that the target is an executable projection
implementation. Runtime validation and analyzers must still reject unsupported
owner-to-target transitions.

The factory's compile-time return type is semantically significant. A factory
typed as `JavaScriptAppResource` invokes
`IExecutableProjection` even if it returns a
`ViteAppResource` instance at runtime. Integrations should return the intended
specific resource type, and named `RunAsFoo` wrappers should preserve that type.
The framework should not use reflection to select a different contract from the
runtime type.

#### Advantages

- One common API rather than one extension method per integration.
- Required arguments remain available through resource construction.
- The resource type explicitly identifies Vite rather than relying on
`AddViteApp` as a method-name convention.
- No reflection is needed.
- Third-party integrations can participate without adding methods to a central
extension class.
- Per-resource convenience methods can be implemented on top of this API.

#### Disadvantages

- More advanced syntax for ordinary AppHost authors.
- Public resource types add API surface that existing method-only
registrations do not need.
- Generated SDKs cannot rely directly on C# factory callbacks and generic
interface constraints.
- Resource constructors may expose details that named `AddFoo` methods
currently hide.
- Each derived implementation must explicitly compose any defaults it wants to
reuse from its base resource family.
- Generic inference and diagnostic quality need prototyping.

### Candidate API shape 3: generic `RunAs` with an integration-owned factory

Instead of exposing the resource constructor, integrations could add factory
extensions to `ResourceProjectionFactoryContext`:

```csharp
container.RunAs(
context => context.CreateViteApp(
appDirectory: "../frontend",
runScriptName: "dev"),
vite => vite.WithRunScript("start"));
```

`CreateViteApp` would normalize paths, validate required arguments, and return a
fully constructed `ViteAppResource`. The framework would still invoke
`IExecutableProjection.ApplyDefaultConfiguration`; the factory
does not apply defaults itself.

#### Advantages

- Keeps constructors internal.
- Gives integrations control over validation and required arguments.
- Keeps resource construction and path normalization behind an
integration-owned factory.
- Preserves the single generic `RunAs` materialization path.

#### Disadvantages

- Adds factory-context extension methods to the public API.
- More ceremony than either `RunAsViteApp` or direct typed construction.
- Generated SDKs still need a representation for the C# factory callback.
- Factory method discovery may be weaker than builder extension discovery.

## Alternative Designs

### Relationship between the API shapes

These API shapes are not mutually exclusive.

The generic typed-factory form can be the authoring primitive:

```csharp
public static IResourceBuilder RunAsViteApp(
this IResourceBuilder builder,
string appDirectory,
string runScriptName = "dev",
Action>? configure = null)
where TOwner : IResource
{
return builder.RunAs(
context => new ViteAppResource(
context.Owner.Name,
command: "npm",
workingDirectory: context.ResolvePath(appDirectory),
runScriptName: runScriptName),
configure);
}
```

Aspire could expose per-resource convenience methods only for common or
otherwise difficult integrations while leaving the generic API available for
advanced and third-party scenarios.

The design should be evaluated against IntelliSense noise:

- Broadly visible `RunAsFoo` methods may overwhelm builders for resources that
cannot legally use them.
- A generic `RunAs` method with a resource factory is compact but
shifts discovery from extension methods to projection-capable resource
types or integration factories.
- Generated SDKs likely require named methods or generated capability methods
even if C# uses the generic primitive.

## Risks

### Auxiliary resources

Installers and rebuilders must not be discovered by observing which resources an
`AddFoo` implementation happens to add. The configuration contract should
register them explicitly:

```csharp
void IExecutableProjection.ApplyDefaultConfiguration(
IResourceBuilder builder)
{
builder.WithJavaScriptAppDefaults();
builder.WithArgs(...);
builder.WithVSCodeDebugging(...);

if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
{
var installer = builder.AddOwnedSetupResource(
new JavaScriptInstallerResource(
$"{builder.Resource.GetOwnerOrSelf().Name}-installer",
builder.Resource.WorkingDirectory));

builder.WaitForCompletion(installer);
}
}
```

The projection system should record:

- Which owner and selected projection resource own the auxiliary resource.
- Whether the auxiliary resource is active for the selected operation.
- Whether it should be hidden or excluded from manifests.
- Which logical owner its relationships and events refer to.
- Whether repeated projection configuration reuses or replaces it.
- What happens if the selected projection is replaced before model completion.

The direct `AddViteApp` path would use the same builder-based hook. In that case
the Vite resource is both owner and effective resource, but the installer is
still registered through the same owned setup-resource API.

### Annotation inheritance remains unresolved

Container projections in microsoft/aspire#19880 share the owner's exact
annotation collection. That is appropriate for the compatibility and migration
goals of that change, but it should not automatically become the rule for every
source-to-target projection.

Container-to-executable projection may encounter:

- Container image, registry, and pull policy annotations with no process
equivalent.
- Entrypoints and arguments whose meaning depends on image metadata.
- Bind mounts, volumes, and container filesystem paths.
- Container network aliases and hostnames.
- Endpoints whose target ports need process-style allocation.
- Environment values containing container paths or container DNS names.
- Container command health checks.
- Container build and push pipeline behavior.
- Unknown third-party annotations.

At the same time, references, waits, relationships, environment values,
endpoints, health checks, and telemetry may represent logical workload
configuration that users expect to survive the projection.

The following policies should be researched rather than selected by assumption:

1. Inherit owner annotations by default and suppress known incompatible types.
2. Inherit by default and let the target projection transform or replace
incompatible values.
3. Inherit only recognized logical categories.
4. Let each source-to-target adapter define its own inheritance policy.
5. Introduce annotation applicability metadata or registration-time scoping.
6. Use different defaults for known container owners and shape-neutral owners.

Research should include configuration applied both before and after the
projection call. Call ordering must not accidentally decide whether a setting
belongs to the owner, the original shape, or the selected projection.

The target projection's own defaults are not optional: its
`ApplyDefaultConfiguration` hook always runs when the projection is
materialized. The unresolved question is how three configuration layers compose:

1. Target-native defaults applied by the projection contract.
2. Owner configuration inherited or adapted across the shape boundary.
3. Explicit customization applied through the `RunAs` callback or later owner
builder calls.

The implementation must define precedence between those layers independently
of incidental call timing. In particular, target defaults should not overwrite
explicit user configuration merely because projection realization happens
later in model construction.

### Owner and effective resource in deferred callbacks

Current projection-aware infrastructure canonicalizes many callback resources
to the owner. Language integrations frequently need concrete effective-resource
state such as:

- `WorkingDirectory`
- `Command`
- Vite configuration paths
- Python virtual environment and entrypoint metadata
- Package-manager selection
- Language-specific debugger configuration

`ApplyDefaultConfiguration` receives a builder only for synchronous
configuration. Implementations should not capture and retain that builder in
deferred callbacks. A callback should use its callback context, resolve the
effective resource when shape-specific state is required, and canonicalize to
the owner when registering notifications, relationships, or resource-keyed
state.

A generalized projection model may therefore need to expose both identities:

```csharp
public IResource Owner { get; }

public TResource EffectiveResource { get; }
```

Existing callback contracts cannot be changed casually. Additive properties or
new projection-aware contexts may be required. The design must make it difficult
for integrations to store `EffectiveResource` where canonical identity is
required while still making typed effective state available for shape-specific
configuration.

### Validation and diagnostics

Generic constraints cannot express all invalid source-to-target combinations.
The implementation may need the same layered enforcement used by container
projections:

- Static analyzer diagnostics for known-invalid C# receivers.
- Capability exclusions or named adapters for generated SDKs.
- Runtime validation for reflection, suppressed diagnostics, and custom generic
code.
- Validation that the factory returns a non-null resource implementing the exact
self-typed contract selected by `TResource`.
- Validation that a factory returns a resource with the owner's name.
- Validation that the builder passed to `ApplyDefaultConfiguration` represents
that same resource instance.
- Validation of the projection's annotation-storage contract.
- Validation that only one effective shape is selected for an operation.
- Validation that transitions between legacy `ProjectResource` and executable
shapes are rejected.
- Binary compatibility coverage proving that an integration compiled before the
new contracts can still load, derive from existing resource types, register
resources, and mutate annotations without implementing new abstract members.

Diagnostics should identify both the owner shape and requested projection shape
and explain which projection targets are supported.

### Alternatives not pursued

#### Remove and replace the owner

Rejected because builders and resource references obtained before conversion
become stale. This violates the central projection identity invariant and
creates delayed failures in references, waits, endpoints, events, and
resource-keyed state.

#### Temporary donor resource

Rejected because copied annotations and callbacks can retain the donor's
identity. Setup resources, endpoint references, event subscriptions, and CLR
properties may also remain associated with an object that is no longer in the
model.

#### Arbitrary `AddFoo` interception

Rejected as the primary design because the framework cannot reliably distinguish
the projection from resources added as dependencies or companions. Making those
roles explicit leads back to the proposed projection contract and owned-resource
extensions without requiring transactional interception.

#### Descriptor-only executable strategy

Not sufficient as the general solution. A raw command, working directory, and
argument recipe do not capture the setup resources, debugger behavior,
endpoints, certificate configuration, pipeline behavior, and framework-specific
defaults provided by language integrations.

#### Legacy `ProjectResource` to executable projection

Rejected in either direction because both sides already define complete
host-process execution models. Aspire cannot provide a reliable or useful
configuration adaptation between them.

### Suggested investigation sequence

This proposal should be validated with prototypes rather than committed as one
large cross-cutting implementation:

1. Extract generic JavaScript and Vite defaults into reusable builder helpers
without changing `AddJavaScriptApp` or `AddViteApp` behavior.
2. Implement the exact self-typed projection contracts on
`JavaScriptAppResource` and `ViteAppResource`, and route the existing direct
methods through the shared materialization helper.
3. Prototype container-to-Vite projection and verify configuration ordering,
owner-only model membership, and typed callback behavior.
4. Model the JavaScript installer explicitly as an owned setup resource.
5. Compare `RunAsViteApp`, raw resource-factory, and integration-owned factory
syntax in C# and generated SDKs.
6. Extract `DotnetProjectResource` defaults so `AddDotnetProject` uses the same
builder-based contract without changing behavior.
7. Prototype container-to-`DotnetProjectResource` projection, including project
path normalization, launch configuration, coordinated builds, and hidden
build resources.
8. Exercise configuration applied before and after projection.
9. Evaluate annotation inheritance using environment, endpoints, health checks,
mounts, arguments, and unknown custom annotations.
10. Confirm owner identity through references, waits, events, notifications,
dashboard state, and DCP realization.

### Open questions

1. Should the public contract be named
`IExecutableProjection`, or would a name emphasizing
configuration better distinguish it from a realized projection instance?
2. Should the framework require the projection interface directly on the
executable resource, or also support an external adapter for resource types
that cannot be changed?
3. Should C# primarily expose named `RunAsFoo` methods, generic
`RunAs` with a resource factory, or both?
4. At what granularity should base-family defaults be extracted so specific
implementations can explicitly compose or replace them safely?
5. When several `AddFoo` methods currently return one resource type, which
registrations should gain an internal subtype for configuration identity,
and which justify a new additive entry point that exposes a subtype-specific
builder API?
6. How should projection-capable resources be discovered in IntelliSense and
generated SDKs?
7. What annotation inheritance policy should apply to container-to-executable
projections?
8. Should shape-neutral owners use a different inheritance policy from
containers?
9. How are projection-local annotations represented without changing the public
`IResource.Annotations` type or breaking externally compiled resources?
10. How should settings applied to the owner after `RunAs` participate in the
selected projection?
11. How should owned auxiliary resources be reused, replaced, or removed when
the selected projection changes?
12. Which callback contexts need separate owner and effective-resource
properties?
13. Should the generic factory accept the owner directly, or a richer context
containing the owner, operation, services, and application builder?
14. Should direct caller invocation of `ApplyDefaultConfiguration` be
unsupported, guarded against repetition, or required to be idempotent?
15. How should generated SDKs represent factory-based construction that is
natural in C# but not portable across languages?

Contributor guide

Open the contributing guide

Research direction

Start by reading the existing container projection behavior from microsoft/aspire#19880, then trace the AddFoo, RunAsFoo, and CreateResourceBuilder entry points named here. Compare direct and projected registration, and document decisions for annotation inheritance, auxiliary-resource lifecycle, API shape, and generated SDK behavior. Done means a validated design that preserves the listed owner and identity invariants.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
cloud, devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.