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 @@ -662,6 +662,40 @@ public void GetUtf8BytesIsUsedForMrwFallback()
Assert.AreEqual(Helpers.GetExpectedFromFile(), methodBody);
}

[Test]
public void ReferencedExtensibleEnumUsesInlineConstruction()
{
// A property typed as a referenced extensible enum (a value-type struct) must deserialize via
// inline construction, not the generic ModelReaderWriter.Read<T> fallback (which throws at
// runtime because the struct has no MRW builder).
var externalEnum = InputFactory.StringEnum(
"ExternalKind",
[("value1", "value1"), ("value2", "value2")],
isExtensible: true,
external: new InputExternalTypeMetadata("System.Guid", null, null));
var property = InputFactory.Property("kind", externalEnum);

var inputModel = InputFactory.Model("mockInputModel", properties: [property]);
var (_, serialization) = CreateModelAndSerialization(inputModel);

var deserializationMethod = serialization.Methods.Single(m => m.Signature.Name.StartsWith("Deserialize"));
var methodBody = deserializationMethod.BodyStatements!.ToDisplayString();

Assert.IsFalse(
methodBody.Contains("ModelReaderWriter.Read<global::System.Guid>"),
"Referenced extensible enum should not use the ModelReaderWriter.Read<T> fallback.");
Assert.IsTrue(
methodBody.Contains("new global::System.Guid("),
"Referenced extensible enum should be constructed inline from its underlying value.");

// Serialization must use the enum's underlying value (ToString for a string-backed enum),
// not a model write.
var writeBody = serialization.BuildJsonModelWriteCoreMethod().BodyStatements!.ToDisplayString();
Assert.IsTrue(
writeBody.Contains("WriteStringValue(") && writeBody.Contains("ToString()"),
"Referenced extensible enum should serialize via its underlying string value.");
}

[Test]
public void TestBuildDeserializationMethodNestedSARD()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class CSharpType
{
private readonly Type? _type;
private object? _literal;
private readonly Type? _underlyingType;
private Type? _underlyingType;
private IReadOnlyList<CSharpType>? _unionItemTypes;

private bool? _isReadOnlyMemory;
Expand Down Expand Up @@ -549,6 +549,37 @@ public CSharpType WithNullable(bool isNullable)
? new CSharpType(FrameworkType, Arguments, isNullable)
: new CSharpType(Name, Namespace, IsValueType, isNullable, DeclaringType, Arguments, IsPublic, IsStruct, BaseType, _underlyingType);

// Preserve explicit enum semantics for framework types (e.g. referenced extensible enums,
// which are structs and are not recognized as enums via reflection). The framework constructor
// recomputes the underlying type from reflection and would otherwise drop it.
if (IsFrameworkType && _underlyingType is not null)
{
type._underlyingType = _underlyingType;
}

type._literal = _literal;
type._unionItemTypes = _unionItemTypes;

return type;
}

