Skip to content
Merged
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 @@ -7,6 +7,8 @@
using System.Linq;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Providers;
using Microsoft.TypeSpec.Generator.Tests.Common;
using NUnit.Framework;

Expand Down Expand Up @@ -144,6 +146,53 @@ public void TestCreateSerializations_ReturnsBothMrwAndMultipart_WhenJsonAndMpfdU
"Expected a multipart serialization provider for a model with MultipartFormData usage.");
}

// ScmTypeFactory overrides CreateModelCore to return ScmModelProvider. External-type
// handling lives in the (non-overridable) base TypeFactory.CreateModel, so it must still
// apply here. This guards against regressing the fix by re-introducing external handling
// into CreateModelCore only, which would silently drop the discriminator for the Scm path.
[Test]
public void ExternalBaseModel_MapsToSystemObjectModelProvider_AndForwardsDiscriminator_ThroughScmTypeFactory()
{
var baseModel = InputFactory.Model(
"Animal",
properties:
[
InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true),
InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
],
external: new InputExternalTypeMetadata("System.Exception", null, null));
var derivedModel = InputFactory.Model(
"Pet",
baseModel: baseModel,
discriminatedKind: "pet",
properties:
[
InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true),
InputFactory.Property("trained", InputPrimitiveType.Boolean, isRequired: true),
]);

MockHelpers.LoadMockGenerator(inputModels: () => [baseModel, derivedModel]);

// The external base maps to a SystemObjectModelProvider even though ScmTypeFactory
// overrides CreateModelCore.
var baseProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(baseModel);
Assert.IsInstanceOf<SystemObjectModelProvider>(baseProvider);

// The derived model is a regular ScmModelProvider whose base is the SystemObjectModelProvider.
var derivedProvider = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(derivedModel) as ModelProvider;
Assert.IsNotNull(derivedProvider);
Assert.IsInstanceOf<ScmModelProvider>(derivedProvider);
Assert.IsInstanceOf<SystemObjectModelProvider>(derivedProvider!.BaseModelProvider);

// Some constructor forwards the discriminator literal to the external base.
var forwardsDiscriminator = derivedProvider.Constructors.Any(
c => c.Signature.Initializer is { IsBase: true } init &&
init.Arguments.Any(a => a.ToDisplayString() == "\"pet\""));
Assert.IsTrue(
forwardsDiscriminator,
"Expected a base constructor call forwarding the discriminator value \"pet\" to the external base.");
}

