Repository: alloy-framework/alloy
Pull request #222 (2025-06-26 20:13:44)

CSharp type parameters support
Add support for type parmaeters for methods and rethink how it was done for class/interface to align more with the typescript way.
Provide nice formatting in case of overflow as well

github-actions[bot]: <!--chronus-github-change-commenter-->
:x: There is undocummented changes. Run `chronus add` to add a changeset or [click here](https://github.com/timotheeguerin/alloy/new/type-parameters?filename=.chronus/changes/type-parameters-2025-5-26-20-14-8.md&value=---%0A%23%20Change%20versionKind%20to%20one%20of%3A%20breaking%2C%20feature%2C%20fix%2C%20internal%0AchangeKind%3A%20fix%0Apackages%3A%0A%20%20-%20%22%40alloy-js%2Fcsharp%22%0A---%0A%0ACSharp%20type%20parameters%20support).

**The following packages have changes but are not documented.**
 - :x:`@alloy-js/csharp`

<details>
<summary>Show changes</summary>


</details>

pkg-pr-new[bot]: 
[Open in StackBlitz](https://pkg.pr.new/template/82bc7d91-91e2-4e84-9b39-ca5b61ecbf89)


<details><summary><b>@alloy-js/babel-plugin</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-plugin-jsx-dom-expressions</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-plugin-jsx-dom-expressions@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/babel-preset</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/babel-preset@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/cli</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/cli@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/core</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/core@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/create</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/create@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/csharp</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/csharp@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/java</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/java@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/json</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/json@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/markdown</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/markdown@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/prettier-plugin-alloy</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/prettier-plugin-alloy@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/rollup-plugin</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/rollup-plugin@222
```

</p></details>
      
    

<details><summary><b>@alloy-js/typescript</b></summary><p>

```
npm i https://pkg.pr.new/alloy-framework/alloy/@alloy-js/typescript@222
```

</p></details>
      
    

_commit: <a href="https://github.com/alloy-framework/alloy/runs/44881343019"><code>735d55f</code></a>_


Patch:
diff --git a/.chronus/changes/type-parameters-2025-5-26-20-31-22.md b/.chronus/changes/type-parameters-2025-5-26-20-31-22.md
new file mode 100644
index 0000000..4cf2fa8
--- /dev/null
+++ b/.chronus/changes/type-parameters-2025-5-26-20-31-22.md
@@ -0,0 +1,8 @@
+---
+# Change versionKind to one of: breaking, feature, fix, internal
+changeKind: feature
+packages:
+  - "@alloy-js/csharp"
+---
+
+Support for type parameters for interface, class methods, interface methods
diff --git a/cspell.yaml b/cspell.yaml
index d44e1d3..388823d 100644
--- a/cspell.yaml
+++ b/cspell.yaml
@@ -5,6 +5,7 @@ dictionaries:
   - node
   - typescript
 words:
+  - arrayify
   - cref
 useGitignore: true
 enableGlobDot: true
diff --git a/packages/csharp/src/components/ClassDeclaration.tsx b/packages/csharp/src/components/ClassDeclaration.tsx
index 5eba44e..9e0bae6 100644
--- a/packages/csharp/src/components/ClassDeclaration.tsx
+++ b/packages/csharp/src/components/ClassDeclaration.tsx
@@ -12,6 +12,9 @@ import { CSharpMemberScope, useCSharpScope } from "../symbols/scopes.js";
 import { Name } from "./Name.jsx";
 import { ParameterProps, Parameters } from "./Parameters.jsx";
 import { DocWhen } from "./doc/comment.jsx";
+import { TypeParameterConstraints } from "./type-parameters/type-parameter-constraints.jsx";
+import { TypeParameterProps } from "./type-parameters/type-parameter.jsx";
+import { TypeParameters } from "./type-parameters/type-parameters.jsx";
 
 export interface ClassModifiers {
   readonly abstract?: boolean;
@@ -36,7 +39,20 @@ export interface ClassDeclarationProps
   /** Doc comment */
   doc?: core.Children;
   refkey?: core.Refkey;
-  typeParameters?: Record<string, core.Refkey>;
+
+  /**
+   * Type parameters for the class
+   *
+   * @example
+   * ```tsx
+   * <ClassDeclaration name="MyClass" typeParameters={["T"]} />
+   * ```
+   * This will produce:
+   * ```csharp
+   * public class MyClass<T>
+   * ```
+   */
+  typeParameters?: (string | TypeParameterProps)[];
 
   /** Base class that this class extends */
   baseType?: core.Children;
@@ -84,31 +100,6 @@ export function ClassDeclaration(props: ClassDeclarationProps) {
     owner: thisClassSymbol,
   });
 
-  let typeParams: core.Children;
-  if (props.typeParameters) {
-    const typeParamNames = new Array<string>();
-    for (const entry of Object.entries(props.typeParameters)) {
-      typeParamNames.push(
-        useCSharpNamePolicy().getName(entry[0], "type-parameter"),
-      );
-      // create a symbol for each type param so its
-      // refkey resolves to the type param's name
-      new CSharpOutputSymbol(entry[0], {
-        scope: thisClassScope,
-        refkeys: entry[1],
-      });
-    }
-    typeParams = (
-      <group>
-        {"<"}
-        <core.For each={typeParamNames} comma line>
-          {(name) => name}
-        </core.For>
-        {">"}
-      </group>
-    );
-  }
-
   const bases = [
     ...(props.baseType ? [props.baseType] : []),
     ...(props.interfaceTypes || []),
@@ -123,8 +114,13 @@ export function ClassDeclaration(props: ClassDeclarationProps) {
     <core.Declaration symbol={thisClassSymbol}>
       <DocWhen doc={props.doc} />
       {modifiers}class <Name />
-      {typeParams}
+      {props.typeParameters && (
+        <TypeParameters parameters={props.typeParameters} />
+      )}
       {base}
+      {props.typeParameters && (
+        <TypeParameterConstraints parameters={props.typeParameters} />
+      )}
       {!props.children && ";"}
       {props.children && (
         <core.Block newline>
diff --git a/packages/csharp/src/components/ClassMethod.tsx b/packages/csharp/src/components/ClassMethod.tsx
index da9fe42..a296489 100644
--- a/packages/csharp/src/components/ClassMethod.tsx
+++ b/packages/csharp/src/components/ClassMethod.tsx
@@ -18,6 +18,9 @@ import { CSharpOutputSymbol } from "../symbols/csharp-output-symbol.js";
 import { CSharpMemberScope, useCSharpScope } from "../symbols/scopes.js";
 import { ParameterProps, Parameters } from "./Parameters.jsx";
 import { DocWhen } from "./doc/comment.jsx";
+import { TypeParameterConstraints } from "./type-parameters/type-parameter-constraints.jsx";
+import { TypeParameterProps } from "./type-parameters/type-parameter.jsx";
+import { TypeParameters } from "./type-parameters/type-parameters.jsx";
 
 /** Method modifiers. Can only be one. */
 export interface ClassMethodModifiers {
@@ -51,6 +54,20 @@ export interface ClassMethodProps
 
   /** Doc comment */
   doc?: Children;
+
+  /**
+   * Type parameters for the method
+   *
+   * @example
+   * ```tsx
+   * <InterfaceMethod name="Test" typeParameters={["T"]} />
+   * ```
+   * This will produce:
+   * ```csharp
+   * public void Test<T>()
+   * ```
+   */
+  typeParameters?: (TypeParameterProps | string)[];
 }
 
 // a C# class method
@@ -86,7 +103,14 @@ export function ClassMethod(props: ClassMethodProps) {
       <Scope value={methodScope}>
         <DocWhen doc={props.doc} />
         {modifiers}
-        {returns} {name}({params})
+        {returns}{" "}
+        {props.typeParameters && (
+          <TypeParameters parameters={props.typeParameters} />
+        )}
+        {name}({params})
+        {props.typeParameters && (
+          <TypeParameterConstraints parameters={props.typeParameters} />
+        )}
         {props.abstract ? ";" : <Block newline>{props.children}</Block>}
       </Scope>
     </MemberDeclaration>
diff --git a/packages/csharp/src/components/index.ts b/packages/csharp/src/components/index.ts
index 5383bac..1123992 100644
--- a/packages/csharp/src/components/index.ts
+++ b/packages/csharp/src/components/index.ts
@@ -15,5 +15,6 @@ export * from "./property/property.jsx";
 export * from "./record/declaration.js";
 export * from "./Reference.js";
 export * from "./SourceFile.js";
+export type { TypeParameterProps } from "./type-parameters/type-parameter.jsx";
 export * from "./UsingDirective.js";
 export * from "./var/declaration.jsx";
diff --git a/packages/csharp/src/components/interface/declaration.tsx b/packages/csharp/src/components/interface/declaration.tsx
index 07e026c..ba755cf 100644
--- a/packages/csharp/src/components/interface/declaration.tsx
+++ b/packages/csharp/src/components/interface/declaration.tsx
@@ -10,6 +10,9 @@ import { CSharpOutputSymbol } from "../../symbols/csharp-output-symbol.js";
 import { CSharpMemberScope } from "../../symbols/scopes.js";
 import { DocWhen } from "../doc/comment.jsx";
 import { Name } from "../Name.jsx";
+import { TypeParameterConstraints } from "../type-parameters/type-parameter-constraints.jsx";
+import { TypeParameterProps } from "../type-parameters/type-parameter.jsx";
+import { TypeParameters } from "../type-parameters/type-parameters.jsx";
 
 export interface InterfaceModifiers {
   readonly partial?: boolean;
@@ -27,7 +30,20 @@ export interface InterfaceDeclarationProps
   /** Doc comment */
   doc?: core.Children;
   refkey?: core.Refkey;
-  typeParameters?: Record<string, core.Refkey>;
+
+  /**
+   * Type parameters for the interface
+   *
+   * @example
+   * ```tsx
+   * <InterfaceDeclaration name="IList" typeParameters={["T"]} />
+   * ```
+   * This will produce:
+   * ```csharp
+   * public interface IList<T>
+   * ```
+   */
+  typeParameters?: (TypeParameterProps | string)[];
 }
 
 /**
@@ -65,31 +81,6 @@ export function InterfaceDeclaration(props: InterfaceDeclarationProps) {
     owner: thisInterfaceSymbol,
   });
 
-  let typeParams: core.Children;
-  if (props.typeParameters) {
-    const typeParamNames = new Array<string>();
-    for (const entry of Object.entries(props.typeParameters)) {
-      typeParamNames.push(
-        useCSharpNamePolicy().getName(entry[0], "type-parameter"),
-      );
-      // create a symbol for each type param so its
-      // refkey resolves to the type param's name
-      new CSharpOutputSymbol(entry[0], {
-        scope: thisInterfaceScope,
-        refkeys: entry[1],
-      });
-    }
-    typeParams = (
-      <group>
-        {"<"}
-        <core.For each={typeParamNames} comma line>
-          {(name) => name}
-        </core.For>
-        {">"}
-      </group>
-    );
-  }
-
   const modifiers = computeModifiersPrefix([
     getAccessModifier(props),
     getInterfaceModifiers(props),
@@ -98,7 +89,12 @@ export function InterfaceDeclaration(props: InterfaceDeclarationProps) {
     <core.Declaration symbol={thisInterfaceSymbol}>
       <DocWhen doc={props.doc} />
       {modifiers}interface <Name />
-      {typeParams}
+      {props.typeParameters && (
+        <TypeParameters parameters={props.typeParameters} />
+      )}
+      {props.typeParameters && (
+        <TypeParameterConstraints parameters={props.typeParameters} />
+      )}
       {props.children ?
         <core.Block newline>
           <core.Scope value={thisInterfaceScope}>{props.children}</core.Scope>
diff --git a/packages/csharp/src/components/interface/method.tsx b/packages/csharp/src/components/interface/method.tsx
index e562946..7a9e83e 100644
--- a/packages/csharp/src/components/interface/method.tsx
+++ b/packages/csharp/src/components/interface/method.tsx
@@ -17,6 +17,9 @@ import { CSharpOutputSymbol } from "../../symbols/csharp-output-symbol.js";
 import { CSharpMemberScope, useCSharpScope } from "../../symbols/scopes.js";
 import { ParameterProps, Parameters } from "../Parameters.jsx";
 import { DocWhen } from "../doc/comment.jsx";
+import { TypeParameterConstraints } from "../type-parameters/type-parameter-constraints.jsx";
+import { TypeParameterProps } from "../type-parameters/type-parameter.jsx";
+import { TypeParameters } from "../type-parameters/type-parameters.jsx";
 
 /** Method modifiers. Can only be one. */
 export interface InterfaceMethodModifiers {
@@ -33,6 +36,19 @@ export interface InterfaceMethodProps
   refkey?: Refkey;
   children?: Children;
   parameters?: Array<ParameterProps>;
+  /**
+   * Type parameters for the method
+   *
+   * @example
+   * ```tsx
+   * <InterfaceMethod name="Test" typeParameters={["T"]} />
+   * ```
+   * This will produce:
+   * ```csharp
+   * public void Test<T>()
+   * ```
+   */
+  typeParameters?: (TypeParameterProps | string)[];
   returns?: Children;
 
   /** Doc comment */
@@ -72,7 +88,14 @@ export function InterfaceMethod(props: InterfaceMethodProps) {
       <Scope value={methodScope}>
         <DocWhen doc={props.doc} />
         {modifiers}
-        {props.returns ?? "void"} {name}({params})
+        {props.returns ?? "void"}{" "}
+        {props.typeParameters && (
+          <TypeParameters parameters={props.typeParameters} />
+        )}
+        {name}({params})
+        {props.typeParameters && (
+          <TypeParameterConstraints parameters={props.typeParameters} />
+        )}
         {props.children ?
           <Block newline>{props.children}</Block>
         : ";"}
diff --git a/packages/csharp/src/components/type-parameters/type-parameter-constraints.tsx b/packages/csharp/src/components/type-parameters/type-parameter-constraints.tsx
new file mode 100644
index 0000000..604c586
--- /dev/null
+++ b/packages/csharp/src/components/type-parameters/type-parameter-constraints.tsx
@@ -0,0 +1,46 @@
+import { Children, code, For, Indent } from "@alloy-js/core";
+import { TypeParameterProps } from "./type-parameter.jsx";
+import { normalizeParameters } from "./type-parameters.jsx";
+
+export interface TypeParameterConstraintsProps {
+  /** Parameters */
+  parameters: (TypeParameterProps | string)[];
+}
+
+export function TypeParameterConstraints(
+  props: TypeParameterConstraintsProps,
+): Children {
+  const parameters = normalizeParameters(props.parameters);
+  if (!parameters.some((x) => x.constraints)) {
+    return null;
+  }
+  return (
+    <group>
+      <Indent line>
+        <For each={parameters} hardline>
+          {(param) => <TypeParameterConstraint {...param} />}
+        </For>
+      </Indent>
+    </group>
+  );
+}
+
+function TypeParameterConstraint(props: TypeParameterProps): Children {
+  const constraints = arrayify(props.constraints);
+  return (
+    <>
+      where {code`${props.name} : `}
+      <group>
+        <Indent softline>
+          <For each={constraints} comma line>
+            {(constraint) => code`${constraint}`}
+          </For>
+        </Indent>
+      </group>
+    </>
+  );
+}
+
+function arrayify<T>(value: T | T[]): T[] {
+  return Array.isArray(value) ? value : [value];
+}
diff --git a/packages/csharp/src/components/type-parameters/type-parameter.tsx b/packages/csharp/src/components/type-parameters/type-parameter.tsx
new file mode 100644
index 0000000..2e053ed
--- /dev/null
+++ b/packages/csharp/src/components/type-parameters/type-parameter.tsx
@@ -0,0 +1,35 @@
+import { Children, MemberDeclaration, refkey, Refkey } from "@alloy-js/core";
+import { useCSharpNamePolicy } from "../../name-policy.js";
+import { CSharpOutputSymbol } from "../../symbols/csharp-output-symbol.js";
+import { useCSharpScope } from "../../symbols/scopes.js";
+
+/**
+ * Information for a TypeScript generic type parameter.
+ */
+export interface TypeParameterProps {
+  /**
+   * The name of the type parameter.
+   */
+  readonly name: string;
+
+  /**
+   * The parameter constraint
+   */
+  readonly constraints?: Children | Children[];
+
+  /**
+   * A refkey or array of refkeys for this type parameter.
+   */
+  readonly refkey?: Refkey | Refkey[];
+}
+
+export function TypeParameter(props: TypeParameterProps) {
+  const name = useCSharpNamePolicy().getName(props.name, "type-parameter");
+  const scope = useCSharpScope();
+  const symbol = new CSharpOutputSymbol(name, {
+    scope,
+    refkeys: props.refkey ?? refkey(props.name),
+  });
+
+  return <MemberDeclaration symbol={symbol}>{name}</MemberDeclaration>;
+}
diff --git a/packages/csharp/src/components/type-parameters/type-parameters.tsx b/packages/csharp/src/components/type-parameters/type-parameters.tsx
new file mode 100644
index 0000000..12296eb
--- /dev/null
+++ b/packages/csharp/src/components/type-parameters/type-parameters.tsx
@@ -0,0 +1,72 @@
+import { For, Indent, taggedComponent } from "@alloy-js/core";
+import { TypeParameter, TypeParameterProps } from "./type-parameter.jsx";
+
+export const typeParametersTag = Symbol.for("csharp.type-parameters");
+
+export interface TypeParametersProps {
+  /** Parameters */
+  parameters: (TypeParameterProps | string)[];
+}
+
+/**
+ * Represent type parameters
+ *
+ * @example
+ * ```ts
+ * <A, B extends string>
+ * ```
+ */
+export const TypeParameters = taggedComponent(
+  typeParametersTag,
+  function TypeParameters(props: TypeParametersProps) {
+    const typeParameters = normalizeParameters(props.parameters);
+
+    // const typeParameters = normalizeAndDeclareParameters(props.parameters);
+
+    // onCleanup(() => {
+    //   for (const param of typeParameters) {
+    //     param.symbol.delete();
+    //   }
+    // });
+
+    return (
+      <>
+        {"<"}
+        <group>
+          <Indent softline>
+            <For each={typeParameters} comma line>
+              {(param) => <TypeParameter {...param} />}
+            </For>
+            <ifBreak>,</ifBreak>
+          </Indent>
+        </group>
+        {">"}
+      </>
+    );
+  },
+);
+
+export function normalizeParameters(
+  parameters: (TypeParameterProps | string)[],
+): TypeParameterProps[] {
+  return parameters.map((param) => {
+    if (typeof param === "string") {
+      return { name: param };
+    }
+    return param;
+  });
+}
+
+// export function declareParameter(
+//   parameters: TypeParameterProps[],
+// ): TypeParameterProps[] {
+//   return parameters.map((param) => {
+//     return {
+//       ...param,
+//       symbol: new CSharpOutputSymbol(entry[0], {
+//         scope: thisClassScope,
+//         refkeys: entry[1],
+//       }),
+//     };
+//   });
+// }


Test patch:
diff --git a/packages/csharp/src/components/interface/declaration.test.tsx b/packages/csharp/src/components/interface/declaration.test.tsx
index d368f32..cf39b6f 100644
--- a/packages/csharp/src/components/interface/declaration.test.tsx
+++ b/packages/csharp/src/components/interface/declaration.test.tsx
@@ -1,6 +1,10 @@
+import { List, refkey } from "@alloy-js/core";
 import { describe, expect, it } from "vitest";
 import { TestNamespace } from "../../../test/utils.jsx";
+import { SourceFile } from "../SourceFile.jsx";
+import { TypeParameterProps } from "../type-parameters/type-parameter.jsx";
 import { InterfaceDeclaration } from "./declaration.jsx";
+import { InterfaceProperty } from "./property.jsx";
 
 it("declares class with no members", () => {
   expect(
@@ -54,3 +58,86 @@ it("specify doc comment", () => {
     interface TestInterface;
   `);
 });
