Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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,51 @@ 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;
}
}
}

return null;
}

public ModelProvider? BaseModelProvider
=> _baseModelProvider ??= BuildBaseModelProvider();
Expand Down Expand Up @@ -241,7 +285,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 +300,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
Expand Up @@ -1507,6 +1507,41 @@ 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);

// But BaseTypeProvider should have the properties of the external base type
// 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);
}

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; }
}
}
Loading