Skip to content

fix(http-server-csharp): missing namespace imports in MergePatchUpdate-generated C# files#9896

Merged
markcowl merged 6 commits intomainfrom
copilot/fix-missing-namespace-imports
Mar 5, 2026
Merged

fix(http-server-csharp): missing namespace imports in MergePatchUpdate-generated C# files#9896
markcowl merged 6 commits intomainfrom
copilot/fix-missing-namespace-imports

Conversation

Copy link
Contributor

Copilot AI commented Mar 4, 2026

  • Investigate the bug: #createEnumContext doesn't set file.meta["ResolvedNamespace"], causing resolveReferenceFromScopes to get undefined namespace for enums and fall back to unqualified name without adding the import
  • Plan fix
  • Add failing test case with enum property in MergePatchUpdate (verifies using Microsoft.Contoso; appears)
  • Fix primary bug: Add file.meta[this.#nsKey] = namespace to #createEnumContext in service.ts
  • Fix secondary bug: Remove AddedScope single-namespace limitation in checkOrAddNamespaceToScope in interfaces.ts
  • Add comprehensive namespace import tests covering:
    • MergePatchUpdate with 2 enum properties from 2 different sub-namespaces (validates multi-import fix)
    • Regular model with enum from sub-namespace (validates 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
  • Add regression tests for AddedScope removal:
    • Model with base class in one sub-namespace AND property from another sub-namespace → both using directives emitted
    • Model with two enum properties from the SAME namespace → only one using emitted (deduplication)
  • Merge latest from main branch (2 new commits: enum backward compat, spec-dashboard overview)
  • All 98 tests pass
  • Add changeset for @typespec/http-server-csharp
  • Formatting and linting pass
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: Missing Namespace Imports in C# Code Generated from MergePatchUpdate through http-server-csharp</issue_title>
<issue_description>### Describe the bug

This problem seems to be happened when model T(in this case Product) consists only of enum and built-in types.
I'm not sure this happens in other cases.

versions
@typespec/compiler version 1.3.0
@typespec/http version 1.3.0
@typespec/http-server-csharp version 0.58.0-alpha.19

main.tsp

import "@typespec/http";

using Http;
@service(#{ title: "Product Service" })
namespace DemoService;

model Product {
  id: uint32;
  name: string;
  category: ProductCategory;
}

enum ProductCategory {
  Electronics,
  Books,
  Clothing,
  Home
}

@route("/products")
@tag("Products")
interface Products {
  /** List products */
  @get list(): Product[];

  /** Read products */
  @get read(@path id: string): Product;
  
  /** Create a product */
  @post create(@body body: Product): Product;
  
  /** Update a product */
  @patch update(@path id: string, @body body: MergePatchUpdate<Product>): Product;
  
  /** Delete a product */
  @delete delete(@path id: string): void;
}

generated ProductMergePatchUpdate.cs

// Generated by @typespec/http-server-csharp
// <auto-generated />

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using TypeSpec.Helpers.JsonConverters;
using TypeSpec.Helpers;

namespace TypeSpec.Http
{

    public partial class ProductMergePatchUpdate
    {
        public UInt32? Id { get; set; }

        public string Name { get; set; }

        [JsonConverter(typeof(JsonStringEnumConverter))]
        public ProductCategory? Category { get; set; } // ProductCategory NOT BE RESOLVED WITHOUT A NAMESPACE IMPORT (using DemoService;)


    }
}

ProductMergePatchUpdate.cs should be like this.

// Generated by @typespec/http-server-csharp
// <auto-generated />

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using TypeSpec.Helpers.JsonConverters;
using TypeSpec.Helpers;
using DemoService; // THIS SHOULD BE HERE AND THIS CODE WILL BE COMPILED SUCCESSFULY

namespace TypeSpec.Http
{

    public partial class ProductMergePatchUpdate
    {
        public UInt32? Id { get; set; }

        public string Name { get; set; }

        [JsonConverter(typeof(JsonStringEnumConverter))]
        public ProductCategory? Category { get; set; }


    }
}

Reproduction

main.tsp

import "@typespec/http";

using Http;
@service(#{ title: "Product Service" })
namespace DemoService;

model Product {
  id: uint32;
  name: string;
  category: ProductCategory;
}

enum ProductCategory {
  Electronics,
  Books,
  Clothing,
  Home
}

@route("/products")
@tag("Products")
interface Products {
  /** List products */
  @get list(): Product[];

  /** Read products */
  @get read(@path id: string): Product;
  
  /** Create a product */
  @post create(@body body: Product): Product;
  
  /** Update a product */
  @patch update(@path id: string, @body body: MergePatchUpdate<Product>): Product;
  
  /** Delete a product */
  @delete delete(@path id: string): void;
}

generated ProductMergePatchUpdate.cs

// Generated by @typespec/http-server-csharp
// <auto-generated />

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using TypeSpec.Helpers.JsonConverters;
using TypeSpec.Helpers;

namespace TypeSpec.Http
{

    public partial class ProductMergePatchUpdate
    {
        public UInt32? Id { get; set; }

        public string Name { get; set; }

        [JsonConverter(typeof(JsonStringEnumConverter))]
        public ProductCategory? Category { get; set; }


    }
}

Checklist

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

- 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>
Copilot AI changed the title [WIP] Fix missing namespace imports in C# code generation fix(http-server-csharp): missing namespace imports in MergePatchUpdate-generated C# files Mar 4, 2026
@pkg-pr-new
Copy link

pkg-pr-new bot commented Mar 4, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-server-csharp@9896

commit: 80951df

@markcowl markcowl marked this pull request as ready for review March 4, 2026 20:51
@azure-sdk
Copy link
Collaborator

You can try these changes here

🛝 Playground 🌐 Website 🛝 VSCode Extension

@markcowl
Copy link
Contributor

markcowl commented Mar 4, 2026

@copilot analyze how the logic for including a namespace in a sourceFile and suggest some more comprehensive tests to find any other places where a namespace might be needed and is not included.

@markcowl
Copy link
Contributor

markcowl commented Mar 4, 2026

@copilot Also, pull in the latest from the main branch

…pe 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>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot analyze how the logic for including a namespace in a sourceFile and suggest some more comprehensive tests to find any other places where a namespace might be needed and is n...

Here's the analysis and the tests I added (commit 8eef0c4):

Namespace inclusion logic — there are two mechanisms:

  1. resolveReferenceFromScopes (used by emitTypeReferencereference()): reads file.meta["ResolvedNamespace"] from the declaring file, then calls checkOrAddNamespaceToScope on the referencing file to add a using import if the namespaces differ. This is how property types (enums, models) get imports added during model emission. Our fix ensured enum/string-union files set their ResolvedNamespace.

  2. CSharpType.getTypeReference(scope) (used by getCSharpType(..., contextNamespace) in interface/controller codegen): checks isNamespaceInScope and adds the import. This path passes contextNamespace (the interface's own namespace) as the type's namespace, so it handles types that live in the same namespace as the interface.

Scenarios tested in 8eef0c4:

Test What it validates
MergePatchUpdate with 2 enums from 2 sub-namespaces Both using directives present — confirms the removed AddedScope single-import cap
Regular model with enum from sub-namespace createEnumContext fix helps non-MergePatch model files too
MergePatchUpdate with optional (?) enum from different namespace Nullable enum properties still get the import
MergePatchUpdate with string-enum union from different namespace String-enum unions go through createEnumContext too; using for the source namespace is present
MergePatchUpdate with array of models from different namespace Import is added even when model arrays are recursively MergePatch-transformed

Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot Also, pull in the latest from the main branch

Done in 773846f — merged 7 commits from main (no conflicts). All 96 tests still pass.

@markcowl
Copy link
Contributor

markcowl commented Mar 4, 2026

@copilot You removed an update to sourceFile.meta["AddedScope"] in your PR how is this metadata used in the emitter? What are the potential problems that could be caused by removing this piece of metadata? Suggest any missing tests that verify that no currently working scenarios were broken.

@markcowl
Copy link
Contributor

markcowl commented Mar 4, 2026

@copilot and as always, pull the latest from main whenever you add commits to this branch

…edup)

Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot You removed an update to sourceFile.meta["AddedScope"] in your PR how is this metadata used in the emitter? What are the potential problems that could be caused by removi...

How sourceFile.meta["AddedScope"] was used

AddedScope was an internal guard inside checkOrAddNamespaceToScope. It stored the first dynamically-added using namespace (a string). On every subsequent call for the same source file, if AddedScope was already set, the function returned false instead of adding another using. No other code path in the emitter reads this key — it was purely self-referential.

Potential problems from removing it

The only caller that handles a false return is resolveReferenceFromScopes in utils.ts:

if (checkOrAddNamespaceToScope(declNs, ref.scope)) {
  return targetDeclaration.name;            // short name — using was added
}
return declNs ? `${declNs}.${targetDeclaration.name} ` : undefined;  // fully qualified

With AddedScope in place, the second (and any subsequent) cross-namespace type reference in a file fell through to the fully-qualified name. With AddedScope removed and imports.has(ns) as the only deduplication guard, all cross-namespace references now get a using directive and use the short name.

Two theoretical problems:

  1. Name ambiguity — if two different imported namespaces each contain a type with the same short name, both using directives are now added, which would require fully-qualified names to disambiguate in the C# code. The old behavior handled this non-deterministically (first namespace got using, second was fully qualified). In practice this is extremely unlikely in well-designed APIs, and the new behaviour is the standard C# approach.
  2. False false was masking a real bug — a model that inherited from a base class in namespace A and had a property type from namespace B would previously emit only one using (whichever was encountered first), leaving the other as a fully-qualified reference. This was a hidden bug fixed by removing AddedScope.

New regression tests added in 82add0a

Test What it verifies
"Emits using for base class namespace and separate property namespace" Model with base class in Models sub-namespace and an enum property in Colors sub-namespace → both using Microsoft.Contoso.Models; and using Microsoft.Contoso.Colors; present (previously broken with AddedScope)
"Emits only one using directive when multiple properties share the same external namespace" Two enum properties from the same Colors sub-namespace → only one using Microsoft.Contoso.Colors; (deduplication via imports.has(ns))

Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot and as always, pull the latest from main whenever you add commits to this branch

Done in edaaf78 — merged 2 new commits from main (enum backward compat #9906, spec-dashboard overview #9891). All 98 tests still pass.

@markcowl markcowl added this pull request to the merge queue Mar 5, 2026
Merged via the queue into main with commit 76e219d Mar 5, 2026
24 checks passed
@markcowl markcowl deleted the copilot/fix-missing-namespace-imports branch March 5, 2026 19:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Missing Namespace Imports in C# Code Generated from MergePatchUpdate<T> through http-server-csharp

4 participants