+
+describe("with type parameters", () => {
+  it("reference parameters", () => {
+    const typeParameters: TypeParameterProps[] = [
+      {
+        name: "T",
+        refkey: refkey(),
+      },
+      {
+        name: "U",
+        refkey: refkey(),
+      },
+    ];
+
+    expect(
+      <TestNamespace>
+        <SourceFile path="Test.cs">
+          <InterfaceDeclaration
+            public
+            name="Test"
+            typeParameters={typeParameters}
+          >
+            <List>
+              <InterfaceProperty
+                name="PropA"
+                type={typeParameters[0].refkey}
+                get
+                set
+              />
+              <InterfaceProperty
+                name="PropB"
+                type={typeParameters[1].refkey}
+                get
+                set
+              />
+            </List>
+          </InterfaceDeclaration>
+        </SourceFile>
+      </TestNamespace>,
+    ).toRenderTo(`
+      namespace TestCode
+      {
+        public interface Test<T, U>
+        {
+          T PropA { get; set; }
+          U PropB { get; set; }
+        }
+      }
+    `);
+  });
+
+  it("defines with constraints", () => {
+    const typeParameters: TypeParameterProps[] = [
+      {
+        name: "T",
+        constraints: "IFoo",
+      },
+      {
+        name: "U",
+        constraints: "IBar",
+      },
+    ];
+
+    expect(
+      <TestNamespace>
+        <InterfaceDeclaration
+          public
+          name="Test"
+          typeParameters={typeParameters}
+        >
+          // Body
+        </InterfaceDeclaration>
+      </TestNamespace>,
+    ).toRenderTo(`
+      public interface Test<T, U>
+        where T : IFoo
+        where U : IBar
+      {
+        // Body
+      }
+    `);
+  });
+});
diff --git a/packages/csharp/src/components/interface/method.test.tsx b/packages/csharp/src/components/interface/method.test.tsx
index e09262c..b179417 100644
--- a/packages/csharp/src/components/interface/method.test.tsx
+++ b/packages/csharp/src/components/interface/method.test.tsx
@@ -1,6 +1,9 @@
+import { refkey } from "@alloy-js/core";
 import { Children } from "@alloy-js/core/jsx-runtime";
 import { describe, expect, it } from "vitest";
 import { TestNamespace } from "../../../test/utils.jsx";
