Skip to content

Annotation-Based Open Discriminated Union for Aspire Resources #8984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
davidfowl opened this issue Apr 27, 2025 · 0 comments
Open

Annotation-Based Open Discriminated Union for Aspire Resources #8984

davidfowl opened this issue Apr 27, 2025 · 0 comments
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication breaking-change Issue or PR that represents a breaking API or functional change over a prerelease.

Comments

@davidfowl
Copy link
Member

Today every concrete resource class—ProjectResource, ContainerResource, AzureServiceBusResource, etc.—owns state in its own fields.
When real workflows demand the same logical resource appear differently in run-mode and publish-mode, our model currently must

  1. remove the original resource instance
  2. create another concrete type
  3. copy annotations by hand

That breaks identity (two resource IDs), scatters logs, confuses diff tooling, and forces hacks inside helpers such as RunAsContainer(), RunAsEmulator(), and PublishAsDockerFile().

We already tie container identity to the annotation-collection reference; this proposal finishes that idea for all resources:

  • All observable state lives in the annotation collection.
  • A discriminator annotation — ResourceTypeAnnotation.ResourceKind : Type — declares the current shape.
  • Wrapper classes (views) expose ergonomic APIs but share the same annotation spine.
Dev inner-loop Publish output
.NET Project OCI container
Emulator container Azure PaaS service
Local Redis container Connection-string parameter pointing at a shared cache

Identity never changes; we simply switch the tag.

graph TD
    subgraph "Annotations (identity)"
        A["{ annotations … ; tag = ResourceKind }"]
    end
    A -- viewed-as --> B[ProjectResource]
    A -- viewed-as --> C[ContainerResource]
    A -- viewed-as --> D[AzureBicepResource]
    A -- viewed-as --> E[ConnectionStringParam]
Loading

Code sketches (same helpers, new engine)

// Project ⇒ container on publish
builder.AddProject("web")
       .PublishAsDockerFile(); // internally retags to ContainerResource

// Azure PaaS ⇒ emulator container on local run
builder.AddAzureServiceBus("events")
       .RunAsEmulator();      // retags to ContainerResource

// Container ⇒ external hosted cache for prod
builder.AddRedis("cache")
       .PublishAsConnectionString(); // retags to ConnectionStringParameter

Execution plan (high-level)

Phase A — helper façade

  • Add IsKind<T>(), TryGet<T>() (initial impl = is/as).
  • Replace direct is, as, OfType<T>() in the codebase.
  • Roslyn analyzer forbids new violations.
  • Behaviour identical to today.

Phase B — tag & identity

  • Introduce ResourceTypeAnnotation and Resource.ResourceKind.
  • Equals/GetHashCode now rely on the annotation-collection reference.
  • Helpers switch to the tag internally.
  • Identity stable across view switches.

Phase C — move data to annotations

  • Create *Annotation classes (e.g. ContainerEntryPointAnnotation).
  • Properties wrap annotations; helpers retag instead of delete + clone.

Annotation collections become read-only after model-build; cloning must be explicit.


Helper API details

public static bool IsKind<T>(this IResource r)
    => r.ResourceKind == typeof(T);

public static bool TryGet<T>(this IResource r,
                             [NotNullWhen(true)] out T? view)
    where T : class, IResource
{
    if (r.ResourceKind == typeof(T))
    {
        view = r as T ??
               (T)Activator.CreateInstance(typeof(T), r.Name, r.Annotations)!;
        return true;
    }
    view = null;
    return false;
}

Every resource kind must expose a (string name, ResourceAnnotationCollection ann) constructor; an analyzer will enforce this.


Trade-offs & potential issues

  • Exhaustiveness – compiler no longer warns if a new kind is unhandled.
    Mitigation: default branches plus analyzer checks.
  • Performance – reflection in the helpers.
    Mitigation: cache typeof(T) comparisons and profile.
  • External extensions using is/as – will break when the tag diverges from CLR type.
    Mitigation: analyzer package + migration docs; optional runtime guard.
  • Annotation mutability – copy-on-write or concurrent edits could corrupt identity.
    Mitigation: freeze collection reference after build; mutations through WithAnnotation.
  • Constructor convention – new kinds must add the two-arg ctor.
    Mitigation: analyzer + project template.
  • Versioning – equality semantics change.
    Mitigation: land in next major release; debug shim to detect old behaviour.

Unsolved / open design gaps

View-specific members remain callable after a view switch.

Example: you create an AzureBicepResource, call RunAsEmulator(), so it is now viewed as a container, yet bicepResource.Outputs["primaryKey"] is still accessible—even though those outputs are meaningless in run-mode.

Unanswered questions:

  • Do we introduce publish-only / run-only capabilities so annotations can self-describe validity?
  • Should runtime guards throw (or assert) when a publish-only member is accessed under a run-mode tag?
  • Can a Roslyn analyzer warn when publish-only members are used in run-time code paths?
  • At minimum we need docs that state: after a view switch certain members are undefined and accessing them is user error.

These remain open and must be tracked as follow-up work once the union mechanics are in place.

See #7251 for an initial prototype

@davidfowl davidfowl added area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication breaking-change Issue or PR that represents a breaking API or functional change over a prerelease. labels Apr 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication breaking-change Issue or PR that represents a breaking API or functional change over a prerelease.
Projects
None yet
Development

No branches or pull requests

1 participant