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 38f74bddf37..7f3596fb040 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
@@ -48,5 +48,11 @@ public enum BackCompatibilityChangeCategory
/// A fixed enum member was re-added to preserve a member that existed in the last contract but is no longer produced by the current spec.
EnumMemberAddedFromLastContract,
+
+ /// A back-compat model constructor was re-added to preserve a public constructor that existed in the last contract but is no longer produced by the current spec.
+ ConstructorAddedFromLastContract,
+
+ /// A back-compat model constructor could not be reconstructed from the last contract and was skipped.
+ ConstructorAddedFromLastContractSkipped,
}
}
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 c3fc8efec62..1bf8d934f9f 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;
@@ -774,6 +775,221 @@ protected internal override ConstructorProvider[] BuildConstructors()
return [.. constructors];
}
+ ///
+ /// Restores previously-published public constructors that the current generation would otherwise
+ /// drop. The primary scenario is a previously required property becoming optional: the corresponding
+ /// parameter is removed from the initialization constructor, which is a source-breaking change for
+ /// callers that construct the model positionally. When the previous public constructor can be safely
+ /// reconstructed - i.e. every one of its extra parameters still maps to a settable property whose
+ /// name and type are unchanged (or a property renamed via a codegen customization but keeping the
+ /// same type) - a back-compat overload is added that chains to the current public constructor and
+ /// assigns the extra properties.
+ ///
+ protected internal override IReadOnlyList BuildConstructorsForBackCompatibility(IEnumerable originalConstructors)
+ {
+ if (LastContractView?.Constructors is not { Count: > 0 } previousConstructors)
+ {
+ return base.BuildConstructorsForBackCompatibility(originalConstructors);
+ }
+
+ var constructors = new List(base.BuildConstructorsForBackCompatibility(originalConstructors));
+ var restorablePropertyLookup = BuildRestorablePropertyLookup();
+
+ foreach (var previousConstructor in previousConstructors)
+ {
+ if (!IsPublicApi(previousConstructor.Signature.Modifiers))
+ {
+ continue;
+ }
+
+ var previousParameters = previousConstructor.Signature.Parameters;
+
+ if (previousParameters.Count == 0)
+ {
+ continue;
+ }
+
+ if (BackCompatHelper.IsConstructorRemovalAcceptedInBaseline(this, previousConstructor.Signature))
+ {
+ continue;
+ }
+
+ // If a constructor with the same parameters already exists - either still generated or
+ // supplied by custom code (which lives in the canonical view) - there is nothing to restore.
+ if (constructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters))
+ || CanonicalView.Constructors.Any(c => BackCompatHelper.ParametersMatch(c.Signature.Parameters, previousParameters)))
+ {
+ continue;
+ }
+
+ if (TryBuildRestoredConstructor(previousConstructor, constructors, restorablePropertyLookup, out var restoredConstructor))
+ {
+ constructors.Add(restoredConstructor);
+ CodeModelGenerator.Instance.Emitter.Info(
+ $"Restored constructor '{Name}({string.Join(", ", previousParameters.Select(p => p.Type.Name))})' to match last contract.",
+ BackCompatibilityChangeCategory.ConstructorAddedFromLastContract);
+ }
+ else
+ {
+ CodeModelGenerator.Instance.Emitter.Info(
+ $"Could not restore constructor '{Name}({string.Join(", ", previousParameters.Select(p => p.Type.Name))})' from the last contract; a property name or type has changed.",
+ BackCompatibilityChangeCategory.ConstructorAddedFromLastContractSkipped);
+ }
+ }
+
+ return constructors;
+ }
+
+ private bool TryBuildRestoredConstructor(
+ ConstructorProvider previousConstructor,
+ IReadOnlyList currentConstructors,
+ Dictionary restorablePropertyLookup,
+ [NotNullWhen(true)] out ConstructorProvider? restoredConstructor)
+ {
+ restoredConstructor = null;
+ var previousParameters = previousConstructor.Signature.Parameters;
+
+ // Find the public constructor to chain to: its parameters must form an in-order subsequence of
+ // the previous constructor's parameters. Prefer the closest one.
+ ConstructorProvider? targetConstructor = null;
+ foreach (var candidate in currentConstructors)
+ {
+ if (!IsPublicApi(candidate.Signature.Modifiers)
+ || candidate.Signature.Parameters.Count >= previousParameters.Count)
+ {
+ continue;
+ }
+
+ // Check whether this candidate would improve on the current target before performing the
+ // more expensive subsequence lookup.
+ if ((targetConstructor == null
+ || candidate.Signature.Parameters.Count > targetConstructor.Signature.Parameters.Count)
+ && IsParameterSubsequence(candidate.Signature.Parameters, previousParameters))
+ {
+ targetConstructor = candidate;
+ }
+ }
+
+ if (targetConstructor == null)
+ {
+ return false;
+ }
+
+ var targetParameters = targetConstructor.Signature.Parameters;
+ var restoredParameters = new List(previousParameters.Count);
+ var initializerArguments = new List(targetParameters.Count);
+ var extraAssignments = new List<(PropertyProvider Property, ParameterProvider Parameter)>();
+ int targetIndex = 0;
+
+ foreach (var previousParameter in previousParameters)
+ {
+ if (targetIndex < targetParameters.Count
+ && targetParameters[targetIndex].Equals(previousParameter))
+ {
+ var keptParameter = PartialMethodCustomization.CloneParameterWithName(
+ targetParameters[targetIndex],
+ previousParameter.Name,
+ removeDefault: false,
+ validation: ParameterValidationType.None);
+ restoredParameters.Add(keptParameter);
+ initializerArguments.Add(keptParameter);
+ targetIndex++;
+ continue;
+ }
+
+ if (!restorablePropertyLookup.TryGetValue(previousParameter.Name, out var property)
+ || !property.Type.AreNamesEqual(previousParameter.Type))
+ {
+ return false;
+ }
+
+ var restoredParameter = PartialMethodCustomization.CloneParameterWithName(
+ property.AsParameter,
+ previousParameter.Name,
+ removeDefault: true);
+
+ restoredParameters.Add(restoredParameter);
+ extraAssignments.Add((property, restoredParameter));
+ }
+
+ // Every target parameter must be consumed and at least one extra property must be assigned,
+ // otherwise the restored constructor would be redundant or would produce an invalid chained call.
+ if (targetIndex != targetParameters.Count || extraAssignments.Count == 0)
+ {
+ return false;
+ }
+
+ var bodyStatements = new List(extraAssignments.Count);
+ foreach (var (property, parameter) in extraAssignments)
+ {
+ ValueExpression assignee = property.BackingField is null ? property : property.BackingField;
+ ValueExpression value = parameter;
+ if (CSharpType.RequiresToList(parameter.Type, property.Type))
+ {
+ value = parameter.Type.IsNullable ? value.NullConditional().ToList() : value.ToList();
+ }
+
+ bodyStatements.Add(assignee.Assign(value).Terminate());
+ }
+
+ var signature = new ConstructorSignature(
+ Type,
+ $"Initializes a new instance of {Type:C}",
+ previousConstructor.Signature.Modifiers,
+ restoredParameters,
+ initializer: new ConstructorInitializer(false, initializerArguments));
+
+ restoredConstructor = new ConstructorProvider(signature, bodyStatements, this);
+ return true;
+ }
+
+ private static bool IsParameterSubsequence(
+ IReadOnlyList subset,
+ IReadOnlyList full)
+ {
+ if (subset.Count > full.Count)
+ {
+ return false;
+ }
+
+ int matched = 0;
+ foreach (var parameter in full)
+ {
+ if (matched == subset.Count)
+ {
+ break;
+ }
+
+ if (subset[matched].Equals(parameter))
+ {
+ matched++;
+ }
+ }
+
+ return matched == subset.Count;
+ }
+
+ private Dictionary BuildRestorablePropertyLookup()
+ {
+ var lookup = new Dictionary();
+ foreach (var property in CanonicalView.Properties)
+ {
+ if (!IsPublicApi(property.Modifiers) || !property.Body.HasSetter || property.WireInfo == null)
+ {
+ continue;
+ }
+
+ lookup[property.AsParameter.Name] = property;
+
+ if (property.OriginalName != null)
+ {
+ lookup.TryAdd(property.OriginalName.ToVariableName(), property);
+ }
+ }
+
+ return lookup;
+ }
+
///
/// Determines if this model should have a dual constructor pattern.
/// This is needed when the model shares the same discriminator property name as its base model
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs
index 9d1d98a9785..5dbcb7f0aa1 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Providers/PartialMethodCustomization.cs
@@ -213,12 +213,15 @@ public static MethodSignature BuildPartialSignature(
// Clones a ParameterProvider with a new name (and optionally without its default value)
// while preserving all generator metadata. Returns the source unchanged when no change is
// needed.
- private static ParameterProvider CloneParameterWithName(
+ internal static ParameterProvider CloneParameterWithName(
ParameterProvider source,
string newName,
- bool removeDefault)
+ bool removeDefault,
+ ParameterValidationType? validation = null)
{
- if (source.Name == newName && !(removeDefault && source.DefaultValue != null))
+ if (source.Name == newName
+ && !(removeDefault && source.DefaultValue != null)
+ && (validation == null || validation == source.Validation))
{
return source;
}
@@ -238,7 +241,7 @@ private static ParameterProvider CloneParameterWithName(
initializationValue: source.InitializationValue,
location: source.Location,
wireInfo: source.WireInfo,
- validation: source.Validation,
+ validation: validation ?? source.Validation,
inputParameter: source.InputParameter)
{
SpreadSource = source.SpreadSource,
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs
index f0717c007ff..4497c86e746 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/src/Utilities/BackCompatHelper.cs
@@ -66,6 +66,35 @@ public static bool IsMethodRemovalAcceptedInBaseline(TypeProvider enclosingType,
return true;
}
+ ///
+ /// Returns true when the removal of a previously-published constructor — identified by the
+ /// enclosing type's fully-qualified name and the exact parameter types — has been accepted in the
+ /// ApiCompat baseline, in which case back compatibility must not restore it. Constructors are
+ /// recorded in the baseline as the .ctor member of their declaring type. Emits an
+ /// informational log entry when a suppression is honored.
+ ///
+ public static bool IsConstructorRemovalAcceptedInBaseline(TypeProvider enclosingType, ConstructorSignature previousSignature)
+ {
+ var parameterTypes = new CSharpType[previousSignature.Parameters.Count];
+ for (int i = 0; i < parameterTypes.Length; i++)
+ {
+ parameterTypes[i] = previousSignature.Parameters[i].Type;
+ }
+
+ if (CodeModelGenerator.Instance.SourceInputModel?.ApiCompatBaseline.IsMethodRemovalSuppressed(
+ enclosingType.Type.FullyQualifiedName,
+ ".ctor",
+ parameterTypes) != true)
+ {
+ return false;
+ }
+
+ CodeModelGenerator.Instance.Emitter.Info(
+ $"Skipping back-compat for '{enclosingType.Type.FullyQualifiedName}..ctor'; removal is accepted in the ApiCompat baseline.",
+ BackCompatibilityChangeCategory.BaselineAcceptedRemovalSkipped);
+ return true;
+ }
+
///
/// Finds the current method that has the same parameter set as
/// (matched by name and return type) but in a different order, or null when there is none.
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 88ab6c206b8..35a0832b33e 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
@@ -2329,6 +2329,303 @@ await MockHelpers.LoadMockGeneratorAsync(
Assert.AreEqual("baseProp", publicConstructor!.Signature.Parameters[0].Name);
}
+ [Test]
+ public async Task BackCompat_RequiredToOptionalConstructorIsRestored()
+ {
+ // "resources" was required in the last contract (so the initialization constructor
+ // accepted it), but the current spec relaxes it to optional which would otherwise drop
+ // it from the constructor. The previously published constructor should be restored.
+ var inputModel = InputFactory.Model(
+ "MockInputModel",
+ usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json,
+ properties:
+ [
+ InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
+ InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false),
+ ]);
+
+ 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);
+
+ // Before back-compat processing the public constructor only takes "name".
+ var publicCtorBefore = modelProvider!.Constructors.SingleOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public));
+ Assert.IsNotNull(publicCtorBefore);
+ Assert.AreEqual(1, publicCtorBefore!.Signature.Parameters.Count);
+
+ modelProvider.ProcessTypeForBackCompatibility();
+
+ // After back-compat processing the previously published (name, resources) constructor is restored.
+ var restoredCtor = modelProvider.Constructors.SingleOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNotNull(restoredCtor, "Expected the (name, resources) constructor to be restored for back compat");
+ Assert.AreEqual("name", restoredCtor!.Signature.Parameters[0].Name);
+ Assert.AreEqual("resources", restoredCtor.Signature.Parameters[1].Name);
+ Assert.IsTrue(restoredCtor.Signature.Parameters[1].Type.Equals(typeof(string)));
+
+ // It chains to the current (name) constructor and assigns the extra property in its body.
+ var initializer = restoredCtor.Signature.Initializer;
+ Assert.IsNotNull(initializer);
+ Assert.IsFalse(initializer!.IsBase);
+ Assert.AreEqual(1, initializer.Arguments.Count);
+ Assert.AreEqual("name", initializer.Arguments[0].ToDisplayString());
+
+ var body = restoredCtor.BodyStatements!.ToDisplayString();
+ Assert.AreEqual(ParameterValidationType.None, restoredCtor.Signature.Parameters[0].Validation);
+ Assert.IsFalse(body.Contains("Argument.AssertNotNull(name"), $"Did not expect duplicated name validation in restored constructor, was: {body}");
+ Assert.IsTrue(body.Contains("Resources = resources"), $"Expected the body to assign Resources, was: {body}");
+ // "resources" is now an optional property, so the restored back-compat overload does not null-check it.
+ Assert.AreEqual(ParameterValidationType.None, restoredCtor.Signature.Parameters[1].Validation);
+ Assert.IsFalse(body.Contains("Argument.AssertNotNull(resources"), $"Did not expect null validation for the now-optional resources parameter, was: {body}");
+
+ // Validate the full generated model, including the restored constructor, against the expected output.
+ var writer = new TypeProviderWriter(modelProvider);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public async Task BackCompat_ConstructorNotRestoredWhenPropertyRemoved()
+ {
+ // "resources" existed in the last contract constructor but has been removed entirely from
+ // the current spec, so the previous constructor cannot be safely restored.
+ var inputModel = InputFactory.Model(
+ "MockInputModel",
+ usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json,
+ properties:
+ [
+ InputFactory.Property("name", InputPrimitiveType.String, 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 twoParamPublicCtor = modelProvider.Constructors.FirstOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNull(twoParamPublicCtor, "The constructor should not be restored when a property was removed");
+
+ var writer = new TypeProviderWriter(modelProvider);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public async Task BackCompat_ConstructorNotRestoredWhenLastContractMissing()
+ {
+ // No last contract exists for the model, so nothing should be restored.
+ var inputModel = InputFactory.Model(
+ "MockInputModel",
+ usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json,
+ properties:
+ [
+ InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
+ InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false),
+ ]);
+
+ 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);
+ Assert.IsNull(modelProvider!.LastContractView);
+
+ modelProvider.ProcessTypeForBackCompatibility();
+
+ var twoParamPublicCtor = modelProvider.Constructors.FirstOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNull(twoParamPublicCtor);
+
+ var writer = new TypeProviderWriter(modelProvider);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public async Task BackCompat_OptionalValueTypeConstructorParameterIsRestored()
+ {
+ // "count" was a required value type in the last contract, so the initialization constructor
+ // accepted it. Relaxing it to optional drops it; the previously published constructor is
+ // restored, and because it is a value type no null validation is emitted.
+ var inputModel = InputFactory.Model(
+ "MockInputModel",
+ usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json,
+ properties:
+ [
+ InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
+ InputFactory.Property("count", InputPrimitiveType.Int32, isRequired: false),
+ ]);
+
+ 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 restoredCtor = modelProvider.Constructors.SingleOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNotNull(restoredCtor, "Expected the (name, count) constructor to be restored for back compat");
+ Assert.AreEqual("count", restoredCtor!.Signature.Parameters[1].Name);
+ // The restored parameter keeps the previously published non-nullable value type.
+ Assert.IsTrue(restoredCtor.Signature.Parameters[1].Type.Equals(typeof(int)));
+ Assert.AreEqual(ParameterValidationType.None, restoredCtor.Signature.Parameters[1].Validation);
+
+ var body = restoredCtor.BodyStatements!.ToDisplayString();
+ Assert.IsTrue(body.Contains("Count = count"), $"Expected the body to assign Count, was: {body}");
+ // Value types never emit a null check.
+ Assert.IsFalse(body.Contains("AssertNotNull(count"), $"Did not expect null validation for a value type, was: {body}");
+
+ var writer = new TypeProviderWriter(modelProvider);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public async Task BackCompat_RenamedPropertyConstructorIsRestored()
+ {
+ // The spec property "resources" is renamed to "ResourceList" via a [CodeGenMember]
+ // customization. The previously published constructor's "resources" parameter must still
+ // be matched to the renamed property (via its OriginalName) so the constructor is restored.
+ var inputModel = InputFactory.Model(
+ "MockInputModel",
+ usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json,
+ properties:
+ [
+ InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
+ InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false),
+ ]);
+
+ await MockHelpers.LoadMockGeneratorAsync(
+ inputModelTypes: [inputModel],
+ compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(),
+ lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(
+ method: "BackCompat_RenamedPropertyConstructorIsRestored_LastContract"));
+
+ var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider;
+ Assert.IsNotNull(modelProvider);
+
+ modelProvider!.ProcessTypeForBackCompatibility();
+
+ var restoredCtor = modelProvider.Constructors.SingleOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNotNull(restoredCtor, "Expected the (name, resources) constructor to be restored for back compat");
+ // The restored parameter keeps the previously published (pre-rename) name.
+ Assert.AreEqual("resources", restoredCtor!.Signature.Parameters[1].Name);
+
+ // The body assigns the current, renamed property.
+ var body = restoredCtor.BodyStatements!.ToDisplayString();
+ Assert.IsTrue(body.Contains("ResourceList = resources"), $"Expected the body to assign the renamed property, was: {body}");
+
+ var writer = new TypeProviderWriter(modelProvider);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [TestCase(".txt")]
+ [TestCase(".xml")]
+ public async Task BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline(string baselineExtension)
+ {
+ // "resources" was required in the last contract and is now optional, which would normally
+ // cause the previous (name, resources) constructor to be restored. However its removal is
+ // accepted in the ApiCompat baseline (tested in both the txt and xml formats), so the
+ // constructor must not be resurrected.
+ var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: baselineExtension);
+
+ var inputModel = InputFactory.Model(
+ "MockInputModel",
+ usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json,
+ properties:
+ [
+ InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
+ InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false),
+ ]);
+
+ await MockHelpers.LoadMockGeneratorAsync(
+ inputModelTypes: [inputModel],
+ lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(
+ method: "BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract"),
+ apiCompatBaseline: baseline);
+
+ var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider;
+ Assert.IsNotNull(modelProvider);
+
+ modelProvider!.ProcessTypeForBackCompatibility();
+
+ // The (name, resources) constructor removal is accepted in the baseline, so it is not restored.
+ var restoredCtor = modelProvider.Constructors.FirstOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNull(restoredCtor, "The constructor should not be restored when its removal is accepted in the baseline");
+
+ var writer = new TypeProviderWriter(modelProvider);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
+ [Test]
+ public async Task BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode()
+ {
+ // "resources" was required in the last contract and is now optional, which would normally
+ // cause the previous (name, resources) constructor to be restored. Here the user has replaced
+ // that constructor with their own custom implementation, so the generator must not add a
+ // colliding back-compat overload.
+ var inputModel = InputFactory.Model(
+ "MockInputModel",
+ usage: InputModelTypeUsage.Input | InputModelTypeUsage.Json,
+ properties:
+ [
+ InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
+ InputFactory.Property("resources", InputPrimitiveType.String, isRequired: false),
+ ]);
+
+ await MockHelpers.LoadMockGeneratorAsync(
+ inputModelTypes: [inputModel],
+ compilation: async () => await Helpers.GetCompilationFromDirectoryAsync(
+ method: "BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode"),
+ lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(
+ method: "BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract"));
+
+ var modelProvider = CodeModelGenerator.Instance.OutputLibrary.TypeProviders.SingleOrDefault(t => t.Name == "MockInputModel") as ModelProvider;
+ Assert.IsNotNull(modelProvider);
+
+ // The custom (name, resources) constructor lives in the canonical view.
+ var customCtor = modelProvider!.CanonicalView.Constructors.SingleOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNotNull(customCtor, "Expected the custom (name, resources) constructor to be present");
+
+ modelProvider.ProcessTypeForBackCompatibility();
+
+ // Because the custom code already provides the (name, resources) constructor, the generator
+ // must not restore a colliding overload of its own.
+ var restoredCtor = modelProvider.Constructors.FirstOrDefault(c =>
+ c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public)
+ && c.Signature.Parameters.Count == 2);
+ Assert.IsNull(restoredCtor, "The constructor should not be restored when it is replaced by custom code");
+
+ var writer = new TypeProviderWriter(modelProvider);
+ var file = writer.Write();
+ Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content);
+ }
+
[Test]
public async Task TestBuildProperties_WithObjectAdditionalPropertiesBackwardCompatibility()
{
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing.cs
new file mode 100644
index 00000000000..61958c1b8b5
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing.cs
@@ -0,0 +1,33 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using Sample;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties;
+
+ public MockInputModel(string name)
+ {
+ global::Sample.Argument.AssertNotNull(name, nameof(name));
+
+ Name = name;
+ }
+
+ internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties)
+ {
+ Name = name;
+ Resources = resources;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; set; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing/UnrelatedModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing/UnrelatedModel.cs
new file mode 100644
index 00000000000..78a0f6a28d2
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenLastContractMissing/UnrelatedModel.cs
@@ -0,0 +1,9 @@
+namespace Sample.Models
+{
+ // Note: this last-contract model has a different name than the spec model
+ // ("MockInputModel"), so no last contract view is found for the model.
+ public partial class UnrelatedModel
+ {
+ public int? Count { get; set; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved.cs
new file mode 100644
index 00000000000..ac6d11111d5
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved.cs
@@ -0,0 +1,30 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using Sample;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties;
+
+ public MockInputModel(string name)
+ {
+ global::Sample.Argument.AssertNotNull(name, nameof(name));
+
+ Name = name;
+ }
+
+ internal MockInputModel(string name, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties)
+ {
+ Name = name;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ public string Name { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved/MockInputModel.cs
new file mode 100644
index 00000000000..776ffa9d8d7
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenPropertyRemoved/MockInputModel.cs
@@ -0,0 +1,18 @@
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ // In the last contract "resources" existed and was part of the constructor, but
+ // the current spec removes the property entirely. Because there is no matching
+ // property to assign, the previous constructor cannot be safely restored.
+ public MockInputModel(string name, string resources)
+ {
+ Name = name;
+ Resources = resources;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.cs
new file mode 100644
index 00000000000..61958c1b8b5
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.cs
@@ -0,0 +1,33 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using Sample;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties;
+
+ public MockInputModel(string name)
+ {
+ global::Sample.Argument.AssertNotNull(name, nameof(name));
+
+ Name = name;
+ }
+
+ internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties)
+ {
+ Name = name;
+ Resources = resources;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; set; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.txt b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.txt
new file mode 100644
index 00000000000..2c4258cf7d3
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.txt
@@ -0,0 +1 @@
+MembersMustExist : Member 'public Sample.Models.MockInputModel..ctor(System.String, System.String)' does not exist in the implementation but it does exist in the contract.
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.xml
new file mode 100644
index 00000000000..36000f351a8
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline.xml
@@ -0,0 +1,7 @@
+
+
+
+ CP0002
+ M:Sample.Models.MockInputModel.#ctor(System.String,System.String)
+
+
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract/MockInputModel.cs
new file mode 100644
index 00000000000..c6e9450a34e
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenRemovalAcceptedInBaseline_LastContract/MockInputModel.cs
@@ -0,0 +1,18 @@
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ // In the last contract "resources" was required so the initialization constructor
+ // accepted it. The current spec relaxes it to optional, which would normally cause the
+ // previous constructor to be restored - but here its removal is accepted in the baseline.
+ public MockInputModel(string name, string resources)
+ {
+ Name = name;
+ Resources = resources;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode.cs
new file mode 100644
index 00000000000..61958c1b8b5
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode.cs
@@ -0,0 +1,33 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using Sample;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties;
+
+ public MockInputModel(string name)
+ {
+ global::Sample.Argument.AssertNotNull(name, nameof(name));
+
+ Name = name;
+ }
+
+ internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties)
+ {
+ Name = name;
+ Resources = resources;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; set; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode/MockInputModel.cs
new file mode 100644
index 00000000000..4dc7415bd15
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode/MockInputModel.cs
@@ -0,0 +1,18 @@
+#nullable disable
+
+using Sample;
+using SampleTypeSpec;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ // The user supplies their own (name, resources) constructor, replacing the one the
+ // generator would otherwise restore for back compat. Restoration must be skipped so the
+ // generated overload does not collide with this custom code.
+ public MockInputModel(string name, string resources) : this(name)
+ {
+ Resources = resources;
+ }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract/MockInputModel.cs
new file mode 100644
index 00000000000..d088ba570fa
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_ConstructorNotRestoredWhenReplacedByCustomCode_LastContract/MockInputModel.cs
@@ -0,0 +1,16 @@
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ // The previously published constructor accepted the required "resources" parameter.
+ public MockInputModel(string name, string resources)
+ {
+ Name = name;
+ Resources = resources;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored.cs
new file mode 100644
index 00000000000..c6e78ea90c8
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored.cs
@@ -0,0 +1,38 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using Sample;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties;
+
+ public MockInputModel(string name)
+ {
+ global::Sample.Argument.AssertNotNull(name, nameof(name));
+
+ Name = name;
+ }
+
+ internal MockInputModel(string name, int count, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties)
+ {
+ Name = name;
+ Count = count;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ public MockInputModel(string name, int count) : this(name)
+ {
+ Count = count;
+ }
+
+ public string Name { get; }
+
+ public int Count { get; set; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored/MockInputModel.cs
new file mode 100644
index 00000000000..adad3e228ab
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_OptionalValueTypeConstructorParameterIsRestored/MockInputModel.cs
@@ -0,0 +1,18 @@
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ // In the last contract "count" was required so the initialization constructor
+ // accepted it. The current spec relaxes it to optional, which drops it from the
+ // constructor unless it is restored for back compat.
+ public MockInputModel(string name, int count)
+ {
+ Name = name;
+ Count = count;
+ }
+
+ public string Name { get; }
+
+ public int Count { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored.cs
new file mode 100644
index 00000000000..beb896a14e3
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored.cs
@@ -0,0 +1,36 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using Sample;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties;
+
+ public MockInputModel(string name)
+ {
+ global::Sample.Argument.AssertNotNull(name, nameof(name));
+
+ Name = name;
+ }
+
+ internal MockInputModel(string name, string resourceList, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties)
+ {
+ Name = name;
+ ResourceList = resourceList;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ public MockInputModel(string name, string resources) : this(name)
+ {
+ ResourceList = resources;
+ }
+
+ public string Name { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored/MockInputModel.cs
new file mode 100644
index 00000000000..e9ed6d5d4a3
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored/MockInputModel.cs
@@ -0,0 +1,14 @@
+#nullable disable
+
+using Sample;
+using SampleTypeSpec;
+using Microsoft.TypeSpec.Generator.Customizations;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ [CodeGenMember("Resources")]
+ public string ResourceList { get; set; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored_LastContract/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored_LastContract/MockInputModel.cs
new file mode 100644
index 00000000000..83aedb284da
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RenamedPropertyConstructorIsRestored_LastContract/MockInputModel.cs
@@ -0,0 +1,16 @@
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ // The previously published constructor used the pre-rename parameter name "resources".
+ public MockInputModel(string name, string resources)
+ {
+ Name = name;
+ Resources = resources;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored.cs
new file mode 100644
index 00000000000..605759a88bd
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored.cs
@@ -0,0 +1,38 @@
+//
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using Sample;
+
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties;
+
+ public MockInputModel(string name)
+ {
+ global::Sample.Argument.AssertNotNull(name, nameof(name));
+
+ Name = name;
+ }
+
+ internal MockInputModel(string name, string resources, global::System.Collections.Generic.IDictionary additionalBinaryDataProperties)
+ {
+ Name = name;
+ Resources = resources;
+ _additionalBinaryDataProperties = additionalBinaryDataProperties;
+ }
+
+ public MockInputModel(string name, string resources) : this(name)
+ {
+ Resources = resources;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; set; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored/MockInputModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored/MockInputModel.cs
new file mode 100644
index 00000000000..bc469c58d83
--- /dev/null
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/Providers/ModelProviders/TestData/ModelProviderTests/BackCompat_RequiredToOptionalConstructorIsRestored/MockInputModel.cs
@@ -0,0 +1,17 @@
+namespace Sample.Models
+{
+ public partial class MockInputModel
+ {
+ // In the last contract, both properties were required so the initialization
+ // constructor accepted both of them.
+ public MockInputModel(string name, string resources)
+ {
+ Name = name;
+ Resources = resources;
+ }
+
+ public string Name { get; }
+
+ public string Resources { get; }
+ }
+}
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs
index 0d0cfddde73..592b12204e2 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/ApiCompatBaselineTests.cs
@@ -365,6 +365,53 @@ public void IsMethodRemovalSuppressedMatchesDictionaryWithModelValue()
Assert.IsFalse(baseline.IsMethodRemovalSuppressed("Ns.Types", "WithDictionary", [dictionaryOfInt]));
}
+ [Test]
+ public void IsMethodRemovalSuppressedMatchesConstructor()
+ {
+ var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline");
+
+ // Ns.Ctors..ctor(System.String, System.Int32) is accepted in the baseline. Constructors are
+ // recorded under the ".ctor" member name with their exact parameter types.
+ Assert.IsTrue(baseline.IsMethodRemovalSuppressed(
+ "Ns.Ctors",
+ ".ctor",
+ [new CSharpType(typeof(string)), new CSharpType(typeof(int))]));
+
+ // The same types in a different order are a different constructor signature.
+ Assert.IsFalse(baseline.IsMethodRemovalSuppressed(
+ "Ns.Ctors",
+ ".ctor",
+ [new CSharpType(typeof(int)), new CSharpType(typeof(string))]));
+
+ // A different arity must not match.
+ Assert.IsFalse(baseline.IsMethodRemovalSuppressed("Ns.Ctors", ".ctor", [new CSharpType(typeof(string))]));
+
+ // A different parameter type in one slot must not match.
+ Assert.IsFalse(baseline.IsMethodRemovalSuppressed(
+ "Ns.Ctors",
+ ".ctor",
+ [new CSharpType(typeof(string)), new CSharpType(typeof(bool))]));
+
+ // A different declaring type must not match.
+ Assert.IsFalse(baseline.IsMethodRemovalSuppressed(
+ "Ns.Other",
+ ".ctor",
+ [new CSharpType(typeof(string)), new CSharpType(typeof(int))]));
+ }
+
+ [Test]
+ public void IsMethodRemovalSuppressedMatchesParameterlessConstructor()
+ {
+ var baseline = Helpers.GetApiCompatBaselineFromFile(fileExtension: _fileExtension, method: "Baseline");
+
+ // Ns.Ctors..ctor() has no parameters; the canonical signature is empty on both sides.
+ Assert.IsTrue(baseline.IsMethodRemovalSuppressed("Ns.Ctors", ".ctor", []));
+
+ // Ns.Foo only has a constructor overload that takes arguments in the baseline, so querying
+ // its parameterless constructor must not match that overload.
+ Assert.IsFalse(baseline.IsMethodRemovalSuppressed("Ns.Foo", ".ctor", []));
+ }
+
[Test]
public void ReferencesSuppressedTypeMatchesDirectType()
{
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt
index 725b47e3795..9769f92a6df 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.txt
@@ -3,6 +3,8 @@ TypesMustExist : Type 'Azure.AI.Projects.Agents.ProjectsAgentProtocol' does not
MembersMustExist : Member 'public Azure.AI.Projects.Agents.ProtocolVersionRecord Azure.AI.Projects.Agents.ProjectsAgentsModelFactory.ProtocolVersionRecord(Azure.AI.Projects.Agents.ProjectsAgentProtocol, System.String)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Void Ns.Foo.Reset()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Ns.Foo..ctor(Ns.Kind, System.String)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Ns.Ctors..ctor(System.String, System.Int32)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Ns.Ctors..ctor()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Ns.Kind Ns.Foo.Kind.get()' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Void Ns.Foo.Kind.set(Ns.Kind)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public System.Void Ns.Foo.Configure(System.Collections.Generic.IDictionary, System.String)' does not exist in the implementation but it does exist in the contract.
diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml
index 6b8132093b2..b683202d4c6 100644
--- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml
+++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator/test/SourceInput/TestData/ApiCompatBaselineTests/Baseline.xml
@@ -20,6 +20,14 @@
CP0002
M:Ns.Foo.#ctor(Ns.Kind,System.String)
+
+ CP0002
+ M:Ns.Ctors.#ctor(System.String,System.Int32)
+
+
+ CP0002
+ M:Ns.Ctors.#ctor
+
CP0002
M:Ns.Foo.get_Kind