From 81978a9f34a830803c1065aa53ff435e8318e961 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 19:19:57 +0000 Subject: [PATCH 01/11] Initial plan From a5b47383294b3c541ea2baa9a7396d0d1f910cf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 19:48:07 +0000 Subject: [PATCH 02/11] make ModelProvider base resolution methods virtual Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/96294dad-fa6c-4b21-a8c7-8fcfb0107c92 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- ...lprovider-base-virtual-2026-5-5-19-21-0.md | 7 ++ .../src/Providers/ModelProvider.cs | 18 ++- .../ModelProviders/ModelProviderTests.cs | 111 ++++++++++++++++++ 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 .chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md diff --git a/.chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md b/.chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md new file mode 100644 index 00000000000..c465ca8d1ed --- /dev/null +++ b/.chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/http-client-csharp" +--- + +Make `ModelProvider.BuildBaseTypeProvider` and `ModelProvider.BuildBaseModelProvider` `protected virtual` so emitters that override `BuildBaseType` can keep `BaseType`, `BaseTypeProvider`, and `BaseModelProvider` consistent. Previously, overriding `BuildBaseType` could leave `BaseModelProvider` walking the original `InputModelType.BaseModel`, producing a base model chain that did not match the generated C# class hierarchy. 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 de172f444a8..8925d25703c 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 @@ -127,7 +127,14 @@ private IReadOnlyList BuildDerivedModels() internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider(); private TypeProvider? _baseTypeProvider; - private TypeProvider? BuildBaseTypeProvider() + /// + /// Builds the representing the base type of this model. + /// Emitters that need to redirect a model's base class can override this method and/or + /// alongside so that + /// , , and + /// remain consistent. + /// + protected virtual TypeProvider? BuildBaseTypeProvider() { // First check if there's a generated base model if (BaseModelProvider != null) @@ -291,7 +298,14 @@ private static bool IsDiscriminator(InputProperty property) return property is InputModelProperty modelProperty && modelProperty.IsDiscriminator; } - private ModelProvider? BuildBaseModelProvider() + /// + /// Builds the representing the base model of this model. + /// Emitters that override to redirect the generated C# base + /// class should also override this method (and/or ) so + /// that , , and + /// stay consistent. + /// + protected virtual ModelProvider? BuildBaseModelProvider() { // consider models that have been customized to inherit from a different generated model if (CustomCodeView?.BaseType != null) 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 7c8e88e49d1..a4c66ec03ca 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 @@ -15,6 +15,8 @@ namespace Microsoft.TypeSpec.Generator.Tests.Providers.ModelProviders { + public class MyFrameworkBase { } + public class ModelProviderTests { [SetUp] @@ -422,6 +424,115 @@ public void BuildBaseType() Assert.AreEqual(baseModel!.Type, derivedModel!.Type.BaseType); } + // Reproduces the scenario from the issue: an emitter overrides BuildBaseType to redirect a + // model's base class to a framework type. Now that BuildBaseTypeProvider and + // BuildBaseModelProvider are virtual, the emitter can override them together so that + // BaseType, BaseTypeProvider, and BaseModelProvider remain in agreement. + [Test] + public void BaseTypeAndBaseModelProvider_AreConsistent_WhenEmitterRedirectsBaseClass() + { + var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); + + // Force-create the input base provider so that, without overrides, BaseModelProvider + // would have walked the input model and returned the ModelProvider for "baseModel". + var inputBaseProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + Assert.IsNotNull(inputBaseProvider); + + var redirectedBaseType = new CSharpType(typeof(MyFrameworkBase)); + var redirectedProvider = new SystemObjectTypeProvider(redirectedBaseType); + var derivedProvider = new RedirectedBaseModelProvider(inputDerived, redirectedProvider); + + // BaseTypeProvider is the redirected provider (not the input model's base provider). + Assert.AreSame(redirectedProvider, derivedProvider.GetBaseTypeProviderForTest()); + + // BaseType is the redirected framework type, NOT the input model's base type. + Assert.AreSame(redirectedProvider.Type, derivedProvider.BaseType); + Assert.AreNotSame(inputBaseProvider!.Type, derivedProvider.BaseType); + + // BaseModelProvider agrees with BaseType. Since the redirected base is not a ModelProvider, + // BaseModelProvider is null instead of returning the original input-model-derived parent. + Assert.IsNull(derivedProvider.BaseModelProvider); + } + + // Verifies that BaseModelProvider and BaseTypeProvider stay consistent in the default flow. + [Test] + public void BaseModelProvider_MatchesBaseTypeProvider_InDefaultFlow() + { + var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); + + // Force-create the input base provider so it is registered in the type factory. + CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + + var derivedProvider = new TestableModelProvider(inputDerived); + Assert.IsNotNull(derivedProvider.BaseModelProvider); + Assert.AreSame(derivedProvider.BaseModelProvider, derivedProvider.GetBaseTypeProviderForTest()); + } + + // Demonstrates that BuildBaseType can also be overridden together with BuildBaseModelProvider + // so that BaseType and BaseModelProvider stay in agreement (the core requirement of the + // referenced issue). + [Test] + public void OverridingBuildBaseTypeAndBuildBaseModelProvider_KeepsBaseTypeConsistent() + { + var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); + CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + + var redirectedBaseType = new CSharpType(typeof(MyFrameworkBase)); + var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, redirectedBaseType); + + Assert.AreEqual(redirectedBaseType, derivedProvider.BaseType); + // The emitter explicitly returned null from BuildBaseModelProvider, so it agrees with + // BaseType (no in-spec ModelProvider matches the redirected framework type). + Assert.IsNull(derivedProvider.BaseModelProvider); + } + + private class RedirectedBaseModelProvider : ModelProvider + { + private readonly TypeProvider _baseTypeProvider; + + public RedirectedBaseModelProvider(InputModelType inputModel, TypeProvider baseTypeProvider) : base(inputModel) + { + _baseTypeProvider = baseTypeProvider; + } + + protected override TypeProvider? BuildBaseTypeProvider() => _baseTypeProvider; + + // Per the recommended pattern, providers that override the base provider should also + // delegate BuildBaseType to BaseTypeProvider so that all three values stay in sync. + protected override CSharpType? BuildBaseType() => BaseTypeProvider?.Type; + + // Override BuildBaseModelProvider together with BuildBaseTypeProvider so the values + // never disagree — _baseTypeProvider may not be a ModelProvider. + protected override ModelProvider? BuildBaseModelProvider() => _baseTypeProvider as ModelProvider; + + internal TypeProvider? GetBaseTypeProviderForTest() => BaseTypeProvider; + } + + private class BuildBaseTypeOverridingModelProvider : ModelProvider + { + private readonly CSharpType _redirectedBaseType; + + public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, CSharpType redirectedBaseType) : base(inputModel) + { + _redirectedBaseType = redirectedBaseType; + } + + protected override CSharpType? BuildBaseType() => _redirectedBaseType; + + // The emitter overrides BuildBaseModelProvider to keep it in sync with the redirected BaseType. + protected override ModelProvider? BuildBaseModelProvider() => null; + } + + private class TestableModelProvider : ModelProvider + { + public TestableModelProvider(InputModelType inputModel) : base(inputModel) { } + + internal TypeProvider? GetBaseTypeProviderForTest() => BaseTypeProvider; + } + [Test] public void BuildModelAsStruct() { From 90148c5972c35ad4718fe0b1d6baa9280f99eef4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 19:50:58 +0000 Subject: [PATCH 03/11] revert changelog entry per review feedback Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/60279f78-9af0-440e-97b7-7026b39783c9 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../fix-modelprovider-base-virtual-2026-5-5-19-21-0.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md diff --git a/.chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md b/.chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md deleted file mode 100644 index c465ca8d1ed..00000000000 --- a/.chronus/changes/fix-modelprovider-base-virtual-2026-5-5-19-21-0.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -changeKind: fix -packages: - - "@typespec/http-client-csharp" ---- - -Make `ModelProvider.BuildBaseTypeProvider` and `ModelProvider.BuildBaseModelProvider` `protected virtual` so emitters that override `BuildBaseType` can keep `BaseType`, `BaseTypeProvider`, and `BaseModelProvider` consistent. Previously, overriding `BuildBaseType` could leave `BaseModelProvider` walking the original `InputModelType.BaseModel`, producing a base model chain that did not match the generated C# class hierarchy. From b54661235109b1e202720c1c919cf1886e34a252 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 20:12:06 +0000 Subject: [PATCH 04/11] revert BuildBaseTypeProvider to private; tighten test comment Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/5b6fcac7-58a2-4f8e-be3e-2b2870fbeed0 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 12 +-- .../ModelProviders/ModelProviderTests.cs | 80 +------------------ 2 files changed, 4 insertions(+), 88 deletions(-) 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 8925d25703c..0b84b814211 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 @@ -127,14 +127,7 @@ private IReadOnlyList BuildDerivedModels() internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider(); private TypeProvider? _baseTypeProvider; - /// - /// Builds the representing the base type of this model. - /// Emitters that need to redirect a model's base class can override this method and/or - /// alongside so that - /// , , and - /// remain consistent. - /// - protected virtual TypeProvider? BuildBaseTypeProvider() + private TypeProvider? BuildBaseTypeProvider() { // First check if there's a generated base model if (BaseModelProvider != null) @@ -301,8 +294,7 @@ private static bool IsDiscriminator(InputProperty property) /// /// Builds the representing the base model of this model. /// Emitters that override to redirect the generated C# base - /// class should also override this method (and/or ) so - /// that , , and + /// class should also override this method so that and /// stay consistent. /// protected virtual ModelProvider? BuildBaseModelProvider() 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 a4c66ec03ca..d5c8716d23a 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 @@ -424,55 +424,8 @@ public void BuildBaseType() Assert.AreEqual(baseModel!.Type, derivedModel!.Type.BaseType); } - // Reproduces the scenario from the issue: an emitter overrides BuildBaseType to redirect a - // model's base class to a framework type. Now that BuildBaseTypeProvider and - // BuildBaseModelProvider are virtual, the emitter can override them together so that - // BaseType, BaseTypeProvider, and BaseModelProvider remain in agreement. - [Test] - public void BaseTypeAndBaseModelProvider_AreConsistent_WhenEmitterRedirectsBaseClass() - { - var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); - var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); - - // Force-create the input base provider so that, without overrides, BaseModelProvider - // would have walked the input model and returned the ModelProvider for "baseModel". - var inputBaseProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); - Assert.IsNotNull(inputBaseProvider); - - var redirectedBaseType = new CSharpType(typeof(MyFrameworkBase)); - var redirectedProvider = new SystemObjectTypeProvider(redirectedBaseType); - var derivedProvider = new RedirectedBaseModelProvider(inputDerived, redirectedProvider); - - // BaseTypeProvider is the redirected provider (not the input model's base provider). - Assert.AreSame(redirectedProvider, derivedProvider.GetBaseTypeProviderForTest()); - - // BaseType is the redirected framework type, NOT the input model's base type. - Assert.AreSame(redirectedProvider.Type, derivedProvider.BaseType); - Assert.AreNotSame(inputBaseProvider!.Type, derivedProvider.BaseType); - - // BaseModelProvider agrees with BaseType. Since the redirected base is not a ModelProvider, - // BaseModelProvider is null instead of returning the original input-model-derived parent. - Assert.IsNull(derivedProvider.BaseModelProvider); - } - - // Verifies that BaseModelProvider and BaseTypeProvider stay consistent in the default flow. - [Test] - public void BaseModelProvider_MatchesBaseTypeProvider_InDefaultFlow() - { - var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); - var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); - - // Force-create the input base provider so it is registered in the type factory. - CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); - - var derivedProvider = new TestableModelProvider(inputDerived); - Assert.IsNotNull(derivedProvider.BaseModelProvider); - Assert.AreSame(derivedProvider.BaseModelProvider, derivedProvider.GetBaseTypeProviderForTest()); - } - - // Demonstrates that BuildBaseType can also be overridden together with BuildBaseModelProvider - // so that BaseType and BaseModelProvider stay in agreement (the core requirement of the - // referenced issue). + // Verifies that overriding BuildBaseType together with BuildBaseModelProvider keeps + // BaseType and BaseModelProvider in agreement. [Test] public void OverridingBuildBaseTypeAndBuildBaseModelProvider_KeepsBaseTypeConsistent() { @@ -489,28 +442,6 @@ public void OverridingBuildBaseTypeAndBuildBaseModelProvider_KeepsBaseTypeConsis Assert.IsNull(derivedProvider.BaseModelProvider); } - private class RedirectedBaseModelProvider : ModelProvider - { - private readonly TypeProvider _baseTypeProvider; - - public RedirectedBaseModelProvider(InputModelType inputModel, TypeProvider baseTypeProvider) : base(inputModel) - { - _baseTypeProvider = baseTypeProvider; - } - - protected override TypeProvider? BuildBaseTypeProvider() => _baseTypeProvider; - - // Per the recommended pattern, providers that override the base provider should also - // delegate BuildBaseType to BaseTypeProvider so that all three values stay in sync. - protected override CSharpType? BuildBaseType() => BaseTypeProvider?.Type; - - // Override BuildBaseModelProvider together with BuildBaseTypeProvider so the values - // never disagree — _baseTypeProvider may not be a ModelProvider. - protected override ModelProvider? BuildBaseModelProvider() => _baseTypeProvider as ModelProvider; - - internal TypeProvider? GetBaseTypeProviderForTest() => BaseTypeProvider; - } - private class BuildBaseTypeOverridingModelProvider : ModelProvider { private readonly CSharpType _redirectedBaseType; @@ -526,13 +457,6 @@ public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, CSharpTyp protected override ModelProvider? BuildBaseModelProvider() => null; } - private class TestableModelProvider : ModelProvider - { - public TestableModelProvider(InputModelType inputModel) : base(inputModel) { } - - internal TypeProvider? GetBaseTypeProviderForTest() => BaseTypeProvider; - } - [Test] public void BuildModelAsStruct() { From 893720d96dd3abb04dfc8fd70f9fd54c195d1579 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 May 2026 20:19:13 +0000 Subject: [PATCH 05/11] test: redirect BaseModelProvider to a custom ModelProvider type Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/cab911a0-953a-4889-b96d-18aaccf1af83 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelProviders/ModelProviderTests.cs | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) 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 d5c8716d23a..663ccd88e28 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 @@ -15,8 +15,6 @@ namespace Microsoft.TypeSpec.Generator.Tests.Providers.ModelProviders { - public class MyFrameworkBase { } - public class ModelProviderTests { [SetUp] @@ -424,8 +422,9 @@ public void BuildBaseType() Assert.AreEqual(baseModel!.Type, derivedModel!.Type.BaseType); } - // Verifies that overriding BuildBaseType together with BuildBaseModelProvider keeps - // BaseType and BaseModelProvider in agreement. + // Verifies an emitter can override BuildBaseType and BuildBaseModelProvider together to + // redirect the C# base class to a custom ModelProvider, keeping BaseType and + // BaseModelProvider in agreement. [Test] public void OverridingBuildBaseTypeAndBuildBaseModelProvider_KeepsBaseTypeConsistent() { @@ -433,28 +432,33 @@ public void OverridingBuildBaseTypeAndBuildBaseModelProvider_KeepsBaseTypeConsis var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); - var redirectedBaseType = new CSharpType(typeof(MyFrameworkBase)); - var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, redirectedBaseType); + var customBaseProvider = new CustomBaseModelProvider(inputBase); + var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, customBaseProvider); + + Assert.AreEqual(customBaseProvider.Type, derivedProvider.BaseType); + Assert.AreSame(customBaseProvider, derivedProvider.BaseModelProvider); + } + + private class CustomBaseModelProvider : ModelProvider + { + public CustomBaseModelProvider(InputModelType inputModel) : base(inputModel) { } - Assert.AreEqual(redirectedBaseType, derivedProvider.BaseType); - // The emitter explicitly returned null from BuildBaseModelProvider, so it agrees with - // BaseType (no in-spec ModelProvider matches the redirected framework type). - Assert.IsNull(derivedProvider.BaseModelProvider); + protected override string BuildName() => "CustomBase"; } private class BuildBaseTypeOverridingModelProvider : ModelProvider { - private readonly CSharpType _redirectedBaseType; + private readonly ModelProvider _redirectedBaseProvider; - public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, CSharpType redirectedBaseType) : base(inputModel) + public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, ModelProvider redirectedBaseProvider) : base(inputModel) { - _redirectedBaseType = redirectedBaseType; + _redirectedBaseProvider = redirectedBaseProvider; } - protected override CSharpType? BuildBaseType() => _redirectedBaseType; + protected override CSharpType? BuildBaseType() => _redirectedBaseProvider?.Type; // The emitter overrides BuildBaseModelProvider to keep it in sync with the redirected BaseType. - protected override ModelProvider? BuildBaseModelProvider() => null; + protected override ModelProvider? BuildBaseModelProvider() => _redirectedBaseProvider; } [Test] From 4b74ab9e5a9bef0e64668853ed0c3fbb805adbc7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 17:12:36 +0000 Subject: [PATCH 06/11] invert BuildBaseType/BuildBaseModelProvider dependency; auto-resolve via CSharpTypeMap Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/bffba24d-7668-4405-8fcb-3b80bf9f3d9a Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 121 +++++++++--------- .../ModelProviders/ModelProviderTests.cs | 88 +++++++++++-- 2 files changed, 143 insertions(+), 66 deletions(-) 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 0b84b814211..b044c7f5553 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 @@ -187,7 +187,55 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N protected override CSharpType? BuildBaseType() { - return BaseModelProvider?.Type; + // Custom code may redirect the base class to either another generated model + // (with or without a resolved namespace) or to an external type. Resolve it first + // so that BaseType is the source of truth and BaseModelProvider can derive from it. + if (CustomCodeView?.BaseType != null) + { + var customBase = CustomCodeView.BaseType; + + // If the custom base type doesn't have a resolved namespace, then try to resolve it from the input model map. + // This will happen if a model is customized to inherit from another generated model, but that generated model + // was not also defined in custom code so Roslyn does not recognize it. + if (string.IsNullOrEmpty(customBase.Namespace)) + { + // Cheap check: the base model may already be created and registered under the right name. + if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue( + customBase.Name, out var resolvedProvider) && + resolvedProvider is ModelProvider resolvedModel) + { + return resolvedModel.Type; + } + + // Force-create all input models so that visitors run (which may rename models + // via TypeProvider.Update) and TypeProvidersByName is fully populated. + // This is a no-op for models that have already been created. + foreach (var model in CodeModelGenerator.Instance.InputLibrary.InputNamespace.Models) + { + CodeModelGenerator.Instance.TypeFactory.CreateModel(model); + } + + if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue( + customBase.Name, out resolvedProvider) && + resolvedProvider is ModelProvider resolvedAfterCreate) + { + return resolvedAfterCreate.Type; + } + } + + // Custom base type with a namespace (or unresolvable empty-namespace name): return as-is. + // BuildBaseModelProvider will look it up in CSharpTypeMap to find a matching ModelProvider, if any. + return customBase; + } + + if (_inputModel.BaseModel == null) + { + return null; + } + + // CreateModel registers the resulting ModelProvider in CSharpTypeMap, which is what + // BuildBaseModelProvider uses to resolve BaseType back to its ModelProvider. + return CodeModelGenerator.Instance.TypeFactory.CreateModel(_inputModel.BaseModel)?.Type; } protected override TypeProvider[] BuildSerializationProviders() @@ -293,69 +341,28 @@ private static bool IsDiscriminator(InputProperty property) /// /// Builds the representing the base model of this model. - /// Emitters that override to redirect the generated C# base - /// class should also override this method so that and - /// stay consistent. + /// By default this looks up in the + /// and returns the registered , if any. This means emitters that override + /// to redirect the generated C# base class get a consistent + /// automatically — it will be the matching generated model when the + /// new base type is another generated model, or when it is an external + /// (e.g., framework) type. /// protected virtual ModelProvider? BuildBaseModelProvider() { - // consider models that have been customized to inherit from a different generated model - if (CustomCodeView?.BaseType != null) - { - var baseType = CustomCodeView.BaseType; - - // If the custom base type doesn't have a resolved namespace, then try to resolve it from the input model map. - // This will happen if a model is customized to inherit from another generated model, but that generated model - // was not also defined in custom code so Roslyn does not recognize it. - if (string.IsNullOrEmpty(baseType.Namespace)) - { - // Cheap check: the base model may already be created and registered under the right name. - if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue( - baseType.Name, out var resolvedProvider) && - resolvedProvider is ModelProvider resolvedModel) - { - return resolvedModel; - } - - // Force-create all input models so that visitors run (which may rename models - // via TypeProvider.Update) and TypeProvidersByName is fully populated. - // This is a no-op for models that have already been created. - foreach (var model in CodeModelGenerator.Instance.InputLibrary.InputNamespace.Models) - { - CodeModelGenerator.Instance.TypeFactory.CreateModel(model); - } - - if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue( - baseType.Name, out resolvedProvider) && - resolvedProvider is ModelProvider resolvedAfterCreate) - { - return resolvedAfterCreate; - } - } - - // Try to find the base type in the CSharpTypeMap - if (baseType != null && CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue( - baseType, - out var customBaseType) && - customBaseType is ModelProvider customBaseModel) - { - return customBaseModel; - } - - // If the custom base type has a namespace (external type), we don't return it here - // as it's handled by BuildBaseTypeProvider() which returns a TypeProvider - if (!string.IsNullOrEmpty(baseType?.Namespace)) - { - return null; - } - } - - if (_inputModel.BaseModel == null) + // Read BaseType (not BuildBaseType()) so the result is cached and so we pick up + // any TypeProvider-level fallbacks (e.g., CustomCodeView?.BaseType). This is safe + // from recursion because BuildBaseType no longer reads BaseModelProvider. + var baseType = BaseType; + if (baseType is null) { return null; } - return CodeModelGenerator.Instance.TypeFactory.CreateModel(_inputModel.BaseModel); + return CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var provider) + && provider is ModelProvider modelProvider + ? modelProvider + : null; } private List BuildAdditionalPropertyFields() 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 663ccd88e28..22e2085920f 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 @@ -422,23 +422,81 @@ public void BuildBaseType() Assert.AreEqual(baseModel!.Type, derivedModel!.Type.BaseType); } - // Verifies an emitter can override BuildBaseType and BuildBaseModelProvider together to - // redirect the C# base class to a custom ModelProvider, keeping BaseType and - // BaseModelProvider in agreement. + // Verifies an emitter that overrides BuildBaseType to redirect to another generated + // ModelProvider gets a consistent BaseModelProvider automatically (resolved via CSharpTypeMap) + // without having to override BuildBaseModelProvider. [Test] - public void OverridingBuildBaseTypeAndBuildBaseModelProvider_KeepsBaseTypeConsistent() + public void OverridingBuildBaseType_AutoResolvesBaseModelProviderForGeneratedModel() { var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); - var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); - CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); + var baseProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + Assert.IsNotNull(baseProvider); + + var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, baseProvider!.Type); + + Assert.AreEqual(baseProvider.Type, derivedProvider.BaseType); + Assert.AreSame(baseProvider, derivedProvider.BaseModelProvider); + } + + // Verifies an emitter that overrides BuildBaseType to redirect to a framework / external + // type (not present in CSharpTypeMap as a ModelProvider) gets BaseModelProvider == null + // automatically — BaseType and BaseModelProvider stay consistent without an extra override. + [Test] + public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForFrameworkType() + { + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); + var frameworkBase = new CSharpType(typeof(InvalidOperationException)); + + var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, frameworkBase); + Assert.AreEqual(frameworkBase, derivedProvider.BaseType); + Assert.IsNull(derivedProvider.BaseModelProvider); + } + + // Verifies an emitter can still explicitly override BuildBaseModelProvider (e.g., to point + // at a custom ModelProvider instance not registered in CSharpTypeMap) and have its override + // honored in addition to BuildBaseType. + [Test] + public void OverridingBuildBaseModelProvider_IsRespected() + { + var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); var customBaseProvider = new CustomBaseModelProvider(inputBase); - var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, customBaseProvider); + + var derivedProvider = new ExplicitBaseModelProviderOverridingModelProvider(inputDerived, customBaseProvider); Assert.AreEqual(customBaseProvider.Type, derivedProvider.BaseType); Assert.AreSame(customBaseProvider, derivedProvider.BaseModelProvider); } + // Default case: when no overrides and no customizations are involved, BaseType and + // BaseModelProvider both resolve to the generated base model. + [Test] + public void BaseModelProvider_DefaultResolvesViaCSharpTypeMap() + { + var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: [], baseModel: inputBase); + + var derivedProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputDerived); + Assert.IsNotNull(derivedProvider); + Assert.IsNotNull(derivedProvider!.BaseModelProvider); + Assert.AreEqual(derivedProvider.BaseModelProvider!.Type, derivedProvider.BaseType); + } + + // When the input model has no base model and there are no overrides, both BaseType and + // BaseModelProvider are null. + [Test] + public void BaseModelProvider_NullWhenNoBase() + { + var inputModel = InputFactory.Model("standaloneModel", usage: InputModelTypeUsage.Input, properties: []); + var modelProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel); + + Assert.IsNotNull(modelProvider); + Assert.IsNull(modelProvider!.BaseType); + Assert.IsNull(modelProvider.BaseModelProvider); + } + private class CustomBaseModelProvider : ModelProvider { public CustomBaseModelProvider(InputModelType inputModel) : base(inputModel) { } @@ -447,17 +505,29 @@ public CustomBaseModelProvider(InputModelType inputModel) : base(inputModel) { } } private class BuildBaseTypeOverridingModelProvider : ModelProvider + { + private readonly CSharpType? _redirectedBaseType; + + public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, CSharpType? redirectedBaseType) : base(inputModel) + { + _redirectedBaseType = redirectedBaseType; + } + + // Only BuildBaseType is overridden — BuildBaseModelProvider is left to resolve via CSharpTypeMap. + protected override CSharpType? BuildBaseType() => _redirectedBaseType; + } + + private class ExplicitBaseModelProviderOverridingModelProvider : ModelProvider { private readonly ModelProvider _redirectedBaseProvider; - public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, ModelProvider redirectedBaseProvider) : base(inputModel) + public ExplicitBaseModelProviderOverridingModelProvider(InputModelType inputModel, ModelProvider redirectedBaseProvider) : base(inputModel) { _redirectedBaseProvider = redirectedBaseProvider; } protected override CSharpType? BuildBaseType() => _redirectedBaseProvider?.Type; - // The emitter overrides BuildBaseModelProvider to keep it in sync with the redirected BaseType. protected override ModelProvider? BuildBaseModelProvider() => _redirectedBaseProvider; } From c629e4c3e7a56777bec025694fd2b0bdfaba5cdf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 17:38:08 +0000 Subject: [PATCH 07/11] add CSharpTypeMap fallback in BuildBaseType; clean up comments Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/6271591a-13ca-4143-abe7-d960f9575dec Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 28 +++++-------------- .../ModelProviders/ModelProviderTests.cs | 14 ---------- 2 files changed, 7 insertions(+), 35 deletions(-) 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 b044c7f5553..d67fa524ed4 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 @@ -187,9 +187,6 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N protected override CSharpType? BuildBaseType() { - // Custom code may redirect the base class to either another generated model - // (with or without a resolved namespace) or to an external type. Resolve it first - // so that BaseType is the source of truth and BaseModelProvider can derive from it. if (CustomCodeView?.BaseType != null) { var customBase = CustomCodeView.BaseType; @@ -199,7 +196,6 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N // was not also defined in custom code so Roslyn does not recognize it. if (string.IsNullOrEmpty(customBase.Namespace)) { - // Cheap check: the base model may already be created and registered under the right name. if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue( customBase.Name, out var resolvedProvider) && resolvedProvider is ModelProvider resolvedModel) @@ -209,7 +205,6 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N // Force-create all input models so that visitors run (which may rename models // via TypeProvider.Update) and TypeProvidersByName is fully populated. - // This is a no-op for models that have already been created. foreach (var model in CodeModelGenerator.Instance.InputLibrary.InputNamespace.Models) { CodeModelGenerator.Instance.TypeFactory.CreateModel(model); @@ -223,8 +218,13 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N } } - // Custom base type with a namespace (or unresolvable empty-namespace name): return as-is. - // BuildBaseModelProvider will look it up in CSharpTypeMap to find a matching ModelProvider, if any. + if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue( + customBase, out var mappedProvider) && + mappedProvider is ModelProvider mappedModel) + { + return mappedModel.Type; + } + return customBase; } @@ -233,8 +233,6 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N return null; } - // CreateModel registers the resulting ModelProvider in CSharpTypeMap, which is what - // BuildBaseModelProvider uses to resolve BaseType back to its ModelProvider. return CodeModelGenerator.Instance.TypeFactory.CreateModel(_inputModel.BaseModel)?.Type; } @@ -339,20 +337,8 @@ private static bool IsDiscriminator(InputProperty property) return property is InputModelProperty modelProperty && modelProperty.IsDiscriminator; } - /// - /// Builds the representing the base model of this model. - /// By default this looks up in the - /// and returns the registered , if any. This means emitters that override - /// to redirect the generated C# base class get a consistent - /// automatically — it will be the matching generated model when the - /// new base type is another generated model, or when it is an external - /// (e.g., framework) type. - /// protected virtual ModelProvider? BuildBaseModelProvider() { - // Read BaseType (not BuildBaseType()) so the result is cached and so we pick up - // any TypeProvider-level fallbacks (e.g., CustomCodeView?.BaseType). This is safe - // from recursion because BuildBaseType no longer reads BaseModelProvider. var baseType = BaseType; if (baseType is null) { 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 22e2085920f..18f66db0fc0 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 @@ -422,9 +422,6 @@ public void BuildBaseType() Assert.AreEqual(baseModel!.Type, derivedModel!.Type.BaseType); } - // Verifies an emitter that overrides BuildBaseType to redirect to another generated - // ModelProvider gets a consistent BaseModelProvider automatically (resolved via CSharpTypeMap) - // without having to override BuildBaseModelProvider. [Test] public void OverridingBuildBaseType_AutoResolvesBaseModelProviderForGeneratedModel() { @@ -439,9 +436,6 @@ public void OverridingBuildBaseType_AutoResolvesBaseModelProviderForGeneratedMod Assert.AreSame(baseProvider, derivedProvider.BaseModelProvider); } - // Verifies an emitter that overrides BuildBaseType to redirect to a framework / external - // type (not present in CSharpTypeMap as a ModelProvider) gets BaseModelProvider == null - // automatically — BaseType and BaseModelProvider stay consistent without an extra override. [Test] public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForFrameworkType() { @@ -454,9 +448,6 @@ public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForFramew Assert.IsNull(derivedProvider.BaseModelProvider); } - // Verifies an emitter can still explicitly override BuildBaseModelProvider (e.g., to point - // at a custom ModelProvider instance not registered in CSharpTypeMap) and have its override - // honored in addition to BuildBaseType. [Test] public void OverridingBuildBaseModelProvider_IsRespected() { @@ -470,8 +461,6 @@ public void OverridingBuildBaseModelProvider_IsRespected() Assert.AreSame(customBaseProvider, derivedProvider.BaseModelProvider); } - // Default case: when no overrides and no customizations are involved, BaseType and - // BaseModelProvider both resolve to the generated base model. [Test] public void BaseModelProvider_DefaultResolvesViaCSharpTypeMap() { @@ -484,8 +473,6 @@ public void BaseModelProvider_DefaultResolvesViaCSharpTypeMap() Assert.AreEqual(derivedProvider.BaseModelProvider!.Type, derivedProvider.BaseType); } - // When the input model has no base model and there are no overrides, both BaseType and - // BaseModelProvider are null. [Test] public void BaseModelProvider_NullWhenNoBase() { @@ -513,7 +500,6 @@ public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, CSharpTyp _redirectedBaseType = redirectedBaseType; } - // Only BuildBaseType is overridden — BuildBaseModelProvider is left to resolve via CSharpTypeMap. protected override CSharpType? BuildBaseType() => _redirectedBaseType; } From a8561505aa627b144731aae88dc80d6138853032 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 17:51:55 +0000 Subject: [PATCH 08/11] add test: BuildBaseType to non-ModelProvider TypeProvider auto-resolves BaseModelProvider to null Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/b96d6016-7d00-4f8f-904a-f073b5f2e121 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelProviders/ModelProviderTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 18f66db0fc0..d49c92448dd 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 @@ -484,6 +484,26 @@ public void BaseModelProvider_NullWhenNoBase() Assert.IsNull(modelProvider.BaseModelProvider); } + [Test] + public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForNonModelTypeProvider() + { + var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); + var nonModelTypeProvider = new NonModelTypeProvider(); + CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[nonModelTypeProvider.Type] = nonModelTypeProvider; + + var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, nonModelTypeProvider.Type); + + Assert.AreEqual(nonModelTypeProvider.Type, derivedProvider.BaseType); + Assert.IsNull(derivedProvider.BaseModelProvider); + } + + private class NonModelTypeProvider : TypeProvider + { + protected override string BuildRelativeFilePath() => "."; + protected override string BuildName() => "NonModelBase"; + protected override string BuildNamespace() => "Custom.Namespace"; + } + private class CustomBaseModelProvider : ModelProvider { public CustomBaseModelProvider(InputModelType inputModel) : base(inputModel) { } From abc39f9973e098c7cc0d2884a5dc15d24cf2f4d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 19:06:24 +0000 Subject: [PATCH 09/11] use CreateModel factory in OverridingBuildBaseModelProvider_IsRespected test Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/2e5a0923-a771-4653-b62b-63eb879324ab Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../Providers/ModelProviders/ModelProviderTests.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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 d49c92448dd..e92641d4723 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 @@ -453,11 +453,12 @@ public void OverridingBuildBaseModelProvider_IsRespected() { var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); - var customBaseProvider = new CustomBaseModelProvider(inputBase); + var customBaseProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + Assert.IsNotNull(customBaseProvider); - var derivedProvider = new ExplicitBaseModelProviderOverridingModelProvider(inputDerived, customBaseProvider); + var derivedProvider = new ExplicitBaseModelProviderOverridingModelProvider(inputDerived, customBaseProvider!); - Assert.AreEqual(customBaseProvider.Type, derivedProvider.BaseType); + Assert.AreEqual(customBaseProvider!.Type, derivedProvider.BaseType); Assert.AreSame(customBaseProvider, derivedProvider.BaseModelProvider); } @@ -504,13 +505,6 @@ private class NonModelTypeProvider : TypeProvider protected override string BuildNamespace() => "Custom.Namespace"; } - private class CustomBaseModelProvider : ModelProvider - { - public CustomBaseModelProvider(InputModelType inputModel) : base(inputModel) { } - - protected override string BuildName() => "CustomBase"; - } - private class BuildBaseTypeOverridingModelProvider : ModelProvider { private readonly CSharpType? _redirectedBaseType; From d389f2a81e289d0cc15764ee1660fdc457b74c93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 19:11:39 +0000 Subject: [PATCH 10/11] use createModelCore mock factory in custom-ModelProvider tests Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/18ff54a4-2957-47ba-a9fe-dad844a23b49 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../ModelProviders/ModelProviderTests.cs | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) 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 e92641d4723..157c9c478b1 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 @@ -427,13 +427,27 @@ public void OverridingBuildBaseType_AutoResolvesBaseModelProviderForGeneratedMod { var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); - var baseProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); - Assert.IsNotNull(baseProvider); + ModelProvider? baseProvider = null; + MockHelpers.LoadMockGenerator(createModelCore: input => + { + if (input == inputBase) + { + return baseProvider = new ModelProvider(input); + } + if (input == inputDerived) + { + return new BuildBaseTypeOverridingModelProvider(input, baseProvider!.Type); + } + return null; + }); - var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, baseProvider!.Type); + var actualBase = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + var actualDerived = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputDerived); - Assert.AreEqual(baseProvider.Type, derivedProvider.BaseType); - Assert.AreSame(baseProvider, derivedProvider.BaseModelProvider); + Assert.IsNotNull(actualBase); + Assert.IsNotNull(actualDerived); + Assert.AreEqual(actualBase!.Type, actualDerived!.BaseType); + Assert.AreSame(actualBase, actualDerived.BaseModelProvider); } [Test] @@ -441,11 +455,14 @@ public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForFramew { var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); var frameworkBase = new CSharpType(typeof(InvalidOperationException)); + MockHelpers.LoadMockGenerator(createModelCore: input => + input == inputDerived ? new BuildBaseTypeOverridingModelProvider(input, frameworkBase) : null); - var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, frameworkBase); + var actualDerived = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputDerived); - Assert.AreEqual(frameworkBase, derivedProvider.BaseType); - Assert.IsNull(derivedProvider.BaseModelProvider); + Assert.IsNotNull(actualDerived); + Assert.AreEqual(frameworkBase, actualDerived!.BaseType); + Assert.IsNull(actualDerived.BaseModelProvider); } [Test] @@ -453,13 +470,27 @@ public void OverridingBuildBaseModelProvider_IsRespected() { var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); - var customBaseProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); - Assert.IsNotNull(customBaseProvider); + ModelProvider? baseProvider = null; + MockHelpers.LoadMockGenerator(createModelCore: input => + { + if (input == inputBase) + { + return baseProvider = new ModelProvider(input); + } + if (input == inputDerived) + { + return new ExplicitBaseModelProviderOverridingModelProvider(input, baseProvider!); + } + return null; + }); - var derivedProvider = new ExplicitBaseModelProviderOverridingModelProvider(inputDerived, customBaseProvider!); + var actualBase = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); + var actualDerived = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputDerived); - Assert.AreEqual(customBaseProvider!.Type, derivedProvider.BaseType); - Assert.AreSame(customBaseProvider, derivedProvider.BaseModelProvider); + Assert.IsNotNull(actualBase); + Assert.IsNotNull(actualDerived); + Assert.AreEqual(actualBase!.Type, actualDerived!.BaseType); + Assert.AreSame(actualBase, actualDerived.BaseModelProvider); } [Test] @@ -490,12 +521,15 @@ public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForNonMod { var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); var nonModelTypeProvider = new NonModelTypeProvider(); + MockHelpers.LoadMockGenerator(createModelCore: input => + input == inputDerived ? new BuildBaseTypeOverridingModelProvider(input, nonModelTypeProvider.Type) : null); CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[nonModelTypeProvider.Type] = nonModelTypeProvider; - var derivedProvider = new BuildBaseTypeOverridingModelProvider(inputDerived, nonModelTypeProvider.Type); + var actualDerived = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputDerived); - Assert.AreEqual(nonModelTypeProvider.Type, derivedProvider.BaseType); - Assert.IsNull(derivedProvider.BaseModelProvider); + Assert.IsNotNull(actualDerived); + Assert.AreEqual(nonModelTypeProvider.Type, actualDerived!.BaseType); + Assert.IsNull(actualDerived.BaseModelProvider); } private class NonModelTypeProvider : TypeProvider From e35a22eae82bd0cb16a7bbe4920690db706dfa3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 19:18:58 +0000 Subject: [PATCH 11/11] make BuildBaseModelProvider private and remove override-respect test Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/488ed6a5-0f9f-409a-896f-0c806507dc52 Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com> --- .../src/Providers/ModelProvider.cs | 2 +- .../ModelProviders/ModelProviderTests.cs | 42 ------------------- 2 files changed, 1 insertion(+), 43 deletions(-) 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 d67fa524ed4..e916fb35c29 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 @@ -337,7 +337,7 @@ private static bool IsDiscriminator(InputProperty property) return property is InputModelProperty modelProperty && modelProperty.IsDiscriminator; } - protected virtual ModelProvider? BuildBaseModelProvider() + private ModelProvider? BuildBaseModelProvider() { var baseType = BaseType; if (baseType is null) 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 157c9c478b1..39f189fb0af 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 @@ -465,34 +465,6 @@ public void OverridingBuildBaseType_AutoResolvesBaseModelProviderToNullForFramew Assert.IsNull(actualDerived.BaseModelProvider); } - [Test] - public void OverridingBuildBaseModelProvider_IsRespected() - { - var inputBase = InputFactory.Model("baseModel", usage: InputModelTypeUsage.Input, properties: []); - var inputDerived = InputFactory.Model("derivedModel", usage: InputModelTypeUsage.Input, properties: []); - ModelProvider? baseProvider = null; - MockHelpers.LoadMockGenerator(createModelCore: input => - { - if (input == inputBase) - { - return baseProvider = new ModelProvider(input); - } - if (input == inputDerived) - { - return new ExplicitBaseModelProviderOverridingModelProvider(input, baseProvider!); - } - return null; - }); - - var actualBase = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputBase); - var actualDerived = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputDerived); - - Assert.IsNotNull(actualBase); - Assert.IsNotNull(actualDerived); - Assert.AreEqual(actualBase!.Type, actualDerived!.BaseType); - Assert.AreSame(actualBase, actualDerived.BaseModelProvider); - } - [Test] public void BaseModelProvider_DefaultResolvesViaCSharpTypeMap() { @@ -551,20 +523,6 @@ public BuildBaseTypeOverridingModelProvider(InputModelType inputModel, CSharpTyp protected override CSharpType? BuildBaseType() => _redirectedBaseType; } - private class ExplicitBaseModelProviderOverridingModelProvider : ModelProvider - { - private readonly ModelProvider _redirectedBaseProvider; - - public ExplicitBaseModelProviderOverridingModelProvider(InputModelType inputModel, ModelProvider redirectedBaseProvider) : base(inputModel) - { - _redirectedBaseProvider = redirectedBaseProvider; - } - - protected override CSharpType? BuildBaseType() => _redirectedBaseProvider?.Type; - - protected override ModelProvider? BuildBaseModelProvider() => _redirectedBaseProvider; - } - [Test] public void BuildModelAsStruct() {