From f011c2bb3e0325a2b8f276cb39fdaea6a769085b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:53:43 +0000 Subject: [PATCH 1/3] Initial plan From c3f81d6fb7181fdab81e776a4f874af72952464b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:06:15 +0000 Subject: [PATCH 2/3] fix(http-server-csharp): use correct ASP.NET Core result methods for non-200/204 status codes Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- ...mitter-status-codes-2026-03-04-00-54-22.md | 7 +++ .../http-server-csharp/src/lib/service.ts | 17 +++---- packages/http-server-csharp/src/lib/utils.ts | 25 ++++++++++ .../test/generation.test.ts | 47 +++++++++++++++++++ 4 files changed, 88 insertions(+), 8 deletions(-) create mode 100644 .chronus/changes/fix-csharp-emitter-status-codes-2026-03-04-00-54-22.md diff --git a/.chronus/changes/fix-csharp-emitter-status-codes-2026-03-04-00-54-22.md b/.chronus/changes/fix-csharp-emitter-status-codes-2026-03-04-00-54-22.md new file mode 100644 index 00000000000..22be7cedb04 --- /dev/null +++ b/.chronus/changes/fix-csharp-emitter-status-codes-2026-03-04-00-54-22.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-server-csharp" +--- + +Fix controller generation to use correct ASP.NET Core result methods for non-200/204 status codes. Previously all operations returned `Ok(...)` or `NoContent()` regardless of the declared status code. Now operations returning 202 use `Accepted(...)`, and other status codes use `StatusCode(code, ...)`. diff --git a/packages/http-server-csharp/src/lib/service.ts b/packages/http-server-csharp/src/lib/service.ts index 6f9b671f398..b39a79cb8fc 100644 --- a/packages/http-server-csharp/src/lib/service.ts +++ b/packages/http-server-csharp/src/lib/service.ts @@ -101,6 +101,7 @@ import { getCSharpIdentifier, getCSharpStatusCode, getCSharpType, + getControllerReturnStatement, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, getFreePort, @@ -873,7 +874,7 @@ export async function $onEmit(context: EmitContext) }); const hasResponseValue = response.name !== "void"; - const resultString = `${status === 204 ? "NoContent" : "Ok"}`; + const returnStatement = getControllerReturnStatement(status, hasResponseValue); if (!this.#isMultipartRequest(httpOperation)) { return this.emitter.result.declaration( operation.name, @@ -887,9 +888,9 @@ export async function $onEmit(context: EmitContext) ${ hasResponseValue ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); - return ${resultString}(result);` + ${returnStatement}` : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); - return ${resultString}();` + ${returnStatement}` } }`, ); @@ -915,9 +916,9 @@ export async function $onEmit(context: EmitContext) ${ hasResponseValue ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); - return ${resultString}(result);` + ${returnStatement}` : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); - return ${resultString}();` + ${returnStatement}` } }`, ); @@ -956,7 +957,7 @@ export async function $onEmit(context: EmitContext) }); const hasResponseValue = response.name !== "void"; - const resultString = `${status === 204 ? "NoContent" : "Ok"}`; + const returnStatement = getControllerReturnStatement(status, hasResponseValue); return this.emitter.result.declaration( operation.name, code` @@ -969,9 +970,9 @@ export async function $onEmit(context: EmitContext) ${ hasResponseValue ? `var result = await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); - return ${resultString}(result);` + ${returnStatement}` : `await ${this.emitter.getContext().resourceName}Impl.${operationName}Async(${getBusinessLogicCallParameters(parameters)}); - return ${resultString}();` + ${returnStatement}` } }`, ); diff --git a/packages/http-server-csharp/src/lib/utils.ts b/packages/http-server-csharp/src/lib/utils.ts index ead1828c0f7..2b64cb84fea 100644 --- a/packages/http-server-csharp/src/lib/utils.ts +++ b/packages/http-server-csharp/src/lib/utils.ts @@ -1032,6 +1032,31 @@ export function getCSharpStatusCode(entry: HttpStatusCodesEntry): string | undef } } +/** + * Returns the full return statement for a controller action based on the HTTP status code. + * Maps well-known status codes to their idiomatic ASP.NET Core ControllerBase methods, + * and falls back to `StatusCode(code, ...)` for all other numeric codes. + */ +export function getControllerReturnStatement( + status: HttpStatusCodesEntry, + hasValue: boolean, +): string { + if (typeof status === "number") { + switch (status) { + case 200: + return hasValue ? "return Ok(result);" : "return Ok();"; + case 202: + return hasValue ? "return Accepted(result);" : "return Accepted();"; + case 204: + return "return NoContent();"; + default: + return hasValue ? `return StatusCode(${status}, result);` : `return StatusCode(${status});`; + } + } + // Fallback for ranges and "*" + return hasValue ? "return Ok(result);" : "return Ok();"; +} + export function isEmptyResponseModel(program: Program, model: Type): boolean { if (model.kind !== "Model") return false; if (model.properties.size === 0) return true; diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index 7f5c0175c66..130ed3a0408 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -2023,6 +2023,53 @@ model FileAttachmentMultipartRequest { ); }); +it("Produces Accepted result for 202 response with body", async () => { + await compileAndValidateMultiple( + tester, + ` + model AcceptedResponse { + @statusCode statusCode: 202; + jobId: string; + } + + @post + op startJob(): AcceptedResponse; + `, + [["ContosoOperationsController.cs", ["return Accepted(result)"]]], + ); +}); + +it("Produces Accepted result for 202 response without body", async () => { + await compileAndValidateMultiple( + tester, + ` + model AcceptedNoBodyResponse { + @statusCode statusCode: 202; + } + + @post + op startJob(): AcceptedNoBodyResponse; + `, + [["ContosoOperationsController.cs", ["return Accepted()"]]], + ); +}); + +it("Produces StatusCode result for 201 response with body", async () => { + await compileAndValidateMultiple( + tester, + ` + model CreatedResponse { + @statusCode statusCode: 201; + id: string; + } + + @post + op createResource(): CreatedResponse; + `, + [["ContosoOperationsController.cs", ["return StatusCode(201, result)"]]], + ); +}); + const multipartSpec = ` @error model NotFoundErrorResponse { From bc9e7dc0996d03db618fa9fa278f633f87ff4cf8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:48:05 +0000 Subject: [PATCH 3/3] chore(http-server-csharp): fix formatting (pnpm format) Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- packages/http-server-csharp/src/lib/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/http-server-csharp/src/lib/service.ts b/packages/http-server-csharp/src/lib/service.ts index b39a79cb8fc..db6615db3d4 100644 --- a/packages/http-server-csharp/src/lib/service.ts +++ b/packages/http-server-csharp/src/lib/service.ts @@ -101,9 +101,9 @@ import { getCSharpIdentifier, getCSharpStatusCode, getCSharpType, - getControllerReturnStatement, getCSharpTypeForIntrinsic, getCSharpTypeForScalar, + getControllerReturnStatement, getFreePort, getHttpDeclParameters, getImports,