private static InputModelProperty FilePartProperty(string name)
=> InputFactory.Property(
name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ private static TypeProvider[] BuildModels()
foreach (var inputModel in input.Models)
{
var outputModel = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel);
if (outputModel != null)
if (outputModel != null && outputModel is not SystemObjectModelProvider)
{
models.Add(outputModel);
var unknownVariant = inputModel.DiscriminatedSubtypes.Values.FirstOrDefault(m => m.IsUnknownDiscriminatorModel);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,13 @@ protected internal TypeFactory()
// (e.g., when BuildBaseModelProvider triggers CreateModel for all input models).
InputTypeToModelProvider[model] = null;

modelProvider = CreateModelCore(model);
// A model marked as external maps to a type that already exists in a framework or
// referenced assembly, so it must not be generated. Represent it with a
// SystemObjectModelProvider so that generated models deriving from it still get a
// ModelProvider base (enabling constructor chaining and discriminator forwarding)
// rather than a bare TypeProvider that cannot serve as a base model. This is handled
// here, before the overridable CreateModelCore, so all generators share the behavior.
modelProvider = CreateExternalModel(model) ?? CreateModelCore(model);

foreach (var visitor in Visitors)
{
Expand All @@ -203,7 +209,25 @@ protected internal TypeFactory()
return modelProvider;
}

protected virtual ModelProvider? CreateModelCore(InputModelType model) => new ModelProvider(model);
protected virtual ModelProvider? CreateModelCore(InputModelType model)
=> new ModelProvider(model);

/// <summary>
/// Maps an external <see cref="InputModelType"/> (one marked via <c>@alternateType</c>) to a
/// <see cref="SystemObjectModelProvider"/> that wraps the resolved framework/referenced type.
/// Returns <c>null</c> when the model is not external or the external type cannot be resolved,
/// in which case normal model creation proceeds.
/// </summary>
private SystemObjectModelProvider? CreateExternalModel(InputModelType model)
{
if (model.External == null)
{
return null;
}

var externalType = CreateExternalType(model.External);
return externalType != null ? new SystemObjectModelProvider(externalType, model) : null;
}

/// <summary>
/// Factory method for creating the <see cref="ModelFactoryProvider"/> that emits the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,5 +370,178 @@ public void Constructor_ThrowsOnNullSystemType()
var inputModel = InputFactory.Model("Resource", properties: []);
Assert.Throws<ArgumentNullException>(() => new SystemObjectModelProvider(null!, inputModel));
}

// -------------------------------------------------------------------
// 9. A derived discriminated model forwards its discriminator value to a
// SystemObjectModelProvider base constructor. This is impossible with
// SystemObjectTypeProvider because it cannot serve as a BaseModelProvider.
// -------------------------------------------------------------------

[Test]
public void DerivedDiscriminatedModel_ForwardsDiscriminatorToSystemObjectModelProviderBase()
{
var baseInputModel = InputFactory.Model(
"BaseModel",
properties:
[
InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true),
InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
]);
var derivedInputModel = InputFactory.Model(
"DerivedModel",
baseModel: baseInputModel,
discriminatedKind: "one",
properties:
[
InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true),
InputFactory.Property("color", InputPrimitiveType.String, isRequired: true),
]);

var systemType = CreateSystemCSharpType("BaseModelData", "TestFramework");
MockHelpers.LoadMockGenerator(
inputModelTypes: [baseInputModel, derivedInputModel],
createModelCore: (model) =>
model.Name == "BaseModel"
? new SystemObjectModelProvider(systemType, model)
: new ModelProvider(model));

var derivedProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider;
Assert.IsNotNull(derivedProvider);
Assert.IsInstanceOf<SystemObjectModelProvider>(derivedProvider!.BaseModelProvider);

var publicCtor = derivedProvider.Constructors.FirstOrDefault(
c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public));
Assert.IsNotNull(publicCtor);

var initializer = publicCtor!.Signature.Initializer;
Assert.IsNotNull(initializer);
Assert.IsTrue(initializer!.IsBase);

// The base constructor call must forward the discriminator literal "one".
Assert.IsTrue(
initializer.Arguments.Any(a => a.ToDisplayString() == "\"one\""),
"Expected the base constructor call to forward the discriminator value \"one\". " +
"Actual arguments: " + string.Join(", ", initializer.Arguments.Select(a => a.ToDisplayString())));
}

// -------------------------------------------------------------------
// 10. End-to-end: a base model marked external (External metadata) is mapped to a
// SystemObjectModelProvider by the default factory, is not emitted, and a derived
// discriminated model forwards its discriminator value to the external base.
// -------------------------------------------------------------------

[Test]
public void ExternalBaseModel_MapsToSystemObjectModelProvider_AndForwardsDiscriminator()
{
var baseInputModel = InputFactory.Model(
"BaseModel",
properties:
[
InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true),
InputFactory.Property("name", InputPrimitiveType.String, isRequired: true),
],
external: new InputExternalTypeMetadata("System.Exception", null, null));
var derivedInputModel = InputFactory.Model(
"DerivedModel",
baseModel: baseInputModel,
discriminatedKind: "one",
properties:
[
InputFactory.Property("kind", InputPrimitiveType.String, isRequired: true, isDiscriminator: true),
InputFactory.Property("color", InputPrimitiveType.String, isRequired: true),
]);

// No createModelCore override: the default (real) CreateModelCore must perform the mapping.
var mockGenerator = MockHelpers.LoadMockGenerator(
inputModelTypes: [baseInputModel, derivedInputModel]);

