diff --git a/.chronus/changes/csharp-namespace-scope-chain-2026-2-4.md b/.chronus/changes/csharp-namespace-scope-chain-2026-2-4.md
new file mode 100644
index 000000000..4f840f781
--- /dev/null
+++ b/.chronus/changes/csharp-namespace-scope-chain-2026-2-4.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: fix
+packages:
+ - "@alloy-js/csharp"
+---
+
+Fix namespace scope-chain construction for dotted namespaces declared inside `SourceFile` so refkey resolution no longer emits incorrect qualification or unnecessary using directives. Adds regression coverage for sibling and multi-level nested namespace scenarios.
\ No newline at end of file
diff --git a/packages/csharp/src/components/namespace.ref.test.tsx b/packages/csharp/src/components/namespace.ref.test.tsx
index 2b4fc4eb1..94c656d78 100644
--- a/packages/csharp/src/components/namespace.ref.test.tsx
+++ b/packages/csharp/src/components/namespace.ref.test.tsx
@@ -162,3 +162,32 @@ it("can be referenced by refkey", () => {
TestClass;
`);
});
+
+it("references types across sibling namespaces under the same parent", () => {
+ const classRef = refkey();
+
+ const tree = (
+
+ );
+
+ expect(tree).toRenderTo(`
+ namespace Parent {
+ namespace Models {
+ class User;
+ }
+ namespace Services {
+ Models.User;
+ }
+ }
+ `);
+});
diff --git a/packages/csharp/src/components/namespace/namespace.test.tsx b/packages/csharp/src/components/namespace/namespace.test.tsx
index c7c2c628d..a3ce06a2b 100644
--- a/packages/csharp/src/components/namespace/namespace.test.tsx
+++ b/packages/csharp/src/components/namespace/namespace.test.tsx
@@ -1,9 +1,13 @@
import { TestNamespace } from "#test/utils.jsx";
-import { Output } from "@alloy-js/core";
+import { Output, refkey } from "@alloy-js/core";
import { d } from "@alloy-js/core/testing";
import { expect, it } from "vitest";
+import { createCSharpNamePolicy } from "../../name-policy.js";
import { ClassDeclaration } from "../class/declaration.jsx";
+import { Constructor } from "../constructor/constructor.jsx";
+import { Field } from "../field/field.jsx";
import { SourceFile } from "../source-file/source-file.jsx";
+import { StructDeclaration } from "../struct/declaration.jsx";
import { Namespace } from "./namespace.jsx";
it("defines multiple namespaces and source files with unique content", () => {
@@ -141,3 +145,40 @@ it("define nested namespace in sourcefile", () => {
}
`);
});
+
+it("contains a struct with a private field initialized by a constructor", () => {
+ const fieldRefkey = refkey();
+ const paramRefkey = refkey();
+
+ const tree = (
+
+ );
+
+ expect(tree).toRenderTo(d`
+ namespace TestNamespace.Test {
+ public struct MyStruct
+ {
+ private int _value;
+ public MyStruct(int value)
+ {
+ _value = value;
+ }
+ }
+ }
+ `);
+});
diff --git a/packages/csharp/src/components/namespace/namespace.tsx b/packages/csharp/src/components/namespace/namespace.tsx
index d70aff421..3298547b0 100644
--- a/packages/csharp/src/components/namespace/namespace.tsx
+++ b/packages/csharp/src/components/namespace/namespace.tsx
@@ -1,8 +1,12 @@
import { Block, Namekey, Refkey } from "@alloy-js/core";
import { Children } from "@alloy-js/core/jsx-runtime";
-import { NamespaceContext } from "../../contexts/namespace.js";
+import {
+ NamespaceContext,
+ useNamespaceContext,
+} from "../../contexts/namespace.js";
import { useSourceFileScope } from "../../scopes/source-file.js";
import { createNamespaceSymbol } from "../../symbols/factories.js";
+import { NamespaceSymbol } from "../../symbols/namespace.js";
import { NamespaceScope } from "../namespace-scopes.jsx";
import { NamespaceName } from "./namespace-name.jsx";
@@ -12,6 +16,32 @@ export interface NamespaceProps {
children?: Children;
}
+/**
+ * Wraps children with namespace scopes for each level of the namespace
+ * hierarchy, stopping at the `stopAt` namespace symbol (which is already in
+ * scope from an enclosing NamespaceScopes).
+ */
+function wrapWithNamespaceScopes(
+ symbol: NamespaceSymbol,
+ children: Children,
+ stopAt?: NamespaceSymbol,
+): Children {
+ if (symbol === stopAt) {
+ return children;
+ }
+
+ const scopeChildren = (
+ {children}
+ );
+
+ const enclosing = symbol.enclosingNamespace;
+ if (enclosing && !enclosing.isGlobal && enclosing !== stopAt) {
+ return wrapWithNamespaceScopes(enclosing, scopeChildren, stopAt);
+ }
+
+ return scopeChildren;
+}
+
export function Namespace(props: NamespaceProps) {
const namespaceSymbol = createNamespaceSymbol(props.name, {
refkeys: props.refkey,
@@ -25,16 +55,23 @@ export function Namespace(props: NamespaceProps) {
);
} else {
+ const nsContext = useNamespaceContext();
+ const hasOuterNamespace = nsContext && !nsContext.symbol.isGlobal;
+
sfScope.hasBlockNamespace = true;
return (
<>
- namespace {" "}
+ namespace{" "}
+ {" "}
-
-
- {props.children}
-
-
+ {wrapWithNamespaceScopes(
+ namespaceSymbol,
+ props.children,
+ nsContext?.symbol,
+ )}
>
);