diff --git a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs index 62b520113b..028293dd28 100644 --- a/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Mvc.Performance/MvcEndpointDatasourceBenchmark.cs @@ -110,7 +110,8 @@ namespace Microsoft.AspNetCore.Mvc.Performance { var dataSource = new MvcEndpointDataSource( actionDescriptorCollectionProvider, - new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty()))); + new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), + new MockParameterPolicyFactory()); return dataSource; } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs index 5531b44ced..f2fc4ea77b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/Routing/AttributeRouteInfo.cs @@ -1,6 +1,8 @@ // 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 Microsoft.AspNetCore.Routing; + namespace Microsoft.AspNetCore.Mvc.Routing { /// diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs index ad152657df..42d3b18af4 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs @@ -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"; diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs index e8b52c012e..9928708f73 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiActionsDoNotRequireExplicitModelValidationCheckAnalyzer.cs @@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers // if (!ModelState.IsValid) // or // if (ModelState.IsValid == false) - // If the conditional is misisng a true condition or has an else expression, skip this operation. + // If the conditional is missing a true condition or has an else expression, skip this operation. return; } @@ -170,8 +170,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var constantValue = ((ILiteralOperation)operation).ConstantValue; if (!constantValue.HasValue || - !(constantValue.Value is bool boolCostantVaue) || - boolCostantVaue != expectedConstantValue) + !(constantValue.Value is bool boolConstantValue) || + boolConstantValue != expectedConstantValue) { return false; } @@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers if (propertyReference.Instance?.Kind != OperationKind.PropertyReference) { - // Verify this is refering to the ModelState property on the current controller instance + // Verify this is referring to the ModelState property on the current controller instance return false; } diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerFacts.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerFacts.cs index 84534a5d9f..d010d58894 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerFacts.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerFacts.cs @@ -25,7 +25,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers return false; } - if (!method.ContainingType.HasAttribute(symbolCache.IApiBehaviorMetadata, inherit: true)) + if (!method.ContainingType.HasAttribute(symbolCache.IApiBehaviorMetadata, inherit: true) && + !method.ContainingAssembly.HasAttribute(symbolCache.IApiBehaviorMetadata)) { return false; } diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs index 563c3ad570..135d742feb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs @@ -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"; diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs index d2eec34199..cd42b8a157 100644 --- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseTypeProvider.cs @@ -38,12 +38,12 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer var runtimeReturnType = GetRuntimeReturnType(declaredReturnType); var responseMetadataAttributes = GetResponseMetadataAttributes(action); - if (responseMetadataAttributes.Count == 0 && + if (!HasSignificantMetadataProvider(responseMetadataAttributes) && action.Properties.TryGetValue(typeof(ApiConventionResult), out var result)) { // Action does not have any conventions. Use conventions on it if present. var apiConventionResult = (ApiConventionResult)result; - responseMetadataAttributes = apiConventionResult.ResponseMetadataProviders; + responseMetadataAttributes.AddRange(apiConventionResult.ResponseMetadataProviders); } var defaultErrorType = typeof(void); @@ -56,11 +56,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer return apiResponseTypes; } - private IReadOnlyList GetResponseMetadataAttributes(ControllerActionDescriptor action) + private static List GetResponseMetadataAttributes(ControllerActionDescriptor action) { if (action.FilterDescriptors == null) { - return Array.Empty(); + return new List(); } // This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer return action.FilterDescriptors .Select(fd => fd.Filter) .OfType() - .ToArray(); + .ToList(); } private ICollection GetApiResponseTypes( @@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer } // Unwrap the type if it's a Task. The Task (non-generic) case was already handled. - Type unwrappedType = declaredReturnType; + var unwrappedType = declaredReturnType; if (declaredReturnType.IsGenericType && declaredReturnType.GetGenericTypeDefinition() == typeof(Task<>)) { @@ -228,5 +228,24 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer { return statusCode >= 400 && statusCode < 500; } + + private static bool HasSignificantMetadataProvider(IReadOnlyList providers) + { + for (var i = 0; i < providers.Count; i++) + { + var provider = providers[i]; + + if (provider is ProducesAttribute producesAttribute && producesAttribute.Type is null) + { + // ProducesAttribute that does not specify type is considered not significant. + continue; + } + + // Any other IApiResponseMetadataProvider is considered significant + return true; + } + + return false; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApiControllerAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApiControllerAttribute.cs index 0f1f1627bf..0be60296de 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApiControllerAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApiControllerAttribute.cs @@ -2,16 +2,20 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Mvc.Internal; namespace Microsoft.AspNetCore.Mvc { /// - /// Indicates that a type and all derived types are used to serve HTTP API responses. The presence of - /// this attribute can be used to target conventions, filters and other behaviors based on the purpose - /// of the controller. + /// Indicates that a type and all derived types are used to serve HTTP API responses. + /// + /// Controllers decorated with this attribute are configured with features and behavior targeted at improving the + /// developer experience for building APIs. + /// + /// + /// When decorated on an assembly, all controllers in the assembly will be treated as controllers with API behavior. + /// /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] public class ApiControllerAttribute : ControllerAttribute, IApiBehaviorMetadata { } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiBehaviorApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiBehaviorApplicationModelProvider.cs new file mode 100644 index 0000000000..62831919de --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiBehaviorApplicationModelProvider.cs @@ -0,0 +1,140 @@ +// 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 System.Reflection; +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, + IModelMetadataProvider modelMetadataProvider, + IClientErrorFactory clientErrorFactory, + ILoggerFactory loggerFactory) + { + var options = apiBehaviorOptions.Value; + + ActionModelConventions = new List() + { + 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 + { + inferParameterBindingInfoConvention, + }; + } + + /// + /// Order is set to execute after the and allow any other user + /// that configure routing to execute. + /// + public int Order => -1000 + 100; + + public List ActionModelConventions { get; } + + public List ControllerModelConventions { get; } + + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + } + + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + foreach (var controller in context.Result.Controllers) + { + if (!IsApiController(controller)) + { + 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) + { + for (var i = 0; i < selectorModel.Count; i++) + { + if (selectorModel[i].AttributeRouteModel != null) + { + return true; + } + } + + return false; + } + } + + private static bool IsApiController(ControllerModel controller) + { + if (controller.Attributes.OfType().Any()) + { + return true; + } + + var controllerAssembly = controller.ControllerType.Assembly; + var assemblyAttributes = controllerAssembly.GetCustomAttributes(); + return assemblyAttributes.OfType().Any(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiConventionApplicationModelConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiConventionApplicationModelConvention.cs new file mode 100644 index 0000000000..2ab2023a09 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiConventionApplicationModelConvention.cs @@ -0,0 +1,82 @@ +// 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 +{ + /// + /// An that discovers + /// + /// from applied or . + /// that applies to the action. + /// + /// + public class ApiConventionApplicationModelConvention : IActionModelConvention + { + /// + /// Initializes a new instance of . + /// + /// The error type to be used. Use + /// when no default error type is to be inferred. + /// + public ApiConventionApplicationModelConvention(ProducesErrorResponseTypeAttribute defaultErrorResponseType) + { + DefaultErrorResponseType = defaultErrorResponseType ?? throw new ArgumentNullException(nameof(defaultErrorResponseType)); + } + + /// + /// Gets the default that is associated with an action + /// when no attribute is discovered. + /// + 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) + { + var controller = action.Controller; + var apiConventionAttributes = controller.Attributes.OfType().ToArray(); + if (apiConventionAttributes.Length == 0) + { + var controllerAssembly = controller.ControllerType.Assembly; + apiConventionAttributes = controllerAssembly.GetCustomAttributes().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().FirstOrDefault() ?? + action.Controller.Attributes.OfType().FirstOrDefault() ?? + action.Controller.ControllerType.Assembly.GetCustomAttribute() ?? + DefaultErrorResponseType; + + action.Properties[typeof(ProducesErrorResponseTypeAttribute)] = errorTypeAttribute; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiVisibilityConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiVisibilityConvention.cs new file mode 100644 index 0000000000..fb269048cf --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ApiVisibilityConvention.cs @@ -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 +{ + /// + /// A that sets Api Explorer visibility. + /// + 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; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs index 07e6a20c48..3283a630a0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/AttributeRouteModel.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Text; using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Mvc.ApplicationModels { @@ -220,6 +221,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels } public static string ReplaceTokens(string template, IDictionary values) + { + return ReplaceTokens(template, values, routeTokenTransformer: null); + } + + public static string ReplaceTokens(string template, IDictionary values, IParameterTransformer routeTokenTransformer) { var builder = new StringBuilder(); var state = TemplateParserState.Plaintext; @@ -371,6 +377,11 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels throw new InvalidOperationException(message); } + if (routeTokenTransformer != null) + { + value = routeTokenTransformer.Transform(value); + } + builder.Append(value); if (c == '[') diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ClientErrorResultFilterConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ClientErrorResultFilterConvention.cs new file mode 100644 index 0000000000..0fff758d4c --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ClientErrorResultFilterConvention.cs @@ -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 +{ + /// + /// An that adds a + /// to that transforms . + /// + 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; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ConsumesConstraintForFormFileParameterConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ConsumesConstraintForFormFileParameterConvention.cs new file mode 100644 index 0000000000..714856e11d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/ConsumesConstraintForFormFileParameterConvention.cs @@ -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 +{ + /// + /// An that adds a with multipart/form-data + /// to controllers containing form file () parameters. + /// + 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().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; + } + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/InferParameterBindingInfoConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/InferParameterBindingInfoConvention.cs new file mode 100644 index 0000000000..3ed892bc70 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/InferParameterBindingInfoConvention.cs @@ -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 +{ + /// + /// A that + /// + /// infers binding sources for parameters + /// for bound properties and parameters. + /// + /// + public class InferParameterBindingInfoConvention : IControllerModelConvention + { + private readonly IModelMetadataProvider _modelMetadataProvider; + + public InferParameterBindingInfoConvention( + IModelMetadataProvider modelMetadataProvider) + { + _modelMetadataProvider = modelMetadataProvider ?? throw new ArgumentNullException(nameof(modelMetadataProvider)); + } + + /// + /// Gets or sets a value that determines if model binding sources are inferred for action parameters on controllers is suppressed. + /// + 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; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/InvalidModelStateFilterConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/InvalidModelStateFilterConvention.cs new file mode 100644 index 0000000000..6d15ae2d58 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/InvalidModelStateFilterConvention.cs @@ -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 +{ + /// + /// An that adds a + /// to that responds to invalid + /// + 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; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/RouteTokenTransformerConvention.cs b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/RouteTokenTransformerConvention.cs new file mode 100644 index 0000000000..d7645077af --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/ApplicationModels/RouteTokenTransformerConvention.cs @@ -0,0 +1,41 @@ +// 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.Routing; + +namespace Microsoft.AspNetCore.Mvc.ApplicationModels +{ + /// + /// An that sets attribute routing token replacement + /// to use the specified on selectors. + /// + public class RouteTokenTransformerConvention : IActionModelConvention + { + private readonly IParameterTransformer _parameterTransformer; + + /// + /// Creates a new instance of with the specified . + /// + /// The to use with attribute routing token replacement. + public RouteTokenTransformerConvention(IParameterTransformer parameterTransformer) + { + if (parameterTransformer == null) + { + throw new ArgumentNullException(nameof(parameterTransformer)); + } + + _parameterTransformer = parameterTransformer; + } + + public void Apply(ActionModel action) + { + if (ShouldApply(action)) + { + action.Properties[typeof(IParameterTransformer)] = _parameterTransformer; + } + } + + protected virtual bool ShouldApply(ActionModel action) => true; + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs index 8b1eba5fbf..161189b871 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Builder/MvcEndpointInfo.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Builder // Data we parse from the pattern will be used to fill in the rest of the constraints or // defaults. The parser will throw for invalid routes. ParsedPattern = RoutePatternFactory.Parse(pattern, defaults, constraints); - Constraints = BuildConstraints(parameterPolicyFactory); + ParameterPolicies = BuildParameterPolicies(ParsedPattern.Parameters, parameterPolicyFactory); Defaults = defaults; // Merge defaults outside of RoutePattern because the defaults will already have values from pattern @@ -50,33 +50,30 @@ namespace Microsoft.AspNetCore.Builder // Inline and non-inline defaults merged into one public RouteValueDictionary MergedDefaults { get; } - public IDictionary> Constraints { get; } + public IDictionary> ParameterPolicies { get; } public RouteValueDictionary DataTokens { get; } public RoutePattern ParsedPattern { get; private set; } - private Dictionary> BuildConstraints(ParameterPolicyFactory parameterPolicyFactory) + internal static Dictionary> BuildParameterPolicies(IReadOnlyList parameters, ParameterPolicyFactory parameterPolicyFactory) { - var constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var policies = new Dictionary>(StringComparer.OrdinalIgnoreCase); - foreach (var parameter in ParsedPattern.Parameters) + foreach (var parameter in parameters) { foreach (var parameterPolicy in parameter.ParameterPolicies) { var createdPolicy = parameterPolicyFactory.Create(parameter, parameterPolicy); - if (createdPolicy is IRouteConstraint routeConstraint) + if (!policies.TryGetValue(parameter.Name, out var policyList)) { - if (!constraints.TryGetValue(parameter.Name, out var paramConstraints)) - { - paramConstraints = new List(); - constraints.Add(parameter.Name, paramConstraints); - } - - paramConstraints.Add(routeConstraint); + policyList = new List(); + policies.Add(parameter.Name, policyList); } + + policyList.Add(createdPolicy); } } - return constraints; + return policies; } } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IApiBehaviorMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Core/IApiBehaviorMetadata.cs similarity index 80% rename from src/Microsoft.AspNetCore.Mvc.Core/Internal/IApiBehaviorMetadata.cs rename to src/Microsoft.AspNetCore.Mvc.Core/IApiBehaviorMetadata.cs index b73c1ba56e..41a053ebed 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/IApiBehaviorMetadata.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/IApiBehaviorMetadata.cs @@ -3,13 +3,13 @@ using Microsoft.AspNetCore.Mvc.Filters; -namespace Microsoft.AspNetCore.Mvc.Internal +namespace Microsoft.AspNetCore.Mvc { /// /// An interface for . See /// for details. /// - public interface IApiBehaviorMetadata : IFilterMetadata + internal interface IApiBehaviorMetadata : IFilterMetadata { } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs index a212c50ea4..fd5911ea07 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilter.cs @@ -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 _logger; - /// - /// Gets the filter order. Defaults to -2000 so that it runs early. - /// - public int Order => -2000; - public ClientErrorResultFilter( IClientErrorFactory clientErrorFactory, ILogger logger) @@ -26,6 +22,11 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + /// + /// Gets the filter order. Defaults to -2000 so that it runs early. + /// + public int Order => FilterOrder; + public void OnResultExecuted(ResultExecutedContext context) { } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilterFactory.cs new file mode 100644 index 0000000000..e4665fcc5d --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ClientErrorResultFilterFactory.cs @@ -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(serviceProvider); + return resultFilter; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ModelStateInvalidFilter.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ModelStateInvalidFilter.cs index 43aa96daec..c535bb118b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ModelStateInvalidFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ModelStateInvalidFilter.cs @@ -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 /// 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 for more detailed info. /// /// - public int Order => -2000; + public int Order => FilterOrder; /// public bool IsReusable => true; diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ModelStateInvalidFilterFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ModelStateInvalidFilterFactory.cs new file mode 100644 index 0000000000..3f5c81dc98 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/ModelStateInvalidFilterFactory.cs @@ -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>(); + var loggerFactory = serviceProvider.GetRequiredService(); + + return new ModelStateInvalidFilter(options.Value, loggerFactory.CreateLogger()); + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs deleted file mode 100644 index 9eeb83769a..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ApiBehaviorApplicationModelProvider.cs +++ /dev/null @@ -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, - IModelMetadataProvider modelMetadataProvider, - IClientErrorFactory clientErrorFactory, - ILoggerFactory loggerFactory) - { - _apiBehaviorOptions = apiBehaviorOptions.Value; - _modelMetadataProvider = modelMetadataProvider; - _logger = loggerFactory.CreateLogger(); - - if (!_apiBehaviorOptions.SuppressModelStateInvalidFilter && _apiBehaviorOptions.InvalidModelStateResponseFactory == null) - { - throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( - typeof(ApiBehaviorOptions), - nameof(ApiBehaviorOptions.InvalidModelStateResponseFactory))); - } - - _modelStateInvalidFilter = new ModelStateInvalidFilter( - apiBehaviorOptions.Value, - loggerFactory.CreateLogger()); - - _clientErrorResultFilter = new ClientErrorResultFilter( - clientErrorFactory, - loggerFactory.CreateLogger()); - } - - /// - /// Order is set to execute after the and allow any other user - /// that configure routing to execute. - /// - 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().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().ToArray(); - if (conventions.Length == 0) - { - var controllerAssembly = controllerModel.ControllerType.Assembly; - conventions = controllerAssembly.GetCustomAttributes().ToArray(); - } - - foreach (var actionModel in controllerModel.Actions) - { - if (!isApiController && !actionModel.Attributes.OfType().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().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().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().FirstOrDefault() ?? - actionModel.Controller.Attributes.OfType().FirstOrDefault() ?? - actionModel.Controller.ControllerType.Assembly.GetCustomAttribute(); - - 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; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs index 4c7e4aa8f2..174457bbcc 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ControllerActionDescriptorBuilder.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; using Resources = Microsoft.AspNetCore.Mvc.Core.Resources; namespace Microsoft.AspNetCore.Mvc.Internal @@ -389,15 +390,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal { try { + actionDescriptor.Properties.TryGetValue(typeof(IParameterTransformer), out var transformer); + var routeTokenTransformer = transformer as IParameterTransformer; + actionDescriptor.AttributeRouteInfo.Template = AttributeRouteModel.ReplaceTokens( actionDescriptor.AttributeRouteInfo.Template, - actionDescriptor.RouteValues); + actionDescriptor.RouteValues, + routeTokenTransformer); if (actionDescriptor.AttributeRouteInfo.Name != null) { actionDescriptor.AttributeRouteInfo.Name = AttributeRouteModel.ReplaceTokens( actionDescriptor.AttributeRouteInfo.Name, - actionDescriptor.RouteValues); + actionDescriptor.RouteValues, + routeTokenTransformer); } } catch (InvalidOperationException ex) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index 9c1e7dd801..7f45a49b09 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -9,7 +9,6 @@ using System.Text; using System.Threading; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; @@ -23,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal { private readonly IActionDescriptorCollectionProvider _actions; private readonly MvcEndpointInvokerFactory _invokerFactory; + private readonly ParameterPolicyFactory _parameterPolicyFactory; // The following are protected by this lock for WRITES only. This pattern is similar // to DefaultActionDescriptorChangeProvider - see comments there for details on @@ -34,7 +34,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal public MvcEndpointDataSource( IActionDescriptorCollectionProvider actions, - MvcEndpointInvokerFactory invokerFactory) + MvcEndpointInvokerFactory invokerFactory, + ParameterPolicyFactory parameterPolicyFactory) { if (actions == null) { @@ -46,8 +47,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal throw new ArgumentNullException(nameof(invokerFactory)); } + if (parameterPolicyFactory == null) + { + throw new ArgumentNullException(nameof(parameterPolicyFactory)); + } + _actions = actions; _invokerFactory = invokerFactory; + _parameterPolicyFactory = parameterPolicyFactory; ConventionalEndpointInfos = new List(); @@ -113,7 +120,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal // // Start with an order of '1' for conventional routes as attribute routes have a default order of '0'. // This is for scenarios dealing with migrating existing Router based code to Endpoint Routing world. - var conventionalRouteOrder = 0; + var conventionalRouteOrder = 1; // Check each of the conventional patterns to see if the action would be reachable // If the action and pattern are compatible then create an endpoint with the @@ -143,90 +150,38 @@ namespace Microsoft.AspNetCore.Mvc.Internal continue; } - var newPathSegments = endpointInfo.ParsedPattern.PathSegments.ToList(); - - for (var i = 0; i < newPathSegments.Count; i++) - { - // Check if the pattern can be shortened because the remaining parameters are optional - // - // e.g. Matching pattern {controller=Home}/{action=Index}/{id?} against HomeController.Index - // can resolve to the following endpoints: - // - /Home/Index/{id?} - // - /Home - // - / - if (UseDefaultValuePlusRemainingSegmentsOptional(i, action, endpointInfo, newPathSegments)) - { - var subPathSegments = newPathSegments.Take(i); - - var subEndpoint = CreateEndpoint( - action, - endpointInfo.Name, - GetPattern(ref patternStringBuilder, subPathSegments), - subPathSegments, - endpointInfo.Defaults, - ++conventionalRouteOrder, - endpointInfo, - endpointInfo.DataTokens, - suppressLinkGeneration: false, - suppressPathMatching: false); - endpoints.Add(subEndpoint); - } - - List segmentParts = null; // Initialize only as needed - var segment = newPathSegments[i]; - for (var j = 0; j < segment.Parts.Count; j++) - { - var part = segment.Parts[j]; - - if (part.IsParameter && - part is RoutePatternParameterPart parameterPart && - action.RouteValues.ContainsKey(parameterPart.Name)) - { - if (segmentParts == null) - { - segmentParts = segment.Parts.ToList(); - } - - // Replace parameter with literal value - segmentParts[j] = RoutePatternFactory.LiteralPart(action.RouteValues[parameterPart.Name]); - } - } - - // A parameter part was replaced so replace segment with updated parts - if (segmentParts != null) - { - newPathSegments[i] = RoutePatternFactory.Segment(segmentParts); - } - } - - var endpoint = CreateEndpoint( + conventionalRouteOrder = CreateEndpoints( + endpoints, + ref patternStringBuilder, action, - endpointInfo.Name, - GetPattern(ref patternStringBuilder, newPathSegments), - newPathSegments, + conventionalRouteOrder, + endpointInfo.ParsedPattern, + endpointInfo.MergedDefaults, endpointInfo.Defaults, - ++conventionalRouteOrder, - endpointInfo, + endpointInfo.Name, endpointInfo.DataTokens, + endpointInfo.ParameterPolicies, suppressLinkGeneration: false, suppressPathMatching: false); - endpoints.Add(endpoint); } } else { - var endpoint = CreateEndpoint( + var attributeRoutePattern = RoutePatternFactory.Parse(action.AttributeRouteInfo.Template); + + CreateEndpoints( + endpoints, + ref patternStringBuilder, action, - action.AttributeRouteInfo.Name, - action.AttributeRouteInfo.Template, - RoutePatternFactory.Parse(action.AttributeRouteInfo.Template).PathSegments, - nonInlineDefaults: null, action.AttributeRouteInfo.Order, - action.AttributeRouteInfo, + attributeRoutePattern, + attributeRoutePattern.Defaults, + nonInlineDefaults: null, + action.AttributeRouteInfo.Name, dataTokens: null, - suppressLinkGeneration: action.AttributeRouteInfo.SuppressLinkGeneration, - suppressPathMatching: action.AttributeRouteInfo.SuppressPathMatching); - endpoints.Add(endpoint); + allParameterPolicies: null, + action.AttributeRouteInfo.SuppressLinkGeneration, + action.AttributeRouteInfo.SuppressPathMatching); } } @@ -246,6 +201,112 @@ namespace Microsoft.AspNetCore.Mvc.Internal // Step 4 - trigger old token oldCancellationTokenSource?.Cancel(); } + } + + // CreateEndpoints processes the route pattern, replacing area/controller/action parameters with endpoint values + // Because of default values it is possible for a route pattern to resolve to multiple endpoints + private int CreateEndpoints( + List endpoints, + ref StringBuilder patternStringBuilder, + ActionDescriptor action, + int routeOrder, + RoutePattern routePattern, + IReadOnlyDictionary allDefaults, + IReadOnlyDictionary nonInlineDefaults, + string name, + RouteValueDictionary dataTokens, + IDictionary> allParameterPolicies, + bool suppressLinkGeneration, + bool suppressPathMatching) + { + var newPathSegments = routePattern.PathSegments.ToList(); + + for (var i = 0; i < newPathSegments.Count; i++) + { + // Check if the pattern can be shortened because the remaining parameters are optional + // + // e.g. Matching pattern {controller=Home}/{action=Index}/{id?} against HomeController.Index + // can resolve to the following endpoints: + // - /Home/Index/{id?} + // - /Home + // - / + if (UseDefaultValuePlusRemainingSegmentsOptional(i, action, allDefaults, newPathSegments)) + { + var subPathSegments = newPathSegments.Take(i); + + var subEndpoint = CreateEndpoint( + action, + name, + GetPattern(ref patternStringBuilder, subPathSegments), + subPathSegments, + nonInlineDefaults, + routeOrder++, + dataTokens, + suppressLinkGeneration, + suppressPathMatching); + endpoints.Add(subEndpoint); + } + + List segmentParts = null; // Initialize only as needed + var segment = newPathSegments[i]; + for (var j = 0; j < segment.Parts.Count; j++) + { + var part = segment.Parts[j]; + + if (part.IsParameter && + part is RoutePatternParameterPart parameterPart && + action.RouteValues.ContainsKey(parameterPart.Name)) + { + if (segmentParts == null) + { + segmentParts = segment.Parts.ToList(); + } + if (allParameterPolicies == null) + { + allParameterPolicies = MvcEndpointInfo.BuildParameterPolicies(routePattern.Parameters, _parameterPolicyFactory); + } + + var parameterRouteValue = action.RouteValues[parameterPart.Name]; + + // Replace parameter with literal value + if (allParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicies)) + { + // Check if the parameter has a transformer policy + // Use the first transformer policy + for (var k = 0; k < parameterPolicies.Count; k++) + { + if (parameterPolicies[k] is IParameterTransformer parameterTransformer) + { + parameterRouteValue = parameterTransformer.Transform(parameterRouteValue); + break; + } + } + } + + segmentParts[j] = RoutePatternFactory.LiteralPart(parameterRouteValue); + } + } + + // A parameter part was replaced so replace segment with updated parts + if (segmentParts != null) + { + newPathSegments[i] = RoutePatternFactory.Segment(segmentParts); + } + } + + var endpoint = CreateEndpoint( + action, + name, + GetPattern(ref patternStringBuilder, newPathSegments), + newPathSegments, + nonInlineDefaults, + routeOrder++, + dataTokens, + suppressLinkGeneration, + suppressPathMatching); + endpoints.Add(endpoint); + + return routeOrder; string GetPattern(ref StringBuilder sb, IEnumerable segments) { @@ -265,7 +326,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal private bool UseDefaultValuePlusRemainingSegmentsOptional( int segmentIndex, ActionDescriptor action, - MvcEndpointInfo endpointInfo, + IReadOnlyDictionary allDefaults, List pathSegments) { // Check whether the remaining segments are all optional and one or more of them is @@ -287,7 +348,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (action.RouteValues.ContainsKey(parameterPart.Name)) { - if (endpointInfo.MergedDefaults[parameterPart.Name] is string defaultValue + if (allDefaults.TryGetValue(parameterPart.Name, out var v) + && v is string defaultValue && action.RouteValues.TryGetValue(parameterPart.Name, out var routeValue) && string.Equals(defaultValue, routeValue, StringComparison.OrdinalIgnoreCase)) { @@ -346,11 +408,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal { // Check that the value matches against constraints on that parameter // e.g. For {controller:regex((Home|Login))} the controller value must match the regex - if (endpointInfo.Constraints.TryGetValue(routeKey, out var constraints)) + if (endpointInfo.ParameterPolicies.TryGetValue(routeKey, out var parameterPolicies)) { - foreach (var constraint in constraints) + foreach (var policy in parameterPolicies) { - if (!constraint.Match(httpContext: null, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest)) + if (policy is IRouteConstraint constraint + && !constraint.Match(httpContext: null, NullRouter.Instance, routeKey, new RouteValueDictionary(action.RouteValues), RouteDirection.IncomingRequest)) { // Did not match constraint return false; @@ -372,7 +435,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal IEnumerable segments, object nonInlineDefaults, int order, - object source, RouteValueDictionary dataTokens, bool suppressLinkGeneration, bool suppressPathMatching) @@ -394,7 +456,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal action, routeName, new RouteValueDictionary(action.RouteValues), - source, dataTokens, suppressLinkGeneration, suppressPathMatching); @@ -413,7 +474,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal ActionDescriptor action, string routeName, RouteValueDictionary requiredValues, - object source, RouteValueDictionary dataTokens, bool suppressLinkGeneration, bool suppressPathMatching) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ProducesErrorResponseTypeAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/ProducesErrorResponseTypeAttribute.cs index f74a826e93..b9c579cea8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ProducesErrorResponseTypeAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ProducesErrorResponseTypeAttribute.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc /// /// Initializes a new instance of . /// - /// The error type. + /// The error type. Use to indicate the absence of a default error type. public ProducesErrorResponseTypeAttribute(Type type) { Type = type ?? throw new ArgumentNullException(nameof(type)); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/breakingchanges.netcore.json b/src/Microsoft.AspNetCore.Mvc.Core/breakingchanges.netcore.json new file mode 100644 index 0000000000..3fd0e6ddff --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/breakingchanges.netcore.json @@ -0,0 +1,6 @@ +[ + { + "TypeId": "public class Microsoft.AspNetCore.Mvc.ApiControllerAttribute : Microsoft.AspNetCore.Mvc.ControllerAttribute, Microsoft.AspNetCore.Mvc.Internal.IApiBehaviorMetadata", + "Kind": "Removal" + } +] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/FileProviderRazorProject.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/FileProviderRazorProject.cs index 52af4ae3f3..9e5718fa2f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/FileProviderRazorProject.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/FileProviderRazorProject.cs @@ -16,20 +16,20 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal private readonly IFileProvider _provider; private readonly IHostingEnvironment _hostingEnvironment; - public FileProviderRazorProjectFileSystem(IRazorViewEngineFileProviderAccessor accessor, IHostingEnvironment hostingEnviroment) + public FileProviderRazorProjectFileSystem(IRazorViewEngineFileProviderAccessor accessor, IHostingEnvironment hostingEnvironment) { if (accessor == null) { throw new ArgumentNullException(nameof(accessor)); } - if (hostingEnviroment == null) + if (hostingEnvironment == null) { - throw new ArgumentNullException(nameof(hostingEnviroment)); + throw new ArgumentNullException(nameof(hostingEnvironment)); } _provider = accessor.FileProvider; - _hostingEnvironment = hostingEnviroment; + _hostingEnvironment = hostingEnvironment; } public override RazorProjectItem GetItem(string path) diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs index 73633bf59c..e3f3355fde 100644 --- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Internal/PageSaveTempDataPropertyFilter.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages Subject = context.HandlerInstance; var tempData = _tempDataFactory.GetTempData(context.HttpContext); - SetPropertyVaules(tempData); + SetPropertyValues(tempData); } public void OnPageHandlerExecuted(PageHandlerExecutedContext context) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs index 96228d5b7b..e375ae94bc 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ControllerSaveTempDataPropertyFilter.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures Subject = context.Controller; var tempData = _tempDataFactory.GetTempData(context.HttpContext); - SetPropertyVaules(tempData); + SetPropertyValues(tempData); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs index 2ecf4cbbef..52e98e0677 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataFilter.cs @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// public void OnResourceExecuted(ResourceExecutedContext context) { - // If there is an unhandled exception, we would like to avoid setting tempdata as + // If there is an unhandled exception, we would like to avoid setting tempdata as // the end user is going to see an error page anyway and also it helps us in avoiding // accessing resources like Session too late in the request lifecyle where SessionFeature might // not be available. diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs index c560584798..ab4c79dff8 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/SaveTempDataPropertyFilterBase.cs @@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal /// Sets the values of the properties of from . /// /// The . - protected void SetPropertyVaules(ITempDataDictionary tempData) + protected void SetPropertyValues(ITempDataDictionary tempData) { if (Properties == null) { diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs index aee457d5a6..5ea1f29b9f 100644 --- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/ApiResponseTypeProviderTest.cs @@ -550,6 +550,119 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer }); } + [Fact] + public void GetApiResponseTypes_CombinesProducesAttributeAndConventions() + { + // Arrange + var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel)); + actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/json"), FilterScope.Controller)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(200), + new ProducesResponseTypeAttribute(400), + new ProducesDefaultResponseTypeAttribute(), + }); + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails)); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.True(responseType.IsDefaultResponse); + Assert.Equal(typeof(ProblemDetails), responseType.Type); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(typeof(DerivedModel), responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }, + responseType => + { + Assert.Equal(400, responseType.StatusCode); + Assert.Equal(typeof(ProblemDetails), responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }); + } + + [Fact] + public void GetApiResponseTypes_DoesNotCombineProducesAttributeThatSpecifiesType() + { + // Arrange + var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel)); + actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/json") { Type = typeof(string) }, FilterScope.Controller)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(200), + new ProducesResponseTypeAttribute(400), + new ProducesDefaultResponseTypeAttribute(), + }); + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails)); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(typeof(string), responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }); + } + + [Fact] + public void GetApiResponseTypes_DoesNotCombineProducesResponseTypeAttributeThatSpecifiesStatusCode() + { + // Arrange + var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel)); + actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[] + { + new ProducesResponseTypeAttribute(200), + }); + actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails)); + + var provider = GetProvider(); + + // Act + var result = provider.GetApiResponseTypes(actionDescriptor); + + // Assert + Assert.Collection( + result.OrderBy(r => r.StatusCode), + responseType => + { + Assert.Equal(200, responseType.StatusCode); + Assert.Equal(typeof(DerivedModel), responseType.Type); + Assert.False(responseType.IsDefaultResponse); + Assert.Collection( + responseType.ApiResponseFormats, + format => Assert.Equal("application/json", format.MediaType)); + }); + } + private static ApiResponseTypeProvider GetProvider() { var mvcOptions = new MvcOptions diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs index e4a446485d..81f17cb58b 100644 --- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs @@ -1181,7 +1181,7 @@ namespace Microsoft.AspNetCore.Mvc.Description } [Fact] - public void GetApiDescription_ParameterDescription_IsRequiredNotSet_IfNotValiatingTopLevelNodes() + public void GetApiDescription_ParameterDescription_IsRequiredNotSet_IfNotValidatingTopLevelNodes() { // Arrange var action = CreateActionDescriptor(nameof(RequiredParameter)); @@ -2081,7 +2081,7 @@ namespace Microsoft.AspNetCore.Mvc.Description { } - private void AcceptsRedundantMetadata([FromQuery] RedundentMetadata r) + private void AcceptsRedundantMetadata([FromQuery] RedundantMetadata r) { } @@ -2247,7 +2247,7 @@ namespace Microsoft.AspNetCore.Mvc.Description public string Name { get; set; } } - private class RedundentMetadata + private class RedundantMetadata { [FromQuery] public int Id { get; set; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ActionModelTest.cs similarity index 100% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ActionModelTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ActionModelTest.cs diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs new file mode 100644 index 0000000000..1d00e52dc2 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiBehaviorApplicationModelProviderTest.cs @@ -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()) + { + 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(() => 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()) + { + Controller = controllerModel, + }; + controllerModel.Actions.Add(actionModel); + + var parameter = method.GetParameters()[0]; + var parameterModel = new ParameterModel(parameter, Array.Empty()) + { + 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()); + Assert.NotEmpty(actionModel.Filters.OfType()); + 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(c), + c => Assert.IsType(c), + c => Assert.IsType(c), + c => Assert.IsType(c), + c => + { + var convention = Assert.IsType(c); + Assert.Equal(typeof(ProblemDetails), convention.DefaultErrorResponseType.Type); + }); + + Assert.Collection( + provider.ControllerModelConventions, + c => + { + var convention = Assert.IsType(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()); + } + + [Fact] + public void Constructor_DoesNotAddInvalidModelStateFilterConvention_IfSuppressModelStateInvalidFilterIsSet() + { + // Arrange + var provider = GetProvider(new ApiBehaviorOptions { SuppressModelStateInvalidFilter = true }); + + // Act & Assert + Assert.Empty(provider.ActionModelConventions.OfType()); + } + + [Fact] + public void Constructor_DoesNotAddConsumesConstraintForFormFileParameterConvention_IfSuppressConsumesConstraintForFormFileParametersIsSet() + { + // Arrange + var provider = GetProvider(new ApiBehaviorOptions { SuppressConsumesConstraintForFormFileParameters = true }); + + // Act & Assert + Assert.Empty(provider.ActionModelConventions.OfType()); + } + + [Fact] + public void Constructor_SetsSuppressInferBindingSourcesForParametersIsSet() + { + // Arrange + var provider = GetProvider(new ApiBehaviorOptions { SuppressInferBindingSourcesForParameters = true }); + + // Act & Assert + var convention = Assert.Single(provider.ControllerModelConventions.OfType()); + 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()); + 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(), + 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; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiConventionApplicationModelConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiConventionApplicationModelConventionTest.cs new file mode 100644 index 0000000000..d658c7dc73 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiConventionApplicationModelConventionTest.cs @@ -0,0 +1,189 @@ +// 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_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()); + var action = new ActionModel(typeof(object).GetMethods()[0], Array.Empty()) + { + Controller = controller, + }; + var convention = GetConvention(expected); + + // Act + convention.Apply(action); + + // Assert + var attribute = GetProperty(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(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(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(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(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(ActionModel action) + { + return Assert.IsType(action.Properties[typeof(TValue)]); + } + + private static ActionModel GetActionModel( + string actionName, + object[] actionAttributes = null, + object[] controllerAttributes = null) + { + actionAttributes = actionAttributes ?? Array.Empty(); + 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; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiVisibilityConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiVisibilityConventionTest.cs new file mode 100644 index 0000000000..3928909e04 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ApiVisibilityConventionTest.cs @@ -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]), + }; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/AttributeRouteModelTests.cs similarity index 99% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/AttributeRouteModelTests.cs index c7a578850b..dc312a50bd 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/AttributeRouteModelTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/AttributeRouteModelTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using Microsoft.AspNetCore.Routing; using Xunit; namespace Microsoft.AspNetCore.Mvc.ApplicationModels diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ClientErrorResultFilterConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ClientErrorResultFilterConventionTest.cs new file mode 100644 index 0000000000..4fc09baf2b --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ClientErrorResultFilterConventionTest.cs @@ -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()); + } + + private ClientErrorResultFilterConvention GetConvention() + { + return new ClientErrorResultFilterConvention(); + } + + private static ActionModel GetActionModel() + { + var action = new ActionModel(typeof(object).GetMethods()[0], new object[0]); + + return action; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ConsumesConstraintForFormFileParameterConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ConsumesConstraintForFormFileParameterConventionTest.cs new file mode 100644 index 0000000000..9f7d07402a --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ConsumesConstraintForFormFileParameterConventionTest.cs @@ -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(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(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; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ControllerModelTest.cs similarity index 100% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ControllerModelTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ControllerModelTest.cs diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/InferParameterBindingInfoConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/InferParameterBindingInfoConventionTest.cs new file mode 100644 index 0000000000..7619c52b19 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/InferParameterBindingInfoConventionTest.cs @@ -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(() => 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(() => 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(() => 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(actionModel); + Assert.Same(BindingSource.Body, model.BindingInfo.BindingSource); + + var cancellationToken = GetParameterModel(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(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 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 RequestPredicateStatic => (c) => true; + public static Func PropertyFilterStatic => (c) => true; + + public Func RequestPredicate => RequestPredicateStatic; + + public Func 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 TestProperty { get; set; } + + public IActionResult SomeAction([FromQuery] List 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; + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/InvalidModelStateFilterConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/InvalidModelStateFilterConventionTest.cs new file mode 100644 index 0000000000..c53357d4f1 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/InvalidModelStateFilterConventionTest.cs @@ -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()); + } + + + private static ActionModel GetActionModel() + { + var action = new ActionModel(typeof(object).GetMethods()[0], new object[0]); + + return action; + } + + private InvalidModelStateFilterConvention GetConvention() + { + return new InvalidModelStateFilterConvention(); + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ParameterModelTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ParameterModelTest.cs similarity index 100% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/ParameterModelTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/ParameterModelTest.cs diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/PropertyModelTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/PropertyModelTest.cs similarity index 100% rename from test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModel/PropertyModelTest.cs rename to test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/PropertyModelTest.cs diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/RouteTokenTransformerConventionTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/RouteTokenTransformerConventionTest.cs new file mode 100644 index 0000000000..aabefa456e --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ApplicationModels/RouteTokenTransformerConventionTest.cs @@ -0,0 +1,99 @@ +// 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.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Routing; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModels +{ + public class RouteTokenTransformerConventionTest + { + [Fact] + public void Apply_NullAttributeRouteModel_NoOp() + { + // Arrange + var convention = new RouteTokenTransformerConvention(new TestParameterTransformer()); + + var model = new ActionModel(GetMethodInfo(), Array.Empty()); + model.Selectors.Add(new SelectorModel() + { + AttributeRouteModel = null + }); + + // Act + convention.Apply(model); + + // Assert + Assert.Null(model.Selectors[0].AttributeRouteModel); + } + + [Fact] + public void Apply_HasAttributeRouteModel_SetRouteTokenTransformer() + { + // Arrange + var transformer = new TestParameterTransformer(); + var convention = new RouteTokenTransformerConvention(transformer); + + var model = new ActionModel(GetMethodInfo(), Array.Empty()); + model.Selectors.Add(new SelectorModel() + { + AttributeRouteModel = new AttributeRouteModel() + }); + + // Act + convention.Apply(model); + + // Assert + Assert.True(model.Properties.TryGetValue(typeof(IParameterTransformer), out var routeTokenTransformer)); + Assert.Equal(transformer, routeTokenTransformer); + } + + [Fact] + public void Apply_ShouldApplyFalse_NoOp() + { + // Arrange + var transformer = new TestParameterTransformer(); + var convention = new CustomRouteTokenTransformerConvention(transformer); + + var model = new ActionModel(GetMethodInfo(), Array.Empty()); + model.Selectors.Add(new SelectorModel() + { + AttributeRouteModel = new AttributeRouteModel() + }); + + // Act + convention.Apply(model); + + // Assert + Assert.False(model.Properties.TryGetValue(typeof(IParameterTransformer), out _)); + } + + private MethodInfo GetMethodInfo() + { + return typeof(RouteTokenTransformerConventionTest).GetMethod(nameof(GetMethodInfo), BindingFlags.NonPublic | BindingFlags.Instance); + } + + private class TestParameterTransformer : IParameterTransformer + { + public string Transform(string value) + { + return value; + } + } + + private class CustomRouteTokenTransformerConvention : RouteTokenTransformerConvention + { + public CustomRouteTokenTransformerConvention(IParameterTransformer parameterTransformer) : base(parameterTransformer) + { + } + + protected override bool ShouldApply(ActionModel action) + { + return false; + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatterMappingsTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatterMappingsTest.cs index 15252dc783..a181b5528b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatterMappingsTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Formatters/FormatterMappingsTest.cs @@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters var options = new FormatterMappings(); options.SetMediaTypeMappingForFormat(setFormat, MediaTypeHeaderValue.Parse(contentType)); - // Act + // Act var returnMediaType = options.GetMediaTypeMappingForFormat(getFormat); // Assert @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters [InlineData("application/*")] [InlineData("*/json")] [InlineData("*/*")] - public void FormatterMappings_Wildcardformat(string format) + public void FormatterMappings_WildcardFormat(string format) { // Arrange var options = new FormatterMappings(); @@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters mediaType = MediaTypeHeaderValue.Parse("application/bar"); options.SetMediaTypeMappingForFormat("bar", mediaType); - // Act + // Act var cleared = options.ClearMediaTypeMappingForFormat(format); // Assert diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs deleted file mode 100644 index 23241174ca..0000000000 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ApiBehaviorApplicationModelProviderTest.cs +++ /dev/null @@ -1,1591 +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.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reflection; -using System.Reflection.Emit; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; -using Moq; -using Xunit; - -[assembly: Microsoft.AspNetCore.Mvc.ProducesErrorResponseType(typeof(InvalidEnumArgumentException))] - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - public class ApiBehaviorApplicationModelProviderTest - { - [Fact] - public void OnProvidersExecuting_AddsModelStateInvalidFilter_IfTypeIsAnnotatedWithAttribute() - { - // Arrange - var context = GetContext(typeof(TestApiController)); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions); - Assert.Single(actionModel.Filters.OfType()); - } - - [Fact] - public void OnProvidersExecuting_DoesNotAddModelStateInvalidFilterToController_IfFeatureIsDisabledViaOptions() - { - // Arrange - var context = GetContext(typeof(TestApiController)); - var options = new ApiBehaviorOptions - { - SuppressModelStateInvalidFilter = true, - }; - - var provider = GetProvider(options); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions); - Assert.Empty(actionModel.Filters.OfType()); - } - - [Fact] - public void OnProvidersExecuting_AddsModelStateInvalidFilter_IfActionIsAnnotatedWithAttribute() - { - // Arrange - var context = GetContext(typeof(SimpleController)); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - Assert.Collection( - Assert.Single(context.Result.Controllers).Actions.OrderBy(a => a.ActionName), - action => - { - Assert.Single(action.Filters.OfType()); - }, - action => - { - Assert.Empty(action.Filters.OfType()); - }); - } - - [Fact] - public void OnProvidersExecuting_SkipsAddingFilterToActionIfFeatureIsDisabledUsingOptions() - { - // Arrange - var context = GetContext(typeof(SimpleController)); - var options = new ApiBehaviorOptions - { - SuppressModelStateInvalidFilter = true, - }; - - var provider = GetProvider(options); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - Assert.Collection( - Assert.Single(context.Result.Controllers).Actions.OrderBy(a => a.ActionName), - action => - { - Assert.Empty(action.Filters.OfType()); - }, - action => - { - Assert.Empty(action.Filters.OfType()); - }); - } - - [Fact] - public void OnProvidersExecuting_MakesControllerVisibleInApiExplorer_IfItIsAnnotatedWithAttribute() - { - // Arrange - var context = GetContext(typeof(TestApiController)); - var options = new ApiBehaviorOptions - { - SuppressModelStateInvalidFilter = true, - }; - - var provider = GetProvider(options); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - Assert.True(controller.ApiExplorer.IsVisible); - } - - [Fact] - public void OnProvidersExecuting_DoesNotModifyVisibilityInApiExplorer_IfValueIsAlreadySet() - { - // Arrange - var context = GetContext(typeof(TestApiController)); - context.Result.Controllers[0].ApiExplorer.IsVisible = false; - var options = new ApiBehaviorOptions - { - SuppressModelStateInvalidFilter = true, - }; - - var provider = GetProvider(options); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - Assert.False(controller.ApiExplorer.IsVisible); - } - - [Fact] - public void OnProvidersExecuting_ThrowsIfControllerWithAttribute_HasActionsWithoutAttributeRouting() - { - // Arrange - var actionName = $"{typeof(ActionsWithoutAttributeRouting).FullName}.{nameof(ActionsWithoutAttributeRouting.Index)} ({typeof(ActionsWithoutAttributeRouting).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 context = GetContext(typeof(ActionsWithoutAttributeRouting)); - var provider = GetProvider(); - - // Act & Assert - var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal(expected, ex.Message); - } - - [Fact] - public void InferBindingSourceForParameter_ReturnsPath_IfParameterNameExistsInRouteAsSimpleToken() - { - // Arrange - var actionName = nameof(ParameterBindingController.SimpleRouteToken); - var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); - var provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.InferBindingSourceForParameter(parameter); - - // Assert - Assert.Same(BindingSource.Path, result); - } - - [Fact] - public void InferBindingSourceForParameter_ReturnsPath_IfParameterAppearsInAnyRoutes_MultipleRoutes() - { - // Arrange - var actionName = nameof(ParameterBindingController.ParameterInMultipleRoutes); - var parameter = GetParameterModel(typeof(ParameterBindingController), actionName); - var provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.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 provider = GetProvider(); - - // Act - var result = provider.InferBindingSourceForParameter(parameter); - - // Assert - Assert.Same(BindingSource.Body, result); - } - - [Fact] - public void OnProvidersExecuting_DoesNotInferBindingSourceForParametersWithBindingInfo() - { - // Arrange - var actionName = nameof(ParameterWithBindingInfo.Action); - var provider = GetProvider(); - var context = GetContext(typeof(ParameterWithBindingInfo)); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controllerModel = Assert.Single(context.Result.Controllers); - var actionModel = Assert.Single(controllerModel.Actions, a => a.ActionName == actionName); - var parameterModel = Assert.Single(actionModel.Parameters); - Assert.NotNull(parameterModel.BindingInfo); - Assert.Same(BindingSource.Custom, parameterModel.BindingInfo.BindingSource); - } - - [Fact] - public void OnProvidersExecuting_Throws_IfMultipleParametersAreInferredAsBodyBound() - { - // Arrange - var expected = -$@"Action '{typeof(ControllerWithMultipleInferredFromBodyParameters).FullName}.{nameof(ControllerWithMultipleInferredFromBodyParameters.Action)} ({typeof(ControllerWithMultipleInferredFromBodyParameters).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 context = GetContext(typeof(ControllerWithMultipleInferredFromBodyParameters)); - var provider = GetProvider(); - - // Act & Assert - var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal(expected, ex.Message); - } - - [Fact] - public void OnProvidersExecuting_Throws_IfMultipleParametersAreInferredOrSpecifiedAsBodyBound() - { - // Arrange - var expected = -$@"Action '{typeof(ControllerWithMultipleInferredOrSpecifiedFromBodyParameters).FullName}.{nameof(ControllerWithMultipleInferredOrSpecifiedFromBodyParameters.Action)} ({typeof(ControllerWithMultipleInferredOrSpecifiedFromBodyParameters).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 context = GetContext(typeof(ControllerWithMultipleInferredOrSpecifiedFromBodyParameters)); - var provider = GetProvider(); - - // Act & Assert - var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal(expected, ex.Message); - } - - [Fact] - public void OnProvidersExecuting_Throws_IfMultipleParametersAreFromBody() - { - // Arrange - var expected = -$@"Action '{typeof(ControllerWithMultipleFromBodyParameters).FullName}.{nameof(ControllerWithMultipleFromBodyParameters.Action)} ({typeof(ControllerWithMultipleFromBodyParameters).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 context = GetContext(typeof(ControllerWithMultipleFromBodyParameters)); - var provider = GetProvider(); - - // Act & Assert - var ex = Assert.Throws(() => provider.OnProvidersExecuting(context)); - Assert.Equal(expected, ex.Message); - } - - [Fact] - public void OnProvidersExecuting_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinder_AndExplicitName() - { - // Arrange - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var actionName = nameof(ModelBinderOnParameterController.ModelBinderAttributeWithExplicitModelName); - var context = GetContext(typeof(ModelBinderOnParameterController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 OnProvidersExecuting_PreservesBindingInfo_WhenInferringFor_ParameterWithModelBinderType() - { - // Arrange - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var actionName = nameof(ModelBinderOnParameterController.ModelBinderType); - var context = GetContext(typeof(ModelBinderOnParameterController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ModelBinderOnParameterController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 PreservesBindingSourceInference_ForFromQueryParameter_WithDefaultName() - { - // Arrange - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var actionName = nameof(ParameterBindingController.FromQuery); - var context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - var parameter = Assert.Single(action.Parameters); - - var bindingInfo = parameter.BindingInfo; - Assert.NotNull(bindingInfo); - Assert.Same(BindingSource.Query, bindingInfo.BindingSource); - Assert.Equal(string.Empty, bindingInfo.BinderModelName); - } - - [Fact] - public void PreservesBindingSourceInference_ForFromQueryParameterOnComplexType_WithCustomName() - { - // Arrange - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var actionName = nameof(ParameterBindingController.FromQueryOnComplexTypeWithCustomName); - var context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - var parameter = Assert.Single(action.Parameters); - - var bindingInfo = parameter.BindingInfo; - Assert.NotNull(bindingInfo); - Assert.Same(BindingSource.Path, bindingInfo.BindingSource); - Assert.Equal(string.Empty, bindingInfo.BinderModelName); - } - - [Fact] - public void PreservesBindingSourceInference_ForFromRouteParameterOnComplexType_WithCustomName() - { - // Arrange - var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var actionName = nameof(ParameterBindingController.FromRouteOnComplexTypeWithCustomName); - var context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 context = GetContext(typeof(ParameterBindingController), modelMetadataProvider); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var controller = Assert.Single(context.Result.Controllers); - var action = Assert.Single(controller.Actions, a => a.ActionName == actionName); - 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 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 provider = GetProvider(); - - // Act - provider.InferParameterBindingSources(actionModel); - - // Assert - var model = GetParameterModel(actionModel); - Assert.Same(BindingSource.Body, model.BindingInfo.BindingSource); - - var cancellationToken = GetParameterModel(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 provider = GetProvider(); - - // Act - var result = provider.InferBindingSourceForParameter(parameter); - - // Assert - Assert.Same(BindingSource.Query, result); - } - - [Fact] - public void InferBoundPropertyModelPrefixes_SetsModelPrefix_ForComplexTypeFromValueProvider() - { - // Arrange - var controller = GetControllerModel(typeof(ControllerWithBoundProperty)); - - var provider = GetProvider(); - - // Act - provider.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 provider = GetProvider(); - - // Act - provider.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 provider = GetProvider(); - - // Act - provider.InferParameterModelPrefixes(action); - - // Assert - var parameter = Assert.Single(action.Parameters); - Assert.Equal(string.Empty, parameter.BindingInfo.BinderModelName); - } - - [Fact] - public void AddMultipartFormDataConsumesAttribute_NoOpsIfBehaviorIsDisabled() - { - // Arrange - var actionName = nameof(ParameterBindingController.FromFormParameter); - var action = GetActionModel(typeof(ParameterBindingController), actionName); - var options = new ApiBehaviorOptions - { - SuppressConsumesConstraintForFormFileParameters = true, - InvalidModelStateResponseFactory = _ => null, - }; - var provider = GetProvider(options); - - // Act - provider.AddMultipartFormDataConsumesAttribute(action); - - // Assert - Assert.Empty(action.Filters); - } - - [Fact] - public void AddMultipartFormDataConsumesAttribute_NoOpsIfConsumesConstraintIsAlreadyPresent() - { - // Arrange - var actionName = nameof(ParameterBindingController.ActionWithConsumesAttribute); - var action = GetActionModel(typeof(ParameterBindingController), actionName); - var options = new ApiBehaviorOptions - { - SuppressConsumesConstraintForFormFileParameters = true, - InvalidModelStateResponseFactory = _ => null, - }; - var provider = GetProvider(options); - - // Act - provider.AddMultipartFormDataConsumesAttribute(action); - - // Assert - var attribute = Assert.Single(action.Filters); - var consumesAttribute = Assert.IsType(attribute); - Assert.Equal("application/json", Assert.Single(consumesAttribute.ContentTypes)); - } - - [Fact] - public void AddMultipartFormDataConsumesAttribute_AddsConsumesAttribute_WhenActionHasFromFormFileParameter() - { - // Arrange - var actionName = nameof(ParameterBindingController.FormFileParameter); - var action = GetActionModel(typeof(ParameterBindingController), actionName); - action.Parameters[0].BindingInfo = new BindingInfo - { - BindingSource = BindingSource.FormFile, - }; - var provider = GetProvider(); - - // Act - provider.AddMultipartFormDataConsumesAttribute(action); - - // Assert - var attribute = Assert.Single(action.Filters); - var consumesAttribute = Assert.IsType(attribute); - Assert.Equal("multipart/form-data", Assert.Single(consumesAttribute.ContentTypes)); - } - - [Fact] - public void DiscoverApiConvention_DoesNotAddConventionItem_IfActionHasProducesResponseTypeAttribute() - { - // Arrange - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()); - actionModel.Filters.Add(new ProducesResponseTypeAttribute(200)); - var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) }; - - // Act - ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes); - - // Assert - Assert.Empty(actionModel.Properties); - } - - [Fact] - public void DiscoverApiConvention_DoesNotAddConventionItem_IfActionHasProducesAttribute() - { - // Arrange - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()); - actionModel.Filters.Add(new ProducesAttribute(typeof(object))); - var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) }; - - // Act - ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes); - - // Assert - Assert.Empty(actionModel.Properties); - } - - [Fact] - public void DiscoverApiConvention_DoesNotAddConventionItem_IfNoConventionMatches() - { - // Arrange - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.NoMatch)), - Array.Empty()); - var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) }; - - // Act - ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes); - - // Assert - Assert.Empty(actionModel.Properties); - } - - [Fact] - public void DiscoverApiConvention_AddsConventionItem_IfConventionMatches() - { - // Arrange - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()); - var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) }; - - // Act - ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes); - - // Assert - Assert.Collection( - actionModel.Properties, - kvp => - { - Assert.Equal(typeof(ApiConventionResult), kvp.Key); - Assert.NotNull(kvp.Value); - }); - } - - [Fact] - public void DiscoverApiConvention_AddsConventionItem_IfActionHasNonConventionBasedFilters() - { - // Arrange - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()); - var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) }; - - // Act - ApiBehaviorApplicationModelProvider.DiscoverApiConvention(actionModel, attributes); - - // Assert - Assert.Collection( - actionModel.Properties, - kvp => - { - Assert.Equal(typeof(ApiConventionResult), kvp.Key); - Assert.NotNull(kvp.Value); - }); - } - - [Fact] - public void DiscoverErrorResponseType_SetsProblemDetails_IfActionHasNoAttributes() - { - // Arrange - var expected = typeof(ProblemDetails); - var controllerModel = new ControllerModel(typeof(object).GetTypeInfo(), new[] { new object() }); - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()) - { - Controller = controllerModel, - }; - var provider = GetProvider(); - - // Act - provider.DiscoverErrorResponseType(actionModel); - - // Assert - Assert.Collection( - actionModel.Properties, - kvp => - { - Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.Equal(expected, value.Type); - }); - } - - [Fact] - public void DiscoverErrorResponseType_DoesNotSetDefaultProblemDetailsResponse_IfSuppressMapClientErrorsIsSet() - { - // Arrange - var expected = typeof(ProblemDetails); - var controllerModel = new ControllerModel(typeof(object).GetTypeInfo(), new[] { new object() }); - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()) - { - Controller = controllerModel, - }; - var provider = GetProvider(new ApiBehaviorOptions - { - InvalidModelStateResponseFactory = _ => null, - SuppressMapClientErrors = true, - }); - - // Act - provider.DiscoverErrorResponseType(actionModel); - - // Assert - Assert.Empty(actionModel.Properties); - } - - [Fact] - public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnControllerAssembly() - { - // Arrange - var expected = typeof(InvalidEnumArgumentException); - var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new object() }); - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()) - { - Controller = controllerModel, - }; - var provider = GetProvider(); - - // Act - provider.DiscoverErrorResponseType(actionModel); - - // Assert - Assert.Collection( - actionModel.Properties, - kvp => - { - Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.Equal(expected, value.Type); - }); - } - - [Fact] - public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnController() - { - // Arrange - var expected = typeof(InvalidTimeZoneException); - var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new ProducesErrorResponseTypeAttribute(expected) }); - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - Array.Empty()) - { - Controller = controllerModel, - }; - var provider = GetProvider(); - - // Act - provider.DiscoverErrorResponseType(actionModel); - - // Assert - Assert.Collection( - actionModel.Properties, - kvp => - { - Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.Equal(expected, value.Type); - }); - } - - [Fact] - public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnAction() - { - // Arrange - var expected = typeof(InvalidTimeZoneException); - var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new ProducesErrorResponseTypeAttribute(typeof(Guid)) }); - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - new[] { new ProducesErrorResponseTypeAttribute(expected) }) - { - Controller = controllerModel, - }; - var provider = GetProvider(); - - // Act - provider.DiscoverErrorResponseType(actionModel); - - // Assert - Assert.Collection( - actionModel.Properties, - kvp => - { - Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.Equal(expected, value.Type); - }); - } - - [Fact] - public void DiscoverErrorResponseType_AllowsVoidsType() - { - // Arrange - var expected = typeof(void); - var actionModel = new ActionModel( - typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), - new[] { new ProducesErrorResponseTypeAttribute(expected) }); - var provider = GetProvider(); - - // Act - provider.DiscoverErrorResponseType(actionModel); - - // Assert - Assert.Collection( - actionModel.Properties, - kvp => - { - Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.Equal(expected, value.Type); - }); - } - - [Fact] - public void OnProvidersExecuting_AddsClientErrorResultFilter() - { - // Arrange - var context = GetContext(typeof(TestApiController)); - var provider = GetProvider(); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions); - Assert.Single(actionModel.Filters.OfType()); - } - - [Fact] - public void OnProvidersExecuting_DoesNotAddClientErrorResultFilter_IfFeatureIsDisabled() - { - // Arrange - var context = GetContext(typeof(TestApiController)); - var options = new ApiBehaviorOptions - { - SuppressMapClientErrors = true, - InvalidModelStateResponseFactory = _ => null, - }; - var provider = GetProvider(options); - - // Act - provider.OnProvidersExecuting(context); - - // Assert - var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions); - Assert.Empty(actionModel.Filters.OfType()); - } - - // A dynamically generated type in an assembly that has an ApiConventionAttribute. - private static TypeBuilder CreateTestControllerType() - { - var attributeBuilder = new CustomAttributeBuilder( - typeof(ApiConventionTypeAttribute).GetConstructor(new[] { typeof(Type) }), - new[] { typeof(DefaultApiConventions) }); - - var assemblyName = new AssemblyName("TestAssembly"); - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); - assemblyBuilder.SetCustomAttribute(attributeBuilder); - - var module = assemblyBuilder.DefineDynamicModule(assemblyName.Name); - var controllerType = module.DefineType("TestController"); - return controllerType; - } - - private static ApiBehaviorApplicationModelProvider GetProvider( - ApiBehaviorOptions options = null, - IModelMetadataProvider modelMetadataProvider = null) - { - options = options ?? new ApiBehaviorOptions - { - InvalidModelStateResponseFactory = _ => null, - }; - var optionsAccessor = Options.Create(options); - - var loggerFactory = NullLoggerFactory.Instance; - modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider(); - return new ApiBehaviorApplicationModelProvider( - optionsAccessor, - modelMetadataProvider, - Mock.Of(), - 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 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 static ParameterModel GetParameterModel(Type controllerType, string actionName) - { - var action = GetActionModel(controllerType, actionName); - return Assert.Single(action.Parameters); - } - - private static ParameterModel GetParameterModel(ActionModel action) - { - return Assert.Single(action.Parameters.Where(x => typeof(T).IsAssignableFrom(x.ParameterType))); - } - - [ApiController] - [Route("TestApi")] - private class TestApiController : ControllerBase - { - [HttpGet] - public IActionResult TestAction() => null; - } - - private class SimpleController : ControllerBase - { - public IActionResult ActionWithoutFilter() => null; - - [TestApiBehavior] - [HttpGet("/Simple/ActionWithFilter")] - public IActionResult ActionWithFilter() => null; - } - - [ApiController] - private class ActionsWithoutAttributeRouting - { - public IActionResult Index() => null; - } - - [AttributeUsage(AttributeTargets.Method)] - private class TestApiBehavior : Attribute, IApiBehaviorMetadata - { - } - - [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 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; - } - - private class CustomRequestPredicateAndPropertyFilterProviderAttribute : Attribute, IRequestPredicateProvider, IPropertyFilterProvider - { - public static Func RequestPredicateStatic => (c) => true; - public static Func PropertyFilterStatic => (c) => true; - - public Func RequestPredicate => RequestPredicateStatic; - - public Func PropertyFilter => PropertyFilterStatic; - } - - [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); - } - - [ApiController] - private class ControllerWithBoundProperty - { - [FromQuery] - public TestModel TestProperty { get; set; } - - public IActionResult SomeAction([FromQuery] TestModel test) => null; - } - - [ApiController] - private class ControllerWithBoundCollectionProperty - { - [FromQuery] - public List TestProperty { get; set; } - - public IActionResult SomeAction([FromQuery] List test) => null; - } - - private class Car { } - - [ApiController] - private class ControllerWithMultipleInferredFromBodyParameters - { - [HttpGet("test")] - public IActionResult Action(TestModel a, Car b) => null; - } - - [ApiController] - private class ControllerWithMultipleInferredOrSpecifiedFromBodyParameters - { - [HttpGet("test")] - public IActionResult Action(TestModel a, [FromBody] int b) => null; - } - - [ApiController] - private class ControllerWithMultipleFromBodyParameters - { - [HttpGet("test")] - public IActionResult Action([FromBody] decimal a, [FromBody] int b) => null; - } - - [ApiController] - private class ParameterWithBindingInfo - { - [HttpGet("test")] - public IActionResult Action([ModelBinder(typeof(object))] Car car) => null; - } - - private class TestApiConventionController - { - public IActionResult NoMatch() => null; - - public IActionResult Delete(int id) => null; - } - - 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(); - } - } - } -} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs index 4af3ea19f5..98e358cabc 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultApplicationModelProviderTest.cs @@ -869,7 +869,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal } [Fact] - public void CreateActionModel_AttributeRouteOnAction_CreatesOneActionInforPerRouteTemplate() + public void CreateActionModel_AttributeRouteOnAction_CreatesOneActionInfoPerRouteTemplate() { // Arrange var builder = new TestApplicationModelProvider(); @@ -951,7 +951,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal [Theory] [InlineData(typeof(SingleRouteAttributeController))] [InlineData(typeof(MultipleRouteAttributeController))] - public void CreateActionModel_RouteOnController_CreatesOneActionInforPerRouteTemplateOnAction(Type controller) + public void CreateActionModel_RouteOnController_CreatesOneActionInfoPerRouteTemplateOnAction(Type controller) { // Arrange var builder = new TestApplicationModelProvider(); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs index 17f88cea1a..21fd57a11b 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/MvcEndpointDataSourceTests.cs @@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.Equal(displayName, matcherEndpoint.DisplayName); Assert.Equal(order, matcherEndpoint.Order); - Assert.Equal(template, matcherEndpoint.RoutePattern.RawText); + Assert.Equal("Template!", matcherEndpoint.RoutePattern.RawText); } [Fact] @@ -134,21 +134,42 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.True(actionInvokerCalled); } + public static TheoryData GetSingleActionData_Conventional + { + get => GetSingleActionData(true); + } + + public static TheoryData GetSingleActionData_Attribute + { + get => GetSingleActionData(false); + } + + private static TheoryData GetSingleActionData(bool isConventionalRouting) + { + var data = new TheoryData + { + {"{controller}/{action}/{id?}", new[] { "TestController/TestAction/{id?}" }}, + {"{controller}/{id?}", isConventionalRouting ? new string[] { } : new[] { "TestController/{id?}" }}, + {"{action}/{id?}", isConventionalRouting ? new string[] { } : new[] { "TestAction/{id?}" }}, + {"{Controller}/{Action}/{id?}", new[] { "TestController/TestAction/{id?}" }}, + {"{CONTROLLER}/{ACTION}/{id?}", new[] { "TestController/TestAction/{id?}" }}, + {"{controller}/{action=TestAction}", new[] { "TestController", "TestController/TestAction" }}, + {"{controller}/{action=TestAction}/{id?}", new[] { "TestController", "TestController/TestAction/{id?}" }}, + {"{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestController", "TestController/TestAction/{id?}" }}, + {"{controller}/{action}/{*catchAll}", new[] { "TestController/TestAction/{*catchAll}" }}, + {"{controller}/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" }}, + {"{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" }}, + {"{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" }}, + {"{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" }}, + {"{controller:upper-case}/{action=TestAction}.{ext?}", new[] { "TESTCONTROLLER", "TESTCONTROLLER/TestAction.{ext?}" }}, + }; + + return data; + } + [Theory] - [InlineData("{controller}/{action}/{id?}", new[] { "TestController/TestAction/{id?}" })] - [InlineData("{controller}/{id?}", new string[] { })] - [InlineData("{action}/{id?}", new string[] { })] - [InlineData("{Controller}/{Action}/{id?}", new[] { "TestController/TestAction/{id?}" })] - [InlineData("{CONTROLLER}/{ACTION}/{id?}", new[] { "TestController/TestAction/{id?}" })] - [InlineData("{controller}/{action=TestAction}", new[] { "TestController", "TestController/TestAction" })] - [InlineData("{controller}/{action=TestAction}/{id?}", new[] { "TestController", "TestController/TestAction/{id?}" })] - [InlineData("{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestController", "TestController/TestAction/{id?}" })] - [InlineData("{controller}/{action}/{*catchAll}", new[] { "TestController/TestAction/{*catchAll}" })] - [InlineData("{controller}/{action=TestAction}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{*catchAll}" })] - [InlineData("{controller}/{action=TestAction}/{id?}/{*catchAll}", new[] { "TestController", "TestController/TestAction/{id?}/{*catchAll}" })] - [InlineData("{controller}/{action}.{ext?}", new[] { "TestController/TestAction.{ext?}" })] - [InlineData("{controller}/{action=TestAction}.{ext?}", new[] { "TestController", "TestController/TestAction.{ext?}" })] - public void Endpoints_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns) + [MemberData(nameof(GetSingleActionData_Conventional))] + public void Endpoints_Conventional_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns) { // Arrange var actionDescriptorCollection = GetActionDescriptorCollection( @@ -168,6 +189,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal Assert.Collection(endpoints, inspectors); } + [Theory] + [MemberData(nameof(GetSingleActionData_Attribute))] + public void Endpoints_AttributeRouting_SingleAction(string endpointInfoRoute, string[] finalEndpointPatterns) + { + // Arrange + var actionDescriptorCollection = GetActionDescriptorCollection( + attributeRouteTemplate: endpointInfoRoute, + new { controller = "TestController", action = "TestAction" }); + var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); + + // Act + var endpoints = dataSource.Endpoints; + + // Assert + var inspectors = finalEndpointPatterns + .Select(t => new Action(e => Assert.Equal(t, Assert.IsType(e).RoutePattern.RawText))) + .ToArray(); + + // Assert + Assert.Collection(endpoints, inspectors); + } + [Theory] [InlineData("{area}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })] [InlineData("{controller}/{action}/{id?}", new string[] { })] @@ -175,6 +218,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal [InlineData("{area=TestArea}/{controller}/{action=TestAction}/{id?}", new[] { "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area=TestArea}/{controller=TestController}/{action=TestAction}/{id?}", new[] { "", "TestArea", "TestArea/TestController", "TestArea/TestController/TestAction/{id?}" })] [InlineData("{area:exists}/{controller}/{action}/{id?}", new[] { "TestArea/TestController/TestAction/{id?}" })] + [InlineData("{area:exists:upper-case}/{controller}/{action}/{id?}", new[] { "TESTAREA/TestController/TestAction/{id?}" })] public void Endpoints_AreaSingleAction(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange @@ -188,6 +232,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); services.Configure(routeOptionsSetup.Configure); + services.Configure(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute, serviceProvider: services.BuildServiceProvider())); @@ -335,6 +383,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal [Theory] [InlineData("{controller}/{action}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController1/TestAction3", "TestController2/TestAction1" })] [InlineData("{controller}/{action:regex((TestAction1|TestAction2))}", new[] { "TestController1/TestAction1", "TestController1/TestAction2", "TestController2/TestAction1" })] + [InlineData("{controller}/{action:regex((TestAction1|TestAction2)):upper-case}", new[] { "TestController1/TESTACTION1", "TestController1/TESTACTION2", "TestController2/TESTACTION1" })] public void Endpoints_MultipleActions(string endpointInfoRoute, string[] finalEndpointTemplates) { // Arrange @@ -708,14 +757,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal var services = new ServiceCollection(); services.AddSingleton(actionDescriptorCollectionProvider); + services.AddRouting(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); + var serviceProvider = services.BuildServiceProvider(); var dataSource = new MvcEndpointDataSource( actionDescriptorCollectionProvider, - mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty()))); + mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty())), + serviceProvider.GetRequiredService()); return dataSource; } + private class UpperCaseParameterTransform : IParameterTransformer + { + public string Transform(string value) + { + return value?.ToUpperInvariant(); + } + } + private MvcEndpointInfo CreateEndpointInfo( string name, string template, @@ -726,13 +789,18 @@ namespace Microsoft.AspNetCore.Mvc.Internal { if (serviceProvider == null) { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddRouting(); + var services = new ServiceCollection(); + services.AddRouting(); + services.AddSingleton(typeof(UpperCaseParameterTransform), new UpperCaseParameterTransform()); - var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); - serviceCollection.Configure(routeOptionsSetup.Configure); + var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); + services.Configure(routeOptionsSetup.Configure); + services.Configure(options => + { + options.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); - serviceProvider = serviceCollection.BuildServiceProvider(); + serviceProvider = services.BuildServiceProvider(); } var parameterPolicyFactory = serviceProvider.GetRequiredService(); @@ -740,11 +808,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal } private IActionDescriptorCollectionProvider GetActionDescriptorCollection(params object[] requiredValues) + { + return GetActionDescriptorCollection(attributeRouteTemplate: null, requiredValues); + } + + private IActionDescriptorCollectionProvider GetActionDescriptorCollection(string attributeRouteTemplate, params object[] requiredValues) { var actionDescriptors = new List(); foreach (var requiredValue in requiredValues) { - actionDescriptors.Add(CreateActionDescriptor(requiredValue)); + actionDescriptors.Add(CreateActionDescriptor(requiredValue, attributeRouteTemplate)); } var actionDescriptorCollectionProviderMock = new Mock(); @@ -756,10 +829,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal private ActionDescriptor CreateActionDescriptor(string controller, string action, string area = null) { - return CreateActionDescriptor(new { controller = controller, action = action, area = area }); + return CreateActionDescriptor(new { controller = controller, action = action, area = area }, attributeRouteTemplate: null); } - private ActionDescriptor CreateActionDescriptor(object requiredValues) + private ActionDescriptor CreateActionDescriptor(object requiredValues, string attributeRouteTemplate = null) { var actionDescriptor = new ActionDescriptor(); var routeValues = new RouteValueDictionary(requiredValues); @@ -767,6 +840,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal { actionDescriptor.RouteValues[kvp.Key] = kvp.Value?.ToString(); } + if (!string.IsNullOrEmpty(attributeRouteTemplate)) + { + actionDescriptor.AttributeRouteInfo = new AttributeRouteInfo + { + Name = attributeRouteTemplate, + Template = attributeRouteTemplate + }; + } return actionDescriptor; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FloatingPointTypeModelBinderTestOfT.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FloatingPointTypeModelBinderTestOfT.cs index fbcb7e5893..0a8f69b60f 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FloatingPointTypeModelBinderTestOfT.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/FloatingPointTypeModelBinderTestOfT.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders { public abstract class FloatingPointTypeModelBinderTest where TFloatingPoint: struct { - public static TheoryData ConvertableTypeData + public static TheoryData ConvertibleTypeData { get { @@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders protected abstract TFloatingPoint ThirtyTwoThousandPointOne { get; } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsFailure_IfAttemptedValueCannotBeParsed(Type destinationType) { // Arrange @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_CreatesError_IfAttemptedValueCannotBeParsed(Type destinationType) { // Arrange @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_CreatesError_IfAttemptedValueCannotBeCompletelyParsed(Type destinationType) { // Arrange @@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedWhitespace(Type destinationType) { // Arrange @@ -124,7 +124,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedDecimal(Type destinationType) { // Arrange @@ -148,7 +148,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_CreatesError_IfAttemptedValueContainsDisallowedThousandsSeparator(Type destinationType) { // Arrange @@ -172,7 +172,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsFailed_IfValueProviderEmpty(Type destinationType) { // Arrange @@ -236,7 +236,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_Twelve(Type destinationType) { // Arrange @@ -257,7 +257,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] [ReplaceCulture("en-GB", "en-GB")] public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_TwelvePointFive(Type destinationType) { @@ -279,7 +279,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_FrenchTwelvePointFive(Type destinationType) { // Arrange @@ -300,7 +300,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_ThirtyTwoThousand(Type destinationType) { // Arrange @@ -321,7 +321,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_ThirtyTwoThousandPointOne(Type destinationType) { // Arrange @@ -342,7 +342,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsModel_IfAttemptedValueIsValid_FrenchThirtyTwoThousandPointOne(Type destinationType) { // Arrange diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs index 30623f3c72..e50098b146 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders Assert.True(bindingContext.ModelState.ContainsKey("theModelName")); } - public static TheoryData ConvertableTypeData + public static TheoryData ConvertibleTypeData { get { @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_ReturnsFailure_IfTypeCanBeConverted_AndConversionFails(Type destinationType) { // Arrange @@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders } [Theory] - [MemberData(nameof(ConvertableTypeData))] + [MemberData(nameof(ConvertibleTypeData))] public async Task BindModel_CreatesError_WhenTypeConversionIsNull(Type destinationType) { // Arrange diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs index 17669fd0d4..b67c769f57 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorTest.cs @@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } public static TheoryData, IEnumerable> - Valdate_ReturnsExpectedResults_Data + Validate_ReturnsExpectedResults_Data { get { @@ -327,8 +327,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } [Theory] - [MemberData(nameof(Valdate_ReturnsExpectedResults_Data))] - public void Valdate_ReturnsExpectedResults( + [MemberData(nameof(Validate_ReturnsExpectedResults_Data))] + public void Validate_ReturnsExpectedResults( string errorMessage, IEnumerable memberNames, IEnumerable expectedResults) diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs index e616795fcf..3febb5f4e5 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs @@ -39,16 +39,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal adapter.GetErrorMessage(validationContext); // Assert - Assert.True(attribute.Formated); + Assert.True(attribute.Formatted); } public class TestValidationAttribute : ValidationAttribute { - public bool Formated = false; + public bool Formatted = false; public override string FormatErrorMessage(string name) { - Formated = true; + Formatted = true; return base.FormatErrorMessage(name); } } diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/Internal/JsonResultExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/Internal/JsonResultExecutorTest.cs index 81452cc818..2dbbeb1dad 100644 --- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/Internal/JsonResultExecutorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/Internal/JsonResultExecutorTest.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal var context = GetActionContext(); var result = new JsonResult(new { foo = "abcd" }); - var executor = CreateExcutor(); + var executor = CreateExecutor(); // Act await executor.ExecuteAsync(context, result); @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal var result = new JsonResult(new { foo = "abcd" }); result.ContentType = "text/json"; - var executor = CreateExcutor(); + var executor = CreateExecutor(); // Act await executor.ExecuteAsync(context, result); @@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal { Encoding = Encoding.ASCII }.ToString(); - var executor = CreateExcutor(); + var executor = CreateExecutor(); // Act await executor.ExecuteAsync(context, result); @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal context.HttpContext.Response.ContentType = expectedContentType; var result = new JsonResult(new { foo = "abcd" }); - var executor = CreateExcutor(); + var executor = CreateExecutor(); // Act await executor.ExecuteAsync(context, result); @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal context.HttpContext.Response.ContentType = responseContentType; var result = new JsonResult(new { foo = "abcd" }); - var executor = CreateExcutor(); + var executor = CreateExecutor(); // Act await executor.ExecuteAsync(context, result); @@ -147,7 +147,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal serializerSettings.Formatting = Formatting.Indented; var result = new JsonResult(new { foo = "abcd" }, serializerSettings); - var executor = CreateExcutor(); + var executor = CreateExecutor(); // Act await executor.ExecuteAsync(context, result); @@ -165,7 +165,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal var expected = Encoding.UTF8.GetBytes("{\"name\":\"Robert\""); var context = GetActionContext(); var result = new JsonResult(new ModelWithSerializationError()); - var executor = CreateExcutor(); + var executor = CreateExecutor(); // Act try @@ -190,7 +190,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal var expected = "Executing JsonResult, writing value of type 'System.String'."; var context = GetActionContext(); var logger = new StubLogger(); - var executer = CreateExcutor(logger); + var executer = CreateExecutor(logger); var result = new JsonResult("result_value"); // Act @@ -207,7 +207,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal var expected = "Executing JsonResult, writing value of type 'null'."; var context = GetActionContext(); var logger = new StubLogger(); - var executer = CreateExcutor(logger); + var executer = CreateExecutor(logger); var result = new JsonResult(null); // Act @@ -217,7 +217,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Json.Internal Assert.Equal(expected, logger.MostRecentMessage); } - private static JsonResultExecutor CreateExcutor(ILogger logger = null) + private static JsonResultExecutor CreateExecutor(ILogger logger = null) { return new JsonResultExecutor( new TestHttpResponseStreamWriterFactory(), diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs index 6b4b7acd73..a4db6dd1ee 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public async Task ApiExplorer_IsVisible_EnabledWithConvention() { // Arrange & Act - var response = await Client.GetAsync("http://localhost/ApiExplorerVisbilityEnabledByConvention"); + var response = await Client.GetAsync("http://localhost/ApiExplorerVisibilityEnabledByConvention"); var body = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject>(body); @@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public async Task ApiExplorer_IsVisible_DisabledWithConvention() { // Arrange & Act - var response = await Client.GetAsync("http://localhost/ApiExplorerVisbilityDisabledByConvention"); + var response = await Client.GetAsync("http://localhost/ApiExplorerVisibilityDisabledByConvention"); var body = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject>(body); @@ -1277,6 +1277,41 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests }); } + [Fact] + public async Task ApiConvention_ForPostActionWithProducesAttribute() + { + // Arrange + var expectedMediaTypes = new[] { "application/json", "text/json", }; + + // Act + var response = await Client.PostAsync( + $"ApiExplorerResponseTypeWithApiConventionController/PostWithProduces", + new StringContent(string.Empty)); + var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(responseBody); + + // Assert + var description = Assert.Single(result); + Assert.Collection( + description.SupportedResponseTypes.OrderBy(r => r.StatusCode), + responseType => + { + Assert.True(responseType.IsDefaultResponse); + }, + responseType => + { + Assert.Equal(typeof(void).FullName, responseType.ResponseType); + Assert.Equal(201, responseType.StatusCode); + Assert.Empty(responseType.ResponseFormats); + }, + responseType => + { + Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType); + Assert.Equal(400, responseType.StatusCode); + Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType)); + }); + } + [Fact] public async Task ApiConvention_ForPutActionThatMatchesConvention() { diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs index 71502e6f24..9a978d2662 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ConsumesAttributeTestsBase.cs @@ -107,7 +107,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests [ConditionalFact] // Mono issue - https://github.com/aspnet/External/issues/18 [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task DerivedClassLevelAttribute_OveridesBaseClassLevel() + public async Task DerivedClassLevelAttribute_OverridesBaseClassLevel() { // Arrange var input = "(body); + + Assert.Equal("ParameterTransformer", result.Controller); + Assert.Equal("Test", result.Action); + } + + [Fact] + public async Task ParameterTransformer_TokenReplacement_NotFound() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ParameterTransformer/Test"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task AttributeRoutedAction_Parameters_Found() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/EndpointRouting/Index"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_Parameters_DefaultValue_Found() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/EndpointRouting"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_Found() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/_EndpointRouting_/ParameterTransformer"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_NotFound() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/EndpointRouting/ParameterTransformer"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkToSelf() + { + // Arrange + var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/_EndpointRouting_/ParameterTransformer", result.Link); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkWithAmbientController() + { + // Arrange + var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { action = "Get", id = 5 }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/_EndpointRouting_/5", result.Link); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkToAttributeRoutedController() + { + // Arrange + var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { action = "ShowPosts", controller = "Blog" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/Blog/ShowPosts", result.Link); + } + + [Fact] + public async Task AttributeRoutedAction_ParameterTransformer_LinkToConventionalController() + { + // Arrange + var url = LinkFrom("http://localhost/_EndpointRouting_/ParameterTransformer").To(new { action = "Index", controller = "Home" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("EndpointRouting", result.Controller); + Assert.Equal("ParameterTransformer", result.Action); + + Assert.Equal("/", result.Link); + } + [Fact] public async override Task HasEndpointMatch() { @@ -123,5 +287,119 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests // Assert Assert.Equal(HttpStatusCode.MethodNotAllowed, response.StatusCode); } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_NotFound() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/ConventionalTransformer/Index"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_DefaultValue() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_WithParam() + { + // Arrange & Act + var response = await Client.GetAsync("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Param/_value_"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Param", result.Action); + + Assert.Equal("/ConventionalTransformerRoute/_ConventionalTransformer_/Param/_value_", Assert.Single(result.ExpectedUrls)); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_LinkToConventionalController() + { + // Arrange + var url = LinkFrom("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index").To(new { action = "Index", controller = "Home" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + Assert.Equal("/", result.Link); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_LinkToConventionalControllerWithParam() + { + // Arrange + var url = LinkFrom("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index").To(new { action = "Param", controller = "ConventionalTransformer", param = "value" }); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + Assert.Equal("/ConventionalTransformerRoute/_ConventionalTransformer_/Param/_value_", result.Link); + } + + [Fact] + public async Task ConventionalRoutedAction_ParameterTransformer_LinkToSelf() + { + // Arrange + var url = LinkFrom("http://localhost/ConventionalTransformerRoute/_ConventionalTransformer_/Index").To(new {}); + + // Act + var response = await Client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("ConventionalTransformer", result.Controller); + Assert.Equal("Index", result.Action); + Assert.Equal("/ConventionalTransformerRoute/_ConventionalTransformer_", result.Link); + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs index 2425c21f3a..2d8356a19a 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/InputFormatterTests.cs @@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests { // Act var input = "Test"; - var response = await Client.PostAsJsonAsync("PolymorhpicPropertyBinding/Action", input); + var response = await Client.PostAsJsonAsync("PolymorphicPropertyBinding/Action", input); // Assert await response.AssertStatusCodeAsync(HttpStatusCode.OK); @@ -299,7 +299,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests public async Task ValidationUsesModelMetadataFromActualModelType_ForInputFormattedProperties() { // Act - var response = await Client.PostAsJsonAsync("PolymorhpicPropertyBinding/Action", string.Empty); + var response = await Client.PostAsJsonAsync("PolymorphicPropertyBinding/Action", string.Empty); // Assert await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest); diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html index 0e28eac841..00ae75a1c4 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.Encoded.html @@ -95,7 +95,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html index ffa6be6fdf..b0a2e41eb6 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/compiler/resources/HtmlGenerationWebSite.HtmlGeneration_Home.Link.html @@ -96,7 +96,7 @@ - + diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs index 7a49359cd3..9eb3af091e 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ComplexTypeModelBinderIntegrationTest.cs @@ -2659,7 +2659,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests // This covers the case where a key is present, but has an empty value. The type converter // will report an error. [Fact] - public async Task MutableObjectModelBinder_BindsPOCO_TypeConvertedPropertyNonConvertableValue_GetsError() + public async Task MutableObjectModelBinder_BindsPOCO_TypeConvertedPropertyNonConvertibleValue_GetsError() { // Arrange var parameter = new ParameterDescriptor() diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs index 420939c66b..0999087bec 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs @@ -241,7 +241,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } [Fact] - public async Task BindParameter_NonConvertableValue_GetsError() + public async Task BindParameter_NonConvertibleValue_GetsError() { // Arrange var parameterBinder = ModelBindingTestHelper.GetParameterBinder(); @@ -290,7 +290,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests } [Fact] - public async Task BindParameter_NonConvertableValue_GetsCustomErrorMessage() + public async Task BindParameter_NonConvertibleValue_GetsCustomErrorMessage() { // Arrange var parameterType = typeof(int); diff --git a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs index c15912dede..008435da3c 100644 --- a/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.RazorPages.Test/Internal/AuthorizationPageApplicationModelProviderTest.cs @@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal private class TestPageWithDerivedModel : Page { - public DeriviedModel Model => null; + public DerivedModel Model => null; public override Task ExecuteAsync() =>throw new NotImplementedException(); } @@ -121,7 +121,7 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages.Internal } [Authorize(Policy = "Derived")] - private class DeriviedModel : BaseModel + private class DerivedModel : BaseModel { public virtual void OnGet() { diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs index 489c78bd05..d6789c5376 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/PartialTagHelperTest.cs @@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } [Fact] - public async Task ProcessAsync_DoesNotUseModelFromViewdata_IfModelExpressionEvalulatesToNull() + public async Task ProcessAsync_DoesNotUseModelFromViewdata_IfModelExpressionEvaluatesToNull() { // Arrange var bufferScope = new TestViewBufferScope(); @@ -455,7 +455,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers } [Fact] - public async Task ProcessAsync_UsesModelOnViewContextViewData_WhenModelExpresionIsNull() + public async Task ProcessAsync_UsesModelOnViewContextViewData_WhenModelExpressionIsNull() { // Arrange var bufferScope = new TestViewBufferScope(); diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs index fec670ba58..57730851cc 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs @@ -98,7 +98,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers [Theory] [MemberData(nameof(ProcessAsync_GeneratesExpectedOutput_WithNoErrorsData))] - public async Task ProcessAsync_SuppressesOutput_IfClientSideValiationDisabled_WithNoErrorsData( + public async Task ProcessAsync_SuppressesOutput_IfClientSideValidationDisabled_WithNoErrorsData( ModelStateDictionary modelStateDictionary) { // Arrange diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultDisplayTemplatesTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultDisplayTemplatesTest.cs index 73e3b0cc08..e9526d1d62 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultDisplayTemplatesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultDisplayTemplatesTest.cs @@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void ObjectTemplate_HonoursHideSurroundingHtml() + public void ObjectTemplate_HonorsHideSurroundingHtml() { // Arrange var expected = @@ -231,7 +231,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal } [Fact] - public void HiddenInputTemplate_HonoursHideSurroundingHtml() + public void HiddenInputTemplate_HonorsHideSurroundingHtml() { // Arrange var model = "Model string"; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs index a8b6659a91..ad94d3bf9e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs @@ -374,7 +374,7 @@ Environment.NewLine; } [Fact] - public void ObjectTemplate_HonoursHideSurroundingHtml() + public void ObjectTemplate_HonorsHideSurroundingHtml() { // Arrange var expected = @@ -470,7 +470,7 @@ Environment.NewLine; } [Fact] - public void HiddenInputTemplate_HonoursHideSurroundingHtml() + public void HiddenInputTemplate_HonorsHideSurroundingHtml() { // Arrange var expected = ""; diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributePropertyProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributePropertyProviderTest.cs index 48f0726884..1ab02127a9 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributePropertyProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/ViewDataAttributePropertyProviderTest.cs @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal Assert.Collection( result.OrderBy(p => p.Key), property => Assert.Equal(nameof(BaseController.BaseProperty), property.PropertyInfo.Name), - property => Assert.Equal(nameof(DerivedController.DeriviedProperty), property.PropertyInfo.Name)); + property => Assert.Equal(nameof(DerivedController.DerivedProperty), property.PropertyInfo.Name)); } [Fact] @@ -90,7 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal public class DerivedController : BaseController { [ViewData] - public string DeriviedProperty { get; set; } + public string DerivedProperty { get; set; } } public class PropertyWithKeyController diff --git a/test/Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs b/test/Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs index d29b1a2f2b..a9630cba1f 100644 --- a/test/Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs +++ b/test/Mvc.Analyzers.Test/CodeAnalysisExtensionsTest.cs @@ -34,10 +34,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers public async Task GetAttributes_OnNonOverriddenMethod_ReturnsAllAttributesOnCurrentAction() { // Arrange - var compilation = await GetCompilation("GetAttributes_WithoutMethodOverridding"); + var compilation = await GetCompilation("GetAttributes_WithoutMethodOverriding"); var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName); - var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithoutMethodOverridding)}"); - var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithoutMethodOverridding.Method)).First(); + var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithoutMethodOverriding)}"); + var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithoutMethodOverriding.Method)).First(); // Act var attributes = CodeAnalysisExtensions.GetAttributes(method, attribute, inherit: true); diff --git a/test/Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverridding.cs b/test/Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverriding.cs similarity index 70% rename from test/Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverridding.cs rename to test/Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverriding.cs index 653abeb19b..79c2be699b 100644 --- a/test/Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverridding.cs +++ b/test/Mvc.Analyzers.Test/TestFiles/CodeAnalysisExtensionsTest/GetAttributes_WithoutMethodOverriding.cs @@ -1,6 +1,6 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { - public class GetAttributes_WithoutMethodOverridding + public class GetAttributes_WithoutMethodOverriding { [ProducesResponseType(201)] public void Method() { } diff --git a/test/Mvc.Api.Analyzers.Test/ApiControllerFactsTest.cs b/test/Mvc.Api.Analyzers.Test/ApiControllerFactsTest.cs index f096f51455..e0875d0f93 100644 --- a/test/Mvc.Api.Analyzers.Test/ApiControllerFactsTest.cs +++ b/test/Mvc.Api.Analyzers.Test/ApiControllerFactsTest.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.ApiControllerFactsTest; using Microsoft.CodeAnalysis; using Xunit; @@ -110,9 +111,25 @@ namespace TestNamespace Assert.True(result); } - private Task GetCompilation() + [Fact] + public async Task IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly() { - var testSource = MvcTestSource.Read(GetType().Name, "TestFile"); + // Arrange + var compilation = await GetCompilation(nameof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly)); + var symbolCache = new ApiControllerSymbolCache(compilation); + var type = compilation.GetTypeByMetadataName(typeof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssemblyController).FullName); + var method = (IMethodSymbol)type.GetMembers(nameof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssemblyController.Action)).First(); + + // Act + var result = ApiControllerFacts.IsApiControllerAction(symbolCache, method); + + // Assert + Assert.True(result); + } + + private Task GetCompilation(string testFile = "TestFile") + { + var testSource = MvcTestSource.Read(GetType().Name, testFile); var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source }); return project.GetCompilationAsync(); diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/ApiControllerFactsTest/IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/ApiControllerFactsTest/IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly.cs new file mode 100644 index 0000000000..a8b5d0ee8b --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/ApiControllerFactsTest/IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +[assembly: ApiController] + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.ApiControllerFactsTest +{ + public class IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssemblyController : ControllerBase + { + public IActionResult Action() => null; + } +} diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithApiConventionController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithApiConventionController.cs index 8d831395be..dcfb5a9892 100644 --- a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithApiConventionController.cs +++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerResponseTypeWithApiConventionController.cs @@ -27,6 +27,10 @@ namespace ApiExplorerWebSite [ProducesResponseType(403)] public IActionResult PostWithConventions() => null; + [HttpPost] + [Produces("application/json", "text/json")] + public IActionResult PostWithProduces(Product p) => null; + [HttpPost] public Task PostTaskOfProduct(Product p) => null; @@ -47,4 +51,4 @@ namespace ApiExplorerWebSite [ProducesResponseType(409)] public static void CustomConventionMethod() { } } -} \ No newline at end of file +} diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityDisabledByConventionController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityDisabledByConventionController.cs index d29a11bc5f..be93692211 100644 --- a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityDisabledByConventionController.cs +++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityDisabledByConventionController.cs @@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Mvc; namespace ApiExplorerWebSite { - [Route("ApiExplorerVisbilityDisabledByConvention")] - public class ApiExplorerVisbilityDisabledByConventionController : Controller + [Route("ApiExplorerVisibilityDisabledByConvention")] + public class ApiExplorerVisibilityDisabledByConventionController : Controller { [HttpGet] public void Get() diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityEnabledByConventionController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityEnabledByConventionController.cs index 7b674cb022..055e91e0ea 100644 --- a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityEnabledByConventionController.cs +++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerVisibilityEnabledByConventionController.cs @@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Mvc; namespace ApiExplorerWebSite { - [Route("ApiExplorerVisbilityEnabledByConvention")] - public class ApiExplorerVisbilityEnabledByConventionController : Controller + [Route("ApiExplorerVisibilityEnabledByConvention")] + public class ApiExplorerVisibilityEnabledByConventionController : Controller { [HttpGet] public void Get() diff --git a/test/WebSites/ApiExplorerWebSite/Startup.cs b/test/WebSites/ApiExplorerWebSite/Startup.cs index f5f96abfd5..08965fe0bf 100644 --- a/test/WebSites/ApiExplorerWebSite/Startup.cs +++ b/test/WebSites/ApiExplorerWebSite/Startup.cs @@ -28,7 +28,7 @@ namespace ApiExplorerWebSite options.Conventions.Add(new ApiExplorerVisibilityEnabledConvention()); options.Conventions.Add(new ApiExplorerVisibilityDisabledConvention( - typeof(ApiExplorerVisbilityDisabledByConventionController))); + typeof(ApiExplorerVisibilityDisabledByConventionController))); options.Conventions.Add(new ApiExplorerInboundOutboundConvention( typeof(ApiExplorerInboundOutBoundController))); options.Conventions.Add(new ApiExplorerRouteChangeConvention(wellKnownChangeToken)); diff --git a/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs b/test/WebSites/FormatterWebSite/Controllers/PolymorphicPropertyBindingController.cs similarity index 90% rename from test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs rename to test/WebSites/FormatterWebSite/Controllers/PolymorphicPropertyBindingController.cs index 27a2643cfc..2d428f9f4f 100644 --- a/test/WebSites/FormatterWebSite/Controllers/PolymorhpicPropertyBindingController.cs +++ b/test/WebSites/FormatterWebSite/Controllers/PolymorphicPropertyBindingController.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc; namespace FormatterWebSite.Controllers { - public class PolymorhpicPropertyBindingController : ControllerBase + public class PolymorphicPropertyBindingController : ControllerBase { [FromBody] public IModel Person { get; set; } diff --git a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml index ffa657373f..dbd8dd6272 100644 --- a/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml +++ b/test/WebSites/HtmlGenerationWebSite/Views/HtmlGeneration_Home/Link.cshtml @@ -157,7 +157,7 @@ asp-fallback-test-property="visibility" asp-fallback-test-value="hidden" /> - + c.ControllerType == _controllerType)) + { + foreach (var action in controller.Actions) + { + action.Properties[typeof(IParameterTransformer)] = _parameterTransformer; + } + } + } + } +} diff --git a/test/WebSites/RoutingWebSite/Controllers/ConventionalTransformerController.cs b/test/WebSites/RoutingWebSite/Controllers/ConventionalTransformerController.cs new file mode 100644 index 0000000000..b31b2ee8b9 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/ConventionalTransformerController.cs @@ -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. + +using Microsoft.AspNetCore.Mvc; + +namespace RoutingWebSite +{ + public class ConventionalTransformerController : Controller + { + private readonly TestResponseGenerator _generator; + + public ConventionalTransformerController(TestResponseGenerator generator) + { + _generator = generator; + } + + public IActionResult Index() + { + return _generator.Generate(); + } + + public IActionResult Param(string param) + { + return _generator.Generate($"/ConventionalTransformerRoute/_ConventionalTransformer_/Param/{param}"); + } + } +} diff --git a/test/WebSites/RoutingWebSite/Controllers/EndpointRoutingController.cs b/test/WebSites/RoutingWebSite/Controllers/EndpointRoutingController.cs new file mode 100644 index 0000000000..1163302277 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/EndpointRoutingController.cs @@ -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 Microsoft.AspNetCore.Mvc; + +namespace RoutingWebSite +{ + [Route("/{controller:test-transformer}")] + public class EndpointRoutingController : Controller + { + private readonly TestResponseGenerator _generator; + + public EndpointRoutingController(TestResponseGenerator generator) + { + _generator = generator; + } + + [Route("/{controller}/{action=Index}")] + public IActionResult Index() + { + return _generator.Generate("/EndpointRouting/Index", "/EndpointRouting"); + } + + [Route("/{controller:test-transformer}/{action}")] + public IActionResult ParameterTransformer() + { + return _generator.Generate("/_EndpointRouting_/ParameterTransformer"); + } + + [Route("{id}")] + public IActionResult Get(int id) + { + return _generator.Generate("/_EndpointRouting_/" + id); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/Controllers/OrderController.cs b/test/WebSites/RoutingWebSite/Controllers/OrderController.cs index 6f3f49ef2c..f0770ab539 100644 --- a/test/WebSites/RoutingWebSite/Controllers/OrderController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/OrderController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; -namespace RoutingWebSite.Controllers +namespace RoutingWebSite { [Route("Order/[action]/{orderId?}", Name = "Order_[action]")] public class OrderController : Controller diff --git a/test/WebSites/RoutingWebSite/Controllers/ParameterTransformerController.cs b/test/WebSites/RoutingWebSite/Controllers/ParameterTransformerController.cs new file mode 100644 index 0000000000..69991c60b5 --- /dev/null +++ b/test/WebSites/RoutingWebSite/Controllers/ParameterTransformerController.cs @@ -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. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace RoutingWebSite +{ + [Route("[controller]/[action]", Name = "[controller]_[action]")] + public class ParameterTransformerController : Controller + { + private readonly TestResponseGenerator _generator; + + public ParameterTransformerController(TestResponseGenerator generator) + { + _generator = generator; + } + + public IActionResult Test() + { + return _generator.Generate("/_ParameterTransformer_/_Test_"); + } + } +} diff --git a/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs b/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs index e02f385707..97f49e00c4 100644 --- a/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs +++ b/test/WebSites/RoutingWebSite/Controllers/RouteDataController.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; -namespace RoutingWebSite.Controllers +namespace RoutingWebSite { public class RouteDataController : Controller { diff --git a/test/WebSites/RoutingWebSite/RemoveControllerActionDescriptorProvider.cs b/test/WebSites/RoutingWebSite/RemoveControllerActionDescriptorProvider.cs new file mode 100644 index 0000000000..40d40aa355 --- /dev/null +++ b/test/WebSites/RoutingWebSite/RemoveControllerActionDescriptorProvider.cs @@ -0,0 +1,40 @@ +// 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.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace RoutingWebSite +{ + public class RemoveControllerActionDescriptorProvider : IActionDescriptorProvider + { + private readonly Type _controllerType; + + public RemoveControllerActionDescriptorProvider(Type controllerType) + { + _controllerType = controllerType; + } + + public int Order => int.MaxValue; + + public void OnProvidersExecuted(ActionDescriptorProviderContext context) + { + } + + public void OnProvidersExecuting(ActionDescriptorProviderContext context) + { + foreach (var item in context.Results.ToList()) + { + if (item is ControllerActionDescriptor controllerActionDescriptor) + { + if (controllerActionDescriptor.ControllerTypeInfo == _controllerType) + { + context.Results.Remove(item); + } + } + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/RoutingWebSite/Startup.cs b/test/WebSites/RoutingWebSite/Startup.cs index 08a67d963f..653aa4e81f 100644 --- a/test/WebSites/RoutingWebSite/Startup.cs +++ b/test/WebSites/RoutingWebSite/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; namespace RoutingWebSite @@ -14,8 +15,20 @@ namespace RoutingWebSite // Set up application services public void ConfigureServices(IServiceCollection services) { - services.AddMvc() + services + .AddMvc(options => + { + // Add route token transformer to one controller + options.Conventions.Add(new ControllerRouteTokenTransformerConvention( + typeof(ParameterTransformerController), + new TestParameterTransformer())); + }) .SetCompatibilityVersion(CompatibilityVersion.Latest); + services + .AddRouting(options => + { + options.ConstraintMap["test-transformer"] = typeof(TestParameterTransformer); + }); services.AddScoped(); services.AddSingleton(); @@ -32,6 +45,12 @@ namespace RoutingWebSite constraints: new { controller = "DataTokens" }, dataTokens: new { hasDataTokens = true }); + routes.MapRoute( + "ConventionalTransformerRoute", + "ConventionalTransformerRoute/{controller:test-transformer}/{action=Index}/{param:test-transformer?}", + defaults: null, + constraints: new { controller = "ConventionalTransformer" }); + routes.MapAreaRoute( "flightRoute", "adminRoute", diff --git a/test/WebSites/RoutingWebSite/StartupWith21Compat.cs b/test/WebSites/RoutingWebSite/StartupWith21Compat.cs index eb891a53c5..44a6abfc61 100644 --- a/test/WebSites/RoutingWebSite/StartupWith21Compat.cs +++ b/test/WebSites/RoutingWebSite/StartupWith21Compat.cs @@ -1,11 +1,16 @@ // 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.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace RoutingWebSite { @@ -14,11 +19,17 @@ namespace RoutingWebSite // Set up application services public void ConfigureServices(IServiceCollection services) { - services.AddMvc() + services + .AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddScoped(); services.AddSingleton(); + + // EndpointRoutingController is not compatible with old routing + // Remove its action to avoid errors + var actionDescriptorProvider = new RemoveControllerActionDescriptorProvider(typeof(EndpointRoutingController)); + services.TryAddEnumerable(ServiceDescriptor.Singleton(actionDescriptorProvider)); } public void Configure(IApplicationBuilder app) diff --git a/test/WebSites/RoutingWebSite/TestParameterTransformer.cs b/test/WebSites/RoutingWebSite/TestParameterTransformer.cs new file mode 100644 index 0000000000..0af1f4ed7e --- /dev/null +++ b/test/WebSites/RoutingWebSite/TestParameterTransformer.cs @@ -0,0 +1,15 @@ +// 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 Microsoft.AspNetCore.Routing; + +namespace RoutingWebSite +{ + public class TestParameterTransformer : IParameterTransformer + { + public string Transform(string value) + { + return "_" + value + "_"; + } + } +} \ No newline at end of file