diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ApiExplorer/ApiParameterDescription.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ApiExplorer/ApiParameterDescription.cs
index 1f07d2b7ab..f020c6361a 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ApiExplorer/ApiParameterDescription.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ApiExplorer/ApiParameterDescription.cs
@@ -41,5 +41,23 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
/// Gets or sets the parameter descriptor.
///
public ParameterDescriptor ParameterDescriptor { get; set; }
+
+ ///
+ /// Gets or sets a value that determines if the parameter is required.
+ ///
+ ///
+ /// A parameter is considered required if
+ ///
+ /// - it's bound from the request body ().
+ /// - it's a required route value.
+ /// - it has annotations (e.g. BindRequiredAttribute) that indicate it's required.
+ ///
+ ///
+ public bool IsRequired { get; set; }
+
+ ///
+ /// Gets or sets the default value for a parameter.
+ ///
+ public object DefaultValue { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiParameterContext.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiParameterContext.cs
new file mode 100644
index 0000000000..d88281f490
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiParameterContext.cs
@@ -0,0 +1,33 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.AspNetCore.Routing.Template;
+
+namespace Microsoft.AspNetCore.Mvc.ApiExplorer
+{
+ internal class ApiParameterContext
+ {
+ public ApiParameterContext(
+ IModelMetadataProvider metadataProvider,
+ ControllerActionDescriptor actionDescriptor,
+ IReadOnlyList routeParameters)
+ {
+ MetadataProvider = metadataProvider;
+ ActionDescriptor = actionDescriptor;
+ RouteParameters = routeParameters;
+
+ Results = new List();
+ }
+
+ public ControllerActionDescriptor ActionDescriptor { get; }
+
+ public IModelMetadataProvider MetadataProvider { get; }
+
+ public IList Results { get; }
+
+ public IReadOnlyList RouteParameters { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs
index 2b89a26e2c..a9d644e2a8 100644
--- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs
@@ -4,8 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
@@ -177,7 +175,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
var visitor = new PseudoModelBindingVisitor(context, actionParameter);
- ModelMetadata metadata = null;
+ ModelMetadata metadata;
if (_mvcOptions.AllowValidatingTopLevelNodes &&
actionParameter is ControllerParameterDescriptor controllerParameterDescriptor &&
_modelMetadataProvider is ModelMetadataProvider provider)
@@ -232,6 +230,21 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
}
// Next, we want to join up any route parameters with those discovered from the action's parameters.
+ // This will result us in creating a parameter representation for each route parameter that does not
+ // have a mapping parameter or bound property.
+ ProcessRouteParameters(context);
+
+ // Set IsRequired=true
+ ProcessIsRequired(context);
+
+ // Set DefaultValue
+ ProcessParameterDefaultValue(context);
+
+ return context.Results;
+ }
+
+ private void ProcessRouteParameters(ApiParameterContext context)
+ {
var routeParameters = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var routeParameter in context.RouteParameters)
{
@@ -271,8 +284,46 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
Source = BindingSource.Path,
});
}
+ }
- return context.Results;
+ internal static void ProcessIsRequired(ApiParameterContext context)
+ {
+ foreach (var parameter in context.Results)
+ {
+ if (parameter.Source == BindingSource.Body)
+ {
+ parameter.IsRequired = true;
+ }
+
+ if (parameter.ModelMetadata != null && parameter.ModelMetadata.IsBindingRequired)
+ {
+ parameter.IsRequired = true;
+ }
+
+ if (parameter.Source == BindingSource.Path && parameter.RouteInfo != null && !parameter.RouteInfo.IsOptional)
+ {
+ parameter.IsRequired = true;
+ }
+ }
+ }
+
+ internal static void ProcessParameterDefaultValue(ApiParameterContext context)
+ {
+ foreach (var parameter in context.Results)
+ {
+ if (parameter.Source == BindingSource.Path)
+ {
+ parameter.DefaultValue = parameter.RouteInfo?.DefaultValue;
+ }
+ else
+ {
+ if (parameter.ParameterDescriptor is ControllerParameterDescriptor controllerParameter &&
+ ParameterDefaultValues.TryGetDeclaredParameterDefaultValue(controllerParameter.ParameterInfo, out var defaultValue))
+ {
+ parameter.DefaultValue = defaultValue;
+ }
+ }
+ }
}
private ApiParameterRouteInfo CreateRouteInfo(TemplatePart routeParameter)
@@ -416,29 +467,6 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
.ToArray();
}
- private class ApiParameterContext
- {
- public ApiParameterContext(
- IModelMetadataProvider metadataProvider,
- ControllerActionDescriptor actionDescriptor,
- IReadOnlyList routeParameters)
- {
- MetadataProvider = metadataProvider;
- ActionDescriptor = actionDescriptor;
- RouteParameters = routeParameters;
-
- Results = new List();
- }
-
- public ControllerActionDescriptor ActionDescriptor { get; }
-
- public IModelMetadataProvider MetadataProvider { get; }
-
- public IList Results { get; }
-
- public IReadOnlyList RouteParameters { get; }
- }
-
private class ApiParameterDescriptionContext
{
public ModelMetadata ModelMetadata { get; set; }
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ParameterDefaultValues.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ParameterDefaultValues.cs
index 0d4a641e5f..8263da1650 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ParameterDefaultValues.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ParameterDefaultValues.cs
@@ -30,17 +30,30 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static object GetParameterDefaultValue(ParameterInfo parameterInfo)
{
- if (!ParameterDefaultValue.TryGetDefaultValue(parameterInfo, out var defaultValue))
+ TryGetDeclaredParameterDefaultValue(parameterInfo, out var defaultValue);
+ if (defaultValue == null && parameterInfo.ParameterType.IsValueType)
{
- var defaultValueAttribute = parameterInfo.GetCustomAttribute(inherit: false);
- defaultValue = defaultValueAttribute?.Value;
-
- if (defaultValue == null && parameterInfo.ParameterType.IsValueType)
- {
- defaultValue = Activator.CreateInstance(parameterInfo.ParameterType);
- }
+ defaultValue = Activator.CreateInstance(parameterInfo.ParameterType);
}
+
return defaultValue;
}
+
+ public static bool TryGetDeclaredParameterDefaultValue(ParameterInfo parameterInfo, out object defaultValue)
+ {
+ if (ParameterDefaultValue.TryGetDefaultValue(parameterInfo, out defaultValue))
+ {
+ return true;
+ }
+
+ var defaultValueAttribute = parameterInfo.GetCustomAttribute(inherit: false);
+ if (defaultValueAttribute != null)
+ {
+ defaultValue = defaultValueAttribute.Value;
+ return true;
+ }
+
+ return false;
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs
index 4ceafa6995..b661be6034 100644
--- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs
@@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Moq;
@@ -1416,6 +1417,172 @@ namespace Microsoft.AspNetCore.Mvc.Description
Assert.Equal(typeof(string), comments.Type);
}
+ [Fact]
+ public void ProcessIsRequired_SetsTrue_ForFromBodyParameters()
+ {
+ // Arrange
+ var description = new ApiParameterDescription { Source = BindingSource.Body, };
+ var context = GetApiParameterContext(description);
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessIsRequired(context);
+
+ // Assert
+ Assert.True(description.IsRequired);
+ }
+
+ [Fact]
+ public void ProcessIsRequired_SetsTrue_ForParameterDescriptorsWithBindRequired()
+ {
+ // Arrange
+ var description = new ApiParameterDescription
+ {
+ Source = BindingSource.Query,
+ };
+ var context = GetApiParameterContext(description);
+ var modelMetadataProvider = new TestModelMetadataProvider();
+ modelMetadataProvider
+ .ForProperty(nameof(Person.Name))
+ .BindingDetails(d => d.IsBindingRequired = true);
+ description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessIsRequired(context);
+
+ // Assert
+ Assert.True(description.IsRequired);
+ }
+
+ [Fact]
+ public void ProcessIsRequired_SetsTrue_ForRequiredRouteParameterDescriptors()
+ {
+ // Arrange
+ var description = new ApiParameterDescription
+ {
+ Source = BindingSource.Path,
+ RouteInfo = new ApiParameterRouteInfo(),
+ };
+ var context = GetApiParameterContext(description);
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessIsRequired(context);
+
+ // Assert
+ Assert.True(description.IsRequired);
+ }
+
+ [Fact]
+ public void ProcessIsRequired_DoesNotSetToTrue_ByDefault()
+ {
+ // Arrange
+ var description = new ApiParameterDescription();
+ var context = GetApiParameterContext(description);
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessIsRequired(context);
+
+ // Assert
+ Assert.False(description.IsRequired);
+ }
+
+ [Fact]
+ public void ProcessIsRequired_DoesNotSetToTrue_ForParameterDescriptorsWithValidationRequired()
+ {
+ // Arrange
+ var description = new ApiParameterDescription();
+ var context = GetApiParameterContext(description);
+ var modelMetadataProvider = new TestModelMetadataProvider();
+ modelMetadataProvider
+ .ForProperty(nameof(Person.Name))
+ .ValidationDetails(d => d.IsRequired = true);
+ description.ModelMetadata = modelMetadataProvider.GetMetadataForProperty(typeof(Person), nameof(Person.Name));
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessIsRequired(context);
+
+ // Assert
+ Assert.False(description.IsRequired);
+ }
+
+ [Fact]
+ public void ProcessDefaultValue_SetsDefaultRouteValue()
+ {
+ // Arrange
+ var methodInfo = GetType().GetMethod(nameof(ParameterDefaultValue), BindingFlags.Instance | BindingFlags.NonPublic);
+ var parameterInfo = methodInfo.GetParameters()[0];
+
+ var defaultValue = new object();
+ var description = new ApiParameterDescription
+ {
+ Source = BindingSource.Path,
+ RouteInfo = new ApiParameterRouteInfo { DefaultValue = defaultValue },
+ ParameterDescriptor = new ControllerParameterDescriptor
+ {
+ ParameterInfo = parameterInfo,
+ },
+ };
+ var context = GetApiParameterContext(description);
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessParameterDefaultValue(context);
+
+ // Assert
+ Assert.Same(defaultValue, description.DefaultValue);
+ }
+
+ [Fact]
+ public void ProcessDefaultValue_SetsDefaultValue_FromParameterInfo()
+ {
+ // Arrange
+ var methodInfo = GetType().GetMethod(nameof(ParameterDefaultValue), BindingFlags.Instance | BindingFlags.NonPublic);
+ var parameterInfo = methodInfo.GetParameters()[0];
+ var description = new ApiParameterDescription
+ {
+ Source = BindingSource.Query,
+ ParameterDescriptor = new ControllerParameterDescriptor
+ {
+ ParameterInfo = parameterInfo,
+ },
+ };
+ var context = GetApiParameterContext(description);
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessParameterDefaultValue(context);
+
+ // Assert
+ Assert.Equal(10, description.DefaultValue);
+ }
+
+ [Fact]
+ public void ProcessDefaultValue_DoesNotSpecifyDefaultValueForValueTypes_WhenNoValueIsSpecified()
+ {
+ // Arrange
+ var methodInfo = GetType().GetMethod(nameof(AcceptsId_Query), BindingFlags.Instance | BindingFlags.NonPublic);
+ var parameterInfo = methodInfo.GetParameters()[0];
+ var description = new ApiParameterDescription
+ {
+ Source = BindingSource.Query,
+ ParameterDescriptor = new ControllerParameterDescriptor
+ {
+ ParameterInfo = parameterInfo,
+ },
+ };
+ var context = GetApiParameterContext(description);
+
+ // Act
+ DefaultApiDescriptionProvider.ProcessParameterDefaultValue(context);
+
+ // Assert
+ Assert.Null(description.DefaultValue);
+ }
+
+ private static ApiParameterContext GetApiParameterContext(ApiParameterDescription description)
+ {
+ var context = new ApiParameterContext(new EmptyModelMetadataProvider(), new ControllerActionDescriptor(), new TemplatePart[0]);
+ context.Results.Add(description);
+ return context;
+ }
+
private IReadOnlyList GetApiDescriptions(
ActionDescriptor action,
List inputFormatters = null,
@@ -1711,6 +1878,8 @@ namespace Microsoft.AspNetCore.Mvc.Description
{
}
+ private void ParameterDefaultValue(int value = 10) { }
+
private class TestController
{
[FromRoute]
diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
index 774e0de862..a6a69bfccd 100644
--- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/ApiExplorerTest.cs
@@ -1,6 +1,7 @@
// 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.Net;
@@ -1018,6 +1019,75 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal(typeof(string).FullName, feedback.Type);
}
+ [Fact]
+ public async Task ApiExplorer_Parameters_DefaultValue()
+ {
+ // Arrange & Act
+ var response = await Client.GetAsync("ApiExplorerParameters/DefaultValueParameters");
+
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(body);
+
+ // Assert
+ var description = Assert.Single(result);
+ var parameters = description.ParameterDescriptions;
+
+ Assert.Collection(
+ parameters,
+ parameter =>
+ {
+ Assert.Equal("searchTerm", parameter.Name);
+ Assert.Null(parameter.DefaultValue);
+ },
+ parameter =>
+ {
+ Assert.Equal("top", parameter.Name);
+ Assert.Equal("10", parameter.DefaultValue);
+ },
+ parameter =>
+ {
+ Assert.Equal("searchDay", parameter.Name);
+ Assert.Equal(nameof(DayOfWeek.Wednesday), parameter.DefaultValue);
+ });
+ }
+
+ [Fact]
+ public async Task ApiExplorer_Parameters_IsRequired()
+ {
+ // Arrange & Act
+ var response = await Client.GetAsync("ApiExplorerParameters/IsRequiredParameters");
+
+ var body = await response.Content.ReadAsStringAsync();
+ var result = JsonConvert.DeserializeObject>(body);
+
+ // Assert
+ var description = Assert.Single(result);
+ var parameters = description.ParameterDescriptions;
+
+ Assert.Collection(
+ parameters,
+ parameter =>
+ {
+ Assert.Equal("requiredParam", parameter.Name);
+ Assert.True(parameter.IsRequired);
+ },
+ parameter =>
+ {
+ Assert.Equal("notRequiredParam", parameter.Name);
+ Assert.False(parameter.IsRequired);
+ },
+ parameter =>
+ {
+ Assert.Equal("Id", parameter.Name);
+ Assert.True(parameter.IsRequired);
+ },
+ parameter =>
+ {
+ Assert.Equal("Name", parameter.Name);
+ Assert.False(parameter.IsRequired);
+ });
+ }
+
[Fact]
public async Task ApiExplorer_Updates_WhenActionDescriptorCollectionIsUpdated()
{
@@ -1111,6 +1181,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public string Source { get; set; }
public string Type { get; set; }
+
+ public string DefaultValue { get; set; }
+
+ public bool IsRequired { get; set; }
}
// Used to serialize data between client and server
diff --git a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
index fdf64194eb..d365caf755 100644
--- a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
+++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
@@ -28,8 +28,8 @@ namespace ApiExplorerWebSite
public void OnResourceExecuting(ResourceExecutingContext context)
{
- var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
- if (controllerActionDescriptor != null && controllerActionDescriptor.MethodInfo.IsDefined(typeof(PassThruAttribute)))
+ if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor &&
+ controllerActionDescriptor.MethodInfo.IsDefined(typeof(PassThruAttribute)))
{
return;
}
@@ -69,6 +69,8 @@ namespace ApiExplorerWebSite
Name = parameter.Name,
Source = parameter.Source.Id,
Type = parameter.Type?.FullName,
+ DefaultValue = parameter.DefaultValue?.ToString(),
+ IsRequired = parameter.IsRequired,
};
if (parameter.RouteInfo != null)
@@ -143,6 +145,10 @@ namespace ApiExplorerWebSite
public string Source { get; set; }
public string Type { get; set; }
+
+ public string DefaultValue { get; set; }
+
+ public bool IsRequired { get; set; }
}
// Used to serialize data between client and server
diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerParametersController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerParametersController.cs
index a422553574..e890e3d4d3 100644
--- a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerParametersController.cs
+++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerParametersController.cs
@@ -1,7 +1,9 @@
// 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;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace ApiExplorerWebSite.Controllers
{
@@ -28,5 +30,13 @@ namespace ApiExplorerWebSite.Controllers
public void ComplexModel([FromQuery] OrderDTO order)
{
}
+
+ public void DefaultValueParameters(string searchTerm, int top = 10, DayOfWeek searchDay = DayOfWeek.Wednesday)
+ {
+ }
+
+ public void IsRequiredParameters([BindRequired] string requiredParam, string notRequiredParam, Product product)
+ {
+ }
}
}
\ No newline at end of file
diff --git a/test/WebSites/ApiExplorerWebSite/Models/Product.cs b/test/WebSites/ApiExplorerWebSite/Models/Product.cs
index 1844de7e13..245df86d8b 100644
--- a/test/WebSites/ApiExplorerWebSite/Models/Product.cs
+++ b/test/WebSites/ApiExplorerWebSite/Models/Product.cs
@@ -1,12 +1,17 @@
// 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.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
namespace ApiExplorerWebSite
{
public class Product
{
+ [BindRequired]
public int Id { get; set; }
+ [Required]
public string Name { get; set; }
}
}
\ No newline at end of file