+import { SourceFile } from "../SourceFile.jsx";
+import { TypeParameterProps } from "../type-parameters/type-parameter.jsx";
 import { InterfaceDeclaration } from "./declaration.jsx";
 import { InterfaceMethod } from "./method.jsx";
 
@@ -170,3 +173,78 @@ it("specify doc comment", () => {
     }
   `);
 });
+
+describe("with type parameters", () => {
+  it("reference parameters", () => {
+    const typeParameters: TypeParameterProps[] = [
+      {
+        name: "T",
+        refkey: refkey(),
+      },
+      {
+        name: "U",
+        refkey: refkey(),
+      },
+    ];
+
+    expect(
+      <TestNamespace>
+        <SourceFile path="TestFile.cs">
+          <InterfaceDeclaration public name="TestInterface">
+            <InterfaceMethod
+              name="Test"
+              public
+              typeParameters={typeParameters}
+              parameters={[
+                {
+                  name: "paramA",
+                  type: typeParameters[0].refkey,
+                },
+              ]}
+              returns={typeParameters[0].refkey}
+            />
+          </InterfaceDeclaration>
+        </SourceFile>
+      </TestNamespace>,
+    ).toRenderTo(`
+      namespace TestCode
+      {
+        public interface TestInterface
+        {
+          public T <T, U>Test(T paramA);
+        }
+      }
+    `);
+  });
+
+  it("defines with constraints", () => {
+    const typeParameters: TypeParameterProps[] = [
+      {
+        name: "T",
+        constraints: "IFoo",
+      },
+      {
+        name: "U",
+        constraints: "IBar",
+      },
+    ];
+
+    expect(
+      <Wrapper>
+        <InterfaceMethod public name="Test" typeParameters={typeParameters}>
+          // Body
+        </InterfaceMethod>
+      </Wrapper>,
+    ).toRenderTo(`
+      public interface TestInterface
+      {
+        public void <T, U>Test()
+          where T : IFoo
+          where U : IBar
+        {
+          // Body
+        }
+      }
+    `);
+  });
+});
diff --git a/packages/csharp/src/components/type-parameters/type-parameter-constraints.test.tsx b/packages/csharp/src/components/type-parameters/type-parameter-constraints.test.tsx
new file mode 100644
index 0000000..77af41c
--- /dev/null
+++ b/packages/csharp/src/components/type-parameters/type-parameter-constraints.test.tsx
@@ -0,0 +1,93 @@
+import { expect, it } from "vitest";
+import { TypeParameterConstraints } from "./type-parameter-constraints.jsx";
+
+it("renders nothing if there is no constraints", () => {
+  expect(<TypeParameterConstraints parameters={["A", "B"]} />).toRenderTo(``);
+});
+
+it("renders inline if there is only one", () => {
+  expect(
+    <>
+      {"code()"}
+      <TypeParameterConstraints
+        parameters={[{ name: "A", constraints: "string" }]}
+      />
+    </>,
+  ).toRenderTo(`
+    code() where A : string
+  `);
+});
+
+it("renders multiple constraints", () => {
+  expect(
+    <>
+      {"code()"}
+      <TypeParameterConstraints
+        parameters={[{ name: "A", constraints: ["string", "int"] }]}
+      />
+    </>,
+  ).toRenderTo(`
+    code() where A : string, int
+  `);
+});
+
+it("renders newline if constraint is very long", () => {
+  expect(
+    <>
+      {"code()"}
+      <TypeParameterConstraints
+        parameters={[
+          {
+            name: "ThisIsQuiteALongName",
+            constraints: "VeryLongBuilderFactorySingletonThatShouldBeSplit",
+          },
+        ]}
+      />
+    </>,
+  ).toRenderTo(`
+    code()
+      where ThisIsQuiteALongName : VeryLongBuilderFactorySingletonThatShouldBeSplit
+  `);
+});
+
+it("renders multiple constraints on new lines if they are very long", () => {
+  expect(
+    <>
+      {"code()"}
+      <TypeParameterConstraints
+        parameters={[
+          {
+            name: "A",
+            constraints: [
+              "IVeryLongBuilderFactorySingletonThatShouldBeSplitA",
+              "IVeryLongBuilderFactorySingletonThatShouldBeSplitB",
+            ],
+          },
+        ]}
+      />
+    </>,
+  ).toRenderTo(`
+    code()
+      where A :
+        IVeryLongBuilderFactorySingletonThatShouldBeSplitA,
+        IVeryLongBuilderFactorySingletonThatShouldBeSplitB
+  `);
+});
+
+it("declare type parameters using parameters", () => {
+  expect(
+    <>
+      {"code()"}
+      <TypeParameterConstraints
+        parameters={[
+          { name: "A", constraints: "string" },
+          { name: "B", constraints: "string" },
+        ]}
+      />
+    </>,
+  ).toRenderTo(`
+    code()
+      where A : string
+      where B : string
+  `);
+});
diff --git a/packages/csharp/src/components/type-parameters/type-parameters.test.tsx b/packages/csharp/src/components/type-parameters/type-parameters.test.tsx
new file mode 100644
index 0000000..bfc733a
--- /dev/null
+++ b/packages/csharp/src/components/type-parameters/type-parameters.test.tsx
@@ -0,0 +1,19 @@
+import { expect, it } from "vitest";
+import { TestNamespace } from "../../../test/utils.jsx";
+import { TypeParameters } from "./type-parameters.jsx";
+
+it("declare type parameters using parameters names", () => {
+  expect(
+    <TestNamespace>
+      <TypeParameters parameters={["A", "B"]} />
+    </TestNamespace>,
+  ).toRenderTo(`<A, B>`);
+});
+
+it("declare type parameters using parameters", () => {
+  expect(
+    <TestNamespace>
+      <TypeParameters parameters={[{ name: "A" }, { name: "B" }]} />
+    </TestNamespace>,
+  ).toRenderTo(`<A, B>`);
+});
diff --git a/packages/csharp/test/class-declaration.test.tsx b/packages/csharp/test/class-declaration.test.tsx
index 8a2bfa6..f94035a 100644
--- a/packages/csharp/test/class-declaration.test.tsx
+++ b/packages/csharp/test/class-declaration.test.tsx
@@ -1,8 +1,15 @@
 import * as core from "@alloy-js/core";
+import { List, refkey } from "@alloy-js/core";
 import * as coretest from "@alloy-js/core/testing";
 import { describe, expect, it } from "vitest";
+import { TypeParameterProps } from "../src/components/type-parameters/type-parameter.jsx";
 import * as csharp from "../src/index.js";
-import { ClassDeclaration, ClassMember } from "../src/index.js";
+import {
+  ClassDeclaration,
+  ClassMember,
+  Property,
+  SourceFile,
+} from "../src/index.js";
 import * as utils from "./utils.jsx";
 
 it("declares class with no members", () => {
@@ -217,34 +224,77 @@ it("uses refkeys for members, params, and return type", () => {
   `);
 });
 
