Skip to content

Commit aa3ac70

Browse files
committed
Refactor ModelMetadata for trim compatability
1 parent 1592fc1 commit aa3ac70

File tree

5 files changed

+74
-9
lines changed

5 files changed

+74
-9
lines changed

src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Linq.Expressions;
1010
using System.Reflection;
1111
using Microsoft.AspNetCore.Http;
12+
using Microsoft.AspNetCore.Http.Metadata;
1213
using Microsoft.AspNetCore.Mvc.Abstractions;
1314
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
1415
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@@ -30,6 +31,17 @@ public abstract class ModelMetadata : IEquatable<ModelMetadata?>, IModelMetadata
3031
private static readonly ParameterBindingMethodCache ParameterBindingMethodCache
3132
= new(throwOnInvalidMethod: false);
3233

34+
/// <summary>
35+
/// Exposes a feature switch to disable generating model metadata with reflection-heavy strategies.
36+
/// This is primarily intended for use in Minimal API-based scenarios where information is derived from
37+
/// IParameterBindingMetadata
38+
/// </summary>
39+
[FeatureSwitchDefinition("Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported")]
40+
[FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
41+
[FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))]
42+
private static bool IsEnhancedModelMetadataSupported { get; } =
43+
AppContext.TryGetSwitch("Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported", out var isEnhancedModelMetadataSupported) ? isEnhancedModelMetadataSupported : true;
44+
3345
private int? _hashCode;
3446
private IReadOnlyList<ModelMetadata>? _boundProperties;
3547
private IReadOnlyDictionary<ModelMetadata, ModelMetadata>? _parameterMapping;
@@ -44,8 +56,58 @@ private static readonly ParameterBindingMethodCache ParameterBindingMethodCache
4456
protected ModelMetadata(ModelMetadataIdentity identity)
4557
{
4658
Identity = identity;
59+
if (IsEnhancedModelMetadataSupported)
60+
{
61+
InitializeTypeInformation();
62+
}
63+
}
64+
65+
/// <summary>
66+
/// Creates a new <see cref="ModelMetadata"/> from a <see cref="IParameterBindingMetadata"/> instance
67+
/// and its associated type.
68+
/// </summary>
69+
/// <param name="type">The <see cref="Type"/> associated with the <see cref="ModelMetadata"/> generated.</param>
70+
/// <param name="parameterBindingMetadata">The <see cref="IParameterBindingMetadata"/> instance associated with the <see cref="ModelMetadata"/> generated.</param>
71+
protected ModelMetadata(Type type, IParameterBindingMetadata? parameterBindingMetadata)
72+
{
73+
Identity = ModelMetadataIdentity.ForType(type);
4774

48-
InitializeTypeInformation();
75+
InitializeTypeInformationFromType();
76+
if (parameterBindingMetadata is not null)
77+
{
78+
InitializeTypeInformationFromParameterBindingMetadata(parameterBindingMetadata);
79+
}
80+
}
81+
82+
private void InitializeTypeInformationFromType()
83+
{
84+
IsNullableValueType = Nullable.GetUnderlyingType(ModelType) != null;
85+
IsReferenceOrNullableType = !ModelType.IsValueType || IsNullableValueType;
86+
UnderlyingOrModelType = Nullable.GetUnderlyingType(ModelType) ?? ModelType;
87+
88+
if (ModelType == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(ModelType))
89+
{
90+
// Do nothing, not Enumerable.
91+
}
92+
else if (ModelType.IsArray)
93+
{
94+
IsEnumerableType = true;
95+
ElementType = ModelType.GetElementType()!;
96+
}
97+
}
98+
99+
private void InitializeTypeInformationFromParameterBindingMetadata(IParameterBindingMetadata parameterBindingMetadata)
100+
{
101+
// We assume that parameters bound from an endpoint's metadata originated from minimal API's source
102+
// generation layer and are not convertible based on the `TypeConverter`s in MVC.
103+
IsConvertibleType = false;
104+
HasDefaultValue = parameterBindingMetadata.ParameterInfo.HasDefaultValue;
105+
IsParseableType = parameterBindingMetadata.HasTryParse;
106+
IsComplexType = !IsParseableType;
107+
108+
var nullabilityContext = new NullabilityInfoContext();
109+
var nullability = nullabilityContext.Create(parameterBindingMetadata.ParameterInfo);
110+
NullabilityState = nullability?.ReadState ?? NullabilityState.Unknown;
49111
}
50112

51113
/// <summary>
@@ -442,7 +504,7 @@ internal IReadOnlyDictionary<ModelMetadata, ModelMetadata> BoundConstructorPrope
442504
/// from <see cref="string"/> and without a <c>TryParse</c> method. Most POCO and <see cref="IEnumerable"/> types are therefore complex.
443505
/// Most, if not all, BCL value types are simple types.
444506
/// </remarks>
445-
public bool IsComplexType => !IsConvertibleType && !IsParseableType;
507+
public bool IsComplexType { get; private set; }
446508

447509
/// <summary>
448510
/// Gets a value indicating whether or not <see cref="ModelType"/> is a <see cref="Nullable{T}"/>.
@@ -647,12 +709,13 @@ public override int GetHashCode()
647709
return _hashCode.Value;
648710
}
649711

712+
[RequiresUnreferencedCode("Using ModelMetadata with 'Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported=true' is not trim compatible.")]
713+
[RequiresDynamicCode("Using ModelMetadata with 'Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported=true' is not native AOT compatible.")]
650714
private void InitializeTypeInformation()
651715
{
652-
Debug.Assert(ModelType != null);
653-
654716
IsConvertibleType = TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string));
655717
IsParseableType = FindTryParseMethod(ModelType) is not null;
718+
IsComplexType = !IsConvertibleType && !IsParseableType;
656719
IsNullableValueType = Nullable.GetUnderlyingType(ModelType) != null;
657720
IsReferenceOrNullableType = !ModelType.IsValueType || IsNullableValueType;
658721
UnderlyingOrModelType = Nullable.GetUnderlyingType(ModelType) ?? ModelType;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata.ModelMetadata(System.Type! type, Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata? parameterBindingMetadata) -> void

src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using Microsoft.AspNetCore.Mvc.Abstractions;
1111
using Microsoft.AspNetCore.Mvc.Formatters;
1212
using Microsoft.AspNetCore.Mvc.ModelBinding;
13-
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
1413
using Microsoft.AspNetCore.Routing;
1514
using Microsoft.AspNetCore.Routing.Patterns;
1615
using Microsoft.Extensions.DependencyInjection;
@@ -188,7 +187,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
188187
return new ApiParameterDescription
189188
{
190189
Name = name,
191-
ModelMetadata = CreateModelMetadata(paramType),
190+
ModelMetadata = CreateModelMetadata(paramType, parameter),
192191
Source = source,
193192
DefaultValue = parameter.ParameterInfo.DefaultValue,
194193
Type = parameter.ParameterInfo.ParameterType,
@@ -431,8 +430,8 @@ private static ApiResponseType CreateDefaultApiResponseType(Type responseType)
431430
}
432431
}
433432

434-
private static EndpointModelMetadata CreateModelMetadata(Type type) =>
435-
new(ModelMetadataIdentity.ForType(type));
433+
private static EndpointModelMetadata CreateModelMetadata(Type type, IParameterBindingMetadata? parameterBindingMetadata = null) =>
434+
new(type, parameterBindingMetadata);
436435

437436
private static void AddResponseContentTypes(IList<ApiResponseFormat> apiResponseFormats, IReadOnlyList<string> contentTypes)
438437
{

src/Mvc/Mvc.ApiExplorer/src/EndpointModelMetadata.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33

44
using System.Collections.Immutable;
55
using System.Linq;
6+
using Microsoft.AspNetCore.Http.Metadata;
67
using Microsoft.AspNetCore.Mvc.ModelBinding;
78
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
89

910
namespace Microsoft.AspNetCore.Mvc.ApiExplorer;
1011

1112
internal sealed class EndpointModelMetadata : ModelMetadata
1213
{
13-
public EndpointModelMetadata(ModelMetadataIdentity identity) : base(identity)
14+
public EndpointModelMetadata(Type type, IParameterBindingMetadata? parameterBindingMetadata) : base(type, parameterBindingMetadata)
1415
{
1516
IsBindingAllowed = true;
1617
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.NativeAotTests/Microsoft.AspNetCore.OpenApi.NativeAotTests.proj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<TestConsoleAppSourceFiles Include="BasicMinimalApiWithOpenApiDependency.cs">
55
<EnabledProperties>EnableRequestDelegateGenerator</EnabledProperties>
66
<InterceptorNamespaces>Microsoft.AspNetCore.Http.Generated</InterceptorNamespaces>
7+
<DisabledFeatureSwitches>Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported</DisabledFeatureSwitches>
78
</TestConsoleAppSourceFiles>
89
</ItemGroup>
910

0 commit comments

Comments
 (0)