You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
remove the original resource instance
create another concrete type
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 publishbuilder.AddProject("web").PublishAsDockerFile();// internally retags to ContainerResource// Azure PaaS ⇒ emulator container on local runbuilder.AddAzureServiceBus("events").RunAsEmulator();// retags to ContainerResource// Container ⇒ external hosted cache for prodbuilder.AddRedis("cache").PublishAsConnectionString();// retags to ConnectionStringParameter
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.
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
That breaks identity (two resource IDs), scatters logs, confuses diff tooling, and forces hacks inside helpers such as
RunAsContainer(),RunAsEmulator(), andPublishAsDockerFile().We already tie container identity to the annotation-collection reference; this proposal finishes that idea for all resources:
ResourceTypeAnnotation.ResourceKind : Type— declares the current shape..NET ProjectIdentity 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]Code sketches (same helpers, new engine)
Execution plan (high-level)
Phase A — helper façade
IsKind<T>(),TryGet<T>()(initial impl =is/as).is,as,OfType<T>()in the codebase.Phase B — tag & identity
ResourceTypeAnnotationandResource.ResourceKind.Equals/GetHashCodenow rely on the annotation-collection reference.Phase C — move data to annotations
*Annotationclasses (e.g.ContainerEntryPointAnnotation).Annotation collections become read-only after model-build; cloning must be explicit.
Helper API details
Every resource kind must expose a
(string name, ResourceAnnotationCollection ann)constructor; an analyzer will enforce this.Trade-offs & potential issues
Mitigation: default branches plus analyzer checks.
Mitigation: cache
typeof(T)comparisons and profile.is/as– will break when the tag diverges from CLR type.Mitigation: analyzer package + migration docs; optional runtime guard.
Mitigation: freeze collection reference after build; mutations through
WithAnnotation.Mitigation: analyzer + project template.
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, callRunAsEmulator(), so it is now viewed as a container, yetbicepResource.Outputs["primaryKey"]is still accessible—even though those outputs are meaningless in run-mode.Unanswered questions:
These remain open and must be tracked as follow-up work once the union mechanics are in place.
See #7251 for an initial prototype