Skip to content

Commit

Permalink
Refactor ModelMetadata for trim compatability
Browse files Browse the repository at this point in the history
  • Loading branch information
captainsafia committed Jul 17, 2024
1 parent 1592fc1 commit aa3ac70
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 9 deletions.
71 changes: 67 additions & 4 deletions src/Mvc/Mvc.Abstractions/src/ModelBinding/ModelMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
Expand All @@ -30,6 +31,17 @@ public abstract class ModelMetadata : IEquatable<ModelMetadata?>, IModelMetadata
private static readonly ParameterBindingMethodCache ParameterBindingMethodCache
= new(throwOnInvalidMethod: false);

/// <summary>
/// Exposes a feature switch to disable generating model metadata with reflection-heavy strategies.
/// This is primarily intended for use in Minimal API-based scenarios where information is derived from
/// IParameterBindingMetadata
/// </summary>
[FeatureSwitchDefinition("Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported")]
[FeatureGuard(typeof(RequiresDynamicCodeAttribute))]
[FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))]
private static bool IsEnhancedModelMetadataSupported { get; } =
AppContext.TryGetSwitch("Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported", out var isEnhancedModelMetadataSupported) ? isEnhancedModelMetadataSupported : true;

private int? _hashCode;
private IReadOnlyList<ModelMetadata>? _boundProperties;
private IReadOnlyDictionary<ModelMetadata, ModelMetadata>? _parameterMapping;
Expand All @@ -44,8 +56,58 @@ private static readonly ParameterBindingMethodCache ParameterBindingMethodCache
protected ModelMetadata(ModelMetadataIdentity identity)
{
Identity = identity;
if (IsEnhancedModelMetadataSupported)
{
InitializeTypeInformation();
}
}

/// <summary>
/// Creates a new <see cref="ModelMetadata"/> from a <see cref="IParameterBindingMetadata"/> instance
/// and its associated type.
/// </summary>
/// <param name="type">The <see cref="Type"/> associated with the <see cref="ModelMetadata"/> generated.</param>
/// <param name="parameterBindingMetadata">The <see cref="IParameterBindingMetadata"/> instance associated with the <see cref="ModelMetadata"/> generated.</param>
protected ModelMetadata(Type type, IParameterBindingMetadata? parameterBindingMetadata)
{
Identity = ModelMetadataIdentity.ForType(type);

InitializeTypeInformation();
InitializeTypeInformationFromType();
if (parameterBindingMetadata is not null)
{
InitializeTypeInformationFromParameterBindingMetadata(parameterBindingMetadata);
}
}

private void InitializeTypeInformationFromType()
{
IsNullableValueType = Nullable.GetUnderlyingType(ModelType) != null;
IsReferenceOrNullableType = !ModelType.IsValueType || IsNullableValueType;
UnderlyingOrModelType = Nullable.GetUnderlyingType(ModelType) ?? ModelType;

if (ModelType == typeof(string) || !typeof(IEnumerable).IsAssignableFrom(ModelType))
{
// Do nothing, not Enumerable.
}
else if (ModelType.IsArray)
{
IsEnumerableType = true;
ElementType = ModelType.GetElementType()!;
}
}

private void InitializeTypeInformationFromParameterBindingMetadata(IParameterBindingMetadata parameterBindingMetadata)
{
// We assume that parameters bound from an endpoint's metadata originated from minimal API's source
// generation layer and are not convertible based on the `TypeConverter`s in MVC.
IsConvertibleType = false;
HasDefaultValue = parameterBindingMetadata.ParameterInfo.HasDefaultValue;
IsParseableType = parameterBindingMetadata.HasTryParse;
IsComplexType = !IsParseableType;

var nullabilityContext = new NullabilityInfoContext();
var nullability = nullabilityContext.Create(parameterBindingMetadata.ParameterInfo);
NullabilityState = nullability?.ReadState ?? NullabilityState.Unknown;
}

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

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

[RequiresUnreferencedCode("Using ModelMetadata with 'Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported=true' is not trim compatible.")]
[RequiresDynamicCode("Using ModelMetadata with 'Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported=true' is not native AOT compatible.")]
private void InitializeTypeInformation()
{
Debug.Assert(ModelType != null);

IsConvertibleType = TypeDescriptor.GetConverter(ModelType).CanConvertFrom(typeof(string));
IsParseableType = FindTryParseMethod(ModelType) is not null;
IsComplexType = !IsConvertibleType && !IsParseableType;
IsNullableValueType = Nullable.GetUnderlyingType(ModelType) != null;
IsReferenceOrNullableType = !ModelType.IsValueType || IsNullableValueType;
UnderlyingOrModelType = Nullable.GetUnderlyingType(ModelType) ?? ModelType;
Expand Down
1 change: 1 addition & 0 deletions src/Mvc/Mvc.Abstractions/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#nullable enable
Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata.ModelMetadata(System.Type! type, Microsoft.AspNetCore.Http.Metadata.IParameterBindingMetadata? parameterBindingMetadata) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -188,7 +187,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string
return new ApiParameterDescription
{
Name = name,
ModelMetadata = CreateModelMetadata(paramType),
ModelMetadata = CreateModelMetadata(paramType, parameter),
Source = source,
DefaultValue = parameter.ParameterInfo.DefaultValue,
Type = parameter.ParameterInfo.ParameterType,
Expand Down Expand Up @@ -431,8 +430,8 @@ private static ApiResponseType CreateDefaultApiResponseType(Type responseType)
}
}

private static EndpointModelMetadata CreateModelMetadata(Type type) =>
new(ModelMetadataIdentity.ForType(type));
private static EndpointModelMetadata CreateModelMetadata(Type type, IParameterBindingMetadata? parameterBindingMetadata = null) =>
new(type, parameterBindingMetadata);

private static void AddResponseContentTypes(IList<ApiResponseFormat> apiResponseFormats, IReadOnlyList<string> contentTypes)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Mvc/Mvc.ApiExplorer/src/EndpointModelMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@

using System.Collections.Immutable;
using System.Linq;
using Microsoft.AspNetCore.Http.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

namespace Microsoft.AspNetCore.Mvc.ApiExplorer;

internal sealed class EndpointModelMetadata : ModelMetadata
{
public EndpointModelMetadata(ModelMetadataIdentity identity) : base(identity)
public EndpointModelMetadata(Type type, IParameterBindingMetadata? parameterBindingMetadata) : base(type, parameterBindingMetadata)
{
IsBindingAllowed = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TestConsoleAppSourceFiles Include="BasicMinimalApiWithOpenApiDependency.cs">
<EnabledProperties>EnableRequestDelegateGenerator</EnabledProperties>
<InterceptorNamespaces>Microsoft.AspNetCore.Http.Generated</InterceptorNamespaces>
<DisabledFeatureSwitches>Microsoft.AspNetCore.Mvc.ApiExplorer.IsEnhancedModelMetadataSupported</DisabledFeatureSwitches>
</TestConsoleAppSourceFiles>
</ItemGroup>

Expand Down

0 comments on commit aa3ac70

Please sign in to comment.