Skip to content

resolveArmResources incorrectly merges cross-scope LegacyOperations into a single resource #4055

@ArcturusZhang

Description

@ArcturusZhang

Description

resolveArmResources in @azure-tools/typespec-azure-resource-manager incorrectly merges cross-scope LegacyOperations that share the same model into a single resource, instead of separating them into distinct resources per scope.

This affects real-world patterns like the Support SDK, where SupportTicketDetails (decorated with @subscriptionResource) is used by both subscription-scoped (SupportTickets) and tenant-scoped (SupportTicketsNoSubscription) interfaces via LegacyOperations.

Reproduction

@subscriptionResource
model SupportTicketDetails is ProxyResource<SupportTicketProperties> {
  ...ResourceNameParameter<
    Resource = SupportTicketDetails,
    KeyName = "supportTicketName",
    SegmentName = "supportTickets",
    NamePattern = ""
  >;
}

model SupportTicketProperties {
  description?: string;
}

// Subscription-scoped operations (includes SubscriptionIdParameter)
alias SubTicketOps = Azure.ResourceManager.Legacy.LegacyOperations<
  { ...ApiVersionParameter; ...SubscriptionIdParameter; ...Azure.ResourceManager.Legacy.Provider; },
  { @segment("supportTickets") @key @TypeSpec.Http.path supportTicketName: string; }
>;

// Tenant-scoped operations (no SubscriptionIdParameter)
alias TenantTicketOps = Azure.ResourceManager.Legacy.LegacyOperations<
  { ...ApiVersionParameter; ...Azure.ResourceManager.Legacy.Provider; },
  { @segment("supportTickets") @key @TypeSpec.Http.path supportTicketName: string; }
>;

@armResourceOperations
interface SupportTickets {
  get is SubTicketOps.Read<SupportTicketDetails>;
  list is SubTicketOps.List<SupportTicketDetails>;
}

@armResourceOperations
interface SupportTicketsNoSubscription {
  get is TenantTicketOps.Read<SupportTicketDetails>;
  list is TenantTicketOps.List<SupportTicketDetails>;
}

Expected behavior

resolveArmResources should produce 2 separate resources (one per scope):

  1. Subscription-scoped resource with resourceInstancePath: "/subscriptions/{subscriptionId}/providers/.../supportTickets/{supportTicketName}", scope: "Subscription", and its own lifecycle read + list operations
  2. Tenant-scoped resource with resourceInstancePath: "/providers/.../supportTickets/{supportTicketName}", scope: "Tenant", and its own lifecycle read + list operations

Actual behavior

resolveArmResources returns 1 merged resource:

resources.length: 1

Resource:
  typeName: SupportTicketDetails
  resourceInstancePath: /subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/supportTickets/{supportTicketName}
  resourceName: SupportTickets
  scope: Subscription

  lifecycle.read[0]:
    kind=read, operationGroup=SupportTickets
    path=/subscriptions/{subscriptionId}/providers/.../supportTickets/{supportTicketName}

  lifecycle.read[1]:
    kind=read, operationGroup=SupportTicketsNoSubscription
    path=/providers/.../supportTickets/{supportTicketName}

  lists[0]:
    kind=list, operationGroup=SupportTickets
    path=/subscriptions/{subscriptionId}/providers/.../supportTickets

  lists[1]:
    kind=list, operationGroup=SupportTicketsNoSubscription
    path=/providers/.../supportTickets

The operations from both scopes are merged into a single resource. The individual operations do retain their correct paths (e.g., lifecycle.read[1] has the tenant path /providers/.../supportTickets/{name}), but the resource-level scope is "Subscription" and only the subscription resourceInstancePath is used.

Affected package

packages/typespec-azure-resource-manager/src/resource.tsresolveArmResources function

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions