Skip to content

Commit 38de4f5

Browse files
fix(http-server-csharp): resolve ASP.NET Core MVC types in Microsoft.* namespaces (#11458)
## Fixes Fixes #11445 ## Problem Controllers generated by `@typespec/http-server-csharp` failed to compile when the service namespace begins with `Microsoft.` (as every ARM resource provider does). `ControllerBase` and `IActionResult` were emitted as raw string identifiers, so under a namespace like `Microsoft.Contoso.Controllers` they became **bare, unresolved identifiers** — there was no `using Microsoft.AspNetCore.Mvc;` and Alloy could not qualify them relative to the shared `Microsoft` ancestor namespace. ## Fix Emit `ControllerBase` and `IActionResult` as `Microsoft.AspNetCore.Mvc` **library references** (like `[ApiController]` already was), so Alloy's name resolution handles them in every namespace: - **Plain namespace** (e.g. `Contoso`): a single `using Microsoft.AspNetCore.Mvc;` is added and the short names `ControllerBase` / `IActionResult` / `[ApiController]` are used. - **`Microsoft.*` namespace** (e.g. `Microsoft.Contoso`): the types are qualified relative to the enclosing `Microsoft` namespace — `AspNetCore.Mvc.ControllerBase`, `Task<AspNetCore.Mvc.IActionResult>`, `[AspNetCore.Mvc.ApiControllerAttribute]` — all of which resolve and compile. Symbols dedupe through Alloy's `using` set, so no duplicate `using` directives are produced. ## Changes - `src/utils/csharp-libs.tsx`: add `ControllerBase` and `IActionResult` to the `AspNetMvc` library. - `src/components/controllers/controllers.tsx`: `baseType={AspNetMvc.ControllerBase}`. - `src/components/controller-action/controller-action.tsx`: `returns={code\`Task<${AspNetMvc.IActionResult}>\`}`. ## Tests Added `.toRenderTo` regression tests in `src/components/controllers/controllers.test.tsx` covering both a plain namespace and a `Microsoft.*` namespace. Full package suite passes (205 tests).
1 parent 6b0018d commit 38de4f5

6 files changed

Lines changed: 118 additions & 8 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: fix
3+
packages:
4+
- "@typespec/http-server-csharp"
5+
---
6+
7+
Fix generated controllers failing to compile when the service namespace starts with `Microsoft.` (as ARM resource providers do). `ControllerBase` and `IActionResult` are now emitted as `Microsoft.AspNetCore.Mvc` library references so they resolve in every namespace instead of being unresolved bare identifiers.

packages/http-server-csharp/src/components/controller-action/controller-action.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ ${implCall}`;
235235
async
236236
virtual
237237
public
238-
returns={code`Task<IActionResult>`}
238+
returns={code`Task<${AspNetMvc.IActionResult}>`}
239239
parameters={parameters}
240240
attributes={attributes}
241241
doc={getDocComments($, props.operation.sourceType)}

packages/http-server-csharp/src/components/controllers/controllers.test.tsx

Lines changed: 104 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { Tester } from "#test/tester.js";
22
import { type Children } from "@alloy-js/core";
3-
import { createCSharpNamePolicy, SourceFile } from "@alloy-js/csharp";
3+
import { createCSharpNamePolicy, Namespace, SourceFile } from "@alloy-js/csharp";
44
import { t, type TesterInstance } from "@typespec/compiler/testing";
55
import { $ } from "@typespec/compiler/typekit";
66
import { Output } from "@typespec/emitter-framework";
77
import {
88
HttpCanonicalizer,
99
type OperationHttpCanonicalization,
1010
} from "@typespec/http-canonicalization";
11-
import { beforeEach, expect, it } from "vitest";
11+
import { beforeEach, describe, expect, it } from "vitest";
1212
import { BusinessLogicInterface } from "../interfaces/interfaces.jsx";
1313
import { Controller } from "./controllers.jsx";
1414

@@ -18,11 +18,16 @@ beforeEach(async () => {
1818
runner = await Tester.createInstance();
1919
});
2020

21-
function Wrapper(props: { children: Children }) {
21+
function Wrapper(props: { children: Children; namespace?: string }) {
2222
const policy = createCSharpNamePolicy();
23+
const content = props.namespace ? (
24+
<Namespace name={props.namespace}>{props.children}</Namespace>
25+
) : (
26+
props.children
27+
);
2328
return (
2429
<Output program={runner.program} namePolicy={policy}>
25-
<SourceFile path="test.cs">{props.children}</SourceFile>
30+
<SourceFile path="test.cs">{content}</SourceFile>
2631
</Output>
2732
);
2833
}
@@ -32,14 +37,17 @@ function canonicalizeOp(opType: any): OperationHttpCanonicalization {
3237
return canonicalizer.canonicalize(opType) as OperationHttpCanonicalization;
3338
}
3439

35-
it("renders a controller class with an action method", async () => {
40+
async function compilePetStore() {
3641
const { PetStore, listPets } = await runner.compile(t.code`
3742
interface ${t.interface("PetStore")} {
3843
@route("/pets") @get ${t.op("listPets")}(): string[];
3944
}
4045
`);
46+
return { PetStore, canonOp: canonicalizeOp(listPets) };
47+
}
4148

42-
const canonOp = canonicalizeOp(listPets);
49+
it("renders a controller class with an action method", async () => {
50+
const { PetStore, canonOp } = await compilePetStore();
4351

4452
expect(
4553
<Wrapper>
@@ -74,3 +82,93 @@ it("renders a controller class with an action method", async () => {
7482
}
7583
`);
7684
});
85+
86+
// Regression tests for https://github.com/microsoft/typespec/issues/11445.
87+
// `[ApiController]`, `ControllerBase` and `IActionResult` come from
88+
// `Microsoft.AspNetCore.Mvc`. They are emitted as library references so they
89+
// resolve in every service namespace, including namespaces that start with
90+
// `Microsoft.` (as every ARM resource provider does), where Alloy resolves them
91+
// relative to the shared `Microsoft` ancestor namespace instead of importing them.
92+
describe("ASP.NET Core MVC framework references resolve in any namespace (#11445)", () => {
93+
it("imports Microsoft.AspNetCore.Mvc and uses short names for a plain namespace", async () => {
94+
const { PetStore, canonOp } = await compilePetStore();
95+
96+
expect(
97+
<Wrapper namespace="Contoso.Controllers">
98+
<BusinessLogicInterface type={PetStore} />
99+
{"\n"}
100+
<Controller type={PetStore} operations={[canonOp]} />
101+
</Wrapper>,
102+
).toRenderTo(`
103+
using Microsoft.AspNetCore.Mvc;
104+
105+
namespace Contoso.Controllers
106+
{
107+
public interface IPetStore
108+
{
109+
Task<string[]> ListPetsAsync();
110+
}
111+
[ApiController]
112+
public partial class PetStoreController : ControllerBase
113+
{
114+
internal virtual IPetStore PetStoreImpl { get; }
115+
public PetStoreController(IPetStore operations)
116+
{
117+
PetStoreImpl = operations;
118+
}
119+
120+
[HttpGet]
121+
[Route("/pets")]
122+
[ProducesResponseType((int)HttpStatusCode.OK, Type = typeof(string[]))]
123+
public virtual async Task<IActionResult> ListPets()
124+
{
125+
var result = await PetStoreImpl.ListPetsAsync();
126+
return Ok(result);
127+
}
128+
}
129+
}
130+
`);
131+
});
132+
133+
it("qualifies the Mvc types via the ancestor namespace when the namespace starts with Microsoft.", async () => {
134+
const { PetStore, canonOp } = await compilePetStore();
135+
136+
// No `using Microsoft.AspNetCore.Mvc;` is emitted here: the framework types
137+
// are qualified relative to the enclosing `Microsoft` namespace, which
138+
// resolves to `Microsoft.AspNetCore.Mvc.*` in C#. Before the fix these were
139+
// bare, unresolved identifiers (`ControllerBase`, `IActionResult`).
140+
expect(
141+
<Wrapper namespace="Microsoft.Contoso.Controllers">
142+
<BusinessLogicInterface type={PetStore} />
143+
{"\n"}
144+
<Controller type={PetStore} operations={[canonOp]} />
145+
</Wrapper>,
146+
).toRenderTo(`
147+
namespace Microsoft.Contoso.Controllers
148+
{
149+
public interface IPetStore
150+
{
151+
Task<string[]> ListPetsAsync();
152+
}
153+
[AspNetCore.Mvc.ApiControllerAttribute]
154+
public partial class PetStoreController : AspNetCore.Mvc.ControllerBase
155+
{
156+
internal virtual IPetStore PetStoreImpl { get; }
157+
public PetStoreController(IPetStore operations)
158+
{
159+
PetStoreImpl = operations;
160+
}
161+
162+
[AspNetCore.Mvc.HttpGetAttribute]
163+
[AspNetCore.Mvc.RouteAttribute("/pets")]
164+
[AspNetCore.Mvc.ProducesResponseTypeAttribute((int)HttpStatusCode.OK, Type = typeof(string[]))]
165+
public virtual async Task<AspNetCore.Mvc.IActionResult> ListPets()
166+
{
167+
var result = await PetStoreImpl.ListPetsAsync();
168+
return Ok(result);
169+
}
170+
}
171+
}
172+
`);
173+
});
174+
});

packages/http-server-csharp/src/components/controllers/controllers.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function Controller(props: ControllerProps): Children {
3636
name={controllerName}
3737
public
3838
partial
39-
baseType="ControllerBase"
39+
baseType={AspNetMvc.ControllerBase}
4040
attributes={attributes}
4141
>
4242
<cs.Property name={implPropName} type={interfaceRef} internal virtual get />

packages/http-server-csharp/src/utils/csharp-libs.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export const JsonSerialization = createLibrary("System.Text.Json.Serialization",
1919
*/
2020
export const AspNetMvc = createLibrary("Microsoft.AspNetCore.Mvc", {
2121
ApiControllerAttribute: { kind: "class", members: {} },
22+
ControllerBase: { kind: "class", members: {} },
23+
IActionResult: { kind: "interface", members: {} },
2224
RouteAttribute: { kind: "class", members: {} },
2325
HttpGetAttribute: { kind: "class", members: {} },
2426
HttpPostAttribute: { kind: "class", members: {} },

packages/http-server-csharp/test/generation.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// NOTE: This file is LEGACY only. Do NOT add new tests here.
2+
// Add new tests to the co-located component `*.test.tsx` files (or another dedicated
3+
// test file) instead.
14
import { resolveVirtualPath, TesterInstance, TestFileSystem } from "@typespec/compiler/testing";
25
import assert, { deepStrictEqual } from "assert";
36
import { beforeEach, describe, it } from "vitest";

0 commit comments

Comments
 (0)