Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,58 @@ private IReadOnlyList<ModelProvider> BuildDerivedModels()

return [.. derivedModels];
}
internal override TypeProvider? BaseTypeProvider => BaseModelProvider;
internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider();
private TypeProvider? _baseTypeProvider;

private TypeProvider? BuildBaseTypeProvider()
{
// First check if there's a generated base model
if (BaseModelProvider != null)
{
return BaseModelProvider;
}

// If there's a custom base type that's not a generated model, create a provider for it
if (CustomCodeView?.BaseType != null && !string.IsNullOrEmpty(CustomCodeView.BaseType.Namespace))
{
var baseType = CustomCodeView.BaseType;

// Try to find it in the CSharpTypeMap first
if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var existingProvider))
{
return existingProvider;
}

// If not found, try to look it up from Roslyn's customization compilation
var customization = CodeModelGenerator.Instance.SourceInputModel.Customization;
if (customization != null)
{
// Construct the fully qualified metadata name in .NET format
// For generic types, this uses backtick notation (e.g., "System.Collections.Generic.List`1")
var fullyQualifiedName = baseType.IsGenericType
? $"{baseType.Namespace}.{baseType.Name}`{baseType.Arguments.Count}"
: $"{baseType.Namespace}.{baseType.Name}";

var baseTypeSymbol = customization.GetTypeByMetadataName(fullyQualifiedName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Here you are assuming you can get the TypeSymbol of the base type from the custom code.
What if the base type is not defined within the custom code, then you can't get the TypeSymbol.
We need to add a new implementation of TypeProvider, let's name it SystemObjectType, which represents a type from another assembly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right. I've added SystemObjectTypeProvider to handle cases where the base type is from a referenced assembly and cannot be found via GetTypeByMetadataName.

The implementation now:

  1. First tries to find the type in CSharpTypeMap (cached)
  2. Then tries GetTypeByMetadataName on the customization compilation (creates NamedTypeSymbolProvider if found)
  3. Falls back to creating a SystemObjectTypeProvider if the type symbol is not found

Fixed in commit 75c90e5.

if (baseTypeSymbol != null)
{
var baseTypeProvider = new NamedTypeSymbolProvider(baseTypeSymbol, customization);
// Cache it in CSharpTypeMap for future lookups
CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = baseTypeProvider;
return baseTypeProvider;
}
}

// If we couldn't find the type symbol (e.g., type is from a referenced assembly not in customization),
// create a SystemObjectTypeProvider that represents the external type
var systemObjectTypeProvider = new SystemObjectTypeProvider(baseType);
// Cache it in CSharpTypeMap for future lookups
CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = systemObjectTypeProvider;
return systemObjectTypeProvider;
}

return null;
}

public ModelProvider? BaseModelProvider
=> _baseModelProvider ??= BuildBaseModelProvider();
Expand Down Expand Up @@ -241,7 +292,7 @@ private static bool IsDiscriminator(InputProperty property)

private ModelProvider? BuildBaseModelProvider()
{
// consider models that have been customized to inherit from a different model
// consider models that have been customized to inherit from a different generated model
if (CustomCodeView?.BaseType != null)
{
var baseType = CustomCodeView.BaseType;
Expand All @@ -256,13 +307,22 @@ private static bool IsDiscriminator(InputProperty property)
baseType = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(baseInputModel);
}
}

// 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Statements;

namespace Microsoft.TypeSpec.Generator.Providers
{
/// <summary>
/// Represents a type from an external assembly (system or referenced assembly) that is not part of the current generation.
/// This provider is used when a generated model inherits from a type that exists in a referenced assembly
/// but doesn't have a Roslyn type symbol available in the customization compilation.
/// </summary>
internal sealed class SystemObjectTypeProvider : TypeProvider
{
private readonly CSharpType _type;

public SystemObjectTypeProvider(CSharpType type)
{
_type = type ?? throw new ArgumentNullException(nameof(type));

if (string.IsNullOrEmpty(_type.Namespace))
{
throw new ArgumentException("Type must have a namespace", nameof(type));
}
}

private protected sealed override TypeProvider? BuildCustomCodeView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;
private protected sealed override TypeProvider? BuildLastContractView(string? generatedTypeName = default, string? generatedTypeNamespace = default) => null;

protected override string BuildRelativeFilePath() => throw new InvalidOperationException("This type should not be writing in generation");

protected override string BuildName() => _type.Name;

protected override string BuildNamespace() => _type.Namespace;

protected override IReadOnlyList<AttributeStatement> BuildAttributes() => [];

protected override CSharpType? BuildBaseType() => _type.BaseType;

protected override TypeSignatureModifiers BuildDeclarationModifiers()
{
// Default to public class since we don't have symbol information
return TypeSignatureModifiers.Public | TypeSignatureModifiers.Class;
}

protected internal override FieldProvider[] BuildFields() => [];

protected internal override PropertyProvider[] BuildProperties() => [];

protected internal override ConstructorProvider[] BuildConstructors() => [];

protected internal override MethodProvider[] BuildMethods() => [];

protected override bool GetIsEnum() => false;

protected override CSharpType BuildEnumUnderlyingType() => throw new InvalidOperationException("This type is not an enum");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,79 @@ private class TestNamespaceVisitor : NameSpaceVisitor
}
}

