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).
Summary
#10639 added operator deduplication between generated and customization partials in MethodSignatureBase.SignatureComparer. For conversion operators it skips the
Namecomparison and relies on modifiers + return type + parameter types to identify duplicates. The return-type check usesCSharpType.AreNamesEqual, which does not consider nullability. As a result, a generatedimplicit operator T?(string)is incorrectly treated as a duplicate of a customization-definedimplicit operator T(string)(or vice versa) and gets stripped.Repro
In
Azure.AI.Language.QuestionAnswering.Authoringthe customization partialsrc/ImportContentType.csdefines: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.2deletessdk/cognitivelanguage/Azure.AI.Language.QuestionAnswering.Authoring/src/Generated/Models/ImportContentType.csentirely, removing the publicimplicit operator ImportContentType?(string)from the SDK surface.Root cause
In
packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Primitives/MethodSignatureBase.cs(operator branch ofMethodSignatureBaseEqualityComparer.Equals):csharp if (x.ReturnType != null && y.ReturnType != null) { if (!x.ReturnType.AreNamesEqual(y.ReturnType)) { return false; } }CSharpType.AreNamesEqualonly comparesName/FullyQualifiedNameand the genericArgumentslist. TheCSharpTypeconstructor explicitly unwrapsNullable<T>for value types and stores nullability on a separateIsNullableflag, soImportContentTypeandImportContentType?both end up withName == "ImportContentType"and zeroArguments.AreNamesEqualreturns true for the pair, and the dedup comparer treats the conversion operators as the same.Expected behavior
A conversion operator returning
Tand one returningT?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 compareReturnType.IsNullable(or useCSharpType.Equalsdirectly, 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
MethodSignatureComparerTestscoveringimplicit operator T(string)vsimplicit operator T?(string)(not equal).