@@ -802,13 +802,21 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
802802 return base . BuildConstructorsForBackCompatibility ( originalConstructors ) ;
803803 }
804804
805- var constructors = new List < ConstructorProvider > ( base . BuildConstructorsForBackCompatibility ( originalConstructors ) ) ;
806- var restorablePropertyLookup = BuildRestorablePropertyLookup ( ) ;
805+ var originalConstructorList = originalConstructors as IReadOnlyList < ConstructorProvider > ?? [ .. originalConstructors ] ;
807806 IReadOnlyList < ConstructorProvider > candidateConstructors = CustomCodeView ? . Constructors is { Count : > 0 } customConstructors
808- ? [ .. constructors , .. customConstructors ]
809- : constructors ;
807+ ? [ .. originalConstructorList , .. customConstructors ]
808+ : originalConstructorList ;
810809
811- foreach ( var previousConstructor in previousConstructors )
810+ var restorablePreviousConstructors = previousConstructors
811+ . Where ( c => ! BackCompatHelper . IsConstructorRemovalAcceptedInBaseline ( this , c . Signature ) )
812+ . ToList ( ) ;
813+
814+ RestorePreviousConstructorParameterNames ( originalConstructorList , candidateConstructors , restorablePreviousConstructors ) ;
815+
816+ var constructors = new List < ConstructorProvider > ( base . BuildConstructorsForBackCompatibility ( originalConstructorList ) ) ;
817+ var restorablePropertyLookup = BuildRestorablePropertyLookup ( ) ;
818+
819+ foreach ( var previousConstructor in restorablePreviousConstructors )
812820 {
813821 if ( ! MethodSignatureHelper . IsPublicApi ( previousConstructor . Signature . Modifiers ) )
814822 {
@@ -817,19 +825,17 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
817825
818826 var previousParameters = previousConstructor . Signature . Parameters ;
819827
820- if ( BackCompatHelper . IsConstructorRemovalAcceptedInBaseline ( this , previousConstructor . Signature ) )
821- {
822- continue ;
823- }
824-
825828 // A previously published accessible parameterless constructor is dropped when the current
826829 // generation makes a property required. Restore it and drop the generated mocking constructor
827830 // so it is not a duplicate. An accessible parameterless constructor (generated or custom code)
828- // counts as already present; an inaccessible generated mocking constructor does not.
829- if ( ! Type . IsStruct && previousParameters . Count == 0 )
830- {
831- if ( ! constructors . Any ( c => c . Signature . Parameters . Count == 0 && MethodSignatureHelper . IsPublicApi ( c . Signature . Modifiers ) )
832- && ! CanonicalView . Constructors . Any ( c => c . Signature . Parameters . Count == 0 && MethodSignatureHelper . IsPublicApi ( c . Signature . Modifiers ) ) )
831+ // counts as already present; an inaccessible generated mocking constructor does not. A struct
832+ // always exposes a public parameterless constructor via its serialization (mocking)
833+ // constructor, so there is nothing to restore on the model partial.
834+ if ( previousParameters . Count == 0 )
835+ {
836+ if ( ! Type . IsStruct
837+ && ! constructors . Any ( c => c . Signature . Parameters . Count == 0 && MethodSignatureHelper . IsPublicApi ( c . Signature . Modifiers ) )
838+ && ! candidateConstructors . Any ( c => c . Signature . Parameters . Count == 0 && MethodSignatureHelper . IsPublicApi ( c . Signature . Modifiers ) ) )
833839 {
834840 var parameterlessConstructor = BuildBackCompatParameterlessConstructor ( previousConstructor , candidateConstructors ) ;
835841 RemoveGeneratedMockingConstructor ( constructors ) ;
@@ -843,9 +849,9 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
843849 }
844850
845851 // If a constructor with the same parameters already exists - either still generated or
846- // supplied by custom code (which lives in the canonical view) - there is nothing to restore.
852+ // supplied by custom code - there is nothing to restore.
847853 if ( constructors . Any ( c => BackCompatHelper . ParametersMatch ( c . Signature . Parameters , previousParameters ) )
848- || CanonicalView . Constructors . Any ( c => BackCompatHelper . ParametersMatch ( c . Signature . Parameters , previousParameters ) ) )
854+ || candidateConstructors . Any ( c => BackCompatHelper . ParametersMatch ( c . Signature . Parameters , previousParameters ) ) )
849855 {
850856 continue ;
851857 }
@@ -868,6 +874,70 @@ protected internal override IReadOnlyList<ConstructorProvider> BuildConstructors
868874 return constructors ;
869875 }
870876
877+ private void RestorePreviousConstructorParameterNames (
878+ IReadOnlyList < ConstructorProvider > currentConstructors ,
879+ IReadOnlyList < ConstructorProvider > candidateConstructors ,
880+ IReadOnlyList < ConstructorProvider > previousConstructors )
881+ {
882+ const MethodSignatureModifiers privateProtected = MethodSignatureModifiers . Private | MethodSignatureModifiers . Protected ;
883+ foreach ( var previousConstructor in previousConstructors )
884+ {
885+ if ( ! MethodSignatureHelper . IsPublicApi ( previousConstructor . Signature . Modifiers ) )
886+ {
887+ continue ;
888+ }
889+
890+ var previousParameters = previousConstructor . Signature . Parameters ;
891+
892+ // A generated or custom constructor that already matches the previous signature (types and
893+ // names) satisfies the contract; renaming another constructor into it would collide.
894+ if ( candidateConstructors . Any ( c => BackCompatHelper . ParametersMatch ( c . Signature . Parameters , previousParameters ) ) )
895+ {
896+ continue ;
897+ }
898+
899+ var currentConstructor = currentConstructors . FirstOrDefault ( c =>
900+ ( MethodSignatureHelper . IsPublicApi ( c . Signature . Modifiers )
901+ || ( c . Signature . Modifiers & privateProtected ) == privateProtected )
902+ && MethodSignatureBase . SignatureComparer . Equals ( c . Signature , previousConstructor . Signature ) ) ;
903+ if ( currentConstructor is null )
904+ {
905+ continue ;
906+ }
907+
908+ var currentParameters = currentConstructor . Signature . Parameters ;
909+
910+ // A swap or rotation keeps every previous name, so realign the existing parameter objects
911+ // to the previous order - renaming positionally would mis-bind a caller's named argument to
912+ // the wrong property. Otherwise restore names positionally where the types line up.
913+ var currentByName = currentParameters . ToDictionary ( p => p . Name ) ;
914+ IReadOnlyList < ParameterProvider > restoredParameters = previousParameters . All ( p => currentByName . ContainsKey ( p . Name ) )
915+ ? [ .. previousParameters . Select ( p => currentByName [ p . Name ] ) ]
916+ : currentParameters ;
917+ if ( ! restoredParameters . Select ( ( p , i ) => p . Type . AreNamesEqual ( previousParameters [ i ] . Type ) ) . All ( match => match ) )
918+ {
919+ continue ;
920+ }
921+
922+ for ( int i = 0 ; i < restoredParameters . Count ; i ++ )
923+ {
924+ var restoredName = previousParameters [ i ] . Name ;
925+ if ( string . Equals ( restoredParameters [ i ] . Name , restoredName , StringComparison . Ordinal ) )
926+ {
927+ continue ;
928+ }
929+
930+ CodeModelGenerator . Instance . Emitter . Debug (
931+ $ "Preserved parameter name '{ restoredName } ' at position { i } on constructor '{ Name } ' from last contract (instead of '{ restoredParameters [ i ] . Name } ').",
932+ BackCompatibilityChangeCategory . ParameterNamePreserved ) ;
933+ restoredParameters [ i ] . Update ( name : restoredName ) ;
934+ }
935+
936+ currentConstructor . Signature . Update ( parameters : [ .. restoredParameters ] ) ;
937+ currentConstructor . Update ( signature : currentConstructor . Signature ) ;
938+ }
939+ }
940+
871941 private bool TryBuildRestoredConstructor (
872942 ConstructorProvider previousConstructor ,
873943 IReadOnlyList < ConstructorProvider > currentConstructors ,
@@ -878,7 +948,9 @@ private bool TryBuildRestoredConstructor(
878948 var previousParameters = previousConstructor . Signature . Parameters ;
879949
880950 // Find the public constructor to chain to: its parameters must form an in-order subsequence of
881- // the previous constructor's parameters. Prefer the closest one.
951+ // the previous constructor's parameters. Prefer the closest one. Without a chaining target the
952+ // constructor is not restored - a standalone constructor would bypass the current constructor's
953+ // initialization (e.g. the implicit base() call and inherited get-only properties).
882954 ConstructorProvider ? targetConstructor = null ;
883955 foreach ( var candidate in currentConstructors )
884956 {
@@ -898,7 +970,7 @@ private bool TryBuildRestoredConstructor(
898970 }
899971 }
900972
901- if ( targetConstructor == null )
973+ if ( targetConstructor is null )
902974 {
903975 return false ;
904976 }
@@ -925,23 +997,23 @@ private bool TryBuildRestoredConstructor(
925997 continue ;
926998 }
927999
928- if ( ! restorablePropertyLookup . TryGetValue ( previousParameter . Name , out var property )
929- || ! property . Type . AreNamesEqual ( previousParameter . Type ) )
1000+ // Each extra parameter (not consumed by the chain target) must map to a settable, wire-backed
1001+ // property assigned in the restored constructor's body.
1002+ var property = restorablePropertyLookup . TryGetValue ( previousParameter . Name , out var chained )
1003+ && chained . Type . AreNamesEqual ( previousParameter . Type )
1004+ ? chained
1005+ : null ;
1006+
1007+ if ( property is null || extraAssignments . Any ( a => a . Property == property ) )
9301008 {
9311009 return false ;
9321010 }
9331011
934- var restoredParameter = PartialMethodCustomization . CloneParameterWithName (
935- property . AsParameter ,
936- previousParameter . Name ,
937- removeDefault : true ) ;
938-
1012+ var restoredParameter = PartialMethodCustomization . CloneParameterWithName ( property . AsParameter , previousParameter . Name , removeDefault : true ) ;
9391013 restoredParameters . Add ( restoredParameter ) ;
9401014 extraAssignments . Add ( ( property , restoredParameter ) ) ;
9411015 }
9421016
943- // Every target parameter must be consumed and at least one extra property must be assigned,
944- // otherwise the restored constructor would be redundant or would produce an invalid chained call.
9451017 if ( targetIndex != targetParameters . Count || extraAssignments . Count == 0 )
9461018 {
9471019 return false ;
0 commit comments