Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4bc78a7
Initial plan
Copilot May 19, 2026
327c2c7
Support exact client name (TCGC 0.68.0)
Copilot May 19, 2026
43ccb46
Add exact-name tests for all targets + custom-code priority
Copilot May 21, 2026
a846ba9
Regenerate test libraries after IsExactName plumbing
Copilot May 21, 2026
870e9b8
Add serialization + method-parameter coverage for IsExactName, switch…
Copilot May 21, 2026
9ab4f55
Merge remote-tracking branch 'origin/main' into copilot/support-exact…
Copilot May 21, 2026
5080471
Regenerate test projects after merge with main
Copilot May 21, 2026
9133830
Refactor IsExactName serialization tests to use TestData files
Copilot May 21, 2026
997509a
Add @clientName(exact()) demo to Sample-TypeSpec RoundTripModel and r…
Copilot May 21, 2026
81e306e
Fix cspell: ignore Deserializesnake in IsExactNameModelSerializationU…
Copilot May 21, 2026
ce30c85
Merge remote-tracking branch 'origin/main' into copilot/support-exact…
Copilot May 26, 2026
312985a
Support isExactName on enum values (TCGC 0.68.2)
Copilot May 26, 2026
76d5579
Add isExactName support for SdkClientType and SdkServiceMethodBase
Copilot May 26, 2026
327dfcb
Fix prettier formatting in client-converter.ts
Copilot May 27, 2026
8d6f906
Address review feedback: relocate tests, simplify, add TestData & cus…
Copilot May 27, 2026
b6b4d38
Preserve exact C# parameter name when InputParameter.IsExactName is true
Copilot May 27, 2026
7fb0773
Remove explanatory comment in ParameterProvider.GetVariableExpression
Copilot May 27, 2026
ed81d7b
Merge remote-tracking branch 'origin/main' into copilot/support-exact…
May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,17 @@ function fromSdkClient(

const isMultiService = isMultiServiceClient(client);
const clientName =
!client.parent && isMultiService && !client.name.toLowerCase().endsWith("client")
!client.parent &&
isMultiService &&
!client.isExactName &&
!client.name.toLowerCase().endsWith("client")
? `${client.name}Client`
: client.name;

inputClient = {
kind: "client",
name: clientName,
isExactName: client.isExactName,
namespace: client.namespace,
doc: client.doc,
summary: client.summary,
Expand Down Expand Up @@ -204,6 +208,7 @@ function fromSdkClient(
methodParameterSegments: diagnostics.pipe(
getMethodParameterSegments(sdkContext, parameter),
),
isExactName: parameter.isExactName,
Comment thread
jorgerangel-msft marked this conversation as resolved.
});
}
return diagnostics.wrap(parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export function fromSdkServiceMethodOperation(

operation = {
name: method.name,
isExactName: method.isExactName,
resourceName:
getResourceOperation(sdkContext.program, method.operation.__raw.operation)?.resourceType
.name ??
Expand Down Expand Up @@ -273,6 +274,7 @@ function createServiceMethod<T extends InputServiceMethod>(
return diagnostics.wrap({
kind: method.kind,
name: method.name,
isExactName: method.isExactName,
accessibility: method.access,
apiVersions: method.apiVersions,
doc: method.doc,
Expand Down Expand Up @@ -504,6 +506,7 @@ function fromQueryParameter(
crossLanguageDefinitionId: p.crossLanguageDefinitionId,
readOnly: isReadOnly(p),
methodParameterSegments: diagnostics.pipe(getMethodParameterSegments(sdkContext, p)),
isExactName: p.isExactName,
};

sdkContext.__typeCache.updateSdkOperationParameterReferences(p, retVar);
Expand Down Expand Up @@ -539,6 +542,7 @@ function fromPathParameter(
readOnly: isReadOnly(p),
crossLanguageDefinitionId: p.crossLanguageDefinitionId,
methodParameterSegments: diagnostics.pipe(getMethodParameterSegments(sdkContext, p)),
isExactName: p.isExactName,
};

sdkContext.__typeCache.updateSdkOperationParameterReferences(p, retVar);
Expand Down Expand Up @@ -574,6 +578,7 @@ function fromHeaderParameter(
crossLanguageDefinitionId: p.crossLanguageDefinitionId,
methodParameterSegments: diagnostics.pipe(getMethodParameterSegments(sdkContext, p)),
collectionHeaderPrefix: diagnostics.pipe(getCollectionHeaderPrefix(sdkContext, p)),
isExactName: p.isExactName,
};

sdkContext.__typeCache.updateSdkOperationParameterReferences(p, retVar);
Expand Down Expand Up @@ -604,6 +609,7 @@ function fromBodyParameter(
readOnly: isReadOnly(p),
crossLanguageDefinitionId: p.crossLanguageDefinitionId,
methodParameterSegments: diagnostics.pipe(getMethodParameterSegments(sdkContext, p)),
isExactName: p.isExactName,
serializationOptions: p.serializationOptions,
};

Expand Down Expand Up @@ -646,6 +652,7 @@ export function fromMethodParameter(
access: p.access,
decorators: p.decorators,
paramAlias,
isExactName: p.isExactName,
};

sdkContext.__typeCache.updateSdkMethodParameterReferences(p, retVar);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ function fromSdkModelType(
decorators: decorators,
external: fromSdkExternalTypeInfo(modelType),
serializationOptions: modelType.serializationOptions,
isExactName: modelType.isExactName,
} as InputModelType;

sdkContext.__typeCache.updateSdkTypeReferences(modelType, inputModelType);
Expand Down Expand Up @@ -291,6 +292,7 @@ function fromSdkModelProperty(
// A property is defined to be metadata if it is marked `@header`, `@cookie`, `@query`, `@path`.
isHttpMetadata: isHttpMetadata(sdkContext, sdkProperty),
encode: sdkProperty.encode,
isExactName: sdkProperty.isExactName,
} as InputModelProperty;

if (property) {
Expand Down Expand Up @@ -342,6 +344,7 @@ function createEnumType(
usage: sdkType.kind === "enum" ? sdkType.usage : UsageFlags.None,
decorators: sdkType.decorators,
external: fromSdkExternalTypeInfo(sdkType),
isExactName: sdkType.isExactName,
};

sdkContext.__typeCache.updateSdkTypeReferences(sdkType, inputEnumType);
Expand Down Expand Up @@ -431,6 +434,7 @@ function fromUnionType(
namespace: union.namespace,
decorators: union.decorators,
external: fromSdkExternalTypeInfo(union),
isExactName: union.isExactName,
});
}

Expand All @@ -448,6 +452,7 @@ function fromSdkConstantType(
valueType: diagnostics.pipe(fromSdkType(sdkContext, constantType.valueType)),
value: constantType.value,
decorators: constantType.decorators,
isExactName: constantType.isExactName,
};

sdkContext.__typeCache.updateConstantCache(constantType, literalType);
Expand Down Expand Up @@ -488,6 +493,7 @@ function createEnumValueType(
summary: sdkType.summary,
doc: sdkType.doc,
decorators: sdkType.decorators,
isExactName: sdkType.isExactName,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { RequestMethod } from "./request-method.js";

export interface InputOperation {
name: string;
isExactName?: boolean;
resourceName?: string;
summary?: string;
deprecated?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type InputServiceMethod =
interface InputServiceMethodBase {
kind: string;
name: string;
isExactName?: boolean;
accessibility?: string;
apiVersions: string[];
doc?: string;
Expand Down
13 changes: 13 additions & 0 deletions packages/http-client-csharp/emitter/src/type/input-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface InputExternalTypeMetadata {
export interface InputClient extends DecoratedType {
kind: "client";
name: string;
isExactName?: boolean;
namespace: string;
doc?: string;
summary?: string;
Expand Down Expand Up @@ -97,6 +98,8 @@ export interface InputLiteralType extends InputTypeBase {
namespace: string;
valueType: InputPrimitiveType;
value: string | number | boolean | null;
/** Whether the name should be used exactly as-is, without casing transformations. */
isExactName?: boolean;
}

export function isInputLiteralType(type: InputType): type is InputLiteralType {
Expand Down Expand Up @@ -135,6 +138,8 @@ export interface InputUnionType extends InputTypeBase {
name: string;
variantTypes: InputType[];
namespace: string;
/** Whether the name should be used exactly as-is, without casing transformations. */
isExactName?: boolean;
}

export function isInputUnionType(type: InputType): type is InputUnionType {
Expand All @@ -159,6 +164,8 @@ export interface InputModelType extends InputTypeBase {
discriminatorProperty?: InputModelProperty;
baseModel?: InputModelType;
serializationOptions: SerializationOptions;
/** Whether the name should be used exactly as-is, without casing transformations. */
isExactName?: boolean;
}

export interface InputPropertyTypeBase extends DecoratedType {
Expand All @@ -172,6 +179,8 @@ export interface InputPropertyTypeBase extends DecoratedType {
crossLanguageDefinitionId: string;
readOnly: boolean;
access?: AccessFlags;
/** Whether the name should be used exactly as-is, without casing transformations. */
isExactName?: boolean;
}

export interface InputModelProperty extends InputPropertyTypeBase {
Expand Down Expand Up @@ -266,6 +275,8 @@ export interface InputEnumType extends InputTypeBase {
usage: UsageFlags;
access?: AccessFlags;
namespace: string;
/** Whether the name should be used exactly as-is, without casing transformations. */
isExactName?: boolean;
}

export interface InputEnumValueType extends InputTypeBase {
Expand All @@ -274,6 +285,8 @@ export interface InputEnumValueType extends InputTypeBase {
value: string | number;
enumType: InputEnumType;
valueType: InputPrimitiveType;
/** Whether the name should be used exactly as-is, without casing transformations. */
isExactName?: boolean;
}

export interface InputNullableType extends InputTypeBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -688,3 +688,28 @@ describe("client name suffix", () => {
}
});
});

describe("Test isExactName propagation on clients", () => {
let runner: TestHost;

beforeEach(async () => {
runner = await createEmitterTestHost();
});

it("propagates isExactName from @clientName decorator with exact() on a client", async () => {
const program = await typeSpecCompile(
`
@@clientName(Azure.Csharp.Testing, Azure.ClientGenerator.Core.exact("snake_case_client"), "csharp");
op test(): void;
`,
runner,
{ IsTCGCNeeded: true },
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);
const client = root.clients.find((c) => c.name === "snake_case_client");
ok(client);
strictEqual(client.isExactName, true);
});
});
149 changes: 149 additions & 0 deletions packages/http-client-csharp/emitter/test/Unit/model-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1164,3 +1164,152 @@ describe("XML serialization options", () => {
ok(bodyParam.serializationOptions.json);
});
});

describe("Test isExactName propagation", () => {
let runner: TestHost;

beforeEach(async () => {
runner = await createEmitterTestHost();
});

it("propagates isExactName from @clientName decorator with exact() on property", async () => {
const program = await typeSpecCompile(
`
model Book {
@clientName(Azure.ClientGenerator.Core.exact("snake_case_name"), "csharp")
name: string;
}

op test(@body input: Book): Book;
`,
runner,
{ IsTCGCNeeded: true },
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);
const bookModel = root.models.find((m) => m.name === "Book");
ok(bookModel);
const nameProp = bookModel.properties.find((p) => p.name === "snake_case_name");
ok(nameProp);
strictEqual(nameProp.isExactName, true);
});

it("propagates isExactName from @clientName decorator with exact() on model", async () => {
const program = await typeSpecCompile(
`
@clientName(Azure.ClientGenerator.Core.exact("my_exact_model"), "csharp")
model Book {
name: string;
}

op test(@body input: Book): Book;
`,
runner,
{ IsTCGCNeeded: true },
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);
const bookModel = root.models.find((m) => m.name === "my_exact_model");
ok(bookModel);
strictEqual(bookModel.isExactName, true);
});

it("does not set isExactName when @clientName decorator does not use exact()", async () => {
const program = await typeSpecCompile(
`
model Book {
@clientName("regularName")
name: string;
}

op test(@body input: Book): Book;
`,
runner,
{ IsTCGCNeeded: true },
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);
const bookModel = root.models.find((m) => m.name === "Book");
ok(bookModel);
const nameProp = bookModel.properties.find((p) => p.name === "regularName");
ok(nameProp);
strictEqual(nameProp.isExactName, false);
});

it("propagates isExactName from @clientName decorator with exact() on enum", async () => {
const program = await typeSpecCompile(
`
@clientName(Azure.ClientGenerator.Core.exact("my_exact_enum"), "csharp")
enum Color {
Red,
Green,
Blue,
}

op test(@body input: Color): void;
`,
runner,
{ IsTCGCNeeded: true },
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);
const colorEnum = root.enums.find((e) => e.name === "my_exact_enum");
ok(colorEnum);
strictEqual(colorEnum.isExactName, true);
});

it("propagates isExactName from @clientName decorator with exact() on union", async () => {
const program = await typeSpecCompile(
`
@clientName(Azure.ClientGenerator.Core.exact("my_exact_union"), "csharp")
union Color {
string,
"red",
"green",
}

op test(@body input: Color): void;
`,
runner,
{ IsTCGCNeeded: true },
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);
const colorEnum = root.enums.find((e) => e.name === "my_exact_union");
ok(colorEnum);
strictEqual(colorEnum.isExactName, true);
});

it("propagates isExactName from @clientName decorator with exact() on an enum value", async () => {
const program = await typeSpecCompile(
`
enum Color {
Red,
@clientName(Azure.ClientGenerator.Core.exact("snake_case_value"), "csharp")
Green,
Blue,
}

op test(@body input: Color): void;
`,
runner,
{ IsTCGCNeeded: true },
);
const context = createEmitterContext(program);
const sdkContext = await createCSharpSdkContext(context);
const [root] = createModel(sdkContext);
const colorEnum = root.enums.find((e) => e.name === "Color");
ok(colorEnum);
const exactValue = colorEnum.values.find((v) => v.name === "snake_case_value");
ok(exactValue);
strictEqual(exactValue.isExactName, true);
const regularValue = colorEnum.values.find((v) => v.name === "Red");
ok(regularValue);
strictEqual(regularValue.isExactName, false);
});
});
Loading
Loading