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 @@ -94,7 +94,7 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m
_additionalBinaryDataProperty = new(GetAdditionalBinaryDataPropertiesProp);
_additionalProperties = new(() => [.. _model.Properties.Where(p => p.IsAdditionalProperties)]);
_shouldOverrideMethods = _model.BaseModelProvider != null && !_isStruct;
_shouldSkipDerivedSerializationMethodOverrides = _model.BaseModelProvider?.ShouldSkipDerivedSerializationMethodOverrides == true;
_shouldSkipDerivedSerializationMethodOverrides = ShouldSkipDerivedSerializationMethodOverrides(_model.BaseModelProvider);
_utf8JsonWriterSnippet = _utf8JsonWriterParameter.As<Utf8JsonWriter>();
_mrwOptionsParameterSnippet = _serializationOptionsParameter.As<ModelReaderWriterOptions>();
_jsonElementParameterSnippet = _jsonElementDeserializationParam.As<JsonElement>();
Expand Down Expand Up @@ -157,6 +157,57 @@ private static bool IsModelType(CSharpType type)
=> ScmCodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(type, out var baseProvider) &&
baseProvider is ModelProvider;

/// <summary>
/// Determines whether a derived model should skip overriding the generated serialization
/// <c>*Core</c> methods of its base.
/// </summary>
/// <remarks>
/// External/system base models are represented by a <see cref="SystemObjectModelProvider"/>.
/// Such a base only participates in the generated MRW <c>*Core</c> override chain when the
/// wrapped framework/referenced type itself follows the model-reader-writer serialization
/// pattern (i.e. implements <see cref="IJsonModel{T}"/> or <see cref="IPersistableModel{T}"/>,
/// and therefore exposes the overridable <c>*Core</c> methods). When it does, derived models
/// must override those methods rather than hide them (otherwise the compiler reports CS0114).
/// When it does not (for example a hand-authored base such as <c>ResourceData</c>), derived
/// models re-introduce the methods as <c>virtual</c>.
/// </remarks>
private static bool ShouldSkipDerivedSerializationMethodOverrides(ModelProvider? baseModelProvider)
{
if (baseModelProvider is null)
{
return false;
}

if (baseModelProvider is SystemObjectModelProvider systemBase)
{
return !SystemTypeImplementsModelReaderWriter(systemBase.SystemType);
}

return baseModelProvider.ShouldSkipDerivedSerializationMethodOverrides;
}

private static bool SystemTypeImplementsModelReaderWriter(CSharpType systemType)
{
if (!systemType.IsFrameworkType)
{
return false;
}

foreach (var @interface in systemType.FrameworkType.GetInterfaces())
{
if (@interface.IsGenericType)
{
var definition = @interface.GetGenericTypeDefinition();
if (definition == typeof(IJsonModel<>) || definition == typeof(IPersistableModel<>))
{
return true;
}
}
}

return false;
}

protected override ConstructorProvider[] BuildConstructors()
{
List<ConstructorProvider> constructors = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// Licensed under the MIT License.

using System;
using System.ClientModel.Primitives;
using System.Linq;
using System.Text.Json;
using Microsoft.TypeSpec.Generator.ClientModel.Providers;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Primitives;
Expand Down Expand Up @@ -201,5 +203,127 @@ public void JsonModelCreateCore_IsOverride_WhenBaseIsRegularModel()
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
"JsonModelCreateCore should be 'override' with regular base model");
}

