Add IsRequired and DefaultValue to ApiParameterDescription

This commit is contained in:
Pranav K 2018-06-21 11:36:20 -07:00
parent 17d2545b55
commit 6911e192e4
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
9 changed files with 393 additions and 37 deletions

View File

@ -41,5 +41,23 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
/// Gets or sets the parameter descriptor.
/// </summary>
public ParameterDescriptor ParameterDescriptor { get; set; }
/// <summary>
/// Gets or sets a value that determines if the parameter is required.
/// </summary>
/// <remarks>
/// A parameter is considered required if
/// <list type="bullet">
/// <item>it's bound from the request body (<see cref="BindingSource.Body"/>).</item>
/// <item>it's a required route value.</item>
/// <item>it has annotations (e.g. BindRequiredAttribute) that indicate it's required.</item>
/// </list>
/// </remarks>
public bool IsRequired { get; set; }
/// <summary>
/// Gets or sets the default value for a parameter.
/// </summary>
public object DefaultValue { get; set; }
}
}

View File

@ -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<TemplatePart> routeParameters)
{
MetadataProvider = metadataProvider;
ActionDescriptor = actionDescriptor;
RouteParameters = routeParameters;
Results = new List<ApiParameterDescription>();
}
public ControllerActionDescriptor ActionDescriptor { get; }
public IModelMetadataProvider MetadataProvider { get; }
public IList<ApiParameterDescription> Results { get; }
public IReadOnlyList<TemplatePart> RouteParameters { get; }
}
}

View File

@ -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<string, ApiParameterRouteInfo>(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<TemplatePart> routeParameters)
{
MetadataProvider = metadataProvider;
ActionDescriptor = actionDescriptor;
RouteParameters = routeParameters;
Results = new List<ApiParameterDescription>();
}
public ControllerActionDescriptor ActionDescriptor { get; }
public IModelMetadataProvider MetadataProvider { get; }
public IList<ApiParameterDescription> Results { get; }
public IReadOnlyList<TemplatePart> RouteParameters { get; }
}
private class ApiParameterDescriptionContext
{
public ModelMetadata ModelMetadata { get; set; }

View File

@ -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<DefaultValueAttribute>(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<DefaultValueAttribute>(inherit: false);
if (defaultValueAttribute != null)
{
defaultValue = defaultValueAttribute.Value;
return true;
}
return false;
}
}
}

View File

@ -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<Person>(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<Person>(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<ApiDescription> GetApiDescriptions(
ActionDescriptor action,
List<MockInputFormatter> inputFormatters = null,
@ -1711,6 +1878,8 @@ namespace Microsoft.AspNetCore.Mvc.Description
{
}
private void ParameterDefaultValue(int value = 10) { }
private class TestController
{
[FromRoute]

View File

@ -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<List<ApiExplorerData>>(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<List<ApiExplorerData>>(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

View File

@ -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

View File

@ -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)
{
}
}
}

View File

@ -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; }
}
}