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
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-csharp"
---

Make `ModelProvider.BaseModelProvider` derive from `BaseType` so the two cannot return mismatched values when a derived `ModelProvider` overrides `BuildBaseType` or `BaseType`.
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,69 @@ protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.N
CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace :
CodeModelGenerator.Instance.TypeFactory.GetCleanNameSpace(_inputModel.Namespace);

public override CSharpType? BaseType => _baseType ??= ResolveBaseType();

private CSharpType? ResolveBaseType()
{
// CustomCodeView wins over the spec-derived base, mirroring the precedence the
// pre-existing BaseModelProvider logic enforced.
var customBase = CustomCodeView?.BaseType;
if (customBase != null)
{
// If the custom base has no resolved namespace, Roslyn could not see the base
// symbol (typical when the base is a generated model that wasn't itself defined
// in custom code). Resolve it by name against the generated providers.
if (string.IsNullOrEmpty(customBase.Namespace))
{
var resolved = ResolveModelProviderByName(customBase.Name);
if (resolved != null)
{
return resolved.Type;
}
}

return customBase;
}

return BuildBaseType();
}

private static ModelProvider? ResolveModelProviderByName(string name)
{
// Cheap check: the base model may already be created and registered under the right name.
if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue(
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(
name, out resolvedProvider) &&
resolvedProvider is ModelProvider resolvedAfterCreate)
{
return resolvedAfterCreate;
}

return null;
}

protected override CSharpType? BuildBaseType()
{
return BaseModelProvider?.Type;
if (_inputModel.BaseModel == null)
{
return null;
}

return CodeModelGenerator.Instance.TypeFactory.CreateModel(_inputModel.BaseModel)?.Type;
}

protected override TypeProvider[] BuildSerializationProviders()
Expand Down Expand Up @@ -289,65 +349,25 @@ private static bool IsDiscriminator(InputProperty property)
return property is InputModelProperty modelProperty && modelProperty.IsDiscriminator;
}

private ModelProvider? BuildBaseModelProvider()
protected ModelProvider? BuildBaseModelProvider()
{
// consider models that have been customized to inherit from a different generated model
if (CustomCodeView?.BaseType != null)
// BaseType is the source of truth: it already merges the spec-derived base with any
// CustomCodeView base (and resolves namespace-less custom bases). BaseModelProvider is
// simply the matching generated ModelProvider for that BaseType, or null if BaseType
// is external / hand-written.
var baseType = BaseType;
if (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;
}
return null;
}

if (_inputModel.BaseModel == null)
if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var baseProvider) &&
baseProvider is ModelProvider baseModel)
{
return null;
return baseModel;
}

return CodeModelGenerator.Instance.TypeFactory.CreateModel(_inputModel.BaseModel);
return null;
}

private List<FieldProvider> BuildAdditionalPropertyFields()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ private TypeSignatureModifiers BuildDeclarationModifiersInternal()

private protected virtual bool FilterCustomizedMembers => true;

public CSharpType? BaseType => _baseType ??= BuildBaseType() ?? CustomCodeView?.BaseType;
private CSharpType? _baseType;
public virtual CSharpType? BaseType => _baseType ??= BuildBaseType() ?? CustomCodeView?.BaseType;
private protected CSharpType? _baseType;

public WhereExpression? WhereClause => _whereClause ??= BuildWhereClause();
private WhereExpression? _whereClause;
Expand Down
Loading