microsoft / microsoft/typespec

[CSharp] ClientProvider.BuildApiVersionFields produces duplicate field names for multi-service clients

Open
#10,055 2 comments 1 reaction 3 assignees Claimed by @jorgerangel-msft View on GitHub
emitter:client:csharp
Dominant language
Java
Stars
5.9k
Forks
394
Avg merge
1d 23h
Merged PRs (30d)
104

Description

## Bug Report

### Description

When a multi-service client is created via `@client({ service: [ServiceA, ServiceB] })`, `ClientProvider.BuildApiVersionFields()` can produce duplicate field names, causing `BuildMethods()` to crash with:

```
An item with the same key has already been added. Key: serviceTestsApiVersion
at Dictionary2.Add(TKey key, TValue value)
at Enumerable.ToDictionary(...)
at ClientProvider.BuildMethods()
```

### Impact

This crashes the **Azure management plane generator** (`@azure-typespec/http-client-csharp-mgmt`) when generating multi-service SDKs such as Azure Compute (which combines 4 services via `@client({ service: [Compute, ComputeDisk, ComputeGallery, ComputeSku] })`).

The mgmt generator does **not** use the `ClientProvider` or its API version fields at all — it replaces them with its own ARM resource-based client hierarchy. However, the crash occurs during `ClientProvider.BuildMethods()` before the mgmt generator gets a chance to override it, so the mgmt generator cannot work around this without patching the base `ClientProvider`.

### Root Cause

In `ClientOptionsProvider.BuildVersionProperties()`, the property name for each service version is computed via:

```csharp
string name = _inputClient.IsMultiServiceClient
? ClientHelper.BuildNameForService(inputEnumType.Namespace, "Service", "ApiVersion")
: "Version";
```

When two services have namespaces that reduce to the same identifier (e.g. both share a common namespace segment from the C# output namespace), `BuildNameForService` produces the same property name for both services.

Then in `ClientProvider.BuildApiVersionFields()`, when `VersionProperties.Count > 1`:

```csharp
string name = (count > 1) ? ("_" + propertyProvider.Name.ToVariableName()) : text;
```

Both services get the same field name (e.g. `_serviceTestsApiVersion`), and `Fields.ToDictionary()` in `BuildMethods()` crashes on the duplicate key.

### Reproduction

Two ARM services combined via `@client`, with the output C# namespace causing name collisions:

**ServiceOne/main.tsp:**
```typespec
@armProviderNamespace
@service(#{ title: "ServiceOne" })
@versioned(ServiceOne.Versions)
namespace ServiceOne;

enum Versions { v2024_01_01: "2024-01-01" }
// ... resource definition ...
```

**ServiceTwo/main.tsp:**
```typespec
@armProviderNamespace
@service(#{ title: "ServiceTwo" })
@versioned(ServiceTwo.Versions)
namespace ServiceTwo;

enum Versions { v2024_06_01: "2024-06-01" }
// ... resource definition ...
```

**client.tsp:**
```typespec
@client({ name: "MultiServiceClient", service: [ServiceOne, ServiceTwo] })
namespace MultiServiceCombine;
```

**tspconfig.yaml** with namespace `Azure.Generator.MgmtTypeSpec.MultiService.Tests`

### Expected Behavior

Each service should get a unique field name for its API version field, even when namespace segments collide.

### Actual Behavior

Both services produce the same field name `serviceTestsApiVersion`, causing `ToDictionary` to crash.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.