From 7aeb29a893c334796fa1cb6842a1244f9fdad7c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:17:18 +0000 Subject: [PATCH 1/6] Initial plan From d18cd9a57008dd04b738975de17ddd91ccaae8cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:28:33 +0000 Subject: [PATCH 2/6] Add backward compatibility support for missing public constructors Co-authored-by: JonathanCrd <17486462+JonathanCrd@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 136 ++++++++++++++++++ .../src/Providers/TypeProvider.cs | 13 +- .../ModelProviders/ModelProviderTests.cs | 68 +++++++++ .../BaseModel.cs | 13 ++ .../SearchIndexerDataIdentity.cs | 12 ++ 5 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicConstructorWithParameter/BaseModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicParameterlessConstructor/SearchIndexerDataIdentity.cs 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 37f85d828bf..46bad686218 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 @@ -642,6 +642,142 @@ private ConstructorProvider BuildFullConstructor() this); } + protected internal override IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors) + { + if (LastContractView?.Constructors == null || LastContractView.Constructors.Count == 0) + { + return [.. originalConstructors]; + } + + List constructors = [.. originalConstructors]; + HashSet currentConstructorSignatures = new List([.. originalConstructors, .. CustomCodeView?.Constructors ?? []]) + .Select(c => c.Signature) + .ToHashSet(ConstructorSignatureComparer.Instance); + + foreach (var previousConstructor in LastContractView.Constructors) + { + // Skip if the previous constructor already exists in the current version + if (currentConstructorSignatures.Contains(previousConstructor.Signature)) + { + continue; + } + + // Check if this is a public constructor that's missing in the current version + if (previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + { + // Try to create a backward-compatible constructor that delegates to an existing constructor + if (TryBuildBackwardCompatibleConstructor(previousConstructor, constructors, out var backCompatConstructor)) + { + constructors.Add(backCompatConstructor); + } + } + } + + return [.. constructors]; + } + + private bool TryBuildBackwardCompatibleConstructor( + ConstructorProvider previousConstructor, + List currentConstructors, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out ConstructorProvider? backCompatConstructor) + { + backCompatConstructor = null; + + // Find a constructor in the current version that we can delegate to + ConstructorProvider? targetConstructor = null; + foreach (var currentConstructor in currentConstructors) + { + // Look for a constructor with at least as many parameters as the previous one + if (currentConstructor.Signature.Parameters.Count >= previousConstructor.Signature.Parameters.Count) + { + targetConstructor = currentConstructor; + break; + } + } + + if (targetConstructor == null) + { + return false; + } + + // Build the backward-compatible constructor signature + var backCompatSignature = new ConstructorSignature( + Type, + $"Initializes a new instance of {Type:C}", + MethodSignatureModifiers.Public, + previousConstructor.Signature.Parameters); + + // Build the body that delegates to the target constructor + var delegationArguments = new List(); + var targetParams = targetConstructor.Signature.Parameters; + var previousParams = previousConstructor.Signature.Parameters.ToDictionary(p => p.Name); + + foreach (var targetParam in targetParams) + { + if (previousParams.TryGetValue(targetParam.Name, out var previousParam)) + { + // Use the parameter from the previous constructor (implicitly converted to VariableExpression) + delegationArguments.Add(previousParam); + } + else + { + // Use default value for the new parameter + delegationArguments.Add(Literal(null)); + } + } + + // Create the initializer that calls the target constructor + var initializer = new ConstructorInitializer(true, delegationArguments); + + backCompatConstructor = new ConstructorProvider( + signature: backCompatSignature, + bodyStatements: new MethodBodyStatement[] { MethodBodyStatement.Empty }, + this); + + // Update the signature to include the initializer + backCompatConstructor.Signature.Update(initializer: initializer); + + return true; + } + + private class ConstructorSignatureComparer : IEqualityComparer + { + public static readonly ConstructorSignatureComparer Instance = new(); + + public bool Equals(ConstructorSignature? x, ConstructorSignature? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null || y is null) + return false; + + // Compare modifiers + if (x.Modifiers != y.Modifiers) + return false; + + // Compare parameter count + if (x.Parameters.Count != y.Parameters.Count) + return false; + + // Compare each parameter + for (int i = 0; i < x.Parameters.Count; i++) + { + var xParam = x.Parameters[i]; + var yParam = y.Parameters[i]; + + if (!xParam.Type.AreNamesEqual(yParam.Type) || xParam.Name != yParam.Name) + return false; + } + + return true; + } + + public int GetHashCode(ConstructorSignature obj) + { + return HashCode.Combine(obj.Modifiers, obj.Parameters.Count); + } + } + private IEnumerable GetAllBasePropertiesForConstructorInitialization(bool includeAllHierarchyDiscriminator = false) { var properties = new Stack>(); 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 fdafe9ca32c..c5ed0328e12 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 @@ -565,17 +565,26 @@ internal void EnsureBuilt() internal void ProcessTypeForBackCompatibility() { - if (LastContractView?.Methods == null || LastContractView?.Methods.Count == 0) + var hasMethods = LastContractView?.Methods != null && LastContractView.Methods.Count > 0; + var hasConstructors = LastContractView?.Constructors != null && LastContractView.Constructors.Count > 0; + + if (!hasMethods && !hasConstructors) { return; } - Update(methods: BuildMethodsForBackCompatibility(Methods)); + var newMethods = hasMethods ? BuildMethodsForBackCompatibility(Methods) : null; + var newConstructors = hasConstructors ? BuildConstructorsForBackCompatibility(Constructors) : null; + + Update(methods: newMethods, constructors: newConstructors); } protected internal virtual IReadOnlyList BuildMethodsForBackCompatibility(IEnumerable originalMethods) => [.. originalMethods]; + protected internal virtual IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors) + => [.. originalConstructors]; + 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 defefe73703..a81a5943893 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 @@ -1760,5 +1760,73 @@ protected override TypeProvider[] BuildSerializationProviders() return [new Mock() { CallBase = true }.Object]; } } + + [Test] + public async Task BackCompat_MissingPublicParameterlessConstructor() + { + var inputModel = InputFactory.Model( + "SearchIndexerDataIdentity", + properties: + [ + InputFactory.Property("oDataType", InputPrimitiveType.String, isRequired: true) + ], + usage: InputModelTypeUsage.Input | InputModelTypeUsage.Output); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "SearchIndexerDataIdentity") as ModelProvider; + + Assert.IsNotNull(modelProvider); + + // Call ProcessTypeForBackCompatibility to apply backward compatibility logic + modelProvider!.ProcessTypeForBackCompatibility(); + + // Check that a public parameterless constructor is present + var publicParameterlessConstructor = modelProvider.Constructors + .FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && c.Signature.Parameters.Count == 0); + Assert.IsNotNull(publicParameterlessConstructor, "Missing public parameterless constructor from backward compatibility"); + } + + [Test] + public async Task BackCompat_MissingPublicConstructorWithParameter() + { + var discriminatorEnum = InputFactory.StringEnum("kindEnum", [("One", "one"), ("Two", "two")]); + var derivedInputModel = InputFactory.Model( + "DerivedModel", + discriminatedKind: "one", + properties: + [ + InputFactory.Property("kind", InputFactory.EnumMember.String("One", "one", discriminatorEnum), isRequired: true, isDiscriminator: true), + InputFactory.Property("derivedProp", InputPrimitiveType.Int32, isRequired: true) + ]); + var inputModel = InputFactory.Model( + "BaseModel", + properties: + [ + InputFactory.Property("kind", discriminatorEnum, isRequired: false, isDiscriminator: true), + InputFactory.Property("baseProp", InputPrimitiveType.String, isRequired: true), + InputFactory.Property("newProp", InputPrimitiveType.Int32, isRequired: true) // New property added + ], + discriminatedModels: new Dictionary() { { "one", derivedInputModel }}); + + await MockHelpers.LoadMockGeneratorAsync( + inputModelTypes: [inputModel], + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); + + var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "BaseModel") as ModelProvider; + + Assert.IsNotNull(modelProvider); + + // Call ProcessTypeForBackCompatibility to apply backward compatibility logic + modelProvider!.ProcessTypeForBackCompatibility(); + + // Check that a public constructor with one parameter (baseProp only) is present for backward compatibility + var publicConstructorWithOneParam = modelProvider.Constructors + .FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && c.Signature.Parameters.Count == 1); + Assert.IsNotNull(publicConstructorWithOneParam, "Missing public constructor with one parameter from backward compatibility"); + Assert.AreEqual("baseProp", publicConstructorWithOneParam!.Signature.Parameters[0].Name); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicConstructorWithParameter/BaseModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicConstructorWithParameter/BaseModel.cs new file mode 100644 index 00000000000..2b7d1d63862 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicConstructorWithParameter/BaseModel.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Sample.Models +{ + public abstract partial class BaseModel + { + /// Initializes a new instance of BaseModel. + public BaseModel(string baseProp) + { + BaseProp = baseProp; + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicParameterlessConstructor/SearchIndexerDataIdentity.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicParameterlessConstructor/SearchIndexerDataIdentity.cs new file mode 100644 index 00000000000..c60f137472b --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicParameterlessConstructor/SearchIndexerDataIdentity.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Sample.Models +{ + public abstract partial class SearchIndexerDataIdentity + { + /// Initializes a new instance of SearchIndexerDataIdentity. + public SearchIndexerDataIdentity() + { + } + } +} From e6c5cb957d9b5e09bcfdeb96f7d0dd2010d4af60 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 00:30:15 +0000 Subject: [PATCH 3/6] Update documentation for constructor backward compatibility Co-authored-by: JonathanCrd <17486462+JonathanCrd@users.noreply.github.com> --- .../generator/docs/backward-compatibility.md | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index b4822db0035..c061bb99758 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -9,6 +9,7 @@ - [Model Properties](#model-properties) - [API Version Enum](#api-version-enum) - [Non-abstract Base Models](#non-abstract-base-models) + - [Model Constructors](#model-constructors) ## Overview @@ -212,3 +213,56 @@ public class BaseModel public string CommonProperty { get; set; } } ``` + +### Model Constructors + +The generator maintains backward compatibility for model constructors to prevent breaking changes when new properties are added or when constructors are modified. + +#### Scenario: Missing Public Constructor + +**Description:** When a public constructor exists in the previous version but is missing in the newly generated code, the generator automatically adds the missing constructor to maintain backward compatibility. This commonly occurs with: +- Parameterless public constructors on abstract base models +- Public constructors when new required properties are added to a model + +**Example - Parameterless Constructor:** + +Previous version had a public parameterless constructor: + +```csharp +public abstract partial class SearchIndexerDataIdentity +{ + /// Initializes a new instance of SearchIndexerDataIdentity. + public SearchIndexerDataIdentity() + { + } +} +``` + +Current TypeSpec would generate only constructors with parameters: + +```csharp +public abstract partial class SearchIndexerDataIdentity +{ + /// Initializes a new instance of SearchIndexerDataIdentity. + /// A URI fragment specifying the type of identity. + private protected SearchIndexerDataIdentity(string odataType) + { + OdataType = odataType; + } +} +``` + +**Generated Compatibility Constructor:** + +```csharp +/// Initializes a new instance of SearchIndexerDataIdentity. +public SearchIndexerDataIdentity() : this(odataType: default) +{ +} +``` + +**Key Points:** + +- The missing public constructor is automatically generated +- It delegates to an existing constructor, providing default values for any new parameters +- This prevents breaking changes for code that uses the parameterless constructor From 5c51967a7d2d6aa1c8b6b3a4b2d7665a5b73fa41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:19:24 +0000 Subject: [PATCH 4/6] Simplify backward compatibility to only change constructor modifiers Instead of generating additional constructors, only change the modifier from private protected to public on abstract base types when the last contract had a public constructor with matching parameters. This is a simpler and more targeted approach. Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 124 ++++-------------- .../ModelProviders/ModelProviderTests.cs | 55 +++----- .../BaseModel.cs | 0 .../SearchIndexerDataIdentity.cs | 0 .../generator/docs/backward-compatibility.md | 36 +++-- 5 files changed, 65 insertions(+), 150 deletions(-) rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/{BackCompat_MissingPublicConstructorWithParameter => BackCompat_AbstractTypeConstructorAccessibility}/BaseModel.cs (100%) rename packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/{BackCompat_MissingPublicParameterlessConstructor => BackCompat_AbstractTypeConstructorAccessibility_Old}/SearchIndexerDataIdentity.cs (100%) 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 46bad686218..2f1fa32e1c5 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 @@ -644,31 +644,41 @@ private ConstructorProvider BuildFullConstructor() protected internal override IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors) { + // Only handle the case of changing modifiers on abstract base types + if (!DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract)) + { + return [.. originalConstructors]; + } + if (LastContractView?.Constructors == null || LastContractView.Constructors.Count == 0) { return [.. originalConstructors]; } List constructors = [.. originalConstructors]; - HashSet currentConstructorSignatures = new List([.. originalConstructors, .. CustomCodeView?.Constructors ?? []]) - .Select(c => c.Signature) - .ToHashSet(ConstructorSignatureComparer.Instance); + // Check if the last contract had a public constructor with matching parameters foreach (var previousConstructor in LastContractView.Constructors) { - // Skip if the previous constructor already exists in the current version - if (currentConstructorSignatures.Contains(previousConstructor.Signature)) + if (!previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) { continue; } - // Check if this is a public constructor that's missing in the current version - if (previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)) + // Find a matching constructor in the current version by parameter signature + for (int i = 0; i < constructors.Count; i++) { - // Try to create a backward-compatible constructor that delegates to an existing constructor - if (TryBuildBackwardCompatibleConstructor(previousConstructor, constructors, out var backCompatConstructor)) + var currentConstructor = constructors[i]; + + // Check if parameters match (same count and types) + if (ParametersMatch(currentConstructor.Signature.Parameters, previousConstructor.Signature.Parameters)) { - constructors.Add(backCompatConstructor); + // Change the modifier from private protected to public + if (currentConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Private) && + currentConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Protected)) + { + currentConstructor.Signature.Update(modifiers: MethodSignatureModifiers.Public); + } } } } @@ -676,106 +686,22 @@ protected internal override IReadOnlyList BuildConstructors return [.. constructors]; } - private bool TryBuildBackwardCompatibleConstructor( - ConstructorProvider previousConstructor, - List currentConstructors, - [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out ConstructorProvider? backCompatConstructor) + private bool ParametersMatch(IReadOnlyList params1, IReadOnlyList params2) { - backCompatConstructor = null; - - // Find a constructor in the current version that we can delegate to - ConstructorProvider? targetConstructor = null; - foreach (var currentConstructor in currentConstructors) - { - // Look for a constructor with at least as many parameters as the previous one - if (currentConstructor.Signature.Parameters.Count >= previousConstructor.Signature.Parameters.Count) - { - targetConstructor = currentConstructor; - break; - } - } - - if (targetConstructor == null) + if (params1.Count != params2.Count) { return false; } - // Build the backward-compatible constructor signature - var backCompatSignature = new ConstructorSignature( - Type, - $"Initializes a new instance of {Type:C}", - MethodSignatureModifiers.Public, - previousConstructor.Signature.Parameters); - - // Build the body that delegates to the target constructor - var delegationArguments = new List(); - var targetParams = targetConstructor.Signature.Parameters; - var previousParams = previousConstructor.Signature.Parameters.ToDictionary(p => p.Name); - - foreach (var targetParam in targetParams) + for (int i = 0; i < params1.Count; i++) { - if (previousParams.TryGetValue(targetParam.Name, out var previousParam)) - { - // Use the parameter from the previous constructor (implicitly converted to VariableExpression) - delegationArguments.Add(previousParam); - } - else + if (!params1[i].Type.AreNamesEqual(params2[i].Type) || params1[i].Name != params2[i].Name) { - // Use default value for the new parameter - delegationArguments.Add(Literal(null)); - } - } - - // Create the initializer that calls the target constructor - var initializer = new ConstructorInitializer(true, delegationArguments); - - backCompatConstructor = new ConstructorProvider( - signature: backCompatSignature, - bodyStatements: new MethodBodyStatement[] { MethodBodyStatement.Empty }, - this); - - // Update the signature to include the initializer - backCompatConstructor.Signature.Update(initializer: initializer); - - return true; - } - - private class ConstructorSignatureComparer : IEqualityComparer - { - public static readonly ConstructorSignatureComparer Instance = new(); - - public bool Equals(ConstructorSignature? x, ConstructorSignature? y) - { - if (ReferenceEquals(x, y)) - return true; - if (x is null || y is null) return false; - - // Compare modifiers - if (x.Modifiers != y.Modifiers) - return false; - - // Compare parameter count - if (x.Parameters.Count != y.Parameters.Count) - return false; - - // Compare each parameter - for (int i = 0; i < x.Parameters.Count; i++) - { - var xParam = x.Parameters[i]; - var yParam = y.Parameters[i]; - - if (!xParam.Type.AreNamesEqual(yParam.Type) || xParam.Name != yParam.Name) - return false; } - - return true; } - public int GetHashCode(ConstructorSignature obj) - { - return HashCode.Combine(obj.Modifiers, obj.Parameters.Count); - } + return true; } private IEnumerable GetAllBasePropertiesForConstructorInitialization(bool includeAllHierarchyDiscriminator = false) 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 a81a5943893..4ae6598c305 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 @@ -1762,35 +1762,7 @@ protected override TypeProvider[] BuildSerializationProviders() } [Test] - public async Task BackCompat_MissingPublicParameterlessConstructor() - { - var inputModel = InputFactory.Model( - "SearchIndexerDataIdentity", - properties: - [ - InputFactory.Property("oDataType", InputPrimitiveType.String, isRequired: true) - ], - usage: InputModelTypeUsage.Input | InputModelTypeUsage.Output); - - await MockHelpers.LoadMockGeneratorAsync( - inputModelTypes: [inputModel], - lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync()); - - var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "SearchIndexerDataIdentity") as ModelProvider; - - Assert.IsNotNull(modelProvider); - - // Call ProcessTypeForBackCompatibility to apply backward compatibility logic - modelProvider!.ProcessTypeForBackCompatibility(); - - // Check that a public parameterless constructor is present - var publicParameterlessConstructor = modelProvider.Constructors - .FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && c.Signature.Parameters.Count == 0); - Assert.IsNotNull(publicParameterlessConstructor, "Missing public parameterless constructor from backward compatibility"); - } - - [Test] - public async Task BackCompat_MissingPublicConstructorWithParameter() + public async Task BackCompat_AbstractTypeConstructorAccessibility() { var discriminatorEnum = InputFactory.StringEnum("kindEnum", [("One", "one"), ("Two", "two")]); var derivedInputModel = InputFactory.Model( @@ -1806,8 +1778,7 @@ public async Task BackCompat_MissingPublicConstructorWithParameter() properties: [ InputFactory.Property("kind", discriminatorEnum, isRequired: false, isDiscriminator: true), - InputFactory.Property("baseProp", InputPrimitiveType.String, isRequired: true), - InputFactory.Property("newProp", InputPrimitiveType.Int32, isRequired: true) // New property added + InputFactory.Property("baseProp", InputPrimitiveType.String, isRequired: true) ], discriminatedModels: new Dictionary() { { "one", derivedInputModel }}); @@ -1819,14 +1790,22 @@ await MockHelpers.LoadMockGeneratorAsync( Assert.IsNotNull(modelProvider); - // Call ProcessTypeForBackCompatibility to apply backward compatibility logic - modelProvider!.ProcessTypeForBackCompatibility(); + // Without ProcessTypeForBackCompatibility, constructor should be private protected + var privateProtectedConstructor = modelProvider!.Constructors + .FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Private) + && c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Protected) + && c.Signature.Parameters.Count == 1); + Assert.IsNotNull(privateProtectedConstructor, "Expected a private protected constructor before back compat processing"); - // Check that a public constructor with one parameter (baseProp only) is present for backward compatibility - var publicConstructorWithOneParam = modelProvider.Constructors - .FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) && c.Signature.Parameters.Count == 1); - Assert.IsNotNull(publicConstructorWithOneParam, "Missing public constructor with one parameter from backward compatibility"); - Assert.AreEqual("baseProp", publicConstructorWithOneParam!.Signature.Parameters[0].Name); + // Call ProcessTypeForBackCompatibility to apply backward compatibility logic + modelProvider.ProcessTypeForBackCompatibility(); + + // After ProcessTypeForBackCompatibility, constructor should be public to match last contract + var publicConstructor = modelProvider.Constructors + .FirstOrDefault(c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public) + && c.Signature.Parameters.Count == 1); + Assert.IsNotNull(publicConstructor, "Constructor modifier should be changed to public for backward compatibility"); + Assert.AreEqual("baseProp", publicConstructor!.Signature.Parameters[0].Name); } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicConstructorWithParameter/BaseModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility/BaseModel.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicConstructorWithParameter/BaseModel.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility/BaseModel.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicParameterlessConstructor/SearchIndexerDataIdentity.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility_Old/SearchIndexerDataIdentity.cs similarity index 100% rename from packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_MissingPublicParameterlessConstructor/SearchIndexerDataIdentity.cs rename to packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility_Old/SearchIndexerDataIdentity.cs diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index c061bb99758..827552453de 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -216,15 +216,17 @@ public class BaseModel ### Model Constructors -The generator maintains backward compatibility for model constructors to prevent breaking changes when new properties are added or when constructors are modified. +The generator maintains backward compatibility for model constructors on abstract base types to prevent breaking changes when constructor accessibility changes. -#### Scenario: Missing Public Constructor +#### Scenario: Public Constructor on Abstract Base Type -**Description:** When a public constructor exists in the previous version but is missing in the newly generated code, the generator automatically adds the missing constructor to maintain backward compatibility. This commonly occurs with: -- Parameterless public constructors on abstract base models -- Public constructors when new required properties are added to a model +**Description:** When an abstract base type had a public constructor in the previous version, but the current TypeSpec generation would create a `private protected` constructor, the generator automatically changes the modifier to `public` to maintain backward compatibility. -**Example - Parameterless Constructor:** +This commonly occurs when: +- Migrating from autorest-generated code to TypeSpec-generated code +- Abstract base types with discriminators had public parameterless constructors in previous versions + +**Example:** Previous version had a public parameterless constructor: @@ -238,7 +240,7 @@ public abstract partial class SearchIndexerDataIdentity } ``` -Current TypeSpec would generate only constructors with parameters: +Current TypeSpec would generate a private protected constructor: ```csharp public abstract partial class SearchIndexerDataIdentity @@ -252,17 +254,25 @@ public abstract partial class SearchIndexerDataIdentity } ``` -**Generated Compatibility Constructor:** +**Generated Compatibility Result:** + +When a matching public constructor exists in the last contract, the modifier is changed from `private protected` to `public`: ```csharp -/// Initializes a new instance of SearchIndexerDataIdentity. -public SearchIndexerDataIdentity() : this(odataType: default) +public abstract partial class SearchIndexerDataIdentity { + /// Initializes a new instance of SearchIndexerDataIdentity. + /// A URI fragment specifying the type of identity. + public SearchIndexerDataIdentity(string odataType) + { + OdataType = odataType; + } } ``` **Key Points:** -- The missing public constructor is automatically generated -- It delegates to an existing constructor, providing default values for any new parameters -- This prevents breaking changes for code that uses the parameterless constructor +- Only applies to abstract base types +- The constructor must have matching parameters (same count, types, and names) +- The modifier is changed from `private protected` to `public` +- No additional constructors are generated; only the accessibility is adjusted From 7deafa058d378868b3e797092e43837decb4a9a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:20:08 +0000 Subject: [PATCH 5/6] Remove unused test data directory Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../SearchIndexerDataIdentity.cs | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility_Old/SearchIndexerDataIdentity.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility_Old/SearchIndexerDataIdentity.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility_Old/SearchIndexerDataIdentity.cs deleted file mode 100644 index c60f137472b..00000000000 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_AbstractTypeConstructorAccessibility_Old/SearchIndexerDataIdentity.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace Sample.Models -{ - public abstract partial class SearchIndexerDataIdentity - { - /// Initializes a new instance of SearchIndexerDataIdentity. - public SearchIndexerDataIdentity() - { - } - } -} From 7ec1c345fab6a44d798c9202cbd680957494b026 Mon Sep 17 00:00:00 2001 From: jolov Date: Wed, 28 Jan 2026 09:43:36 -0800 Subject: [PATCH 6/6] format --- .../http-client-csharp/generator/docs/backward-compatibility.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/http-client-csharp/generator/docs/backward-compatibility.md b/packages/http-client-csharp/generator/docs/backward-compatibility.md index 827552453de..2b44139c827 100644 --- a/packages/http-client-csharp/generator/docs/backward-compatibility.md +++ b/packages/http-client-csharp/generator/docs/backward-compatibility.md @@ -223,6 +223,7 @@ The generator maintains backward compatibility for model constructors on abstrac **Description:** When an abstract base type had a public constructor in the previous version, but the current TypeSpec generation would create a `private protected` constructor, the generator automatically changes the modifier to `public` to maintain backward compatibility. This commonly occurs when: + - Migrating from autorest-generated code to TypeSpec-generated code - Abstract base types with discriminators had public parameterless constructors in previous versions