Skip to content

[http-client-csharp] Operator dedup treats T and T? return types as identical, dropping nullable conversion operators #10705

@JoshLove-msft

Description

@JoshLove-msft

Summary

#10639 added operator deduplication between generated and customization partials in MethodSignatureBase.SignatureComparer. For conversion operators it skips the Name comparison and relies on modifiers + return type + parameter types to identify duplicates. The return-type check uses CSharpType.AreNamesEqual, which does not consider nullability. As a result, a generated implicit operator T?(string) is incorrectly treated as a duplicate of a customization-defined implicit operator T(string) (or vice versa) and gets stripped.

Repro

In Azure.AI.Language.QuestionAnswering.Authoring the customization partial src/ImportContentType.cs defines:

csharp public static implicit operator ImportContentType(string value) => new ImportContentType(value);

The generator-side partial emits both:

csharp public static implicit operator ImportContentType(string value) => new ImportContentType(value); public static implicit operator ImportContentType?(string value) => value == null ? null : new ImportContentType(value);

After #10639, both generated operators are deduped (including the nullable variant), the generated partial becomes empty and the file is dropped. See azure-sdk-for-net PR #59283 — the bump to 1.0.0-alpha.20260515.2 deletes sdk/cognitivelanguage/Azure.AI.Language.QuestionAnswering.Authoring/src/Generated/Models/ImportContentType.cs entirely, removing the public implicit operator ImportContentType?(string) from the SDK surface.

Root cause

In packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/MethodSignatureBase.cs (operator branch of MethodSignatureBaseEqualityComparer.Equals):

csharp if (x.ReturnType != null && y.ReturnType != null) { if (!x.ReturnType.AreNamesEqual(y.ReturnType)) { return false; } }

CSharpType.AreNamesEqual only compares Name/FullyQualifiedName and the generic Arguments list. The CSharpType constructor explicitly unwraps Nullable<T> for value types and stores nullability on a separate IsNullable flag, so ImportContentType and ImportContentType? both end up with Name == "ImportContentType" and zero Arguments. AreNamesEqual returns true for the pair, and the dedup comparer treats the conversion operators as the same.

Expected behavior

A conversion operator returning T and one returning T? are different operators in C# (different signatures), and the comparer should treat them as such.

Suggested fix

In the operator branch of MethodSignatureBaseEqualityComparer.Equals, also compare ReturnType.IsNullable (or use CSharpType.Equals directly, which already considers nullability), e.g.:

csharp if (x.ReturnType != null && y.ReturnType != null) { if (!x.ReturnType.AreNamesEqual(y.ReturnType) || x.ReturnType.IsNullable != y.ReturnType.IsNullable) { return false; } }

Should also add a regression test to MethodSignatureComparerTests covering implicit operator T(string) vs implicit operator T?(string) (not equal).

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingemitter:client:csharpIssue for the C# client emitter: @typespec/http-client-csharp

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions