From adf4c3ab1633e36c8ae5c3fc3c52365d22259384 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 00:54:32 +0000 Subject: [PATCH 1/4] Initial plan From c42f8c2cbbf54d4556b87c27bb9c3a59054109bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 01:13:32 +0000 Subject: [PATCH 2/4] fix: missing namespace imports in MergePatchUpdate generated C# files - Add `file.meta["ResolvedNamespace"] = namespace` to `#createEnumContext` to match `#createModelContext` behavior. This allows `resolveReferenceFromScopes` to find the enum's namespace and add the appropriate `using` import. - Fix `checkOrAddNamespaceToScope` to support multiple dynamic namespace additions per source file (removes single-namespace `AddedScope` limitation). - Add test: MergePatchUpdate with enum type in different namespace Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- ...rts-mergepatcupdate-2026-03-04-01-11-36.md | 7 ++++ .../http-server-csharp/src/lib/interfaces.ts | 15 ++------ .../http-server-csharp/src/lib/service.ts | 1 + .../test/generation.test.ts | 38 +++++++++++++++++++ 4 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 .chronus/changes/copilot-fix-missing-namespace-imports-mergepatcupdate-2026-03-04-01-11-36.md diff --git a/.chronus/changes/copilot-fix-missing-namespace-imports-mergepatcupdate-2026-03-04-01-11-36.md b/.chronus/changes/copilot-fix-missing-namespace-imports-mergepatcupdate-2026-03-04-01-11-36.md new file mode 100644 index 00000000000..f432bd604af --- /dev/null +++ b/.chronus/changes/copilot-fix-missing-namespace-imports-mergepatcupdate-2026-03-04-01-11-36.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-server-csharp" +--- + +Fix missing `using` namespace imports in C# files generated from `MergePatchUpdate` when model properties reference enum or named types from a different namespace diff --git a/packages/http-server-csharp/src/lib/interfaces.ts b/packages/http-server-csharp/src/lib/interfaces.ts index d7d80357cd1..ea7a5793626 100644 --- a/packages/http-server-csharp/src/lib/interfaces.ts +++ b/packages/http-server-csharp/src/lib/interfaces.ts @@ -84,18 +84,9 @@ export function checkOrAddNamespaceToScope( case "sourceFile": { const fileNameSpace = scope.sourceFile.meta["ResolvedNamespace"]; if (fileNameSpace && fileNameSpace.startsWith(ns)) return true; - for (const entry of scope.sourceFile.imports.keys()) { - if (entry === ns) { - return true; - } - } - const added: string | undefined = scope.sourceFile.meta["AddedScope"]; - if (added === undefined) { - scope.sourceFile.imports.set(ns, [ns]); - scope.sourceFile.meta["AddedScope"] = ns; - return true; - } - return false; + if (scope.sourceFile.imports.has(ns)) return true; + scope.sourceFile.imports.set(ns, [ns]); + return true; } default: return false; diff --git a/packages/http-server-csharp/src/lib/service.ts b/packages/http-server-csharp/src/lib/service.ts index 6f9b671f398..335925bf1d5 100644 --- a/packages/http-server-csharp/src/lib/service.ts +++ b/packages/http-server-csharp/src/lib/service.ts @@ -1172,6 +1172,7 @@ export async function $onEmit(context: EmitContext) #createEnumContext(namespace: string, file: SourceFile, name: string): Context { file.imports.set("System.Text.Json", ["System.Text.Json"]); file.imports.set("System.Text.Json.Serialization", ["System.Text.Json.Serialization"]); + file.meta[this.#nsKey] = namespace; return { namespace: namespace, diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index 7f5c0175c66..26c11502b26 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -1204,6 +1204,44 @@ interface Widgets { ); }); +it("Handles MergePatchUpdate with enum type in different namespace", async () => { + await compileAndValidateMultiple( + tester, + ` +enum WidgetColor { + Red, + Blue, + Green +} + +model Widget { + id: string; + weight: int32; + color: WidgetColor; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + /** Update a widget */ + @patch update(@path id: string, @body body: MergePatchUpdate): Widget; +} + `, + [ + [ + "WidgetMergePatchUpdate.cs", + [ + "namespace TypeSpec.Http {", + "using Microsoft.Contoso;", + "public string Id { get; set; }", + "public int? Weight { get; set; }", + "public WidgetColor? Color { get; set; }", + ], + ], + ], + ); +}); + it("Handles user-defined model templates", async () => { await compileAndValidateMultiple( tester, From 8eef0c4c0cdb73bb889c01f39f3e8b5d4b567b97 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:55:21 +0000 Subject: [PATCH 3/4] test: add comprehensive namespace import tests for cross-namespace type references Tests added: - MergePatchUpdate with 2 enum properties from 2 different sub-namespaces (validates the multi-import fix) - Regular model with enum from sub-namespace (validates createEnumContext fix for non-MergePatch case) - MergePatchUpdate with optional enum from different namespace - MergePatchUpdate with string-enum union property from different namespace - MergePatchUpdate with array of models from different namespace Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../test/generation.test.ts | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index 26c11502b26..6822b5f8388 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -1242,6 +1242,193 @@ interface Widgets { ); }); +it("Handles MergePatchUpdate with properties from multiple different sub-namespaces", async () => { + // This test verifies that ALL cross-namespace using directives are emitted when + // multiple enum properties come from different namespaces (tests the removed AddedScope + // single-import limitation in checkOrAddNamespaceToScope). + await compileAndValidateMultiple( + tester, + ` +namespace Colors { + enum WidgetColor { Red, Blue, Green } +} + +namespace Sizes { + enum WidgetSize { Small, Medium, Large } +} + +model Widget { + id: string; + color: Colors.WidgetColor; + size: Sizes.WidgetSize; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + /** Update a widget */ + @patch update(@path id: string, @body body: MergePatchUpdate): Widget; +} + `, + [ + [ + "WidgetMergePatchUpdate.cs", + [ + "namespace TypeSpec.Http {", + "using Microsoft.Contoso.Colors;", + "using Microsoft.Contoso.Sizes;", + "public string Id { get; set; }", + "public WidgetColor? Color { get; set; }", + "public WidgetSize? Size { get; set; }", + ], + ], + ], + ); +}); + +it("Handles model with enum property from a sub-namespace", async () => { + // This test verifies that a regular (non-MergePatch) model whose property references + // an enum from a different namespace gets the correct using directive. + await compileAndValidateMultiple( + tester, + ` +namespace Colors { + enum WidgetColor { Red, Blue, Green } +} + +model Widget { + id: string; + color: Colors.WidgetColor; +} + +@get op getWidget(): Widget; + `, + [ + [ + "Widget.cs", + [ + "namespace Microsoft.Contoso {", + "using Microsoft.Contoso.Colors;", + "public string Id { get; set; }", + "public WidgetColor Color { get; set; }", + ], + ], + ], + ); +}); + +it("Handles MergePatchUpdate with optional enum from different namespace", async () => { + // Optional enums from different namespaces appear as nullable types (WidgetColor?) + // and must still get the correct using directive. + await compileAndValidateMultiple( + tester, + ` +enum WidgetColor { + Red, + Blue, + Green +} + +model Widget { + id: string; + color?: WidgetColor; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + /** Update a widget */ + @patch update(@path id: string, @body body: MergePatchUpdate): Widget; +} + `, + [ + [ + "WidgetMergePatchUpdate.cs", + [ + "namespace TypeSpec.Http {", + "using Microsoft.Contoso;", + "public string Id { get; set; }", + "public WidgetColor? Color { get; set; }", + ], + ], + ], + ); +}); + +it("Handles MergePatchUpdate with string-enum union property from different namespace", async () => { + // String-enum unions (e.g. union Color { "red", "blue" }) also use createEnumContext + // and should get the correct using directive when in a different namespace. + // Note: string-enum unions are MergePatch-transformed, so the property type becomes + // WidgetColorMergePatchUpdate (a new union in TypeSpec.Http), but the using directive + // for the original union's namespace is still needed for the union's definition file. + await compileAndValidateMultiple( + tester, + ` +namespace Colors { + union WidgetColor { Red: "red", Blue: "blue", Green: "green" } +} + +model Widget { + id: string; + color: Colors.WidgetColor; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + /** Update a widget */ + @patch update(@path id: string, @body body: MergePatchUpdate): Widget; +} + `, + [ + [ + "WidgetMergePatchUpdate.cs", + [ + "namespace TypeSpec.Http {", + "using Microsoft.Contoso.Colors;", + "public string Id { get; set; }", + ], + ], + ], + ); +}); + +it("Handles MergePatchUpdate with array of models from different namespace", async () => { + // Arrays of model types from different namespaces are also MergePatch-transformed, + // creating e.g. TagMergePatchUpdateReplaceOnly[] in TypeSpec.Http. + // The using directive for the original model's namespace should still be present. + await compileAndValidateMultiple( + tester, + ` +namespace Tags { + model Tag { name: string; value: string; } +} + +model Widget { + id: string; + tags: Tags.Tag[]; +} + +@route("/widgets") +@tag("Widgets") +interface Widgets { + /** Update a widget */ + @patch update(@path id: string, @body body: MergePatchUpdate): Widget; +} + `, + [ + [ + "WidgetMergePatchUpdate.cs", + [ + "namespace TypeSpec.Http {", + "using Microsoft.Contoso.Tags;", + "public string Id { get; set; }", + ], + ], + ], + ); +}); + it("Handles user-defined model templates", async () => { await compileAndValidateMultiple( tester, From 82add0a5d9314da3e05bb58b73c06f39bbe2c3db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:24:26 +0000 Subject: [PATCH 4/4] test: add regression tests for AddedScope removal (multi-namespace, dedup) Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com> --- .../test/generation.test.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/http-server-csharp/test/generation.test.ts b/packages/http-server-csharp/test/generation.test.ts index 6822b5f8388..17c658e5035 100644 --- a/packages/http-server-csharp/test/generation.test.ts +++ b/packages/http-server-csharp/test/generation.test.ts @@ -1429,6 +1429,76 @@ interface Widgets { ); }); +it("Emits using for base class namespace and separate property namespace (regression: AddedScope cap)", async () => { + // With the old AddedScope guard, checkOrAddNamespaceToScope returned false after adding + // the first dynamic namespace import, forcing subsequent ones to be fully-qualified. + // This test verifies that a model inheriting from a base class in one sub-namespace and + // having a property from a second sub-namespace gets BOTH using directives. + await compileAndValidateMultiple( + tester, + ` +namespace Models { + model ParentWidget { id: string; } +} + +namespace Colors { + enum WidgetColor { Red, Blue, Green } +} + +model Widget extends Models.ParentWidget { + color: Colors.WidgetColor; +} + +@get op getWidget(): Widget; + `, + [ + [ + "Widget.cs", + [ + "namespace Microsoft.Contoso {", + "using Microsoft.Contoso.Models;", + "using Microsoft.Contoso.Colors;", + "public WidgetColor Color { get; set; }", + ": ParentWidget", + ], + ], + ], + ); +}); + +it("Emits only one using directive when multiple properties share the same external namespace", async () => { + // Verifies that the import deduplication (imports.has(ns)) prevents duplicate + // using directives when more than one property references the same external namespace. + await compileAndValidateMultiple( + tester, + ` +namespace Colors { + enum WidgetColor { Red, Blue, Green } + enum BorderColor { Black, White } +} + +model Widget { + id: string; + color: Colors.WidgetColor; + borderColor: Colors.BorderColor; +} + +@get op getWidget(): Widget; + `, + [ + [ + "Widget.cs", + [ + "namespace Microsoft.Contoso {", + "using Microsoft.Contoso.Colors;", + "public WidgetColor Color { get; set; }", + "public BorderColor BorderColor { get; set; }", + ], + ], + ], + ); +}); + it("Handles user-defined model templates", async () => { await compileAndValidateMultiple( tester,