[Test]
public async Task CanCustomizeBaseModelToExternalType()
{
// This test verifies that a model can be customized to inherit from an external base type
// that is not generated during the current generation run (e.g., from another assembly)
var childModel = InputFactory.Model(
"mockInputModel",
properties: [InputFactory.Property("childProp", InputPrimitiveType.String)],
usage: InputModelTypeUsage.Json);

var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [childModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel") as ModelProvider;

// Should have customized base type from external assembly
Assert.IsNotNull(modelProvider);
Assert.IsNotNull(modelProvider!.BaseType);
Assert.IsNotNull(modelProvider.BaseTypeProvider);
Assert.AreEqual("ExternalBaseModel", modelProvider.BaseType!.Name);
Assert.AreEqual("Sample.Models", modelProvider.BaseType!.Namespace);

// The BaseModelProvider should be null since the base is not a generated model
Assert.IsNull(modelProvider.BaseModelProvider);

// BaseTypeProvider should be NamedTypeSymbolProvider because ExternalBaseModel
// is defined in the same file and will be found in the customization compilation
Assert.IsInstanceOf<NamedTypeSymbolProvider>(modelProvider.BaseTypeProvider,
"ExternalBaseModel is in the customization compilation and should use NamedTypeSymbolProvider");

// It should have the properties from the symbol
// ExternalBaseModel has 2 properties: ExternalProperty and ExternalDictionary
Assert.AreEqual(2, modelProvider.BaseTypeProvider!.Properties.Count,
"ExternalBaseModel should have ExternalProperty and ExternalDictionary");
var externalPropertyNames = modelProvider.BaseTypeProvider.Properties.Select(p => p.Name).ToList();
Assert.Contains("ExternalProperty", externalPropertyNames);
Assert.Contains("ExternalDictionary", externalPropertyNames);
}

[Test]
public async Task CanCustomizeBaseModelToSystemType()
{
// This test verifies that a model can be customized to inherit from a system type
// (e.g., System.Exception) which simulates inheriting from types like
// Azure.ResourceManager.TrackedResourceData that are from referenced assemblies.
var childModel = InputFactory.Model(
"mockInputModel",
properties: [InputFactory.Property("childProp", InputPrimitiveType.String)],
usage: InputModelTypeUsage.Json);

var mockGenerator = await MockHelpers.LoadMockGeneratorAsync(
inputModelTypes: [childModel],
compilation: async () => await Helpers.GetCompilationFromDirectoryAsync());

var modelProvider = mockGenerator.Object.OutputLibrary.TypeProviders.Single(t => t.Name == "MockInputModel") as ModelProvider;

// Should have customized base type from system library
Assert.IsNotNull(modelProvider);
Assert.IsNotNull(modelProvider!.BaseType);
Assert.IsNotNull(modelProvider.BaseTypeProvider);
Assert.AreEqual("Exception", modelProvider.BaseType!.Name);
Assert.AreEqual("System", modelProvider.BaseType!.Namespace);

// The BaseModelProvider should be null since the base is not a generated model
Assert.IsNull(modelProvider.BaseModelProvider);

// System types from referenced assemblies are found via GetTypeByMetadataName
// because the test compilation includes system assembly references
Assert.IsInstanceOf<NamedTypeSymbolProvider>(modelProvider.BaseTypeProvider,
"System.Exception is found in the compilation and uses NamedTypeSymbolProvider");
}

private class TestNameVisitor : NameVisitor
{
public TypeProvider? InvokeVisit(TypeProvider type)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#nullable disable

using Sample.Models;
using System.Collections.Generic;

namespace Sample.Models
{
public partial class MockInputModel : ExternalBaseModel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can you add another test case with custom code, which updated the base model to a system type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test CanCustomizeBaseModelToSystemType that inherits from System.Exception. This demonstrates that models can successfully inherit from system types like Azure.ResourceManager.TrackedResourceData. The test shows that system types from referenced assemblies are found via GetTypeByMetadataName and use NamedTypeSymbolProvider.

Fixed in commit 09c89ed.

{
}

// This simulates an external base type from another assembly/namespace
// that is not generated by the current generation run
public class ExternalBaseModel
{
public string ExternalProperty { get; set; }
public IDictionary<string, string> ExternalDictionary { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#nullable disable

using System;

namespace Sample.Models
{
// This test case shows a model inheriting from a system type (System.Exception).
// This simulates inheriting from types like Azure.ResourceManager.TrackedResourceData
// which are from referenced assemblies.
public partial class MockInputModel : Exception
{
}
}