/// <summary>
/// Returns a framework-backed copy of this <see cref="CSharpType"/> that carries explicit enum
/// semantics. This is used for referenced (external) extensible enums, which are implemented as
/// value-type structs and therefore are not recognized as enums via reflection
/// (<see cref="Type.IsEnum"/> is <c>false</c>). Preserving the underlying enum type lets downstream
/// serialization treat the type as an enum (inline construction) instead of falling back to a model read.
/// </summary>
/// <param name="underlyingEnumType">The underlying value type of the enum (e.g. <see cref="string"/> or <see cref="int"/>).</param>
internal CSharpType WithUnderlyingEnumType(Type underlyingEnumType)
{
if (!IsFrameworkType)
{
throw new InvalidOperationException("WithUnderlyingEnumType is only valid for framework types.");
}

var type = new CSharpType(FrameworkType, Arguments, IsNullable);
type._underlyingType = underlyingEnumType;
type._literal = _literal;
type._unionItemTypes = _unionItemTypes;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ protected internal TypeFactory()
// Check if this type has external type information
if (inputType.External != null)
{
return CreateExternalType(inputType.External);
return CreateExternalType(inputType.External, inputType);
}

CSharpType? type;
Expand Down Expand Up @@ -322,30 +322,42 @@ protected virtual ModelFactoryProvider CreateModelFactoryCore(IEnumerable<InputM
/// Factory method for creating a <see cref="CSharpType"/> based on external type properties.
/// </summary>
/// <param name="externalProperties">The <see cref="InputExternalTypeMetadata"/> to convert.</param>
/// <param name="inputType">The originating <see cref="InputType"/>, when available, used to preserve
/// semantics (such as extensible-enum backing) that reflection alone cannot recover from the resolved type.</param>
/// <returns>A <see cref="CSharpType"/> representing the external type, or null if the type cannot be resolved.</returns>
private CSharpType? CreateExternalType(InputExternalTypeMetadata externalProperties)
private CSharpType? CreateExternalType(InputExternalTypeMetadata externalProperties, InputType? inputType = null)
{
// 1. Try to create a framework type from the fully qualified name. This stays as the
// first attempt because it's free (no I/O) and is the source of truth for BCL types.
var frameworkType = CreateFrameworkType(externalProperties.Identity);
if (frameworkType != null)
{
return new CSharpType(frameworkType);
// Resolve the type: first as a framework type from the fully qualified name (free, no I/O, and the
// source of truth for BCL types), then, on a miss, dynamically from the NuGet package named in the
// metadata. ExternalTypeReferenceResolver consults a process-wide cache populated by the eager
// pre-walk in CSharpGen.ExecuteAsync, resolving on-demand when the cache misses.
var resolvedType = CreateFrameworkType(externalProperties.Identity);
if (resolvedType == null && !string.IsNullOrEmpty(externalProperties.Package))
{
resolvedType = ExternalTypeReferenceResolver.TryResolve(externalProperties);
}

// 2. Fallback: dynamically resolve the type from the NuGet package named in the metadata.
// ExternalTypeReferenceResolver consults a process-wide cache populated by the eager
// pre-walk in CSharpGen.ExecuteAsync; on a miss it resolves on-demand.
if (!string.IsNullOrEmpty(externalProperties.Package))
if (resolvedType != null)
{
var resolvedType = ExternalTypeReferenceResolver.TryResolve(externalProperties);
if (resolvedType != null)
var externalType = new CSharpType(resolvedType);

// A referenced extensible enum is implemented as a value-type struct, so the resolved
// framework type is not recognized as an enum via reflection and loses its enum semantics.
// Rebuild it preserving the underlying enum type so downstream serialization emits inline
// construction (e.g. new Kind(value)) instead of a broken ModelReaderWriter.Read<T> fallback.
if (inputType is InputEnumType { IsExtensible: true } externalEnum)
{
return new CSharpType(resolvedType);
var underlyingType = CreateCSharpType(externalEnum.ValueType);
if (underlyingType is { IsFrameworkType: true })
{
externalType = externalType.WithUnderlyingEnumType(underlyingType.FrameworkType);
}
}

return externalType;
}

// 3. Neither path worked — emit a diagnostic that explains what was attempted.
// Neither path resolved the type — emit a diagnostic that explains what was attempted.
// Each branch is a self-contained sentence so the final message reads naturally and
// doesn't repeat "could not be resolved".
var details = string.IsNullOrEmpty(externalProperties.Package)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,68 @@ public void CreateCSharpType_ExternalEnum_ResolvesToExternalFrameworkType()
Assert.AreEqual(typeof(Uri), type.FrameworkType);
}

[Test]
public void CreateCSharpType_ExternalExtensibleStringEnum_PreservesEnumSemantics()
{
// A referenced extensible enum is implemented as a value-type struct, so reflection does not
// report it as an enum. The resolved external type must still carry enum semantics (underlying
// type + struct) so serialization emits inline construction instead of a broken model read.
var input = InputFactory.StringEnum(
"ExternalKind",
[("value1", "value1"), ("value2", "value2")],
usage: InputModelTypeUsage.Input,
isExtensible: true,
external: new InputExternalTypeMetadata("System.Guid", null, null));

var type = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(input);

Assert.IsNotNull(type);
Assert.IsTrue(type!.IsFrameworkType);
Assert.AreEqual(typeof(Guid), type.FrameworkType);
Assert.IsTrue(type.IsEnum);
Assert.IsTrue(type.IsStruct);
Assert.AreEqual(typeof(string), type.UnderlyingEnumType);
}

[Test]
public void CreateCSharpType_ExternalExtensibleInt32Enum_PreservesEnumSemantics()
{
var input = InputFactory.Int32Enum(
"ExternalKind",
[("value1", 1), ("value2", 2)],
usage: InputModelTypeUsage.Input,
isExtensible: true,
external: new InputExternalTypeMetadata("System.Guid", null, null));

var type = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(input);

Assert.IsNotNull(type);
Assert.IsTrue(type!.IsEnum);
Assert.IsTrue(type.IsStruct);
Assert.AreEqual(typeof(int), type.UnderlyingEnumType);
}

[Test]
public void CreateCSharpType_ExternalFixedEnum_DoesNotForceEnumSemantics()
{
// Fixed (non-extensible) external enums are left untouched; they resolve to their external
// framework type as-is (which, for a real .NET enum, already reports enum semantics).
var input = InputFactory.StringEnum(
"ExternalKind",
[("value1", "value1"), ("value2", "value2")],
usage: InputModelTypeUsage.Input,
isExtensible: false,
external: new InputExternalTypeMetadata("System.Uri", null, null));

var type = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(input);

Assert.IsNotNull(type);
Assert.IsTrue(type!.IsFrameworkType);
Assert.AreEqual(typeof(Uri), type.FrameworkType);
Assert.IsFalse(type.IsEnum);
Comment thread
jorgerangel-msft marked this conversation as resolved.
Assert.Throws<InvalidOperationException>(() => _ = type.UnderlyingEnumType);
}

[Test]
public void CreateCSharpType_SelfReferencingModel_DoesNotThrow()
{
Expand Down
Loading