/// <summary>
/// Creates a derived model whose SystemObjectModelProvider base wraps a framework type that
/// itself follows the generated MRW pattern (declares the protected virtual *Core methods).
/// </summary>
private static (ModelProvider Model, MrwSerializationTypeDefinition Serialization) CreateDerivedModelWithMrwSystemBase()
{
var baseProp = InputFactory.Property("Name", InputPrimitiveType.String);
var baseInputModel = InputFactory.Model("Resource", properties: [baseProp]);
var derivedProp = InputFactory.Property("Location", InputPrimitiveType.String);
var derivedInputModel = InputFactory.Model("TrackedResource", properties: [derivedProp], baseModel: baseInputModel);

// The base wraps a framework type that declares the generated MRW *Core methods, so a
// derived model must override them (mirrors deriving from an external MRW-generated type).
var systemType = new CSharpType(typeof(FakeMrwBase));
var systemBase = new SystemObjectModelProvider(systemType, baseInputModel);

var generator = MockHelpers.LoadMockGenerator(
inputModels: () => [baseInputModel, derivedInputModel],
createModelCore: (model) =>
{
if (model.Name == "Resource")
return systemBase;
return new ModelProvider(model);
},
createSerializationsCore: (inputType, typeProvider) =>
inputType is InputModelType modelType && typeProvider is ModelProvider mp
? [new MrwSerializationTypeDefinition(modelType, mp)]
: []);
generator.Object.TypeFactory.RootInputModels.Add(derivedInputModel);
generator.Object.TypeFactory.RootOutputModels.Add(derivedInputModel);

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

var serializations = derived.SerializationProviders;
Assert.AreEqual(1, serializations.Count);
return (derived, (MrwSerializationTypeDefinition)serializations[0]);
}

// -------------------------------------------------------------------
// When the external/system base itself follows the generated MRW pattern (declares the
// protected virtual *Core methods), the derived model must OVERRIDE them rather than
// re-introduce them as virtual. Regression test for issue #11042 where deriving from an
// external MRW-generated base (e.g. OpenAI's ResponseItem) produced CS0114.
// -------------------------------------------------------------------

[Test]
public void PersistableModelCreateCore_IsOverride_WhenSystemBaseDeclaresCoreMethods()
{
var (_, serialization) = CreateDerivedModelWithMrwSystemBase();
var method = serialization.BuildPersistableModelCreateCoreMethod();

Assert.IsNotNull(method);
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
"PersistableModelCreateCore should be 'override' when the system base declares the *Core methods");
Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual),
"PersistableModelCreateCore should NOT be 'virtual' when the system base declares the *Core methods");
}

[Test]
public void JsonModelCreateCore_IsOverride_WhenSystemBaseDeclaresCoreMethods()
{
var (_, serialization) = CreateDerivedModelWithMrwSystemBase();
var method = serialization.BuildJsonModelCreateCoreMethod();

Assert.IsNotNull(method);
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
"JsonModelCreateCore should be 'override' when the system base declares the *Core methods");
Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual),
"JsonModelCreateCore should NOT be 'virtual' when the system base declares the *Core methods");
}

[Test]
public void PersistableModelWriteCore_IsOverride_WhenSystemBaseDeclaresCoreMethods()
{
var (_, serialization) = CreateDerivedModelWithMrwSystemBase();
var method = serialization.BuildPersistableModelWriteCoreMethod();

Assert.IsNotNull(method);
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
"PersistableModelWriteCore should be 'override' when the system base declares the *Core methods");
Assert.IsFalse(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Virtual),
"PersistableModelWriteCore should NOT be 'virtual' when the system base declares the *Core methods");
}

[Test]
public void JsonModelWriteCore_IsOverride_WhenSystemBaseDeclaresCoreMethods()
{
var (_, serialization) = CreateDerivedModelWithMrwSystemBase();
var method = serialization.BuildJsonModelWriteCoreMethod();

Assert.IsNotNull(method);
Assert.IsTrue(method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Override),
"JsonModelWriteCore should be 'override' when the system base declares the *Core methods");
}

/// <summary>
/// A stand-in for an external framework type generated by the MRW emitter. It implements
/// <see cref="IJsonModel{T}"/> (and therefore <see cref="IPersistableModel{T}"/>) just as a
/// generated model does, so the base is recognized as participating in the serialization
/// override chain.
/// </summary>
private class FakeMrwBase : IJsonModel<FakeMrwBase>
{
void IJsonModel<FakeMrwBase>.Write(Utf8JsonWriter writer, ModelReaderWriterOptions options)
{
}

FakeMrwBase IJsonModel<FakeMrwBase>.Create(ref Utf8JsonReader reader, ModelReaderWriterOptions options)
=> this;

BinaryData IPersistableModel<FakeMrwBase>.Write(ModelReaderWriterOptions options)
=> BinaryData.FromString(string.Empty);

FakeMrwBase IPersistableModel<FakeMrwBase>.Create(BinaryData data, ModelReaderWriterOptions options)
=> this;

string IPersistableModel<FakeMrwBase>.GetFormatFromOptions(ModelReaderWriterOptions options)
=> "J";
}
}
}
Loading