Skip to content

Commit 7d060ec

Browse files
Skip convenience method generation for multipart/mixed requests (#10967)
The C# emitter does not support `multipart/mixed` requests, yet since the addition of `multipart/form-data` support it would emit an incorrect convenience method for them. This disables convenience method generation for multipart content types other than `multipart/form-data` and surfaces a diagnostic instead. The detection lives in the emitter so the generator simply honors the resulting `generateConvenienceMethod` flag. ### Changes - **`operation-converter.ts`**: In `fromSdkServiceMethodOperation`, when an operation uses a `multipart/*` content type other than `multipart/form-data`, `generateConvenienceMethod` is set to `false` (protocol methods still emitted) and a warning diagnostic is reported. A small `isUnsupportedMultipart` helper inspects the request media types. - **`lib.ts`**: Added the `unsupported-multipart-convenience-method` warning diagnostic. - **Tests**: Added emitter unit tests in `operation-converter.test.ts` — `multipart/mixed` turns off convenience method generation and emits the warning, while `multipart/form-data` keeps convenience methods with no diagnostic. ```ts const requestMediaTypes = getRequestMediaTypes(method.operation); if (generateConvenience && isUnsupportedMultipart(requestMediaTypes)) { diagnostics.add( createDiagnostic({ code: "unsupported-multipart-convenience-method", format: { methodCrossLanguageDefinitionId: method.crossLanguageDefinitionId }, target: method.__raw ?? NoTarget, }), ); generateConvenience = false; } ``` No existing test specs use `multipart/mixed`, so generated library output is unchanged. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
1 parent 3c918ef commit 7d060ec

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

packages/http-client-csharp/emitter/src/lib/lib.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ const diags: { [code: string]: DiagnosticDefinition<DiagnosticMessages> } = {
9595
default: paramMessage`Convenience method is not supported for PATCH method, it will be turned off. Please set the '@convenientAPI' to false for operation ${"methodCrossLanguageDefinitionId"}.`,
9696
},
9797
},
98+
"unsupported-multipart-convenience-method": {
99+
severity: "warning",
100+
messages: {
101+
default: paramMessage`Convenience method is not supported for multipart content types other than 'multipart/form-data', it will be turned off. Please set the '@convenientAPI' to false for operation ${"methodCrossLanguageDefinitionId"}.`,
102+
},
103+
},
98104
"unsupported-service-method": {
99105
severity: "warning",
100106
messages: {

packages/http-client-csharp/emitter/src/lib/operation-converter.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,20 @@ export function fromSdkServiceMethodOperation(
196196
generateConvenience = false;
197197
}
198198

199+
const requestMediaTypes = getRequestMediaTypes(method.operation);
200+
if (generateConvenience && isUnsupportedMultipart(requestMediaTypes)) {
201+
diagnostics.add(
202+
createDiagnostic({
203+
code: "unsupported-multipart-convenience-method",
204+
format: {
205+
methodCrossLanguageDefinitionId: method.crossLanguageDefinitionId,
206+
},
207+
target: method.__raw ?? NoTarget,
208+
}),
209+
);
210+
generateConvenience = false;
211+
}
212+
199213
operation = {
200214
name: method.name,
201215
isExactName: method.isExactName,
@@ -217,7 +231,7 @@ export function fromSdkServiceMethodOperation(
217231
uri: uri,
218232
path: method.operation.path,
219233
externalDocsUrl: getExternalDocs(sdkContext, method.operation.__raw.operation)?.url,
220-
requestMediaTypes: getRequestMediaTypes(method.operation),
234+
requestMediaTypes: requestMediaTypes,
221235
bufferResponse: true,
222236
generateProtocolMethod: shouldGenerateProtocol(sdkContext, method.operation.__raw.operation),
223237
generateConvenienceMethod: generateConvenience,
@@ -751,6 +765,15 @@ function toStatusCodesArray(range: number | HttpStatusCodeRange): number[] {
751765
return statusCodes;
752766
}
753767

768+
function isUnsupportedMultipart(requestMediaTypes: string[] | undefined): boolean {
769+
return (
770+
requestMediaTypes?.some((mediaType) => {
771+
const normalized = mediaType.toLowerCase();
772+
return normalized.startsWith("multipart/") && normalized !== "multipart/form-data";
773+
}) ?? false
774+
);
775+
}
776+
754777
function getRequestMediaTypes(op: SdkHttpOperation): string[] | undefined {
755778
const contentTypes = op.parameters.filter(
756779
(p) => p.kind === "header" && p.serializedName.toLocaleLowerCase() === "content-type",

packages/http-client-csharp/emitter/test/Unit/operation-converter.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,3 +622,79 @@ describe("Test isExactName propagation on operations and parameters", () => {
622622
strictEqual(method.operation.isExactName, true);
623623
});
624624
});
625+
626+
describe("Multipart convenience method generation", () => {
627+
let runner: TestHost;
628+
629+
beforeEach(async () => {
630+
runner = await createEmitterTestHost();
631+
});
632+
633+
it("disables convenience method for multipart/mixed and reports a diagnostic", async () => {
634+
const program = await typeSpecCompile(
635+
`
636+
model MultipartRequest {
637+
id: HttpPart<string>;
638+
profileImage: HttpPart<File>;
639+
}
640+
641+
@post
642+
@route("/upload")
643+
op upload(
644+
@header contentType: "multipart/mixed",
645+
@multipartBody body: MultipartRequest,
646+
): NoContentResponse;
647+
`,
648+
runner,
649+
{ IsTCGCNeeded: true },
650+
);
651+
const context = createEmitterContext(program);
652+
const sdkContext = await createCSharpSdkContext(context);
653+
const [root, diagnostics] = createModel(sdkContext);
654+
655+
const operation = root.clients[0].methods[0].operation;
656+
ok(operation);
657+
ok(operation.requestMediaTypes?.includes("multipart/mixed"));
658+
// Protocol methods are still generated, but convenience methods are turned off.
659+
strictEqual(operation.generateProtocolMethod, true);
660+
strictEqual(operation.generateConvenienceMethod, false);
661+
662+
const diagnostic = diagnostics.find(
663+
(d) => d.code === "@typespec/http-client-csharp/unsupported-multipart-convenience-method",
664+
);
665+
ok(diagnostic);
666+
strictEqual(diagnostic.severity, "warning");
667+
});
668+
669+
it("keeps convenience method for multipart/form-data without a diagnostic", async () => {
670+
const program = await typeSpecCompile(
671+
`
672+
model MultipartRequest {
673+
profileImage: HttpPart<File>;
674+
}
675+
676+
@post
677+
@route("/upload")
678+
op upload(
679+
@header contentType: "multipart/form-data",
680+
@multipartBody body: MultipartRequest,
681+
): NoContentResponse;
682+
`,
683+
runner,
684+
{ IsTCGCNeeded: true },
685+
);
686+
const context = createEmitterContext(program);
687+
const sdkContext = await createCSharpSdkContext(context);
688+
const [root, diagnostics] = createModel(sdkContext);
689+
690+
const operation = root.clients[0].methods[0].operation;
691+
ok(operation);
692+
ok(operation.requestMediaTypes?.includes("multipart/form-data"));
693+
strictEqual(operation.generateConvenienceMethod, true);
694+
695+
const diagnostic = diagnostics.find(
696+
(d) => d.code === "@typespec/http-client-csharp/unsupported-multipart-convenience-method",
697+
);
698+
strictEqual(diagnostic, undefined);
699+
});
700+
});

0 commit comments

Comments
 (0)