Refactor DefaultPageApplicationModel to use conventions
This commit is contained in:
parent
43d4416a1d
commit
13281613a5
|
|
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
|
||||
public const string HtmlHelperPartialExtensionsType = "Microsoft.AspNetCore.Mvc.Rendering.HtmlHelperPartialExtensions";
|
||||
|
||||
public const string IApiBehaviorMetadata = "Microsoft.AspNetCore.Mvc.Internal.IApiBehaviorMetadata";
|
||||
public const string IApiBehaviorMetadata = "Microsoft.AspNetCore.Mvc.IApiBehaviorMetadata";
|
||||
|
||||
public const string IBinderTypeProviderMetadata = "Microsoft.AspNetCore.Mvc.ModelBinding.IBinderTypeProviderMetadata";
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
|
||||
public const string DefaultStatusCodeAttribute = "Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute";
|
||||
|
||||
public const string IApiBehaviorMetadata = "Microsoft.AspNetCore.Mvc.Internal.IApiBehaviorMetadata";
|
||||
public const string IApiBehaviorMetadata = "Microsoft.AspNetCore.Mvc.IApiBehaviorMetadata";
|
||||
|
||||
public const string IActionResult = "Microsoft.AspNetCore.Mvc.IActionResult";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
internal class ApiBehaviorApplicationModelProvider : IApplicationModelProvider
|
||||
{
|
||||
public ApiBehaviorApplicationModelProvider(
|
||||
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
|
||||
IModelMetadataProvider modelMetadataProvider,
|
||||
IClientErrorFactory clientErrorFactory,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
var options = apiBehaviorOptions.Value;
|
||||
|
||||
ActionModelConventions = new List<IActionModelConvention>()
|
||||
{
|
||||
new ApiVisibilityConvention(),
|
||||
};
|
||||
|
||||
if (!options.SuppressMapClientErrors)
|
||||
{
|
||||
ActionModelConventions.Add(new ClientErrorResultFilterConvention());
|
||||
}
|
||||
|
||||
if (!options.SuppressModelStateInvalidFilter)
|
||||
{
|
||||
ActionModelConventions.Add(new InvalidModelStateFilterConvention());
|
||||
}
|
||||
|
||||
if (!options.SuppressConsumesConstraintForFormFileParameters)
|
||||
{
|
||||
ActionModelConventions.Add(new ConsumesConstraintForFormFileParameterConvention());
|
||||
}
|
||||
|
||||
var defaultErrorType = options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails);
|
||||
var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType);
|
||||
ActionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute));
|
||||
|
||||
var inferParameterBindingInfoConvention = new InferParameterBindingInfoConvention(modelMetadataProvider)
|
||||
{
|
||||
SuppressInferBindingSourcesForParameters = options.SuppressInferBindingSourcesForParameters
|
||||
};
|
||||
ControllerModelConventions = new List<IControllerModelConvention>
|
||||
{
|
||||
inferParameterBindingInfoConvention,
|
||||
};
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Order is set to execute after the <see cref="DefaultApplicationModelProvider"/> and allow any other user
|
||||
/// <see cref="IApplicationModelProvider"/> that configure routing to execute.
|
||||
/// </remarks>
|
||||
public int Order => -1000 + 100;
|
||||
|
||||
public List<IActionModelConvention> ActionModelConventions { get; }
|
||||
|
||||
public List<IControllerModelConvention> ControllerModelConventions { get; }
|
||||
|
||||
public void OnProvidersExecuted(ApplicationModelProviderContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnProvidersExecuting(ApplicationModelProviderContext context)
|
||||
{
|
||||
foreach (var controller in context.Result.Controllers)
|
||||
{
|
||||
if (!controller.Attributes.OfType<IApiBehaviorMetadata>().Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var action in controller.Actions)
|
||||
{
|
||||
// Ensure ApiController is set up correctly
|
||||
EnsureActionIsAttributeRouted(action);
|
||||
|
||||
foreach (var convention in ActionModelConventions)
|
||||
{
|
||||
convention.Apply(action);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var convention in ControllerModelConventions)
|
||||
{
|
||||
convention.Apply(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureActionIsAttributeRouted(ActionModel actionModel)
|
||||
{
|
||||
if (!IsAttributeRouted(actionModel.Controller.Selectors) &&
|
||||
!IsAttributeRouted(actionModel.Selectors))
|
||||
{
|
||||
// Require attribute routing with controllers annotated with ApiControllerAttribute
|
||||
var message = Resources.FormatApiController_AttributeRouteRequired(
|
||||
actionModel.DisplayName,
|
||||
nameof(ApiControllerAttribute));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
bool IsAttributeRouted(IList<SelectorModel> selectorModel)
|
||||
{
|
||||
for (var i = 0; i < selectorModel.Count; i++)
|
||||
{
|
||||
if (selectorModel[i].AttributeRouteModel != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IActionModelConvention"/> that discovers
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ApiConventionResult"/> from applied <see cref="ApiConventionTypeAttribute"/> or <see cref="ApiConventionMethodAttribute"/>.</item>
|
||||
/// <item><see cref="ProducesErrorResponseTypeAttribute"/> that applies to the action.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class ApiConventionApplicationModelConvention : IActionModelConvention
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ApiConventionApplicationModelConvention"/>.
|
||||
/// </summary>
|
||||
/// <param name="defaultErrorResponseType">The error type to be used. Use <see cref="void" />
|
||||
/// when no default error type is to be inferred.
|
||||
/// </param>
|
||||
public ApiConventionApplicationModelConvention(ProducesErrorResponseTypeAttribute defaultErrorResponseType)
|
||||
{
|
||||
DefaultErrorResponseType = defaultErrorResponseType ?? throw new ArgumentNullException(nameof(defaultErrorResponseType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="ProducesErrorResponseTypeAttribute"/> that is associated with an action
|
||||
/// when no attribute is discovered.
|
||||
/// </summary>
|
||||
public ProducesErrorResponseTypeAttribute DefaultErrorResponseType { get; }
|
||||
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (!ShouldApply(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DiscoverApiConvention(action);
|
||||
DiscoverErrorResponseType(action);
|
||||
}
|
||||
|
||||
protected virtual bool ShouldApply(ActionModel action) => true;
|
||||
|
||||
private static void DiscoverApiConvention(ActionModel action)
|
||||
{
|
||||
if (action.Filters.OfType<IApiResponseMetadataProvider>().Any())
|
||||
{
|
||||
// If an action already has providers, don't discover any from conventions.
|
||||
return;
|
||||
}
|
||||
|
||||
var controller = action.Controller;
|
||||
var apiConventionAttributes = controller.Attributes.OfType<ApiConventionTypeAttribute>().ToArray();
|
||||
if (apiConventionAttributes.Length == 0)
|
||||
{
|
||||
var controllerAssembly = controller.ControllerType.Assembly;
|
||||
apiConventionAttributes = controllerAssembly.GetCustomAttributes<ApiConventionTypeAttribute>().ToArray();
|
||||
}
|
||||
|
||||
if (ApiConventionResult.TryGetApiConvention(action.ActionMethod, apiConventionAttributes, out var result))
|
||||
{
|
||||
action.Properties[typeof(ApiConventionResult)] = result;
|
||||
}
|
||||
}
|
||||
|
||||
private void DiscoverErrorResponseType(ActionModel action)
|
||||
{
|
||||
var errorTypeAttribute =
|
||||
action.Attributes.OfType<ProducesErrorResponseTypeAttribute>().FirstOrDefault() ??
|
||||
action.Controller.Attributes.OfType<ProducesErrorResponseTypeAttribute>().FirstOrDefault() ??
|
||||
action.Controller.ControllerType.Assembly.GetCustomAttribute<ProducesErrorResponseTypeAttribute>() ??
|
||||
DefaultErrorResponseType;
|
||||
|
||||
action.Properties[typeof(ProducesErrorResponseTypeAttribute)] = errorTypeAttribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IActionModelConvention"/> that sets Api Explorer visibility.
|
||||
/// </summary>
|
||||
public class ApiVisibilityConvention : IActionModelConvention
|
||||
{
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
if (!ShouldApply(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.Controller.ApiExplorer.IsVisible == null && action.ApiExplorer.IsVisible == null)
|
||||
{
|
||||
// Enable ApiExplorer for the action if it wasn't already explicitly configured.
|
||||
action.ApiExplorer.IsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool ShouldApply(ActionModel action) => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IActionModelConvention"/> that adds a <see cref="IFilterMetadata"/>
|
||||
/// to <see cref="ActionModel"/> that transforms <see cref="IClientErrorActionResult"/>.
|
||||
/// </summary>
|
||||
public class ClientErrorResultFilterConvention : IActionModelConvention
|
||||
{
|
||||
private readonly ClientErrorResultFilterFactory _filterFactory = new ClientErrorResultFilterFactory();
|
||||
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (!ShouldApply(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
action.Filters.Add(_filterFactory);
|
||||
}
|
||||
|
||||
protected virtual bool ShouldApply(ActionModel action) => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IActionModelConvention"/> that adds a <see cref="ConsumesAttribute"/> with <c>multipart/form-data</c>
|
||||
/// to controllers containing form file (<see cref="BindingSource.FormFile"/>) parameters.
|
||||
/// </summary>
|
||||
public class ConsumesConstraintForFormFileParameterConvention : IActionModelConvention
|
||||
{
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (!ShouldApply(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddMultipartFormDataConsumesAttribute(action);
|
||||
}
|
||||
|
||||
protected virtual bool ShouldApply(ActionModel action) => true;
|
||||
|
||||
// Internal for unit testing
|
||||
internal void AddMultipartFormDataConsumesAttribute(ActionModel action)
|
||||
{
|
||||
// Add a ConsumesAttribute if the request does not explicitly specify one.
|
||||
if (action.Filters.OfType<IConsumesActionConstraint>().Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var parameter in action.Parameters)
|
||||
{
|
||||
var bindingSource = parameter.BindingInfo?.BindingSource;
|
||||
if (bindingSource == BindingSource.FormFile)
|
||||
{
|
||||
// If an controller accepts files, it must accept multipart/form-data.
|
||||
action.Filters.Add(new ConsumesAttribute("multipart/form-data"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Resources = Microsoft.AspNetCore.Mvc.Core.Resources;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="IControllerModelConvention"/> that
|
||||
/// <list type="bullet">
|
||||
/// <item>infers binding sources for parameters</item>
|
||||
/// <item><see cref="BindingInfo.BinderModelName"/> for bound properties and parameters.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class InferParameterBindingInfoConvention : IControllerModelConvention
|
||||
{
|
||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||
|
||||
public InferParameterBindingInfoConvention(
|
||||
IModelMetadataProvider modelMetadataProvider)
|
||||
{
|
||||
_modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that determines if model binding sources are inferred for action parameters on controllers is suppressed.
|
||||
/// </summary>
|
||||
public bool SuppressInferBindingSourcesForParameters { get; set; }
|
||||
|
||||
protected virtual bool ShouldApply(ControllerModel controller) => true;
|
||||
|
||||
public void Apply(ControllerModel controller)
|
||||
{
|
||||
if (controller == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
if (!ShouldApply(controller))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InferBoundPropertyModelPrefixes(controller);
|
||||
|
||||
foreach (var action in controller.Actions)
|
||||
{
|
||||
InferParameterBindingSources(action);
|
||||
InferParameterModelPrefixes(action);
|
||||
}
|
||||
}
|
||||
|
||||
internal void InferParameterBindingSources(ActionModel action)
|
||||
{
|
||||
var inferredBindingSources = new BindingSource[action.Parameters.Count];
|
||||
|
||||
for (var i = 0; i < action.Parameters.Count; i++)
|
||||
{
|
||||
var parameter = action.Parameters[i];
|
||||
var bindingSource = parameter.BindingInfo?.BindingSource;
|
||||
if (bindingSource == null)
|
||||
{
|
||||
bindingSource = InferBindingSourceForParameter(parameter);
|
||||
|
||||
parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
|
||||
parameter.BindingInfo.BindingSource = bindingSource;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for unit testing.
|
||||
internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
|
||||
{
|
||||
if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName))
|
||||
{
|
||||
return BindingSource.Path;
|
||||
}
|
||||
|
||||
var bindingSource = IsComplexTypeParameter(parameter) ?
|
||||
BindingSource.Body :
|
||||
BindingSource.Query;
|
||||
|
||||
return bindingSource;
|
||||
}
|
||||
|
||||
// For any complex types that are bound from value providers, set the prefix
|
||||
// to the empty prefix by default. This makes binding much more predictable
|
||||
// and describable via ApiExplorer
|
||||
internal void InferBoundPropertyModelPrefixes(ControllerModel controllerModel)
|
||||
{
|
||||
foreach (var property in controllerModel.ControllerProperties)
|
||||
{
|
||||
if (property.BindingInfo != null &&
|
||||
property.BindingInfo.BinderModelName == null &&
|
||||
property.BindingInfo.BindingSource != null &&
|
||||
!property.BindingInfo.BindingSource.IsGreedy)
|
||||
{
|
||||
var metadata = _modelMetadataProvider.GetMetadataForProperty(
|
||||
controllerModel.ControllerType,
|
||||
property.PropertyInfo.Name);
|
||||
if (metadata.IsComplexType && !metadata.IsCollectionType)
|
||||
{
|
||||
property.BindingInfo.BinderModelName = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void InferParameterModelPrefixes(ActionModel action)
|
||||
{
|
||||
foreach (var parameter in action.Parameters)
|
||||
{
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
if (bindingInfo?.BindingSource != null &&
|
||||
bindingInfo.BinderModelName == null &&
|
||||
!bindingInfo.BindingSource.IsGreedy &&
|
||||
IsComplexTypeParameter(parameter))
|
||||
{
|
||||
parameter.BindingInfo.BinderModelName = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool ParameterExistsInAnyRoute(ActionModel action, string parameterName)
|
||||
{
|
||||
foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(action))
|
||||
{
|
||||
if (route == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var parsedTemplate = TemplateParser.Parse(route.Template);
|
||||
if (parsedTemplate.GetParameter(parameterName) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsComplexTypeParameter(ParameterModel parameter)
|
||||
{
|
||||
// No need for information from attributes on the parameter. Just use its type.
|
||||
var metadata = _modelMetadataProvider
|
||||
.GetMetadataForType(parameter.ParameterInfo.ParameterType);
|
||||
return metadata.IsComplexType && !metadata.IsCollectionType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IActionModelConvention"/> that adds a <see cref="IFilterMetadata"/>
|
||||
/// to <see cref="ActionModel"/> that responds to invalid <see cref="ActionContext.ModelState"/>
|
||||
/// </summary>
|
||||
public class InvalidModelStateFilterConvention : IActionModelConvention
|
||||
{
|
||||
private readonly ModelStateInvalidFilterFactory _filterFactory = new ModelStateInvalidFilterFactory();
|
||||
|
||||
public void Apply(ActionModel action)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
}
|
||||
|
||||
if (!ShouldApply(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
action.Filters.Add(_filterFactory);
|
||||
}
|
||||
|
||||
protected virtual bool ShouldApply(ActionModel action) => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,13 +3,13 @@
|
|||
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
namespace Microsoft.AspNetCore.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="IFilterMetadata"/> interface for <see cref="ApiControllerAttribute"/>. See
|
||||
/// <see cref="ApiControllerAttribute"/> for details.
|
||||
/// </summary>
|
||||
public interface IApiBehaviorMetadata : IFilterMetadata
|
||||
internal interface IApiBehaviorMetadata : IFilterMetadata
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -10,14 +10,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
{
|
||||
internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter
|
||||
{
|
||||
internal const int FilterOrder = -2000;
|
||||
private readonly IClientErrorFactory _clientErrorFactory;
|
||||
private readonly ILogger<ClientErrorResultFilter> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter order. Defaults to -2000 so that it runs early.
|
||||
/// </summary>
|
||||
public int Order => -2000;
|
||||
|
||||
public ClientErrorResultFilter(
|
||||
IClientErrorFactory clientErrorFactory,
|
||||
ILogger<ClientErrorResultFilter> logger)
|
||||
|
|
@ -26,6 +22,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filter order. Defaults to -2000 so that it runs early.
|
||||
/// </summary>
|
||||
public int Order => FilterOrder;
|
||||
|
||||
public void OnResultExecuted(ResultExecutedContext context)
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal sealed class ClientErrorResultFilterFactory : IFilterFactory, IOrderedFilter
|
||||
{
|
||||
public int Order => ClientErrorResultFilter.FilterOrder;
|
||||
|
||||
public bool IsReusable => true;
|
||||
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
var resultFilter = ActivatorUtilities.CreateInstance<ClientErrorResultFilter>(serviceProvider);
|
||||
return resultFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -15,12 +16,22 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
/// </summary>
|
||||
public class ModelStateInvalidFilter : IActionFilter, IOrderedFilter
|
||||
{
|
||||
internal const int FilterOrder = -2000;
|
||||
|
||||
private readonly ApiBehaviorOptions _apiBehaviorOptions;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ModelStateInvalidFilter(ApiBehaviorOptions apiBehaviorOptions, ILogger logger)
|
||||
{
|
||||
_apiBehaviorOptions = apiBehaviorOptions ?? throw new ArgumentNullException(nameof(apiBehaviorOptions));
|
||||
if (!_apiBehaviorOptions.SuppressModelStateInvalidFilter && _apiBehaviorOptions.InvalidModelStateResponseFactory == null)
|
||||
{
|
||||
throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
|
||||
typeof(ApiBehaviorOptions),
|
||||
nameof(ApiBehaviorOptions.InvalidModelStateResponseFactory)));
|
||||
}
|
||||
|
||||
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
|||
/// Look at <see cref="IOrderedFilter.Order"/> for more detailed info.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public int Order => -2000;
|
||||
public int Order => FilterOrder;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsReusable => true;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Infrastructure
|
||||
{
|
||||
internal sealed class ModelStateInvalidFilterFactory : IFilterFactory, IOrderedFilter
|
||||
{
|
||||
public int Order => ModelStateInvalidFilter.FilterOrder;
|
||||
|
||||
public bool IsReusable => true;
|
||||
|
||||
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<IOptions<ApiBehaviorOptions>>();
|
||||
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
|
||||
|
||||
return new ModelStateInvalidFilter(options.Value, loggerFactory.CreateLogger<ModelStateInvalidFilter>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,326 +0,0 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||
using Microsoft.AspNetCore.Mvc.Core;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Routing.Template;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Internal
|
||||
{
|
||||
public class ApiBehaviorApplicationModelProvider : IApplicationModelProvider
|
||||
{
|
||||
private readonly ProducesErrorResponseTypeAttribute DefaultErrorType = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));
|
||||
private readonly ApiBehaviorOptions _apiBehaviorOptions;
|
||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||
private readonly ModelStateInvalidFilter _modelStateInvalidFilter;
|
||||
private readonly ClientErrorResultFilter _clientErrorResultFilter;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ApiBehaviorApplicationModelProvider(
|
||||
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
|
||||
IModelMetadataProvider modelMetadataProvider,
|
||||
IClientErrorFactory clientErrorFactory,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_apiBehaviorOptions = apiBehaviorOptions.Value;
|
||||
_modelMetadataProvider = modelMetadataProvider;
|
||||
_logger = loggerFactory.CreateLogger<ApiBehaviorApplicationModelProvider>();
|
||||
|
||||
if (!_apiBehaviorOptions.SuppressModelStateInvalidFilter && _apiBehaviorOptions.InvalidModelStateResponseFactory == null)
|
||||
{
|
||||
throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
|
||||
typeof(ApiBehaviorOptions),
|
||||
nameof(ApiBehaviorOptions.InvalidModelStateResponseFactory)));
|
||||
}
|
||||
|
||||
_modelStateInvalidFilter = new ModelStateInvalidFilter(
|
||||
apiBehaviorOptions.Value,
|
||||
loggerFactory.CreateLogger<ModelStateInvalidFilter>());
|
||||
|
||||
_clientErrorResultFilter = new ClientErrorResultFilter(
|
||||
clientErrorFactory,
|
||||
loggerFactory.CreateLogger<ClientErrorResultFilter>());
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Order is set to execute after the <see cref="DefaultApplicationModelProvider"/> and allow any other user
|
||||
/// <see cref="IApplicationModelProvider"/> that configure routing to execute.
|
||||
/// </remarks>
|
||||
public int Order => -1000 + 100;
|
||||
|
||||
public void OnProvidersExecuted(ApplicationModelProviderContext context)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnProvidersExecuting(ApplicationModelProviderContext context)
|
||||
{
|
||||
foreach (var controllerModel in context.Result.Controllers)
|
||||
{
|
||||
var isApiController = controllerModel.Attributes.OfType<IApiBehaviorMetadata>().Any();
|
||||
if (isApiController &&
|
||||
controllerModel.ApiExplorer.IsVisible == null)
|
||||
{
|
||||
// Enable ApiExplorer for the controller if it wasn't already explicitly configured.
|
||||
controllerModel.ApiExplorer.IsVisible = true;
|
||||
}
|
||||
|
||||
if (isApiController)
|
||||
{
|
||||
InferBoundPropertyModelPrefixes(controllerModel);
|
||||
}
|
||||
|
||||
var controllerHasSelectorModel = controllerModel.Selectors.Any(s => s.AttributeRouteModel != null);
|
||||
var conventions = controllerModel.Attributes.OfType<ApiConventionTypeAttribute>().ToArray();
|
||||
if (conventions.Length == 0)
|
||||
{
|
||||
var controllerAssembly = controllerModel.ControllerType.Assembly;
|
||||
conventions = controllerAssembly.GetCustomAttributes<ApiConventionTypeAttribute>().ToArray();
|
||||
}
|
||||
|
||||
foreach (var actionModel in controllerModel.Actions)
|
||||
{
|
||||
if (!isApiController && !actionModel.Attributes.OfType<IApiBehaviorMetadata>().Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnsureActionIsAttributeRouted(controllerHasSelectorModel, actionModel);
|
||||
|
||||
AddInvalidModelStateFilter(actionModel);
|
||||
|
||||
AddClientErrorFilter(actionModel);
|
||||
|
||||
InferParameterBindingSources(actionModel);
|
||||
|
||||
InferParameterModelPrefixes(actionModel);
|
||||
|
||||
AddMultipartFormDataConsumesAttribute(actionModel);
|
||||
|
||||
DiscoverApiConvention(actionModel, conventions);
|
||||
|
||||
DiscoverErrorResponseType(actionModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for unit testing
|
||||
internal void AddMultipartFormDataConsumesAttribute(ActionModel actionModel)
|
||||
{
|
||||
if (_apiBehaviorOptions.SuppressConsumesConstraintForFormFileParameters)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a ConsumesAttribute if the request does not explicitly specify one.
|
||||
if (actionModel.Filters.OfType<IConsumesActionConstraint>().Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var parameter in actionModel.Parameters)
|
||||
{
|
||||
var bindingSource = parameter.BindingInfo?.BindingSource;
|
||||
if (bindingSource == BindingSource.FormFile)
|
||||
{
|
||||
// If an action accepts files, it must accept multipart/form-data.
|
||||
actionModel.Filters.Add(new ConsumesAttribute("multipart/form-data"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureActionIsAttributeRouted(bool controllerHasSelectorModel, ActionModel actionModel)
|
||||
{
|
||||
if (!controllerHasSelectorModel && !actionModel.Selectors.Any(s => s.AttributeRouteModel != null))
|
||||
{
|
||||
// Require attribute routing with controllers annotated with ApiControllerAttribute
|
||||
var message = Resources.FormatApiController_AttributeRouteRequired(
|
||||
actionModel.DisplayName,
|
||||
nameof(ApiControllerAttribute));
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddInvalidModelStateFilter(ActionModel actionModel)
|
||||
{
|
||||
if (_apiBehaviorOptions.SuppressModelStateInvalidFilter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(_apiBehaviorOptions.InvalidModelStateResponseFactory != null);
|
||||
actionModel.Filters.Add(_modelStateInvalidFilter);
|
||||
}
|
||||
|
||||
private void AddClientErrorFilter(ActionModel actionModel)
|
||||
{
|
||||
if (_apiBehaviorOptions.SuppressMapClientErrors)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
actionModel.Filters.Add(_clientErrorResultFilter);
|
||||
}
|
||||
|
||||
// Internal for unit testing
|
||||
internal void InferParameterBindingSources(ActionModel actionModel)
|
||||
{
|
||||
if (_modelMetadataProvider == null || _apiBehaviorOptions.SuppressInferBindingSourcesForParameters)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var inferredBindingSources = new BindingSource[actionModel.Parameters.Count];
|
||||
|
||||
for (var i = 0; i < actionModel.Parameters.Count; i++)
|
||||
{
|
||||
var parameter = actionModel.Parameters[i];
|
||||
var bindingSource = parameter.BindingInfo?.BindingSource;
|
||||
if (bindingSource == null)
|
||||
{
|
||||
bindingSource = InferBindingSourceForParameter(parameter);
|
||||
|
||||
parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
|
||||
parameter.BindingInfo.BindingSource = bindingSource;
|
||||
}
|
||||
}
|
||||
|
||||
var fromBodyParameters = actionModel.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(
|
||||
actionModel.DisplayName,
|
||||
nameof(FromQueryAttribute),
|
||||
nameof(FromRouteAttribute),
|
||||
nameof(FromBodyAttribute));
|
||||
|
||||
message += Environment.NewLine + parameters;
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
// For any complex types that are bound from value providers, set the prefix
|
||||
// to the empty prefix by default. This makes binding much more predictable
|
||||
// and describable via ApiExplorer
|
||||
|
||||
// internal for testing
|
||||
internal void InferBoundPropertyModelPrefixes(ControllerModel controllerModel)
|
||||
{
|
||||
foreach (var property in controllerModel.ControllerProperties)
|
||||
{
|
||||
if (property.BindingInfo != null &&
|
||||
property.BindingInfo.BinderModelName == null &&
|
||||
property.BindingInfo.BindingSource != null &&
|
||||
!property.BindingInfo.BindingSource.IsGreedy)
|
||||
{
|
||||
var metadata = _modelMetadataProvider.GetMetadataForProperty(
|
||||
controllerModel.ControllerType,
|
||||
property.PropertyInfo.Name);
|
||||
if (metadata.IsComplexType && !metadata.IsCollectionType)
|
||||
{
|
||||
property.BindingInfo.BinderModelName = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// internal for testing
|
||||
internal void InferParameterModelPrefixes(ActionModel actionModel)
|
||||
{
|
||||
foreach (var parameter in actionModel.Parameters)
|
||||
{
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
if (bindingInfo?.BindingSource != null &&
|
||||
bindingInfo.BinderModelName == null &&
|
||||
!bindingInfo.BindingSource.IsGreedy &&
|
||||
IsComplexTypeParameter(parameter))
|
||||
{
|
||||
parameter.BindingInfo.BinderModelName = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal for unit testing.
|
||||
internal BindingSource InferBindingSourceForParameter(ParameterModel parameter)
|
||||
{
|
||||
if (ParameterExistsInAnyRoute(parameter.Action, parameter.ParameterName))
|
||||
{
|
||||
return BindingSource.Path;
|
||||
}
|
||||
|
||||
var bindingSource = IsComplexTypeParameter(parameter) ?
|
||||
BindingSource.Body :
|
||||
BindingSource.Query;
|
||||
|
||||
return bindingSource;
|
||||
}
|
||||
|
||||
internal static void DiscoverApiConvention(ActionModel actionModel, ApiConventionTypeAttribute[] apiConventionAttributes)
|
||||
{
|
||||
if (actionModel.Filters.OfType<IApiResponseMetadataProvider>().Any())
|
||||
{
|
||||
// If an action already has providers, don't discover any from conventions.
|
||||
return;
|
||||
}
|
||||
|
||||
if (ApiConventionResult.TryGetApiConvention(actionModel.ActionMethod, apiConventionAttributes, out var result))
|
||||
{
|
||||
actionModel.Properties[typeof(ApiConventionResult)] = result;
|
||||
}
|
||||
}
|
||||
|
||||
internal void DiscoverErrorResponseType(ActionModel actionModel)
|
||||
{
|
||||
var errorTypeAttribute =
|
||||
actionModel.Attributes.OfType<ProducesErrorResponseTypeAttribute>().FirstOrDefault() ??
|
||||
actionModel.Controller.Attributes.OfType<ProducesErrorResponseTypeAttribute>().FirstOrDefault() ??
|
||||
actionModel.Controller.ControllerType.Assembly.GetCustomAttribute<ProducesErrorResponseTypeAttribute>();
|
||||
|
||||
if (!_apiBehaviorOptions.SuppressMapClientErrors)
|
||||
{
|
||||
// If ClientErrorFactory is being used and the application does not supply a error response type, assume ProblemDetails.
|
||||
errorTypeAttribute = errorTypeAttribute ?? DefaultErrorType;
|
||||
}
|
||||
|
||||
if (errorTypeAttribute != null)
|
||||
{
|
||||
actionModel.Properties[typeof(ProducesErrorResponseTypeAttribute)] = errorTypeAttribute;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ParameterExistsInAnyRoute(ActionModel actionModel, string parameterName)
|
||||
{
|
||||
foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(actionModel))
|
||||
{
|
||||
if (route == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var parsedTemplate = TemplateParser.Parse(route.Template);
|
||||
if (parsedTemplate.GetParameter(parameterName) != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsComplexTypeParameter(ParameterModel parameter)
|
||||
{
|
||||
// No need for information from attributes on the parameter. Just use its type.
|
||||
var metadata = _modelMetadataProvider
|
||||
.GetMetadataForType(parameter.ParameterInfo.ParameterType);
|
||||
return metadata.IsComplexType && !metadata.IsCollectionType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ProducesErrorResponseTypeAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The error type.</param>
|
||||
/// <param name="type">The error type. Use <see cref="void"/> to indicate the absence of a default error type.</param>
|
||||
public ProducesErrorResponseTypeAttribute(Type type)
|
||||
{
|
||||
Type = type ?? throw new ArgumentNullException(nameof(type));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
{
|
||||
"TypeId": "public class Microsoft.AspNetCore.Mvc.ApiControllerAttribute : Microsoft.AspNetCore.Mvc.ControllerAttribute, Microsoft.AspNetCore.Mvc.Internal.IApiBehaviorMetadata",
|
||||
"Kind": "Removal"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
public class ApiBehaviorApplicationModelProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void OnProvidersExecuting_ThrowsIfControllerWithAttribute_HasActionsWithoutAttributeRouting()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = $"{typeof(TestApiController).FullName}.{nameof(TestApiController.TestAction)} ({typeof(TestApiController).Assembly.GetName().Name})";
|
||||
var expected = $"Action '{actionName}' does not have an attribute route. Action methods on controllers annotated with ApiControllerAttribute must be attribute routed.";
|
||||
|
||||
var controllerModel = new ControllerModel(typeof(TestApiController).GetTypeInfo(), new[] { new ApiControllerAttribute() });
|
||||
var method = typeof(TestApiController).GetMethod(nameof(TestApiController.TestAction));
|
||||
var actionModel = new ActionModel(method, Array.Empty<object>())
|
||||
{
|
||||
Controller = controllerModel,
|
||||
};
|
||||
controllerModel.Actions.Add(actionModel);
|
||||
|
||||
var context = new ApplicationModelProviderContext(new[] { controllerModel.ControllerType });
|
||||
context.Result.Controllers.Add(controllerModel);
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => provider.OnProvidersExecuting(context));
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnProvidersExecuting_AppliesConventions()
|
||||
{
|
||||
// Arrange
|
||||
var controllerModel = new ControllerModel(typeof(TestApiController).GetTypeInfo(), new[] { new ApiControllerAttribute() })
|
||||
{
|
||||
Selectors = { new SelectorModel { AttributeRouteModel = new AttributeRouteModel() } },
|
||||
};
|
||||
|
||||
var method = typeof(TestApiController).GetMethod(nameof(TestApiController.TestAction));
|
||||
|
||||
var actionModel = new ActionModel(method, Array.Empty<object>())
|
||||
{
|
||||
Controller = controllerModel,
|
||||
};
|
||||
controllerModel.Actions.Add(actionModel);
|
||||
|
||||
var parameter = method.GetParameters()[0];
|
||||
var parameterModel = new ParameterModel(parameter, Array.Empty<object>())
|
||||
{
|
||||
Action = actionModel,
|
||||
};
|
||||
actionModel.Parameters.Add(parameterModel);
|
||||
|
||||
var context = new ApplicationModelProviderContext(new[] { controllerModel.ControllerType });
|
||||
context.Result.Controllers.Add(controllerModel);
|
||||
|
||||
var provider = GetProvider();
|
||||
|
||||
// Act
|
||||
provider.OnProvidersExecuting(context);
|
||||
|
||||
// Assert
|
||||
// Verify some of the side-effects of executing API behavior conventions.
|
||||
Assert.True(actionModel.ApiExplorer.IsVisible);
|
||||
Assert.NotEmpty(actionModel.Filters.OfType<ModelStateInvalidFilterFactory>());
|
||||
Assert.NotEmpty(actionModel.Filters.OfType<ClientErrorResultFilterFactory>());
|
||||
Assert.Equal(BindingSource.Body, parameterModel.BindingInfo.BindingSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsUpConventions()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Collection(
|
||||
provider.ActionModelConventions,
|
||||
c => Assert.IsType<ApiVisibilityConvention>(c),
|
||||
c => Assert.IsType<ClientErrorResultFilterConvention>(c),
|
||||
c => Assert.IsType<InvalidModelStateFilterConvention>(c),
|
||||
c => Assert.IsType<ConsumesConstraintForFormFileParameterConvention>(c),
|
||||
c =>
|
||||
{
|
||||
var convention = Assert.IsType<ApiConventionApplicationModelConvention>(c);
|
||||
Assert.Equal(typeof(ProblemDetails), convention.DefaultErrorResponseType.Type);
|
||||
});
|
||||
|
||||
Assert.Collection(
|
||||
provider.ControllerModelConventions,
|
||||
c =>
|
||||
{
|
||||
var convention = Assert.IsType<InferParameterBindingInfoConvention>(c);
|
||||
Assert.False(convention.SuppressInferBindingSourcesForParameters);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DoesNotAddClientErrorResultFilterConvention_IfSuppressMapClientErrorsIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(new ApiBehaviorOptions { SuppressMapClientErrors = true });
|
||||
|
||||
// Act & Assert
|
||||
Assert.Empty(provider.ActionModelConventions.OfType<ClientErrorResultFilterConvention>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DoesNotAddInvalidModelStateFilterConvention_IfSuppressModelStateInvalidFilterIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(new ApiBehaviorOptions { SuppressModelStateInvalidFilter = true });
|
||||
|
||||
// Act & Assert
|
||||
Assert.Empty(provider.ActionModelConventions.OfType<InvalidModelStateFilterConvention>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DoesNotAddConsumesConstraintForFormFileParameterConvention_IfSuppressConsumesConstraintForFormFileParametersIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(new ApiBehaviorOptions { SuppressConsumesConstraintForFormFileParameters = true });
|
||||
|
||||
// Act & Assert
|
||||
Assert.Empty(provider.ActionModelConventions.OfType<ConsumesConstraintForFormFileParameterConvention>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsSuppressInferBindingSourcesForParametersIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(new ApiBehaviorOptions { SuppressInferBindingSourcesForParameters = true });
|
||||
|
||||
// Act & Assert
|
||||
var convention = Assert.Single(provider.ControllerModelConventions.OfType<InferParameterBindingInfoConvention>());
|
||||
Assert.True(convention.SuppressInferBindingSourcesForParameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DoesNotSpecifyDefaultErrorType_IfSuppressMapClientErrorsIsSet()
|
||||
{
|
||||
// Arrange
|
||||
var provider = GetProvider(new ApiBehaviorOptions { SuppressMapClientErrors = true });
|
||||
|
||||
// Act & Assert
|
||||
var convention = Assert.Single(provider.ActionModelConventions.OfType<ApiConventionApplicationModelConvention>());
|
||||
Assert.Equal(typeof(void), convention.DefaultErrorResponseType.Type);
|
||||
}
|
||||
|
||||
private static ApiBehaviorApplicationModelProvider GetProvider(
|
||||
ApiBehaviorOptions options = null)
|
||||
{
|
||||
options = options ?? new ApiBehaviorOptions
|
||||
{
|
||||
InvalidModelStateResponseFactory = _ => null,
|
||||
};
|
||||
var optionsAccessor = Options.Create(options);
|
||||
|
||||
var loggerFactory = NullLoggerFactory.Instance;
|
||||
return new ApiBehaviorApplicationModelProvider(
|
||||
optionsAccessor,
|
||||
new EmptyModelMetadataProvider(),
|
||||
Mock.Of<IClientErrorFactory>(),
|
||||
loggerFactory);
|
||||
}
|
||||
|
||||
private static ApplicationModelProviderContext GetContext(
|
||||
Type type,
|
||||
IModelMetadataProvider modelMetadataProvider = null)
|
||||
{
|
||||
var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() });
|
||||
var mvcOptions = Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true });
|
||||
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
|
||||
var provider = new DefaultApplicationModelProvider(mvcOptions, modelMetadataProvider);
|
||||
provider.OnProvidersExecuting(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private class TestApiController : ControllerBase
|
||||
{
|
||||
public IActionResult TestAction(object value) => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Authorization;
|
||||
using Xunit;
|
||||
|
||||
[assembly: ProducesErrorResponseType(typeof(InvalidEnumArgumentException))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
public class ApiConventionApplicationModelConventionTest
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_DoesNotAddConventionItem_IfActionHasProducesResponseTypeAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = GetActionModel(nameof(TestController.Delete));
|
||||
actionModel.Filters.Add(new ProducesResponseTypeAttribute(200));
|
||||
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(actionModel);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(typeof(ApiConventionResult), actionModel.Properties.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_DoesNotAddConventionItem_IfActionHasProducesAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = GetActionModel(nameof(TestController.Delete));
|
||||
actionModel.Filters.Add(new ProducesAttribute(typeof(object)));
|
||||
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(actionModel);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(typeof(ApiConventionResult), actionModel.Properties.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_DoesNotAddConventionItem_IfNoConventionMatches()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = GetActionModel(nameof(TestController.NoMatch));
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(actionModel);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(typeof(ApiConventionResult), actionModel.Properties.Keys);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_AddsConventionItem_IfConventionMatches()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = GetActionModel(nameof(TestController.Delete));
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(actionModel);
|
||||
|
||||
// Assert
|
||||
var value = actionModel.Properties[typeof(ApiConventionResult)];
|
||||
Assert.NotNull(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_AddsConventionItem_IfActionHasNonConventionBasedFilters()
|
||||
{
|
||||
// Arrange
|
||||
var actionModel = GetActionModel(nameof(TestController.Delete));
|
||||
actionModel.Filters.Add(new AuthorizeFilter());
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(actionModel);
|
||||
|
||||
// Assert
|
||||
var value = actionModel.Properties[typeof(ApiConventionResult)];
|
||||
Assert.NotNull(value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_UsesDefaultErrorType_IfActionHasNoAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = typeof(InvalidFilterCriteriaException);
|
||||
var controller = new ControllerModel(typeof(object).GetTypeInfo(), Array.Empty<object>());
|
||||
var action = new ActionModel(typeof(object).GetMethods()[0], Array.Empty<object>())
|
||||
{
|
||||
Controller = controller,
|
||||
};
|
||||
var convention = GetConvention(expected);
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
var attribute = GetProperty<ProducesErrorResponseTypeAttribute>(action);
|
||||
Assert.Equal(expected, attribute.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_UsesValueFromProducesErrorResponseTypeAttribute_SpecifiedOnControllerAsssembly()
|
||||
{
|
||||
// Arrange
|
||||
var expected = typeof(InvalidEnumArgumentException);
|
||||
var action = GetActionModel(nameof(TestController.Delete));
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
var attribute = GetProperty<ProducesErrorResponseTypeAttribute>(action);
|
||||
Assert.Equal(expected, attribute.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_UsesValueFromProducesErrorResponseTypeAttribute_SpecifiedOnController()
|
||||
{
|
||||
// Arrange
|
||||
var expected = typeof(InvalidTimeZoneException);
|
||||
var action = GetActionModel(
|
||||
nameof(TestController.Delete),
|
||||
controllerAttributes: new[] { new ProducesErrorResponseTypeAttribute(expected) });
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
var attribute = GetProperty<ProducesErrorResponseTypeAttribute>(action);
|
||||
Assert.Equal(expected, attribute.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_UsesValueFromProducesErrorResponseTypeAttribute_SpecifiedOnAction()
|
||||
{
|
||||
// Arrange
|
||||
var expected = typeof(InvalidTimeZoneException);
|
||||
var action = GetActionModel(
|
||||
nameof(TestController.Delete),
|
||||
actionAttributes: new[] { new ProducesErrorResponseTypeAttribute(expected) },
|
||||
controllerAttributes: new[] { new ProducesErrorResponseTypeAttribute(typeof(Guid)) });
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
var attribute = GetProperty<ProducesErrorResponseTypeAttribute>(action);
|
||||
Assert.Equal(expected, attribute.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_AllowsVoidsErrorType()
|
||||
{
|
||||
// Arrange
|
||||
var expected = typeof(void);
|
||||
var action = GetActionModel(nameof(TestController.Delete), new[] { new ProducesErrorResponseTypeAttribute(expected) });
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
var attribute = GetProperty<ProducesErrorResponseTypeAttribute>(action);
|
||||
Assert.Equal(expected, attribute.Type);
|
||||
}
|
||||
|
||||
private ApiConventionApplicationModelConvention GetConvention(Type errorType = null)
|
||||
{
|
||||
errorType = errorType ?? typeof(ProblemDetails);
|
||||
return new ApiConventionApplicationModelConvention(new ProducesErrorResponseTypeAttribute(errorType));
|
||||
}
|
||||
|
||||
private static TValue GetProperty<TValue>(ActionModel action)
|
||||
{
|
||||
return Assert.IsType<TValue>(action.Properties[typeof(TValue)]);
|
||||
}
|
||||
|
||||
private static ActionModel GetActionModel(
|
||||
string actionName,
|
||||
object[] actionAttributes = null,
|
||||
object[] controllerAttributes = null)
|
||||
{
|
||||
actionAttributes = actionAttributes ?? Array.Empty<object>();
|
||||
controllerAttributes = controllerAttributes ?? new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
|
||||
|
||||
var controllerModel = new ControllerModel(typeof(TestController).GetTypeInfo(), controllerAttributes);
|
||||
var actionModel = new ActionModel(typeof(TestController).GetMethod(actionName), actionAttributes)
|
||||
{
|
||||
Controller = controllerModel,
|
||||
};
|
||||
|
||||
controllerModel.Actions.Add(actionModel);
|
||||
|
||||
return actionModel;
|
||||
}
|
||||
|
||||
private class TestController
|
||||
{
|
||||
public IActionResult NoMatch() => null;
|
||||
|
||||
public IActionResult Delete(int id) => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Reflection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
public class ApiVisibilityConventionTest
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_SetsApiExplorerVisibility()
|
||||
{
|
||||
// Arrange
|
||||
var action = GetActionModel();
|
||||
var convention = new ApiVisibilityConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
Assert.True(action.ApiExplorer.IsVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_DoesNotSetApiExplorerVisibility_IfAlreadySpecifiedOnAction()
|
||||
{
|
||||
// Arrange
|
||||
var action = GetActionModel();
|
||||
action.ApiExplorer.IsVisible = false;
|
||||
var convention = new ApiVisibilityConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
Assert.False(action.ApiExplorer.IsVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_DoesNotSetApiExplorerVisibility_IfAlreadySpecifiedOnController()
|
||||
{
|
||||
// Arrange
|
||||
var action = GetActionModel();
|
||||
action.Controller.ApiExplorer.IsVisible = false;
|
||||
var convention = new ApiVisibilityConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
Assert.Null(action.ApiExplorer.IsVisible);
|
||||
}
|
||||
|
||||
private static ActionModel GetActionModel()
|
||||
{
|
||||
return new ActionModel(typeof(object).GetMethods()[0], new object[0])
|
||||
{
|
||||
Controller = new ControllerModel(typeof(object).GetTypeInfo(), new object[0]),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
public class ClientErrorResultFilterConventionTest
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_AddsFilter()
|
||||
{
|
||||
// Arrange
|
||||
var action = GetActionModel();
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
Assert.Single(action.Filters.OfType<ClientErrorResultFilterFactory>());
|
||||
}
|
||||
|
||||
private ClientErrorResultFilterConvention GetConvention()
|
||||
{
|
||||
return new ClientErrorResultFilterConvention();
|
||||
}
|
||||
|
||||
private static ActionModel GetActionModel()
|
||||
{
|
||||
var action = new ActionModel(typeof(object).GetMethods()[0], new object[0]);
|
||||
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
public class ConsumesConstraintForFormFileParameterConventionTest
|
||||
{
|
||||
[Fact]
|
||||
public void AddMultipartFormDataConsumesAttribute_NoOpsIfConsumesConstraintIsAlreadyPresent()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(TestController.ActionWithConsumesAttribute);
|
||||
var action = GetActionModel(typeof(TestController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.AddMultipartFormDataConsumesAttribute(action);
|
||||
|
||||
// Assert
|
||||
var attribute = Assert.Single(action.Filters);
|
||||
var consumesAttribute = Assert.IsType<ConsumesAttribute>(attribute);
|
||||
Assert.Equal("application/json", Assert.Single(consumesAttribute.ContentTypes));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddMultipartFormDataConsumesAttribute_AddsConsumesAttribute_WhenActionHasFromFormFileParameter()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(TestController.FormFileParameter);
|
||||
var action = GetActionModel(typeof(TestController), actionName);
|
||||
action.Parameters[0].BindingInfo = new BindingInfo
|
||||
{
|
||||
BindingSource = BindingSource.FormFile,
|
||||
};
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.AddMultipartFormDataConsumesAttribute(action);
|
||||
|
||||
// Assert
|
||||
var attribute = Assert.Single(action.Filters);
|
||||
var consumesAttribute = Assert.IsType<ConsumesAttribute>(attribute);
|
||||
Assert.Equal("multipart/form-data", Assert.Single(consumesAttribute.ContentTypes));
|
||||
}
|
||||
|
||||
private ConsumesConstraintForFormFileParameterConvention GetConvention()
|
||||
{
|
||||
return new ConsumesConstraintForFormFileParameterConvention();
|
||||
}
|
||||
|
||||
private static ApplicationModelProviderContext GetContext(
|
||||
Type type,
|
||||
IModelMetadataProvider modelMetadataProvider = null)
|
||||
{
|
||||
var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() });
|
||||
var mvcOptions = Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true });
|
||||
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
|
||||
var convention = new DefaultApplicationModelProvider(mvcOptions, modelMetadataProvider);
|
||||
convention.OnProvidersExecuting(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static ControllerModel GetControllerModel(Type controllerType)
|
||||
{
|
||||
var context = GetContext(controllerType);
|
||||
return Assert.Single(context.Result.Controllers);
|
||||
}
|
||||
|
||||
private static ActionModel GetActionModel(Type controllerType, string actionName)
|
||||
{
|
||||
var context = GetContext(controllerType);
|
||||
var controller = Assert.Single(context.Result.Controllers);
|
||||
return Assert.Single(controller.Actions, m => m.ActionName == actionName);
|
||||
}
|
||||
|
||||
private class TestController
|
||||
{
|
||||
[HttpPost("form-file")]
|
||||
public IActionResult FormFileParameter(IFormFile formFile) => null;
|
||||
|
||||
[HttpPost("form-file-collection")]
|
||||
public IActionResult FormFileCollectionParameter(IFormFileCollection formFiles) => null;
|
||||
|
||||
[HttpPost("form-file-sequence")]
|
||||
public IActionResult FormFileSequenceParameter(IFormFile[] formFiles) => null;
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult FromFormParameter([FromForm] string parameter) => null;
|
||||
|
||||
[HttpPost]
|
||||
[Consumes("application/json")]
|
||||
public IActionResult ActionWithConsumesAttribute([FromForm] string parameter) => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,986 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Internal;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
public class InferParameterBindingInfoConventionTest
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_DoesNotInferBindingSourceForParametersWithBindingInfo()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterWithBindingInfo.Action);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterWithBindingInfo), actionName);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameterModel = Assert.Single(action.Parameters);
|
||||
Assert.NotNull(parameterModel.BindingInfo);
|
||||
Assert.Same(BindingSource.Custom, parameterModel.BindingInfo.BindingSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferParameterBindingSources_Throws_IfMultipleParametersAreInferredAsBodyBound()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(MultipleFromBodyController.MultipleInferred);
|
||||
var expected =
|
||||
$@"Action '{typeof(MultipleFromBodyController).FullName}.{actionName} ({typeof(MultipleFromBodyController).Assembly.GetName().Name})' " +
|
||||
"has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body:" +
|
||||
Environment.NewLine + "TestModel a" +
|
||||
Environment.NewLine + "Car b";
|
||||
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(MultipleFromBodyController), actionName);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => convention.InferParameterBindingSources(action));
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferParameterBindingSources_Throws_IfMultipleParametersAreInferredOrSpecifiedAsBodyBound()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(MultipleFromBodyController.InferredAndSpecified);
|
||||
var expected =
|
||||
$@"Action '{typeof(MultipleFromBodyController).FullName}.{actionName} ({typeof(MultipleFromBodyController).Assembly.GetName().Name})' " +
|
||||
"has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body:" +
|
||||
Environment.NewLine + "TestModel a" +
|
||||
Environment.NewLine + "int b";
|
||||
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(MultipleFromBodyController), actionName);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => convention.InferParameterBindingSources(action));
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferParameterBindingSources_Throws_IfMultipleParametersAreFromBody()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(MultipleFromBodyController.MultipleSpecified);
|
||||
var expected =
|
||||
$@"Action '{typeof(MultipleFromBodyController).FullName}.{actionName} ({typeof(MultipleFromBodyController).Assembly.GetName().Name})' " +
|
||||
"has more than one parameter that was specified or inferred as bound from request body. Only one parameter per action may be bound from body. Inspect the following parameters, and use 'FromQueryAttribute' to specify bound from query, 'FromRouteAttribute' to specify bound from route, and 'FromBodyAttribute' for parameters to be bound from body:" +
|
||||
Environment.NewLine + "decimal a" +
|
||||
Environment.NewLine + "int b";
|
||||
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(MultipleFromBodyController), actionName);
|
||||
|
||||
// Act & Assert
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => convention.InferParameterBindingSources(action));
|
||||
Assert.Equal(expected, ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinder_AndExplicitName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ModelBinderOnParameterController.ModelBinderAttributeWithExplicitModelName);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ModelBinderOnParameterController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Equal("top", bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinderType()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ModelBinderOnParameterController.ModelBinderType);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ModelBinderOnParameterController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Custom, bindingInfo.BindingSource);
|
||||
Assert.Null(bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnProvidersExecuting_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinderType_AndExplicitModelName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ModelBinderOnParameterController.ModelBinderTypeWithExplicitModelName);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ModelBinderOnParameterController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Custom, bindingInfo.BindingSource);
|
||||
Assert.Equal("foo", bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInRouteAsSimpleToken()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.SimpleRouteToken);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInRouteAsOptionalToken()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.OptionalRouteToken);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInRouteAsConstrainedToken()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.ConstrainedRouteToken);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInAbsoluteRoute()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.AbsoluteRoute);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInAnyRoutes_MulitpleRoutes()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.ParameterInMultipleRoutes);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInAnyRoute()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.ParameterNotInAllRoutes);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInControllerRoute()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterInController.ActionWithoutRoute);
|
||||
var parameter = GetParameterModel(typeof(ParameterInController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInControllerRoute_AndActionHasRoute()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterInController.ActionWithRoute);
|
||||
var parameter = GetParameterModel(typeof(ParameterInController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInAllActionRoutes()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterInController.MultipleRoute);
|
||||
var parameter = GetParameterModel(typeof(ParameterInController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_DoesNotReturnPath_IfActionRouteOverridesControllerRoute()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterInController.AbsoluteRoute);
|
||||
var parameter = GetParameterModel(typeof(ParameterInController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Query, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterPresentInNonOverriddenControllerRoute()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterInController.MultipleRouteWithOverride);
|
||||
var parameter = GetParameterModel(typeof(ParameterInController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterExistsInRoute_OnControllersWithoutSelectors()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingNoRoutesOnController.SimpleRoute);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingNoRoutesOnController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsPath_IfParameterExistsInAllRoutes_OnControllersWithoutSelectors()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingNoRoutesOnController.ParameterInMultipleRoutes);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingNoRoutesOnController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Path, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_DoesNotReturnPath_IfNeitherActionNorControllerHasTemplate()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingNoRoutesOnController.NoRouteTemplate);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingNoRoutesOnController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Query, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsBodyForComplexTypes()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.ComplexTypeModel);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Body, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferParameterBindingSources_SetsCorrectBindingSourceForComplexTypesWithCancellationToken()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.ComplexTypeModelWithCancellationToken);
|
||||
|
||||
// Use the default set of ModelMetadataProviders so we get metadata details for CancellationToken.
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var context = GetContext(typeof(ParameterBindingController), modelMetadataProvider);
|
||||
var controllerModel = Assert.Single(context.Result.Controllers);
|
||||
var actionModel = Assert.Single(controllerModel.Actions, m => m.ActionName == actionName);
|
||||
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.InferParameterBindingSources(actionModel);
|
||||
|
||||
// Assert
|
||||
var model = GetParameterModel<TestModel>(actionModel);
|
||||
Assert.Same(BindingSource.Body, model.BindingInfo.BindingSource);
|
||||
|
||||
var cancellationToken = GetParameterModel<CancellationToken>(actionModel);
|
||||
Assert.Same(BindingSource.Special, cancellationToken.BindingInfo.BindingSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBindingSourceForParameter_ReturnsBodyForSimpleTypes()
|
||||
{
|
||||
// Arrange
|
||||
var actionName = nameof(ParameterBindingController.SimpleTypeModel);
|
||||
var parameter = GetParameterModel(typeof(ParameterBindingController), actionName);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
var result = convention.InferBindingSourceForParameter(parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Same(BindingSource.Query, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromQueryParameter_WithDefaultName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromQuery);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Null(bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromQueryParameter_WithCustomName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromQueryWithCustomName);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Equal("top", bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromQueryParameterOnComplexType_WithDefaultName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromQueryOnComplexType);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromQueryParameterOnComplexType_WithCustomName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromQueryOnComplexTypeWithCustomName);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Equal("gps", bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromQueryParameterOnCollectionType()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromQueryOnCollectionType);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Null(bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromQueryOnArrayType()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromQueryOnArrayType);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Null(bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_FromQueryOnArrayTypeWithCustomName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromQueryOnArrayTypeWithCustomName);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Equal("ids", bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromRouteParameter_WithDefaultName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromRoute);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Path, bindingInfo.BindingSource);
|
||||
Assert.Null(bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromRouteParameter_WithCustomName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromRouteWithCustomName);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Path, bindingInfo.BindingSource);
|
||||
Assert.Equal("top", bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromRouteParameterOnComplexType_WithDefaultName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromRouteOnComplexType);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Path, bindingInfo.BindingSource);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForFromRouteParameterOnComplexType_WithCustomName()
|
||||
{
|
||||
// Arrange
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.FromRouteOnComplexTypeWithCustomName);
|
||||
var convention = GetConvention();
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Path, bindingInfo.BindingSource);
|
||||
Assert.Equal("gps", bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreservesBindingSourceInference_ForParameterWithRequestPredicateAndPropertyFilterProvider()
|
||||
{
|
||||
// Arrange
|
||||
var expectedPredicate = CustomRequestPredicateAndPropertyFilterProviderAttribute.RequestPredicateStatic;
|
||||
var expectedPropertyFilter = CustomRequestPredicateAndPropertyFilterProviderAttribute.PropertyFilterStatic;
|
||||
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
|
||||
var actionName = nameof(ParameterBindingController.ParameterWithRequestPredicateProvider);
|
||||
var action = GetActionModel(typeof(ParameterBindingController), actionName, modelMetadataProvider);
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action.Controller);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
|
||||
var bindingInfo = parameter.BindingInfo;
|
||||
Assert.NotNull(bindingInfo);
|
||||
Assert.Same(BindingSource.Query, bindingInfo.BindingSource);
|
||||
Assert.Same(expectedPredicate, bindingInfo.RequestPredicate);
|
||||
Assert.Same(expectedPropertyFilter, bindingInfo.PropertyFilterProvider.PropertyFilter);
|
||||
Assert.Null(bindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBoundPropertyModelPrefixes_SetsModelPrefix_ForComplexTypeFromValueProvider()
|
||||
{
|
||||
// Arrange
|
||||
var controller = GetControllerModel(typeof(ControllerWithBoundProperty));
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.InferBoundPropertyModelPrefixes(controller);
|
||||
|
||||
// Assert
|
||||
var property = Assert.Single(controller.ControllerProperties);
|
||||
Assert.Equal(string.Empty, property.BindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferBoundPropertyModelPrefixes_SetsModelPrefix_ForCollectionTypeFromValueProvider()
|
||||
{
|
||||
// Arrange
|
||||
var controller = GetControllerModel(typeof(ControllerWithBoundCollectionProperty));
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.InferBoundPropertyModelPrefixes(controller);
|
||||
|
||||
// Assert
|
||||
var property = Assert.Single(controller.ControllerProperties);
|
||||
Assert.Null(property.BindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferParameterModelPrefixes_SetsModelPrefix_ForComplexTypeFromValueProvider()
|
||||
{
|
||||
// Arrange
|
||||
var action = GetActionModel(typeof(ControllerWithBoundProperty), nameof(ControllerWithBoundProperty.SomeAction));
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.InferParameterModelPrefixes(action);
|
||||
|
||||
// Assert
|
||||
var parameter = Assert.Single(action.Parameters);
|
||||
Assert.Equal(string.Empty, parameter.BindingInfo.BinderModelName);
|
||||
}
|
||||
|
||||
private static InferParameterBindingInfoConvention GetConvention(
|
||||
IModelMetadataProvider modelMetadataProvider = null)
|
||||
{
|
||||
var loggerFactory = NullLoggerFactory.Instance;
|
||||
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
|
||||
return new InferParameterBindingInfoConvention(modelMetadataProvider);
|
||||
}
|
||||
|
||||
private static ApplicationModelProviderContext GetContext(
|
||||
Type type,
|
||||
IModelMetadataProvider modelMetadataProvider = null)
|
||||
{
|
||||
var context = new ApplicationModelProviderContext(new[] { type.GetTypeInfo() });
|
||||
var mvcOptions = Options.Create(new MvcOptions { AllowValidatingTopLevelNodes = true });
|
||||
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
|
||||
var convention = new DefaultApplicationModelProvider(mvcOptions, modelMetadataProvider);
|
||||
convention.OnProvidersExecuting(context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private static ControllerModel GetControllerModel(
|
||||
Type controllerType,
|
||||
IModelMetadataProvider modelMetadataProvider = null)
|
||||
{
|
||||
var context = GetContext(controllerType, modelMetadataProvider);
|
||||
return Assert.Single(context.Result.Controllers);
|
||||
}
|
||||
|
||||
private static ActionModel GetActionModel(
|
||||
Type controllerType,
|
||||
string actionName,
|
||||
IModelMetadataProvider modelMetadataProvider = null)
|
||||
{
|
||||
var context = GetContext(controllerType, modelMetadataProvider);
|
||||
var controller = Assert.Single(context.Result.Controllers);
|
||||
return Assert.Single(controller.Actions, m => m.ActionName == actionName);
|
||||
}
|
||||
|
||||
private static ParameterModel GetParameterModel(Type controllerType, string actionName)
|
||||
{
|
||||
var action = GetActionModel(controllerType, actionName);
|
||||
return Assert.Single(action.Parameters);
|
||||
}
|
||||
|
||||
private static ParameterModel GetParameterModel<T>(ActionModel action)
|
||||
{
|
||||
return Assert.Single(action.Parameters.Where(x => typeof(T).IsAssignableFrom(x.ParameterType)));
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
private class ParameterBindingController
|
||||
{
|
||||
[HttpGet("{parameter}")]
|
||||
public IActionResult ActionWithBoundParameter([FromBody] object parameter) => null;
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public IActionResult SimpleRouteToken(int id) => null;
|
||||
|
||||
[HttpPost("optional/{id?}")]
|
||||
public IActionResult OptionalRouteToken(int id) => null;
|
||||
|
||||
[HttpDelete("delete-by-status/{status:int?}")]
|
||||
public IActionResult ConstrainedRouteToken(object status) => null;
|
||||
|
||||
[HttpPut("/absolute-route/{status:int}")]
|
||||
public IActionResult AbsoluteRoute(object status) => null;
|
||||
|
||||
[HttpPost("multiple/{id}")]
|
||||
[HttpPut("multiple/{id}")]
|
||||
public IActionResult ParameterInMultipleRoutes(int id) => null;
|
||||
|
||||
[HttpPatch("patchroute")]
|
||||
[HttpPost("multiple/{id}")]
|
||||
[HttpPut("multiple/{id}")]
|
||||
public IActionResult ParameterNotInAllRoutes(int id) => null;
|
||||
|
||||
[HttpPut("put-action/{id}")]
|
||||
public IActionResult ComplexTypeModel(TestModel model) => null;
|
||||
|
||||
[HttpPut("put-action/{id}")]
|
||||
public IActionResult SimpleTypeModel(ConvertibleFromString model) => null;
|
||||
|
||||
[HttpPost("form-file")]
|
||||
public IActionResult FormFileParameter(IFormFile formFile) => null;
|
||||
|
||||
[HttpPost("form-file-collection")]
|
||||
public IActionResult FormFileCollectionParameter(IFormFileCollection formFiles) => null;
|
||||
|
||||
[HttpPost("form-file-sequence")]
|
||||
public IActionResult FormFileSequenceParameter(IFormFile[] formFiles) => null;
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult FromFormParameter([FromForm] string parameter) => null;
|
||||
|
||||
[HttpPost]
|
||||
[Consumes("application/json")]
|
||||
public IActionResult ActionWithConsumesAttribute([FromForm] string parameter) => null;
|
||||
|
||||
[HttpPut("cancellation")]
|
||||
public IActionResult ComplexTypeModelWithCancellationToken(TestModel model, CancellationToken cancellationToken) => null;
|
||||
|
||||
[HttpGet("parameter-with-model-binder-attribute")]
|
||||
public IActionResult ModelBinderAttribute([ModelBinder(Name = "top")] int value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromquery")]
|
||||
public IActionResult FromQuery([FromQuery] int value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromquery-and-customname")]
|
||||
public IActionResult FromQueryWithCustomName([FromQuery(Name = "top")] int value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromquery-on-complextype")]
|
||||
public IActionResult FromQueryOnComplexType([FromQuery] GpsCoordinates gpsCoordinates) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromquery-on-complextype-and-customname")]
|
||||
public IActionResult FromQueryOnComplexTypeWithCustomName([FromQuery(Name = "gps")] GpsCoordinates gpsCoordinates) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromquery-on-collection-type")]
|
||||
public IActionResult FromQueryOnCollectionType([FromQuery] ICollection<int> value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromquery-on-array-type")]
|
||||
public IActionResult FromQueryOnArrayType([FromQuery] int[] value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromquery-on-array-type-customname")]
|
||||
public IActionResult FromQueryOnArrayTypeWithCustomName([FromQuery(Name = "ids")] int[] value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromroute")]
|
||||
public IActionResult FromRoute([FromRoute] int value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromroute-and-customname")]
|
||||
public IActionResult FromRouteWithCustomName([FromRoute(Name = "top")] int value) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromroute-on-complextype")]
|
||||
public IActionResult FromRouteOnComplexType([FromRoute] GpsCoordinates gpsCoordinates) => null;
|
||||
|
||||
[HttpGet("parameter-with-fromroute-on-complextype-and-customname")]
|
||||
public IActionResult FromRouteOnComplexTypeWithCustomName([FromRoute(Name = "gps")] GpsCoordinates gpsCoordinates) => null;
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ParameterWithRequestPredicateProvider([CustomRequestPredicateAndPropertyFilterProvider] int value) => null;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
private class ModelBinderOnParameterController
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult ModelBinderAttributeWithExplicitModelName([ModelBinder(Name = "top")] int value) => null;
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ModelBinderType([ModelBinder(typeof(TestModelBinder))] string name) => null;
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ModelBinderTypeWithExplicitModelName([ModelBinder(typeof(TestModelBinder), Name = "foo")] string name) => null;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("/route1/[controller]/[action]/{id}")]
|
||||
[Route("/route2/[controller]/[action]/{id?}")]
|
||||
private class ParameterInController
|
||||
{
|
||||
[HttpGet]
|
||||
public IActionResult ActionWithoutRoute(int id) => null;
|
||||
|
||||
[HttpGet("stuff/{status}")]
|
||||
public IActionResult ActionWithRoute(int id) => null;
|
||||
|
||||
[HttpGet("/absolute-route")]
|
||||
public IActionResult AbsoluteRoute(int id) => null;
|
||||
|
||||
[HttpPut]
|
||||
[HttpPost("stuff/{status}")]
|
||||
public IActionResult MultipleRoute(int id) => null;
|
||||
|
||||
[HttpPut]
|
||||
[HttpPost("~/stuff/{status}")]
|
||||
public IActionResult MultipleRouteWithOverride(int id) => null;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
private class ParameterBindingNoRoutesOnController
|
||||
{
|
||||
[HttpGet("{parameter}")]
|
||||
public IActionResult SimpleRoute(int parameter) => null;
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult NoRouteTemplate(int id) => null;
|
||||
|
||||
[HttpPost("multiple/{id}")]
|
||||
[HttpPut("multiple/{id}")]
|
||||
public IActionResult ParameterInMultipleRoutes(int id) => null;
|
||||
}
|
||||
|
||||
|
||||
private class TestModel { }
|
||||
|
||||
[TypeConverter(typeof(ConvertibleFromStringConverter))]
|
||||
private class ConvertibleFromString { }
|
||||
|
||||
private class ConvertibleFromStringConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
=> sourceType == typeof(string);
|
||||
}
|
||||
|
||||
private class CustomRequestPredicateAndPropertyFilterProviderAttribute : Attribute, IRequestPredicateProvider, IPropertyFilterProvider
|
||||
{
|
||||
public static Func<ActionContext, bool> RequestPredicateStatic => (c) => true;
|
||||
public static Func<ModelMetadata, bool> PropertyFilterStatic => (c) => true;
|
||||
|
||||
public Func<ActionContext, bool> RequestPredicate => RequestPredicateStatic;
|
||||
|
||||
public Func<ModelMetadata, bool> PropertyFilter => PropertyFilterStatic;
|
||||
}
|
||||
|
||||
private class GpsCoordinates
|
||||
{
|
||||
public long Latitude { get; set; }
|
||||
public long Longitude { get; set; }
|
||||
}
|
||||
|
||||
private class TestModelBinder : IModelBinder
|
||||
{
|
||||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
private class ControllerWithBoundProperty
|
||||
{
|
||||
[FromQuery]
|
||||
public TestModel TestProperty { get; set; }
|
||||
|
||||
public IActionResult SomeAction([FromQuery] TestModel test) => null;
|
||||
}
|
||||
|
||||
private class ControllerWithBoundCollectionProperty
|
||||
{
|
||||
[FromQuery]
|
||||
public List<int> TestProperty { get; set; }
|
||||
|
||||
public IActionResult SomeAction([FromQuery] List<int> test) => null;
|
||||
}
|
||||
|
||||
private class Car { }
|
||||
|
||||
private class MultipleFromBodyController
|
||||
{
|
||||
public IActionResult MultipleInferred(TestModel a, Car b) => null;
|
||||
|
||||
public IActionResult InferredAndSpecified(TestModel a, [FromBody] int b) => null;
|
||||
|
||||
public IActionResult MultipleSpecified([FromBody] decimal a, [FromBody] int b) => null;
|
||||
}
|
||||
|
||||
private class ParameterWithBindingInfo
|
||||
{
|
||||
[HttpGet("test")]
|
||||
public IActionResult Action([ModelBinder(typeof(object))] Car car) => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||
{
|
||||
public class InvalidModelStateFilterConventionTest
|
||||
{
|
||||
[Fact]
|
||||
public void Apply_AddsFilter()
|
||||
{
|
||||
// Arrange
|
||||
var action = GetActionModel();
|
||||
var convention = GetConvention();
|
||||
|
||||
// Act
|
||||
convention.Apply(action);
|
||||
|
||||
// Assert
|
||||
Assert.Single(action.Filters.OfType<ModelStateInvalidFilterFactory>());
|
||||
}
|
||||
|
||||
|
||||
private static ActionModel GetActionModel()
|
||||
{
|
||||
var action = new ActionModel(typeof(object).GetMethods()[0], new object[0]);
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
private InvalidModelStateFilterConvention GetConvention()
|
||||
{
|
||||
return new InvalidModelStateFilterConvention();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue