Skip to content
Merged
Next Next commit
change resource scope detection logic.
  • Loading branch information
haiyuazhang committed Sep 15, 2025
commit a47d7a788a2a475d7cb70d54032e274cbecb8cc1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
convertResourceMetadataToArguments,
NonResourceMethod,
ResourceMetadata,
ResourceMethod,
ResourceOperationKind,
ResourceScope
} from "./resource-metadata.js";
Expand Down Expand Up @@ -58,6 +59,9 @@ export async function updateClients(
sdkContext.sdkPackage.models.map((m) => [m.crossLanguageDefinitionId, m])
);
const resourceModels = getAllResourceModels(codeModel);
const resourceModelMap = new Map<string, InputModelType>(
resourceModels.map((m) => [m.crossLanguageDefinitionId, m])
);

const resourceModelToMetadataMap = new Map<string, ResourceMetadata>(
resourceModels.map((m) => [
Expand All @@ -68,7 +72,7 @@ export async function updateClients(
singletonResourceName: getSingletonResource(
m.decorators?.find((d) => d.name == singleton)
),
resourceScope: getResourceScope(m),
resourceScope: ResourceScope.ResourceGroup, // temporary default to ResourceGroup, will be properly set later after methods are populated
methods: [],
parentResourceId: undefined, // this will be populated later
resourceName: m.name
Expand Down Expand Up @@ -137,6 +141,12 @@ export async function updateClients(
resourceModelToMetadataMap.values()
);
}

// update the model's resourceScope based on resource scope decorator if exist or based on the Get method's scope. If neither exist, it will be set to ResourceGroup by default
const model = resourceModelMap.get(modelId);
if (model) {
metadata.resourceScope = getResourceScope(model, metadata.methods);
}
}

// the last step, add the decorator to the resource model
Expand Down Expand Up @@ -290,7 +300,8 @@ function getSingletonResource(
return singletonResource ?? "default";
}

function getResourceScope(model: InputModelType): ResourceScope {
function getResourceScope(model: InputModelType, methods?: ResourceMethod[]): ResourceScope {
// First, check for explicit scope decorators
const decorators = model.decorators;
if (decorators?.some((d) => d.name == tenantResource)) {
return ResourceScope.Tenant;
Expand All @@ -299,6 +310,16 @@ function getResourceScope(model: InputModelType): ResourceScope {
} else if (decorators?.some((d) => d.name == resourceGroupResource)) {
return ResourceScope.ResourceGroup;
}

// Fall back to Get method's scope only if no scope decorators are found
if (methods) {
const getMethod = methods.find(m => m.kind === ResourceOperationKind.Get);
if (getMethod) {
return getMethod.operationScope;
}
}

// Final fallback to ResourceGroup
return ResourceScope.ResourceGroup; // all the templates work as if there is a resource group decorator when there is no such decorator
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import { TestHost } from "@typespec/compiler/testing";
import { createModel } from "@typespec/http-client-csharp";
import { getAllClients, updateClients } from "../src/resource-detection.js";
import { ok, strictEqual } from "assert";
import { resourceMetadata } from "../src/sdk-context-options.js";
import {
resourceMetadata,
tenantResource,
subscriptionResource,
resourceGroupResource
} from "../src/sdk-context-options.js";
import { ResourceScope } from "../src/resource-metadata.js";

describe("Resource Detection", () => {
Expand Down Expand Up @@ -963,4 +968,73 @@ interface Employees {
);
strictEqual(employeeMetadataDecorator.arguments.resourceName, "Employee");
});

it("resource scope determined from Get method when no explicit decorator", async () => {
const program = await typeSpecCompile(
`
@parentResource(SubscriptionLocationResource)
model Employee is ProxyResource<EmployeeProperties> {
...ResourceNameParameter<Employee, Type = EmployeeType>;
}

model EmployeeProperties {
age?: int32;
}

union EmployeeType {
string,
}

interface Operations extends Azure.ResourceManager.Operations {}

@armResourceOperations
interface Employees {
get is ArmResourceRead<Employee>;
}
`,
runner
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const root = createModel(sdkContext);
updateClients(root, sdkContext);

const employeeClient = getAllClients(root).find(
(c) => c.name === "Employees"
);
ok(employeeClient);
const employeeModel = root.models.find((m) => m.name === "Employee");
ok(employeeModel);
const getMethod = employeeClient.methods.find((m) => m.name === "get");
ok(getMethod);

const resourceMetadataDecorator = employeeModel.decorators?.find(
(d) => d.name === resourceMetadata
);
ok(resourceMetadataDecorator);
ok(resourceMetadataDecorator.arguments);

// Verify that the model has NO scope-related decorators
const hasNoScopeDecorators = !employeeModel.decorators?.some((d) =>
d.name === tenantResource ||
d.name === subscriptionResource ||
d.name === resourceGroupResource
);
ok(hasNoScopeDecorators, "Model should have no scope-related decorators to test fallback logic");

// The model should inherit its resourceScope from the Get method's operationScope (Subscription)
// because the Get method operates at subscription scope and there are no explicit scope decorators
strictEqual(
resourceMetadataDecorator.arguments.resourceScope,
"Subscription"
);

// Verify the Get method itself has the correct scope
const getMethodEntry = resourceMetadataDecorator.arguments.methods.find(
(m: any) => m.methodId === getMethod.crossLanguageDefinitionId
);
ok(getMethodEntry);
strictEqual(getMethodEntry.kind, "Get");
strictEqual(getMethodEntry.operationScope, ResourceScope.Subscription);
});
});