Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bd4a53f
Handle custom external model bases
live1206 Jul 7, 2026
6356dc2
Simplify system object model metadata
live1206 Jul 7, 2026
a6471c1
Use model inheritance for system object properties
live1206 Jul 7, 2026
cb4ab51
Add wire path duplicate filtering test
live1206 Jul 7, 2026
02044ac
Use type map for system object base lookup
live1206 Jul 7, 2026
65b59bf
Move system object provider creation to TypeFactory
live1206 Jul 7, 2026
929133e
Use base type providers for custom base serialization
live1206 Jul 7, 2026
6f3c763
Keep base hierarchy helpers internal
live1206 Jul 7, 2026
41b0d05
Revert "Keep base hierarchy helpers internal"
live1206 Jul 7, 2026
b7815f6
Avoid public TypeProvider helper APIs
live1206 Jul 7, 2026
801b710
fix(http-client-csharp): use xml-specific serialization override check
live1206 Jul 8, 2026
7344f89
fix(http-client-csharp): detect customized xml base serialization
live1206 Jul 8, 2026
b64a332
fix(http-client-csharp): validate custom xml core override signature
live1206 Jul 8, 2026
d48bfd9
fix(http-client-csharp): require internal custom xml core overrides
live1206 Jul 8, 2026
92343e0
fix(http-client-csharp): validate external xml core overrides
live1206 Jul 8, 2026
7606827
fix(http-client-csharp): ignore reflected external xml core hooks
live1206 Jul 8, 2026
9434c3a
fix(http-client-csharp): reject sealed xml core overrides
live1206 Jul 8, 2026
15757c7
fix(http-client-csharp): stop xml core lookup at incompatible overrides
live1206 Jul 8, 2026
db7b837
Merge branch 'main' into fix/system-object-custom-base-mrw
live1206 Jul 9, 2026
d927d77
fix(http-client-csharp): use type providers for xml core lookup
live1206 Jul 9, 2026
c4a19ce
fix(http-client-csharp): remove reflection from mrw override lookup
live1206 Jul 9, 2026
6e556ff
Fix custom base review feedback
live1206 Jul 10, 2026
8169c69
Avoid exposing base type provider
live1206 Jul 10, 2026
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 @@ -61,13 +61,83 @@ private record XmlPropertyCategories(

private XmlPropertyCategories? _categorizedXmlProperties;
private XmlPropertyCategories CategorizedXmlProperties => _categorizedXmlProperties ??= CategorizeXmlProperties(ownPropertiesOnly: true);
private bool? _shouldOverrideXmlMethods;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there is an opportunity to consolidate some of this logic with what's in the base implementation of mrw. Could we generalize some of these methods, possibly with additional params, so we don't have to maintain the logic in more than one place?

private bool ShouldOverrideXmlMethods => _shouldOverrideXmlMethods ??= !_isStruct &&
(HasGeneratedBaseSerializationMethod(XmlModelWriteCoreMethodName) ||
HasGeneratedBaseCustomXmlModelWriteCoreMethod() ||
HasCustomBaseXmlModelWriteCoreMethod());

private bool HasGeneratedBaseSerializationMethod(string methodName)
=> _model.BaseModelProvider is not null &&
_model.BaseModelProvider is not SystemObjectModelProvider &&
_model.BaseModelProvider.SerializationProviders
.OfType<MrwSerializationTypeDefinition>()
.Any(serialization => serialization.Methods.Any(method => method.Signature.Name == methodName));

private bool HasGeneratedBaseCustomXmlModelWriteCoreMethod()
=> _model.BaseModelProvider is not null &&
_model.BaseModelProvider is not SystemObjectModelProvider &&
_model.BaseModelProvider.CustomCodeView?.Methods.Any(IsXmlModelWriteCoreMethod) == true;

private bool IsXmlModelWriteCoreMethod(MethodProvider method)
=> IsXmlModelWriteCoreSignature(method) &&
HasInternalOnlyAccessibility(method.Signature.Modifiers) &&
IsOverridable(method.Signature.Modifiers);

private static bool HasInternalOnlyAccessibility(MethodSignatureModifiers modifiers)
=> modifiers.HasFlag(MethodSignatureModifiers.Internal) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's internal can it be public and private? should we just check if it has the internal flag instead of creating this helper method?

!modifiers.HasFlag(MethodSignatureModifiers.Public) &&
!modifiers.HasFlag(MethodSignatureModifiers.Protected) &&
!modifiers.HasFlag(MethodSignatureModifiers.Private);

private bool IsXmlModelWriteCoreSignature(MethodProvider method)
=> method.Signature.Name == XmlModelWriteCoreMethodName &&
!method.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Static) &&
(method.Signature.ReturnType is null || method.Signature.ReturnType.Equals(typeof(void))) &&
method.Signature.Parameters.Count == 2 &&
method.Signature.Parameters[0].Type.Equals(typeof(XmlWriter)) &&
method.Signature.Parameters[1].Type.Equals(typeof(ModelReaderWriterOptions));

private static bool IsOverridable(MethodSignatureModifiers modifiers)
=> !modifiers.HasFlag(MethodSignatureModifiers.Sealed) &&
(modifiers.HasFlag(MethodSignatureModifiers.Virtual) ||
modifiers.HasFlag(MethodSignatureModifiers.Override) ||
modifiers.HasFlag(MethodSignatureModifiers.Abstract));

private bool HasCustomBaseXmlModelWriteCoreMethod()
=> GetCustomSerializationBaseType() is not null &&
_model.BaseType is { } baseType &&
HasCompatibleXmlModelWriteCoreInHierarchy(baseType, []);

private bool HasCompatibleXmlModelWriteCoreInHierarchy(CSharpType type, HashSet<string> visited)
{
if (!visited.Add(type.FullyQualifiedName))
{
return false;
}

var provider = TryGetTypeProvider(type);
if (provider is not null && GetXmlModelWriteCoreCompatibility(provider) is { } isCompatible)
{
return isCompatible;
}

var baseType = provider?.BaseType ?? type.BaseType;
return baseType is not null && HasCompatibleXmlModelWriteCoreInHierarchy(baseType, visited);
}

private bool? GetXmlModelWriteCoreCompatibility(TypeProvider type)
{
var sourceMethod = type.Methods.FirstOrDefault(IsXmlModelWriteCoreSignature);
return sourceMethod is null ? null : IsXmlModelWriteCoreMethod(sourceMethod);
}

