Skip to content

[http-client-csharp] Preserve GA property names across date-time naming normalization #11772

Closed

Description

@live1206

Description

The C# generator's new date-time normalization should apply to new APIs, but it currently renames properties that were already shipped in a GA package under names produced by the Azure management-plane generator's previous naming convention.

This was found in Azure/azure-sdk-for-net#62490 after updating @typespec/http-client-csharp from 1.0.0-alpha.20260819.14 to 1.0.0-alpha.20260825.9 (source commit 210f84c / #11751).

For example:

Spec/input name:          StartTime
Previous MPG output:      StartOn
New MTG canonical output: StartsOn
GA contract contains:     StartOn
Current output:           StartsOn
Desired output:           StartOn

The same issue occurs for other historical management-plane names:

Spec/input name Previous GA name New canonical name
StartTime StartOn StartsOn
EndTime EndOn EndsOn
ExpirationTime ExpireOn ExpiresOn
AccessTierChangeTime AccessTierChangeOn AccessTierChangedOn
LastSyncTimestamp LastSyncTimestamp LastSyncOn

Root cause

PropertyProvider currently checks whether the last contract contains the raw input-derived identifier before applying date-time normalization. Simplified:

var identifierName = inputProperty.Name.ToIdentifierName();
var lastContractProperties = enclosingType.LastContractView?.Properties;

if (lastContractProperties is null ||
    !lastContractProperties.Any(p => p.Name == identifierName))
{
    identifierName = identifierName.NormalizeCSharpAcronyms(
        inputProperty.Type.IsDateTimeInputType());
}

For StartTime, this looks for StartTime in the last contract. The GA assembly contains StartOn, because that was the name produced by MPG's former date-time naming visitor. The lookup therefore misses the shipped property and normalizes it to StartsOn.

The existing last-contract check preserves a raw name that was shipped unchanged, but it does not preserve names produced by an earlier generator naming convention.

Expected behavior

The configured GA ApiCompatVersion should determine whether the old name is retained:

Last contract Expected output
Contains StartOn Preserve StartOn
Contains StartsOn Preserve StartsOn
Contains neither Use the new canonical StartsOn

A property that exists only in a newer preview and not in the configured GA baseline may adopt the new canonical name.

Suggested direction

When a date-time input matches a normalization rule, consider both the new canonical name and known previous generated-name candidate(s) when consulting LastContractView. If the GA contract contains one of those candidates, retain the exact GA name; otherwise use the new canonical name.

Conceptually:

var canonicalName = NormalizeDateTimeName(inputName);
var previousGeneratedNames = GetPreviousDateTimeNames(inputName);
var previousProperty = lastContractProperties?.FirstOrDefault(
    p => p.Name == inputName ||
         p.Name == canonicalName ||
         previousGeneratedNames.Contains(p.Name));

var finalName = previousProperty?.Name ?? canonicalName;

The preservation should occur while the property provider is initially constructed, before constructor parameters and serialization providers are materialized.

If the historical names are Azure-management-specific, MTG could expose a narrow hook for the generator plugin to contribute previous generated-name candidates while MTG owns the last-contract preservation decision. MPG's active date-time naming visitor can then be removed separately, leaving core normalization as the single naming policy for new APIs.

Impact

The Azure SDK regeneration CI reported approximately:

  • 372 distinct removed GA properties
  • 478 ApiCompat CP0002 member errors when getters and setters are counted separately
  • 57 affected GA management-plane projects

Representative confirmed baselines:

  • Azure.ResourceManager.StorageMover 1.4.1 contains StorageMoverScheduleInfo.StartOn and EndOn; it does not contain StartsOn or EndsOn.
  • Azure.ResourceManager.AppService 1.5.0 contains WebAppBackupSchedule.StartOn; it does not contain StartsOn.

This is therefore a shipped binary API compatibility issue, not only API-listing drift.

Regression coverage

Please cover at least:

  1. Previously shipped management name

    • Input: StartTime
    • GA contract: StartOn
    • Output: StartOn
  2. New property

    • Input: StartTime
    • No GA property
    • Output: StartsOn
  3. Already canonical GA name

    • Input: StartTime
    • GA contract: StartsOn
    • Output: StartsOn
  4. Historical EndOn, ExpireOn, compound ChangeOn, and unchanged timestamp forms.

  5. A flattened or resource-data projected property, such as DiskEncryptionSetData.LastKeyRotationTimestamp, because its public GA property may be exposed on a different generated provider than the original wire-model property.

