Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1c15536
Add regression test for legacy date-time property name
live1206 Aug 27, 2026
5a782a7
Preserve legacy date-time property names from GA contracts
live1206 Aug 27, 2026
3644022
Match GA properties by canonical date-time name
live1206 Aug 27, 2026
98ebb26
Only preserve public GA property names
live1206 Aug 27, 2026
f3325b4
Match raw custom properties after date normalization
live1206 Aug 27, 2026
59ef218
fix: resolve customizations against preserved date-time names
jorgerangel-msft Aug 27, 2026
a657a98
fix: scope customization name matching to the targeted spec property
jorgerangel-msft Aug 27, 2026
5cc81bd
fix: resolve renamed custom members by their CodeGenMember target
jorgerangel-msft Aug 27, 2026
47550d8
fix: do not reuse a historical date-time name another property claims
jorgerangel-msft Aug 27, 2026
6b37a8f
fix: do not reuse a historical name claimed by a renamed custom member
jorgerangel-msft Aug 27, 2026
c289d46
fix: constrain historical date-time name matches
jorgerangel-msft Aug 27, 2026
76ba071
fix: derive historical date-time compatibility candidates
live1206 Aug 28, 2026
7402856
fix: prioritize exact-name customization targets
live1206 Aug 28, 2026
a11e60f
fix: respect claims on canonical contract names
live1206 Aug 28, 2026
75f2f44
refactor: match historical date-time names by stem
live1206 Aug 28, 2026
ff031d8
fix: preserve first exact customization match
live1206 Aug 28, 2026
a3572f4
fix: harden date-time customization matching
live1206 Aug 28, 2026
3b5aaa6
fix: reconcile date-time names on emitted providers
live1206 Aug 28, 2026
b7b952e
fix: preserve first exact spec mapping
live1206 Aug 28, 2026
83d94f9
Revert "fix: reconcile date-time names on emitted providers"
live1206 Aug 28, 2026
788eaab
test: make date-time ownership invariants explicit
live1206 Aug 28, 2026
95cb3cc
refactor: make customization map initialization explicit
live1206 Aug 28, 2026
001c070
fix: include inherited custom name claims
live1206 Aug 28, 2026
5b0a731
Revert "fix: include inherited custom name claims"
live1206 Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal class CanonicalTypeProvider : TypeProvider
{
private readonly TypeProvider _generatedTypeProvider;
private readonly Dictionary<string, InputModelProperty> _specPropertiesMap;
private readonly HashSet<string> _exactSpecPropertyNames;
private readonly Dictionary<string, string?> _serializedNameMap;
private readonly Dictionary<InputModelProperty, PropertyProvider> _propertyProviderMap = new();
private readonly HashSet<string> _renamedProperties;
Expand All @@ -30,14 +31,17 @@ public CanonicalTypeProvider(TypeProvider generatedTypeProvider, InputType? inpu
var inputModel = inputType as InputModelType;
_specProperties = inputModel?.Properties ?? [];
_specPropertiesMap = [];
_exactSpecPropertyNames = [];
foreach (var property in _specProperties)
{
var name = property.IsExactName ? property.Name : property.Name.ToIdentifierName();
_specPropertiesMap.TryAdd(name, property);
if (!property.IsExactName)
{
_specPropertiesMap.TryAdd(name.NormalizeCSharpAcronyms(property.Type.IsDateTimeInputType()), property);
}
_exactSpecPropertyNames.Add(name);
}
foreach (var property in _specProperties.Where(p => !p.IsExactName))
{
var name = property.Name.ToIdentifierName();
_specPropertiesMap.TryAdd(name.NormalizeCSharpAcronyms(property.Type.IsDateTimeInputType()), property);
}
_serializedNameMap = BuildSerializationNameMap();
_renamedProperties = (_generatedTypeProvider.CustomCodeView?.Properties ?? [])
Expand Down Expand Up @@ -92,9 +96,23 @@ protected internal override MethodProvider[] BuildMethods()

protected internal override PropertyProvider[] BuildProperties()
{
// Building Properties populates GeneratedPropertiesBySpecName as part of customization filtering.
// Keep these reads together and in this order so the map is never observed mid-build.
var generatedProperties = _generatedTypeProvider.Properties;
var generatedPropertiesBySpecName = _generatedTypeProvider.GeneratedPropertiesBySpecName;
var customProperties = _generatedTypeProvider.CustomCodeView?.Properties ?? [];

// Exact emitted names take precedence over canonical aliases. A canonical alias from an earlier
// property must not shadow a later property that actually emits that name.
foreach (var generatedProperty in generatedPropertiesBySpecName.Values)
{
if (generatedProperty.InputProperty is InputModelProperty preservedSpecProperty &&
_exactSpecPropertyNames.Add(generatedProperty.Name))
{
_specPropertiesMap[generatedProperty.Name] = preservedSpecProperty;
}
}

// Update the serializedName of generated properties if necessary
foreach (var generatedProperty in generatedProperties)
{
Expand Down Expand Up @@ -163,7 +181,9 @@ protected internal override PropertyProvider[] BuildProperties()
// Filter out generated properties that have been customized to avoid duplicates.
// This is needed because EnsureBuilt caches members without applying customization
// filtering, so _generatedTypeProvider.Properties may contain unfiltered results.
var filteredGeneratedProperties = FilterCustomizedProperties(generatedProperties);
var filteredGeneratedProperties = FilterCustomizedProperties(
generatedProperties,
generatedPropertiesBySpecName);

if (_specProperties.Count > 0)
{
Expand All @@ -186,9 +206,9 @@ protected internal override PropertyProvider[] BuildProperties()

foreach (var prop in customProperties)
{
// Check if custom property is in spec
if (_specPropertiesMap.TryGetValue(prop.Name, out var specProp) ||
(prop.OriginalName != null && TryGetSpecProperty(prop.OriginalName, out specProp)))
// Check if custom property is in spec.
if ((prop.OriginalName != null && TryGetSpecProperty(prop.OriginalName, out var specProp)) ||
_specPropertiesMap.TryGetValue(prop.Name, out specProp))
{
inputProperties.Add(specProp);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,43 @@ private PropertyProvider(InputProperty inputProperty, CSharpType propertyType, T
var hasOutputUsage = inputProperty.EnclosingType?.Usage.HasFlag(InputModelTypeUsage.Output) ?? false;
Modifiers = IsDiscriminator || (!hasOutputUsage && _isRequiredNonNullableConstant) ? MethodSignatureModifiers.Internal : MethodSignatureModifiers.Public;
var identifierName = inputProperty.IsExactName ? inputProperty.Name : inputProperty.Name.ToIdentifierName();
var lastContractProperties = enclosingType.LastContractView?.Properties;
var legacyName = identifierName == enclosingType.Name
? $"{identifierName}Property"
: identifierName;
if (!inputProperty.IsExactName &&
(lastContractProperties is null ||
!lastContractProperties.Any(p => p.Name == legacyName)))
if (!inputProperty.IsExactName)
{
identifierName = identifierName
.NormalizeCSharpAcronyms(inputProperty.Type.IsDateTimeInputType());
var isDateTime = inputProperty.Type.IsDateTimeInputType();
var canonicalName = identifierName.NormalizeCSharpAcronyms(isDateTime);
var enclosingTypeName = enclosingType.Name;
var lastContractProperties = enclosingType.LastContractView?.Properties
.Where(p => MethodSignatureHelper.IsPublicApi(p.Modifiers))
.ToList();

// An exact input-identifier match is authoritative: it is the name this property would have had
// before normalization, so it does not need the disambiguation required by normalized candidates.
// The shipped member may carry the enclosing-type collision suffix, so candidates are compared
// against the collision-adjusted form of each name we are looking for.
var previousProperty =
lastContractProperties?.FirstOrDefault(p => p.Name == AvoidPropertyNameCollision(identifierName, enclosingTypeName))
?? lastContractProperties?.FirstOrDefault(p =>
p.Name == AvoidPropertyNameCollision(canonicalName, enclosingTypeName) &&
!IsClaimedBySiblingProperty(p.Name, inputProperty, enclosingType));

if (previousProperty is null &&
isDateTime &&
!identifierName.EndsWith("On", StringComparison.Ordinal))
{
// Both the current and previous conventions render date-time names as <stem>On. Requiring
// that suffix on the contract name prevents a removed property such as StartDate from being
// mistaken for the historical name of StartTime even though both normalize to StartsOn.
var specStem = identifierName.NormalizeCSharpAcronyms().GetDateTimeStem();
previousProperty = specStem is null
? null
: lastContractProperties?.FirstOrDefault(p =>
HasDateTimeStem(p.Name, specStem, enclosingTypeName) &&
p.Type.WithNullable(false).Equals(Type.WithNullable(false)) &&
!IsClaimedBySiblingProperty(p.Name, inputProperty, enclosingType));
}
identifierName = previousProperty?.Name ?? canonicalName;
}
Name = identifierName == enclosingType.Name
? $"{identifierName}Property"
: identifierName;
Name = AvoidPropertyNameCollision(identifierName, enclosingType.Name);
Body = new AutoPropertyBody(propHasSetter, setterModifier, GetPropertyInitializationValue(propertyType, inputProperty));

WireInfo = new PropertyWireInformation(inputProperty);
Expand Down Expand Up @@ -180,6 +203,95 @@ private void BuildDocs()
}
}

private static string AvoidPropertyNameCollision(string propertyName, string enclosingTypeName) =>
propertyName == enclosingTypeName ? $"{propertyName}Property" : propertyName;

private static bool HasDateTimeStem(string contractName, string specStem, string enclosingTypeName)
{
if (contractName == AvoidPropertyNameCollision(enclosingTypeName, enclosingTypeName))
{
contractName = enclosingTypeName;
}

return contractName.EndsWith("On", StringComparison.Ordinal) &&
contractName.GetDateTimeStem() == specStem;
}

private static bool IsClaimedBySiblingProperty(
string contractName,
InputProperty inputProperty,
TypeProvider enclosingType)
{
var enclosingTypeName = enclosingType.Name;

foreach (var sibling in inputProperty.EnclosingType?.Properties ?? [])
Comment thread
JoshLove-msft marked this conversation as resolved.
Resolved
{
if (ReferenceEquals(sibling, inputProperty))
{
continue;
}

if (sibling.IsExactName)
{
if (AvoidPropertyNameCollision(sibling.Name, enclosingTypeName) == contractName)
{
return true;
}

continue;
}

// Compare both the raw identifier and the name the sibling actually emits, since acronym
// normalization can move it onto the shipped name even when the identifier does not match.
var siblingIdentifier = sibling.Name.ToIdentifierName();
var siblingGenerated = siblingIdentifier.NormalizeCSharpAcronyms(sibling.Type.IsDateTimeInputType());
if (AvoidPropertyNameCollision(siblingIdentifier, enclosingTypeName) == contractName ||
AvoidPropertyNameCollision(siblingGenerated, enclosingTypeName) == contractName)
{
return true;
}
}

// A customization that explicitly targets a different spec property owns its public name outright.
// A raw-name customization is excluded: it has no declared target, so it is intended to replace
// whichever generated property ends up with that name, including a preserved one. Fields are
// included because customization filtering treats a custom field as a claim on a property name.
foreach (var customName in GetExplicitlyRenamedCustomNames(enclosingType))
{
if (customName.OriginalName == inputProperty.Name ||
customName.OriginalName == inputProperty.Name.ToIdentifierName())
{
continue;
}

if (customName.Name == contractName)
{
return true;
}
}

return false;
}

private static IEnumerable<(string Name, string OriginalName)> GetExplicitlyRenamedCustomNames(TypeProvider enclosingType)
{
foreach (var customProperty in enclosingType.CustomCodeView?.Properties ?? [])
{
if (customProperty.OriginalName is { } propertyOriginalName)
{
yield return (customProperty.Name, propertyOriginalName);
}
}

foreach (var customField in enclosingType.CustomCodeView?.Fields ?? [])
{
if (customField.OriginalName is { } fieldOriginalName)
{
yield return (customField.Name, fieldOriginalName);
}
}
}

private static bool IsPropertyPrivate(MethodSignatureModifiers modifiers, TypeSignatureModifiers enclosingTypeModifiers)
{
return (modifiers.HasFlag(MethodSignatureModifiers.Private) && !modifiers.HasFlag(MethodSignatureModifiers.Protected))
Expand Down
Loading
Loading