-it("declares class with generic parameters", () => {
-  const typeParameters = {
-    T: core.refkey(),
-    U: core.refkey(),
-  };
+describe("with type parameters", () => {
+  it("reference parameters", () => {
+    const typeParameters: TypeParameterProps[] = [
+      {
+        name: "T",
+        refkey: refkey(),
+      },
+      {
+        name: "U",
+        refkey: refkey(),
+      },
+    ];
 
-  const res = utils.toSourceText(
-    <csharp.ClassDeclaration
-      public
-      name="TestClass"
-      typeParameters={typeParameters}
-    >
-      <csharp.ClassMember public name="memberOne" type={typeParameters.T} />
-      ;<hbr />
-      <csharp.ClassMember private name="memberTwo" type={typeParameters.U} />;
-    </csharp.ClassDeclaration>,
-  );
-
-  expect(res).toBe(coretest.d`
-    namespace TestCode
-    {
+    expect(
+      <utils.TestNamespace>
+        <SourceFile path="Test.cs">
+          <ClassDeclaration
+            public
+            name="TestClass"
+            typeParameters={typeParameters}
+          >
+            <List>
+              <Property name="PropA" type={typeParameters[0].refkey} get set />
+              <Property name="PropB" type={typeParameters[1].refkey} get set />
+            </List>
+          </ClassDeclaration>
+        </SourceFile>
+      </utils.TestNamespace>,
+    ).toRenderTo(`
+      namespace TestCode
+      {
         public class TestClass<T, U>
         {
-            public T MemberOne;
-            private U memberTwo;
+          T PropA { get; set; }
+          U PropB { get; set; }
         }
-    }
-  `);
+      }
+    `);
+  });
+
+  it("defines with constraints", () => {
+    const typeParameters: TypeParameterProps[] = [
+      {
+        name: "T",
+        constraints: "IFoo",
+      },
+      {
+        name: "U",
+        constraints: "IBar",
+      },
+    ];
+
+    expect(
+      <utils.TestNamespace>
+        <ClassDeclaration
+          public
+          name="TestClass"
+          typeParameters={typeParameters}
+        >
+          // Body
+        </ClassDeclaration>
+      </utils.TestNamespace>,
+    ).toRenderTo(`
+      public class TestClass<T, U>
+        where T : IFoo
+        where U : IBar
+      {
+        // Body
+      }
+    `);
+  });
 });
 
 it("declares class with invalid members", () => {