The invalid MRW deserialization that can accompany these renames is tracked separately by #11770.

- by copilot

Activity

  1. github-actions commented on Aug 27, 2026

    @github-actions
    Contributor

    🎯 Issue Triage

    The C# client emitter's new date-time naming normalization (introduced in #11751) breaks binary API compatibility for GA management-plane libraries by renaming previously shipped properties (e.g., StartOn → StartsOn), causing ~372 removed GA properties across 57 projects.

    Category: Bug
    Area: emitter:client:csharp

    ⚠️ No reproduction provided. A minimal repro would help diagnose this issue.

    🔍 Analysis

    The fix requires extending the last-contract lookup to also check known previous generated-name candidates (e.g., both StartOn and StartsOn when input is StartTime), and preserving whichever name the GA contract contains.

    📝 Suggested next steps
    • Extend PropertyProvider to compute previous generated-name candidates for date-time inputs and include them in the LastContractView lookup
    • If historical names are Azure-management-specific, consider exposing a plugin hook in MTG for MPG to contribute previous name candidates
    • Add regression tests covering the 5 scenarios listed in the issue (preserved old name, new property, already-canonical name, historical variants, flattened/projected properties)
    • Coordinate fix with [http-client-csharp] Date-time normalization breaks MRW deserialization with back-compat constructor parameters #11770 (MRW deserialization issue)

    Generated by Agentic Triage

  2. added a commit that references this issue on Aug 28, 2026
    27299b9
  3. live1206 commented on Sep 2, 2026

    @live1206
    ContributorAuthor

    The full Azure SDK management-plane regeneration shows that this is not fully fixed for real Storage integration scenarios, even with the package containing #11773.

    Validation environment:

    Unsupported cases found:

    1. Projected resource-data property

      • Input/spec property: AccessTierChangeTime
      • GA property: FileShareData.AccessTierChangeOn
      • Regenerated property: FileShareData.AccessTierChangedOn

      Reflection against the configured 1.7.0 baseline confirms that AccessTierChangeOn is present and AccessTierChangedOn is absent. The generated FileShareData property projects from FileShareProperties.AccessTierChangedOn, so the real projected-provider association is not preserving the GA name despite the projected-provider regression coverage added by [http-client-csharp] Preserve shipped date-time property names #11773.

    2. Existing customization combined with a preserved historical name

      StorageTableAccessPolicy shipped both ExpireOn and ExpiresOn. Existing custom code supplies the hidden ExpiresOn alias and expects the primary ExpireOn property to remain generated. Regeneration instead selected ExpiresOn and omitted ExpireOn, leaving the customization referring to a missing property and causing compilation failures.

    A temporary Storage-specific customization workaround was added in Azure/azure-sdk-for-net#62505 so the emitter version bump and full regeneration can proceed. That workaround should be removable once these integration cases are handled by the generator.

    The model-factory parameter rename observed in the same package is separate and remains associated with the #11770 back-compat method/serialization path.

    - by copilot

  4. live1206 commented on Sep 2, 2026

    @live1206
    ContributorAuthor

    Closing this issue for MTG. The MTG regression originally reported here was fixed by #11773: when MTG constructs an input-backed public property, it consults the configured LastContractView and preserves the shipped date-time name.

    The remaining Storage findings do not require another MTG fix:

    • FileShareData.AccessTierChangeOn is synthesized later by the management-plane flattening path from the already-normalized internal FileShareProperties.AccessTierChangedOn property. MTG is given a synthesized property name rather than the original input-backed public-property association, so this needs to be handled by the management generator when it projects the property onto the public provider.
    • StorageTableAccessPolicy shipped both ExpireOn and ExpiresOn. MTG can generate one canonical member, while Storage customization supplies the additional compatibility alias/member. This library-specific dual-member arrangement does not warrant general MTG alias inference.

    The synthetic MTG-only regressions explored for these cases were removed because they would impose flattening- or library-specific behavior on MTG’s general property construction.

    - by copilot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions