diff --git a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs index 23e8bb6187..ede016465c 100644 --- a/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.ApiExplorer/DefaultApiDescriptionProvider.cs @@ -162,7 +162,23 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer foreach (var actionParameter in context.ActionDescriptor.Parameters) { var visitor = new PseudoModelBindingVisitor(context, actionParameter); - var metadata = _modelMetadataProvider.GetMetadataForType(actionParameter.ParameterType); + + ModelMetadata metadata = null; + if (actionParameter is ControllerParameterDescriptor controllerParameterDescriptor && + _modelMetadataProvider is ModelMetadataProvider provider) + { + // The default model metadata provider derives from ModelMetadataProvider + // and can therefore supply information about attributes applied to parameters. + metadata = provider.GetMetadataForParameter(controllerParameterDescriptor.ParameterInfo); + } + else + { + // For backward compatibility, if there's a custom model metadata provider that + // only implements the older IModelMetadataProvider interface, access the more + // limited metadata information it supplies. In this scenario, validation attributes + // are not supported on parameters. + metadata = _modelMetadataProvider.GetMetadataForType(actionParameter.ParameterType); + } var bindingContext = ApiParameterDescriptionContext.GetContext( metadata, diff --git a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs index 04a1cf9d50..8ef866008a 100644 --- a/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ApiExplorer.Test/DefaultApiDescriptionProviderTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using System.Text; @@ -912,6 +913,25 @@ namespace Microsoft.AspNetCore.Mvc.Description Assert.Equal(typeof(string), parameter.Type); } + [Fact] + public void GetApiDescription_ParameterDescription_IsRequired() + { + // Arrange + var action = CreateActionDescriptor(nameof(RequiredParameter)); + + // Act + var descriptions = GetApiDescriptions(action); + + // Assert + var description = Assert.Single(descriptions); + var parameter = Assert.Single(description.ParameterDescriptions); + Assert.Equal("name", parameter.Name); + Assert.Same(BindingSource.ModelBinding, parameter.Source); + Assert.Equal(typeof(string), parameter.Type); + Assert.True(parameter.ModelMetadata.IsRequired); + Assert.True(parameter.ModelMetadata.IsBindingRequired); + } + [Fact] public void GetApiDescription_ParameterDescription_SourceFromRouteData() { @@ -1472,11 +1492,12 @@ namespace Microsoft.AspNetCore.Mvc.Description action.Parameters = new List(); foreach (var parameter in action.MethodInfo.GetParameters()) { - action.Parameters.Add(new ParameterDescriptor() + action.Parameters.Add(new ControllerParameterDescriptor() { Name = parameter.Name, ParameterType = parameter.ParameterType, - BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes().OfType()) + BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes().OfType()), + ParameterInfo = parameter }); } @@ -1552,6 +1573,10 @@ namespace Microsoft.AspNetCore.Mvc.Description { } + private void RequiredParameter([BindRequired, Required] string name) + { + } + private void AcceptsProduct_Body([FromBody] Product product) { }