private MethodProvider BuildXmlModelWriteCoreMethod()
{
MethodSignatureModifiers modifiers = _isStruct
? MethodSignatureModifiers.Private
: MethodSignatureModifiers.Internal | MethodSignatureModifiers.Virtual;
if (_shouldOverrideXmlMethods)
if (ShouldOverrideXmlMethods)
{
modifiers = MethodSignatureModifiers.Internal | MethodSignatureModifiers.Override;
}
Expand All @@ -81,7 +151,7 @@ private MethodProvider BuildXmlModelWriteCoreMethod()

private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody()
{
var categorizedProperties = _shouldOverrideXmlMethods
var categorizedProperties = ShouldOverrideXmlMethods
? CategorizedXmlProperties
: AllCategorizedXmlProperties;
var statements = new List<MethodBodyStatement>
Expand All @@ -90,7 +160,7 @@ private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody()
MethodBodyStatement.EmptyLine
};

if (_shouldOverrideXmlMethods)
if (ShouldOverrideXmlMethods)
{
statements.Add(Base.Invoke(XmlModelWriteCoreMethodName, _xmlWriterParameter, _serializationOptionsParameter).Terminate());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ public partial class MrwSerializationTypeDefinition : TypeProvider
private ConstructorProvider? _serializationConstructor;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can avoid all of these changes by just changing the accessibility of BaseTypeProvider:

public TypeProvider? BaseTypeProvider => BaseTypeProviderTypeProvider;
private protected virtual TypeProvider? BaseTypeProviderTypeProvider  => null;

Then in this file we can add a helper:

private bool BaseDeclaresOverridable(string methodName)
{
    for (var provider = _model.BaseTypeProvider; provider is not null; provider = provider.BaseTypeProvider)
    {
        // Prefer the custom view so hand-authored replacements/hooks win over generated members.
        var source = provider.CustomCodeView ?? provider;
        var method = source.Methods.FirstOrDefault(m => m.Signature.Name == methodName);
        if (method is null)
        {
            continue; // not declared at this level; keep walking up
        }

        var mods = method.Signature.Modifiers;
        return !mods.HasFlag(MethodSignatureModifiers.Sealed)
            && (mods.HasFlag(MethodSignatureModifiers.Virtual)
                || mods.HasFlag(MethodSignatureModifiers.Override)
                || mods.HasFlag(MethodSignatureModifiers.Abstract));
    }

    return false;
}

Will this help simplify the proposed changes or am I missing something ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. I exposed BaseTypeProvider and now use it for custom-base XML lookup, while keeping signature/accessibility checks and generated serialization-provider lookup separate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: after checking this again, the public API change did not remove the CSharpType fallback/return-type lookup, so I reverted that part in 8169c69 and kept the targeted correctness fixes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a bit more context: exposing BaseTypeProvider only helps when the base chain is already represented as providers. It is not equivalent here because we still need to handle unresolved/custom external CSharpType chains, mapped SystemObjectModelProvider.SystemType matches, and JSON root-type discovery from JsonModelCreateCore/PersistableModelCreateCore return types. Generated base serialization methods also live on serialization providers, so a simple provider walk would miss part of the current behavior. That is why I reverted the public API change and kept the targeted checks/fallbacks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For unresolved external CSharpType chains, how would we determine if that type has methods we should override?

// Flag to determine if the model should override the serialization methods
private bool? _shouldOverrideMethods;
private bool ShouldOverrideMethods => _shouldOverrideMethods ??= _model.BaseModelProvider != null && !_isStruct;
private bool ShouldOverrideMethods => _shouldOverrideMethods ??= !_isStruct &&
(_model.BaseModelProvider != null || HasCustomBaseMethod(JsonModelWriteCoreMethodName));
private bool? _shouldSkipSerializationMethodOverrides;
private bool ShouldSkipSerializationMethodOverrides => _shouldSkipSerializationMethodOverrides ??= ShouldSkipDerivedSerializationMethodOverrides(_model.BaseModelProvider);
private readonly bool _shouldOverrideXmlMethods;
private bool ShouldSkipSerializationMethodOverrides => _shouldSkipSerializationMethodOverrides ??= ShouldSkipDerivedSerializationMethodOverrides(_model);
private readonly Lazy<PropertyProvider[]> _additionalProperties;

// Unknown discriminator models use their base model as the serialization interface type.
Expand All @@ -100,7 +100,6 @@ public MrwSerializationTypeDefinition(InputModelType inputModel, ModelProvider m
_isStruct = _model.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct);
_supportsXml = inputModel.Usage.HasFlag(InputModelTypeUsage.Xml);
_supportsJson = inputModel.Usage.HasFlag(InputModelTypeUsage.Json) || !_supportsXml;
_shouldOverrideXmlMethods = _model.BaseModelProvider != null && !_isStruct;
_rawDataField = _model.Fields.FirstOrDefault(f => f.Name == AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName);
_additionalBinaryDataProperty = new(GetAdditionalBinaryDataPropertiesProp);
_additionalProperties = new(() => [.. _model.Properties.Where(p => p.IsAdditionalProperties)]);
Expand Down Expand Up @@ -152,7 +151,15 @@ private CSharpType GetRootModelType()
{
// We need to explicitly use the BaseModelProvider when looking up the root type
// to account for any customizations that may have changed the base model.
var returnType = _model.BaseModelProvider?.Type ?? Type;
var returnType = _model.BaseModelProvider?.Type ??
GetCustomBaseRootType() ??
GetCustomBaseMethodReturnType(new HashSet<string>
{
JsonModelCreateCoreMethodName,
PersistableModelCreateCoreMethodName
}) ??
GetCustomSerializationBaseType() ??
Type;
while (returnType.BaseType != null
&& IsModelType(returnType.BaseType))
{
Expand Down Expand Up @@ -180,43 +187,155 @@ private static bool IsModelType(CSharpType type)
/// 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)
private CSharpType? GetCustomSerializationBaseType()
=> _model.BaseModelProvider is null && _model.CustomCodeView?.BaseType is not null
? _model.BaseType
: null;

private CSharpType? GetCustomBaseRootType()
=> GetCustomSerializationBaseType() is { } baseType ? GetRootBaseType(baseType) : null;

private bool HasCustomBaseMethod(string methodName)
=> GetCustomSerializationBaseType() is { } baseType &&
HasMethodInTypeHierarchy(baseType, methodName);

private CSharpType? GetCustomBaseMethodReturnType(IReadOnlySet<string> methodNames)
=> GetCustomSerializationBaseType() is { } baseType
? GetMethodReturnTypeInHierarchy(baseType, methodNames)
: null;

private static bool ShouldSkipDerivedSerializationMethodOverrides(ModelProvider model)
{
if (baseModelProvider is null)
if (model.BaseModelProvider is null)
{
return false;
return model.CustomCodeView?.BaseType is null ||
!HasMethodInTypeHierarchy(model.BaseType!, JsonModelCreateCoreMethodName) &&
!HasMethodInTypeHierarchy(model.BaseType!, PersistableModelCreateCoreMethodName);
}

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

return baseModelProvider.ShouldSkipDerivedSerializationMethodOverrides;
return model.BaseModelProvider.ShouldSkipDerivedSerializationMethodOverrides;
}

private static bool SystemTypeImplementsModelReaderWriter(CSharpType systemType)
private static CSharpType? GetRootBaseType(CSharpType baseType)
{
if (!systemType.IsFrameworkType)
var visited = new HashSet<string>();
CSharpType? rootBaseType = null;
var current = baseType;

while (current is not null && visited.Add(current.FullyQualifiedName))
{
return false;
if (!IsFrameworkRootType(current))
{
rootBaseType = current;
}

current = current.BaseType;
}

return rootBaseType;
}

private static bool HasMethodInTypeHierarchy(CSharpType type, string methodName)
=> GetMethodReturnTypeInHierarchy(type, new HashSet<string> { methodName }) is not null;

private static CSharpType? GetMethodReturnTypeInHierarchy(CSharpType type, IReadOnlySet<string> methodNames)
{
var visited = new HashSet<string>();
return GetMethodReturnTypeInHierarchy(type, methodNames, visited);
}

private static CSharpType? GetMethodReturnTypeInHierarchy(CSharpType type, IReadOnlySet<string> methodNames, HashSet<string> visited)
{
if (!visited.Add(type.FullyQualifiedName))
{
return null;
}

foreach (var @interface in systemType.FrameworkType.GetInterfaces())
if (type.BaseType is not null &&
GetMethodReturnTypeInHierarchy(type.BaseType, methodNames, visited) is { } baseReturnType)
{
if (@interface.IsGenericType)
return baseReturnType;
}

foreach (var methodName in methodNames)
{
if (TryGetMethodReturnType(type, methodName) is { } returnType)
{
var definition = @interface.GetGenericTypeDefinition();
if (definition == typeof(IJsonModel<>) || definition == typeof(IPersistableModel<>))
{
return true;
}
return returnType;
}
}

return false;
return null;
}

private static CSharpType? TryGetMethodReturnType(CSharpType type, string methodName)
{
if (TryGetTypeProvider(type) is { } referencedType)
{
return referencedType.Methods.FirstOrDefault(method => method.Signature.Name == methodName)?.Signature.ReturnType;
}

return null;
}

private static TypeProvider? TryGetTypeProvider(CSharpType type)
{
if (TryGetMappedTypeProvider(type) is { } provider)
{
return provider;
}

return TryGetReferencedType(type);
}

private static TypeProvider? TryGetMappedTypeProvider(CSharpType type)
{
foreach (var (mappedType, provider) in ScmCodeModelGenerator.Instance.TypeFactory.CSharpTypeMap)
{
if (mappedType.FullyQualifiedName == type.FullyQualifiedName ||
provider is SystemObjectModelProvider systemProvider &&
systemProvider.SystemType.FullyQualifiedName == type.FullyQualifiedName)
{
return provider;
}
}

return null;
}

private static TypeProvider? TryGetReferencedType(CSharpType type)
=> string.IsNullOrEmpty(type.Namespace)
? null
: CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(
type.Namespace,
type.Name,
declaringTypeName: type.DeclaringType?.Name,
includeReferencedAssemblies: true);

private static bool IsFrameworkRootType(CSharpType type)
=> (type.IsFrameworkType &&
(type.FrameworkType == typeof(object) || type.FrameworkType == typeof(ValueType))) ||
(type.Namespace == "System" && (type.Name == "Object" || type.Name == "ValueType"));

private static bool SystemTypeImplementsModelReaderWriter(CSharpType systemType)
{
if (TryGetMappedTypeProvider(systemType) is not { } systemTypeProvider)
{
return false;
}

return systemTypeProvider.Implements.Any(IsModelReaderWriterInterface);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does SystemObjectModelProvider actually populate the Implements property?

}

private static bool IsModelReaderWriterInterface(CSharpType type)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already have a similar method in ModelReaderWriterContextDefinition. Should we consider consolidating this into a shared internal helper?

=> type.Namespace == typeof(IJsonModel<>).Namespace &&
(type.Name == nameof(IJsonModel<object>) || type.Name == nameof(IPersistableModel<object>));

protected override ConstructorProvider[] BuildConstructors()
{
List<ConstructorProvider> constructors = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ private static (ModelProvider Model, MrwSerializationTypeDefinition Serializatio
// 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 systemBase = new FakeMrwSystemObjectModelProvider(systemType, baseInputModel);

var generator = MockHelpers.LoadMockGenerator(
inputModels: () => [baseInputModel, derivedInputModel],
Expand Down Expand Up @@ -354,6 +354,19 @@ string IPersistableModel<FakeMrwBase>.GetFormatFromOptions(ModelReaderWriterOpti
=> "J";
}

private class FakeMrwSystemObjectModelProvider : SystemObjectModelProvider
{
private readonly CSharpType _type;

public FakeMrwSystemObjectModelProvider(CSharpType type, InputModelType inputModel) : base(type, inputModel)
{
_type = type;
}

protected internal override CSharpType[] BuildImplements()
=> [new CSharpType(typeof(IJsonModel<>), _type)];
}

private class DelayedBaseModelProvider(InputModelType inputModel) : ModelProvider(inputModel)
{
public ModelProvider? BaseModel { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#nullable disable

using System.ClientModel.Primitives;
using System.Xml;

namespace Sample.Models;

public partial class BaseModel
{
internal void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#nullable disable

using System.ClientModel.Primitives;
using System.Xml;

namespace Sample.Models;

public partial class BaseModel
{
protected virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#nullable disable

using System.ClientModel.Primitives;
using System.Xml;

namespace Sample.Models;

public partial class BaseModel : BaseModelXmlHook
{
internal sealed override void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options)
{
}
}

public class BaseModelXmlHook
{
internal virtual void XmlModelWriteCore(XmlWriter writer, ModelReaderWriterOptions options)
{
}
}
Loading
Loading