// The external base maps to a SystemObjectModelProvider rather than a generated model.
var baseProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(baseInputModel);
Assert.IsInstanceOf<SystemObjectModelProvider>(baseProvider);

// The derived model uses it as its base model provider.
var derivedProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(derivedInputModel) as ModelProvider;
Assert.IsNotNull(derivedProvider);
Assert.IsInstanceOf<SystemObjectModelProvider>(derivedProvider!.BaseModelProvider);

// The derived constructor forwards the discriminator value to the external base.
var publicCtor = derivedProvider.Constructors.FirstOrDefault(
c => c.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public));
Assert.IsNotNull(publicCtor);
var initializer = publicCtor!.Signature.Initializer;
Assert.IsNotNull(initializer);
Assert.IsTrue(initializer!.IsBase);
Assert.IsTrue(
initializer.Arguments.Any(a => a.ToDisplayString() == "\"one\""),
"Expected the base constructor call to forward the discriminator value \"one\". " +
"Actual arguments: " + string.Join(", ", initializer.Arguments.Select(a => a.ToDisplayString())));

// The external base is not emitted as a generated type.
Assert.IsFalse(
CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Any(t => t is SystemObjectModelProvider),
"External base models should not be emitted as generated types.");
}

// -------------------------------------------------------------------
// 11. Fallback: an external model whose type cannot be resolved is generated
// normally (as a regular ModelProvider) rather than being dropped.
// -------------------------------------------------------------------

[Test]
public void ExternalModel_ThatCannotBeResolved_FallsBackToNormalGeneration()
{
var inputModel = InputFactory.Model(
"Widget",
properties: [InputFactory.Property("name", InputPrimitiveType.String, isRequired: true)],
// Not a real framework type and no package metadata, so resolution fails.
external: new InputExternalTypeMetadata("Some.Unresolvable.ExternalType", null, null));

MockHelpers.LoadMockGenerator(inputModelTypes: [inputModel]);

var provider = CodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel);

// Unresolvable external metadata: no SystemObjectModelProvider mapping; generate normally.
Assert.IsNotNull(provider);
Assert.IsNotInstanceOf<SystemObjectModelProvider>(provider);

// And the model is still emitted as a generated type.
Assert.IsTrue(
CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Any(t => t == provider),
"An external model that cannot be resolved should still be generated.");
}

// -------------------------------------------------------------------
// 12. A property typed as an external model resolves to the external type, and the
// external model itself is not generated.
// -------------------------------------------------------------------

[Test]
public void PropertyTypedAsExternalModel_ResolvesToExternalType_AndExternalModelIsNotGenerated()
{
var externalModel = InputFactory.Model(
"ExternalThing",
properties: [InputFactory.Property("name", InputPrimitiveType.String, isRequired: true)],
external: new InputExternalTypeMetadata("System.Exception", null, null));
var containerModel = InputFactory.Model(
"Container",
properties: [InputFactory.Property("thing", externalModel, isRequired: true)]);

MockHelpers.LoadMockGenerator(inputModelTypes: [externalModel, containerModel]);

// The external model maps to a SystemObjectModelProvider and is not emitted.
var externalProvider = CodeModelGenerator.Instance.TypeFactory.CreateModel(externalModel);
Assert.IsInstanceOf<SystemObjectModelProvider>(externalProvider);
Assert.IsFalse(
CodeModelGenerator.Instance.OutputLibrary.TypeProviders.Any(t => t is SystemObjectModelProvider),
"External models must not be emitted as generated types.");

// A property typed as the external model resolves to the external framework type.
var container = CodeModelGenerator.Instance.TypeFactory.CreateModel(containerModel) as ModelProvider;
Assert.IsNotNull(container);
var thingProperty = container!.Properties.FirstOrDefault(p => p.Name == "Thing");
Assert.IsNotNull(thingProperty);
Assert.AreEqual("Exception", thingProperty!.Type.Name);
Assert.AreEqual("System", thingProperty.Type.Namespace);
}
}
}
Loading