diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs index dbd213f608f..bc270e28a71 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/BackCompatibilityChangeCategory.cs @@ -19,8 +19,8 @@ public enum BackCompatibilityChangeCategory /// The shape of a model's AdditionalProperties property was preserved from the last contract. AdditionalPropertiesShapePreserved, - /// A collection property type was preserved from the last contract. - CollectionPropertyTypePreserved, + /// A property type was preserved from the last contract. + PropertyTypePreserved, /// A constructor modifier (e.g. private protected -> public) was preserved from the last contract. ConstructorModifierPreserved, diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs index 7fed8e03981..b43c03385e5 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/EmitterRpc/Emitter.cs @@ -173,7 +173,7 @@ public void WriteBufferedMessages() BackCompatibilityChangeCategory.MethodParameterReordering => "Method Parameter Reordering", BackCompatibilityChangeCategory.ParameterNamePreserved => "Parameter Name Preserved", BackCompatibilityChangeCategory.AdditionalPropertiesShapePreserved => "AdditionalProperties Shape Preserved", - BackCompatibilityChangeCategory.CollectionPropertyTypePreserved => "Collection Property Type Preserved", + BackCompatibilityChangeCategory.PropertyTypePreserved => "Property Type Preserved", BackCompatibilityChangeCategory.ConstructorModifierPreserved => "Constructor Modifier Preserved", BackCompatibilityChangeCategory.EnumMemberReordering => "Enum Member Reordering", BackCompatibilityChangeCategory.ApiVersionEnumMemberAdded => "Api Version Enum Member Added From Last Contract", diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs index 44e817622b4..2e8a6076acc 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ModelProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Microsoft.TypeSpec.Generator.EmitterRpc; @@ -536,20 +537,6 @@ protected internal override PropertyProvider[] BuildProperties() continue; } - // Targeted backcompat fix for the case where properties were previously generated as read-only collections - if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary) - { - if (LastContractPropertiesMap.TryGetValue(outputProperty.Name, - out CSharpType? lastContractPropertyType) && - !outputProperty.Type.Equals(lastContractPropertyType)) - { - outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(property); - CodeModelGenerator.Instance.Emitter.Info( - $"Changed property '{Name}.{outputProperty.Name}' type to '{lastContractPropertyType}' to match last contract.", - BackCompatibilityChangeCategory.CollectionPropertyTypePreserved); - } - } - if (!isDiscriminator) { var derivedProperty = InputDerivedProperties.FirstOrDefault(p => p.Value.ContainsKey(property.Name)).Value?[property.Name]; @@ -1279,6 +1266,56 @@ _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type, }; } + /// + /// Rewrites property types so that, whenever a property exists in the last contract + /// with a different type than the one produced by the current spec, the previous + /// contract's type is preserved. This avoids source-breaking changes for consumers + /// of the library for any kind of property change (collection wrapper, nullability, + /// underlying type, etc.). Users can override this behavior with custom code if they + /// want the new spec's type instead. + /// + protected internal override IReadOnlyList BuildPropertiesForBackCompatibility(IEnumerable originalProperties) + { + var properties = originalProperties as IReadOnlyList ?? [.. originalProperties]; + if (LastContractPropertiesMap.Count == 0) + { + return properties; + } + + foreach (var outputProperty in properties) + { + if (TryGetLastContractPropertyTypeOverride(outputProperty, out var lastContractPropertyType)) + { + var newType = lastContractPropertyType.ApplyInputSpecProperty(outputProperty.InputProperty); + outputProperty.Update(type: newType); + CodeModelGenerator.Instance.Emitter.Info( + $"Changed property '{Name}.{outputProperty.Name}' type to '{lastContractPropertyType}' to match last contract.", + BackCompatibilityChangeCategory.PropertyTypePreserved); + } + } + + return properties; + } + + private bool TryGetLastContractPropertyTypeOverride( + PropertyProvider outputProperty, + [NotNullWhen(true)] out CSharpType? lastContractPropertyType) + { + // Always preserve the last contract's property type when it differs from the + // type produced by the current spec. This prevents source-breaking changes + // for any kind of property change (collection wrapper, nullability, underlying + // type, etc.). Users can override this behavior with custom code if needed. + lastContractPropertyType = null; + if (LastContractPropertiesMap.TryGetValue(outputProperty.Name, out var candidate) && + !candidate.Equals(outputProperty.Type)) + { + lastContractPropertyType = candidate; + return true; + } + + return false; + } + /// /// Determines whether to use object type for AdditionalProperties based on backward compatibility requirements. /// Checks if the last contract (previous version) had an AdditionalProperties property of type IDictionary<string, object>. diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs index 21e6f9c1adf..69170816543 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/ParameterProvider.cs @@ -320,6 +320,10 @@ public void Update( WireInformation? wireInfo = null, ParameterValidationType? validation = null) { + // Reset the cached input variant so that ToPublicInputParameter() recalculates it + // from the updated state rather than returning a stale instance. + _inputParameter = null; + if (name is not null) { Name = name; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs index a7cd7e1a6cc..25a92c7a270 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PropertyProvider.cs @@ -297,6 +297,10 @@ public void Update( if (type != null) { Type = type; + if (_parameter.IsValueCreated && !_parameter.Value.Type.Equals(type)) + { + _parameter.Value.Update(type: type); + } } if (name != null) { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs index 2031d0e11f2..911fb12103c 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/TypeProvider.cs @@ -590,6 +590,7 @@ internal void ProcessTypeForBackCompatibility() { var hasMethods = LastContractView?.Methods != null && LastContractView.Methods.Count > 0; var hasConstructors = LastContractView?.Constructors != null && LastContractView.Constructors.Count > 0; + var hasProperties = LastContractView?.Properties != null && LastContractView.Properties.Count > 0; IEnumerable? newFields = null; if (this is EnumProvider) @@ -608,10 +609,11 @@ internal void ProcessTypeForBackCompatibility() var newMethods = hasMethods ? BuildMethodsForBackCompatibility(Methods) : null; var newConstructors = hasConstructors ? BuildConstructorsForBackCompatibility(Constructors) : null; + var newProperties = hasProperties ? BuildPropertiesForBackCompatibility(Properties) : null; - if (newFields != null || newMethods != null || newConstructors != null) + if (newFields != null || newMethods != null || newConstructors != null || newProperties != null) { - Update(fields: newFields, methods: newMethods, constructors: newConstructors); + Update(fields: newFields, methods: newMethods, constructors: newConstructors, properties: newProperties); } } @@ -624,6 +626,17 @@ protected internal virtual IReadOnlyList BuildMethodsForBackComp protected internal virtual IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors) => [.. originalConstructors]; + /// + /// Called from to apply backward-compatibility + /// adjustments to the set of properties produced for this type. Runs after all visitors so + /// adjustments reflect the final state of the library. Overrides can replace, reorder, or + /// otherwise rewrite properties based on the . + /// + /// The properties as produced from the current input spec. + /// The possibly-adjusted list of properties. + protected internal virtual IReadOnlyList BuildPropertiesForBackCompatibility(IEnumerable originalProperties) + => [.. originalProperties]; + private IReadOnlyList? _enumValues; private bool ShouldGenerate(ConstructorProvider constructor) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs index 7d6bcd7b992..a33a1a88de3 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/ModelProviderTests.cs @@ -981,6 +981,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var itemsProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Items"); Assert.IsNotNull(itemsProperty); @@ -1016,6 +1017,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var elementModelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "ElementModel") as ModelProvider; @@ -1050,6 +1052,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var elementEnumProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "ElementEnum") as EnumProvider; @@ -1079,6 +1082,7 @@ await MockHelpers.LoadMockGeneratorAsync( var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); var itemsProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Items"); Assert.IsNotNull(itemsProperty); @@ -1089,6 +1093,136 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsTrue(moreItemsProperty!.Type.Equals(typeof(IDictionary))); } + [Test] + public async Task BackCompat_NullableScalarPropertyTypeIsRetained() + { + // Regression: when a scalar property was previously generated as nullable + // but the current spec marks it as non-nullable, the previous nullable type + // should be preserved to avoid a source-breaking change. + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); + + var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); + Assert.IsNotNull(countProperty); + // The current spec says non-nullable int, but the last contract had int? – the + // generator should preserve the nullable type for backwards compatibility. + Assert.IsTrue(countProperty!.Type.Equals(new CSharpType(typeof(int), isNullable: true))); + } + + [Test] + public async Task BackCompat_ConstructorParameterTypesMatchOverriddenProperty() + { + // Regression: constructor parameters are built from PropertyProvider.AsParameter, which + // lazily materializes a ParameterProvider capturing property.Type on first access. Visitors + // that inspect constructors/methods before ProcessTypeForBackCompatibility runs can + // materialize AsParameter with the pre-override type, so when back-compat later rewrites + // property.Type, the cached ctor/method signatures would go out of sync. Verify that the + // back-compat pass cascades the type override onto the shared ParameterProvider. + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + + // Simulate a visitor materializing the constructor (and therefore the property's + // AsParameter) before the back-compat pass runs. + var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); + Assert.IsNotNull(countProperty); + _ = countProperty!.AsParameter; + + modelProvider.ProcessTypeForBackCompatibility(); + + // Property type was overridden from int to int? to match the last contract. + var expectedType = new CSharpType(typeof(int), isNullable: true); + Assert.IsTrue(countProperty.Type.Equals(expectedType)); + // The shared ParameterProvider (used by any ctor/method signature built from this + // property) must reflect the overridden type too. + Assert.IsTrue(countProperty.AsParameter.Type.Equals(expectedType)); + Assert.IsTrue(countProperty.AsParameter.ToPublicInputParameter().Type.Equals(expectedType.InputType)); + } + + [Test] + public async Task BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers() + { + // When the property type differs between the last contract and the current spec + // (including a top-level type name change like string vs int), the generator + // preserves the last contract's type to avoid a source-breaking change. Users + // can override this behavior with custom code if needed. + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); + + var countProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Count"); + Assert.IsNotNull(countProperty); + // Last contract has `string Count { get; set; }` and the new spec says int – the + // generator preserves the last contract's type for backwards compatibility. + Assert.IsTrue(countProperty!.Type.Equals(typeof(string))); + } + + [Test] + public async Task BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers() + { + // A scalar (non-collection) enum property whose nullability changed between the + // last contract and the current spec should retain the last contract's nullability. + var statusEnum = InputFactory.StringEnum( + "StatusEnum", + [("Active", "Active"), ("Inactive", "Inactive")], + isExtensible: true); + var inputModel = InputFactory.Model( + "MockInputModel", + properties: + [ + InputFactory.Property("status", statusEnum, isRequired: true), + ]); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + inputEnumTypes: [statusEnum], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider; + Assert.IsNotNull(modelProvider); + modelProvider!.ProcessTypeForBackCompatibility(); + + var statusProperty = modelProvider!.Properties.FirstOrDefault(p => p.Name == "Status"); + Assert.IsNotNull(statusProperty); + // The last contract had StatusEnum? but the spec marks it required/non-nullable – + // the generator should preserve the nullable type to avoid a breaking change. + Assert.IsTrue(statusProperty!.Type.IsNullable); + Assert.AreEqual("StatusEnum", statusProperty.Type.Name); + } + [Test] public async Task BackCompat_NonAbstractTypeIsRespected() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterTypesMatchOverriddenProperty/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterTypesMatchOverriddenProperty/MockInputModel.cs new file mode 100644 index 00000000000..1507b180d81 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorParameterTypesMatchOverriddenProperty/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public int? Count { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers/MockInputModel.cs new file mode 100644 index 00000000000..e9677060506 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_EnumPropertyTypeIsRetainedWhenNullabilityDiffers/MockInputModel.cs @@ -0,0 +1,11 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public StatusEnum? Status { get; set; } + } + + public partial struct StatusEnum + { + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_NullableScalarPropertyTypeIsRetained/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_NullableScalarPropertyTypeIsRetained/MockInputModel.cs new file mode 100644 index 00000000000..1507b180d81 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_NullableScalarPropertyTypeIsRetained/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public int? Count { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers/MockInputModel.cs new file mode 100644 index 00000000000..c37eb2dd8db --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ScalarPropertyTypeOverriddenWhenTypeNameDiffers/MockInputModel.cs @@ -0,0 +1,7 @@ +namespace Sample.Models +{ + public partial class MockInputModel + { + public string Count { get; set; } + } +} diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 3dda7eceeed..e39281c3e78 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -216,11 +216,11 @@ public static PublicModel1 PublicModel1( ### Model Properties -The generator attempts to maintain backward compatibility for model property types, particularly for collection types. +The generator preserves the previous property type whenever it differs from the type produced by the current spec. This applies to all public model properties (scalars, enums, models, and collections), so any property type change is non-source-breaking by default. Users who want the new spec's type to take effect can override this behavior with custom code. #### Scenario: Collection Property Type Changed -**Description:** When a property type changes from a read-only collection to a read-write collection (or vice versa), the generator attempts to preserve the previous property type to avoid breaking changes. +**Description:** When a property type changes from a read-only collection to a read-write collection (or vice versa), the generator preserves the previous property type to avoid breaking changes. **Example:** @@ -242,11 +242,31 @@ public IList Items { get; set; } public IReadOnlyList Items { get; } ``` -**Implementation Details:** +#### Scenario: Scalar/Model Property Type Changed -- The generator compares property types against the `LastContractView` -- For read-write lists and dictionaries, if the previous type was different, the previous type is retained -- A diagnostic message is logged: `"Changed property {ModelName}.{PropertyName} type to {LastContractType} to match last contract."` +**Description:** When the type of a scalar, enum, or model property differs between the last contract and the current spec — whether the change is in nullability, the underlying type, or anything else — the generator preserves the last contract's type. + +**Example:** + +Previous version: + +```csharp +public int? Count { get; set; } +``` + +Current TypeSpec would generate: + +```csharp +public int Count { get; set; } +``` + +**Result:** The generator detects the type mismatch and preserves the previous nullable type: + +```csharp +public int? Count { get; set; } +``` + +A diagnostic message is logged for every overridden property: `"Changed property {ModelName}.{PropertyName} type to {LastContractType} to match last contract."` ### AdditionalProperties Type Preservation