-
Notifications
You must be signed in to change notification settings - Fork 375
Handle custom external model bases #11183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bd4a53f
6356dc2
a6471c1
cb4ab51
02044ac
65b59bf
929133e
6f3c763
41b0d05
b7815f6
801b710
7344f89
b64a332
d48bfd9
92343e0
7606827
9434c3a
15757c7
db7b837
d927d77
c4a19ce
6e556ff
8169c69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -61,13 +61,83 @@ private record XmlPropertyCategories( | |
|
|
||
| private XmlPropertyCategories? _categorizedXmlProperties; | ||
| private XmlPropertyCategories CategorizedXmlProperties => _categorizedXmlProperties ??= CategorizeXmlProperties(ownPropertiesOnly: true); | ||
| private bool? _shouldOverrideXmlMethods; | ||
| 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) && | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
|
|
@@ -81,7 +151,7 @@ private MethodProvider BuildXmlModelWriteCoreMethod() | |
|
|
||
| private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody() | ||
| { | ||
| var categorizedProperties = _shouldOverrideXmlMethods | ||
| var categorizedProperties = ShouldOverrideXmlMethods | ||
| ? CategorizedXmlProperties | ||
| : AllCategorizedXmlProperties; | ||
| var statements = new List<MethodBodyStatement> | ||
|
|
@@ -90,7 +160,7 @@ private MethodBodyStatement[] BuildXmlModelWriteCoreMethodBody() | |
| MethodBodyStatement.EmptyLine | ||
| }; | ||
|
|
||
| if (_shouldOverrideXmlMethods) | ||
| if (ShouldOverrideXmlMethods) | ||
| { | ||
| statements.Add(Base.Invoke(XmlModelWriteCoreMethodName, _xmlWriterParameter, _serializationOptionsParameter).Terminate()); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -74,10 +74,10 @@ public partial class MrwSerializationTypeDefinition : TypeProvider | |
| private ConstructorProvider? _serializationConstructor; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good suggestion. I exposed
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a bit more context: exposing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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)]); | ||
|
|
@@ -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)) | ||
| { | ||
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does |
||
| } | ||
|
|
||
| private static bool IsModelReaderWriterInterface(CSharpType type) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
|
|
||
| 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) | ||
| { | ||
| } | ||
| } |
There was a problem hiding this comment.
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?