Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -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, ...)`.
17 changes: 9 additions & 8 deletions packages/http-server-csharp/src/lib/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {
getCSharpType,
getCSharpTypeForIntrinsic,
getCSharpTypeForScalar,
getControllerReturnStatement,
getFreePort,
getHttpDeclParameters,
getImports,
Expand Down Expand Up @@ -873,7 +874,7 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
});

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,
Expand All @@ -887,9 +888,9 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
${
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}`
}
}`,
);
Expand All @@ -915,9 +916,9 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
${
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}`
}
}`,
);
Expand Down Expand Up @@ -956,7 +957,7 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
});

const hasResponseValue = response.name !== "void";
const resultString = `${status === 204 ? "NoContent" : "Ok"}`;
const returnStatement = getControllerReturnStatement(status, hasResponseValue);
return this.emitter.result.declaration(
operation.name,
code`
Expand All @@ -969,9 +970,9 @@ export async function $onEmit(context: EmitContext<CSharpServiceEmitterOptions>)
${
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}`
}
}`,
);
Expand Down
25 changes: 25 additions & 0 deletions packages/http-server-csharp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
47 changes: 47 additions & 0 deletions packages/http-server-csharp/test/generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading