| File: ApplicationModels\InferParameterBindingInfoConvention.cs | Web Access |
| Project: src\aspnetcore\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; namespace Microsoft.AspNetCore.Mvc.ApplicationModels; /// <summary> /// An <see cref="IActionModelConvention"/> that infers <see cref="BindingInfo.BindingSource"/> for parameters. /// </summary> /// <remarks> /// The goal of this convention is to make intuitive and easy to document <see cref="BindingSource"/> inferences. The rules are: /// <list type="number"> /// <item>A previously specified <see cref="BindingInfo.BindingSource" /> is never overwritten.</item> /// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>), registered in the DI container, is assigned <see cref="BindingSource.Services"/>.</item> /// <item>A complex type parameter (<see cref="ModelMetadata.IsComplexType"/>), not registered in the DI container, is assigned <see cref="BindingSource.Body"/>.</item> /// <item>Parameter with a name that appears as a route value in ANY route template is assigned <see cref="BindingSource.Path"/>.</item> /// <item>All other parameters are <see cref="BindingSource.Query"/>.</item> /// </list> /// </remarks> public class InferParameterBindingInfoConvention : IActionModelConvention { private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IServiceProviderIsService? _serviceProviderIsService; /// <summary> /// Initializes a new instance of <see cref="InferParameterBindingInfoConvention"/>. /// </summary> /// <param name="modelMetadataProvider">The model metadata provider.</param> public InferParameterBindingInfoConvention( IModelMetadataProvider modelMetadataProvider) { _modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider)); } /// <summary> /// Initializes a new instance of <see cref="InferParameterBindingInfoConvention"/>. /// </summary> /// <param name="modelMetadataProvider">The model metadata provider.</param> /// <param name="serviceProviderIsService">The service to determine if the a type is available from the <see cref="IServiceProvider"/>.</param> public InferParameterBindingInfoConvention( IModelMetadataProvider modelMetadataProvider, IServiceProviderIsService serviceProviderIsService) : this(modelMetadataProvider) { _serviceProviderIsService = serviceProviderIsService ?? throw new ArgumentNullException(nameof(serviceProviderIsService)); } internal bool IsInferForServiceParametersEnabled => _serviceProviderIsService != null; /// <summary> /// Called to determine whether the action should apply. /// </summary> /// <param name="action">The action in question.</param> /// <returns><see langword="true"/> if the action should apply.</returns> protected virtual bool ShouldApply(ActionModel action) => true; /// <summary> /// Called to apply the convention to the <see cref="ActionModel"/>. /// </summary> /// <param name="action">The <see cref="ActionModel"/>.</param> public void Apply(ActionModel action) { ArgumentNullException.ThrowIfNull(action); if (!ShouldApply(action)) { return; } InferParameterBindingSources(action); } internal void InferParameterBindingSources(ActionModel action) { for (var i = 0; i < action.Parameters.Count; i++) { var parameter = action.Parameters[i]; var bindingSource = parameter.BindingInfo?.BindingSource; if (bindingSource == null) { parameter.BindingInfo ??= new BindingInfo(); parameter.BindingInfo.BindingSource = InferBindingSourceForParameter(parameter); } } var fromBodyParameters = action.Parameters.Where(p => p.BindingInfo!.BindingSource == BindingSource.Body).ToList(); if (fromBodyParameters.Count > 1) { var parameters = string.Join(Environment.NewLine, fromBodyParameters.Select(p => p.DisplayName)); var message = Resources.FormatApiController_MultipleBodyParametersFound( action.DisplayName, nameof(FromQueryAttribute), nameof(FromRouteAttribute), nameof(FromBodyAttribute)); message += Environment.NewLine + parameters; throw new InvalidOperationException(message); } else if (fromBodyParameters.Count == 1 && fromBodyParameters[0].BindingInfo!.EmptyBodyBehavior == EmptyBodyBehavior.Default && IsOptionalParameter(fromBodyParameters[0])) { fromBodyParameters[0].BindingInfo!.EmptyBodyBehavior = EmptyBodyBehavior.Allow; } } // Internal for unit testing. internal BindingSource? InferBindingSourceForParameter(ParameterModel parameter) { if (IsComplexTypeParameter(parameter, out var metadata)) { if (IsService(parameter.ParameterType)) { return BindingSource.Services; } return metadata.BoundProperties.Any(prop => prop.BindingSource is not null) ? null : BindingSource.Body; } if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName)) { return BindingSource.Path; } return BindingSource.Query; } private bool IsService(Type type) { if (_serviceProviderIsService == null) { return false; } // IServiceProviderIsService will special case IEnumerable<> and always return true // so, in this case checking the element type instead if (type.IsConstructedGenericType && type.GetGenericTypeDefinition() is Type genericDefinition && genericDefinition == typeof(IEnumerable<>)) { type = type.GenericTypeArguments[0]; } return _serviceProviderIsService.IsService(type); } private static bool ParameterExistsInAnyRoute(ActionModel action, string parameterName) { foreach (var selector in ActionAttributeRouteModel.FlattenSelectors(action)) { if (selector.AttributeRouteModel == null) { continue; } var parsedTemplate = TemplateParser.Parse(selector.AttributeRouteModel.Template!); if (parsedTemplate.GetParameter(parameterName) != null) { return true; } } return false; } private bool IsComplexTypeParameter(ParameterModel parameter, out ModelMetadata metadata) { // No need for information from attributes on the parameter. Just use its type. metadata = _modelMetadataProvider.GetMetadataForType(parameter.ParameterInfo.ParameterType); return metadata.IsComplexType; } private bool IsOptionalParameter(ParameterModel parameter) { if (parameter.ParameterInfo.HasDefaultValue) { return true; } if (_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider) { var metadata = modelMetadataProvider.GetMetadataForParameter(parameter.ParameterInfo); return metadata.NullabilityState == NullabilityState.Nullable || metadata.IsNullableValueType; } else { // Cannot be determine if the parameter is optional since the provider // does not provides an option to getMetadata from the parameter info // so, we will NOT treat the parameter as optional. return false; } } }