[Fixes #885] API Explorer: Link Generation
1) Expose the simplified relative path template by cleaning up constraints, optional and catch all tokens from the template. 2) Expose the parameters on the route template as API parameters. 3) Combine parameters from the route and the action descriptor when the parameter doesn't come from the body. #886 will refine this. 4) Expose optionality and constraints for path parameters. Open question: Should we explicitly expose IsCatchAll?
This commit is contained in:
parent
a633ef4f97
commit
3f54492930
|
|
@ -9,7 +9,7 @@ namespace MvcSample.Web.ApiExplorerSamples
|
||||||
[Route("api/Products")]
|
[Route("api/Products")]
|
||||||
public class ProductsController : Controller
|
public class ProductsController : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id:int}")]
|
||||||
public Product GetById(int id)
|
public Product GetById(int id)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -22,7 +22,7 @@ namespace MvcSample.Web.ApiExplorerSamples
|
||||||
}
|
}
|
||||||
|
|
||||||
[Produces("application/json", Type = typeof(ProductOrderConfirmation))]
|
[Produces("application/json", Type = typeof(ProductOrderConfirmation))]
|
||||||
[HttpPut("{id}/Buy")]
|
[HttpPut("{id:int}/Buy")]
|
||||||
public IActionResult Buy(int projectId, int quantity = 1)
|
public IActionResult Buy(int projectId, int quantity = 1)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@
|
||||||
<ul>
|
<ul>
|
||||||
@foreach (var parameter in Model.ParameterDescriptions)
|
@foreach (var parameter in Model.ParameterDescriptions)
|
||||||
{
|
{
|
||||||
<li>@parameter.Name - @parameter.Type.FullName - @parameter.Source.ToString()</li>
|
<li>
|
||||||
|
@parameter.Name - @(parameter?.Type?.FullName ?? "Unknown") - @parameter.Source.ToString()
|
||||||
|
- Constraint: @(parameter?.Constraint?.GetType()?.Name?.Replace("RouteConstraint", "") ?? " none")
|
||||||
|
- Default value: @(parameter?.DefaultValue ?? " none")
|
||||||
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
|
|
@ -22,7 +26,7 @@
|
||||||
{
|
{
|
||||||
<p>Response Formats:</p>
|
<p>Response Formats:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@foreach(var response in Model.SupportedResponseFormats)
|
@foreach (var response in Model.SupportedResponseFormats)
|
||||||
{
|
{
|
||||||
<li>@response.MediaType.RawValue - @response.Formatter.GetType().Name</li>
|
<li>@response.MediaType.RawValue - @response.Formatter.GetType().Name</li>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Description
|
namespace Microsoft.AspNet.Mvc.Description
|
||||||
{
|
{
|
||||||
|
|
@ -18,6 +19,10 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
|
|
||||||
public ApiParameterSource Source { get; set; }
|
public ApiParameterSource Source { get; set; }
|
||||||
|
|
||||||
|
public IRouteConstraint Constraint { get; set; }
|
||||||
|
|
||||||
|
public object DefaultValue { get; set; }
|
||||||
|
|
||||||
public Type Type { get; set; }
|
public Type Type { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,5 +8,6 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
{
|
{
|
||||||
Body,
|
Body,
|
||||||
Query,
|
Query,
|
||||||
|
Path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,10 +3,13 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
|
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Template;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.Description
|
namespace Microsoft.AspNet.Mvc.Description
|
||||||
|
|
@ -19,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
{
|
{
|
||||||
private readonly IOutputFormattersProvider _formattersProvider;
|
private readonly IOutputFormattersProvider _formattersProvider;
|
||||||
private readonly IModelMetadataProvider _modelMetadataProvider;
|
private readonly IModelMetadataProvider _modelMetadataProvider;
|
||||||
|
private readonly IInlineConstraintResolver _constraintResolver;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of <see cref="DefaultApiDescriptionProvider"/>.
|
/// Creates a new instance of <see cref="DefaultApiDescriptionProvider"/>.
|
||||||
|
|
@ -27,10 +31,12 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
|
||||||
public DefaultApiDescriptionProvider(
|
public DefaultApiDescriptionProvider(
|
||||||
IOutputFormattersProvider formattersProvider,
|
IOutputFormattersProvider formattersProvider,
|
||||||
|
IInlineConstraintResolver constraintResolver,
|
||||||
IModelMetadataProvider modelMetadataProvider)
|
IModelMetadataProvider modelMetadataProvider)
|
||||||
{
|
{
|
||||||
_formattersProvider = formattersProvider;
|
_formattersProvider = formattersProvider;
|
||||||
_modelMetadataProvider = modelMetadataProvider;
|
_modelMetadataProvider = modelMetadataProvider;
|
||||||
|
_constraintResolver = constraintResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -64,21 +70,19 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
string httpMethod,
|
string httpMethod,
|
||||||
string groupName)
|
string groupName)
|
||||||
{
|
{
|
||||||
|
var parsedTemplate = ParseTemplate(action);
|
||||||
|
|
||||||
var apiDescription = new ApiDescription()
|
var apiDescription = new ApiDescription()
|
||||||
{
|
{
|
||||||
ActionDescriptor = action,
|
ActionDescriptor = action,
|
||||||
GroupName = groupName,
|
GroupName = groupName,
|
||||||
HttpMethod = httpMethod,
|
HttpMethod = httpMethod,
|
||||||
RelativePath = GetRelativePath(action),
|
RelativePath = GetRelativePath(parsedTemplate),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (action.Parameters != null)
|
var templateParameters = parsedTemplate?.Parameters?.ToList() ?? new List<TemplatePart>();
|
||||||
{
|
|
||||||
foreach (var parameter in action.Parameters)
|
GetParameters(apiDescription, action.Parameters, templateParameters);
|
||||||
{
|
|
||||||
apiDescription.ParameterDescriptions.Add(GetParameter(parameter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
|
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
|
||||||
|
|
||||||
|
|
@ -121,6 +125,44 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
return apiDescription;
|
return apiDescription;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void GetParameters(
|
||||||
|
ApiDescription apiDescription,
|
||||||
|
IList<ParameterDescriptor> parameterDescriptors,
|
||||||
|
IList<TemplatePart> templateParameters)
|
||||||
|
{
|
||||||
|
if (parameterDescriptors != null)
|
||||||
|
{
|
||||||
|
foreach (var parameter in parameterDescriptors)
|
||||||
|
{
|
||||||
|
// Process together parameters that appear on the path template and on the
|
||||||
|
// action descriptor and do not come from the body.
|
||||||
|
TemplatePart templateParameter = null;
|
||||||
|
if (parameter.BodyParameterInfo == null)
|
||||||
|
{
|
||||||
|
templateParameter = templateParameters
|
||||||
|
.FirstOrDefault(p => p.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (templateParameter != null)
|
||||||
|
{
|
||||||
|
templateParameters.Remove(templateParameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiDescription.ParameterDescriptions.Add(GetParameter(parameter, templateParameter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateParameters.Count > 0)
|
||||||
|
{
|
||||||
|
// Process parameters that only appear on the path template if any.
|
||||||
|
foreach (var templateParameter in templateParameters)
|
||||||
|
{
|
||||||
|
var parameterDescription = GetParameter(parameterDescriptor: null, templateParameter: templateParameter);
|
||||||
|
apiDescription.ParameterDescriptions.Add(parameterDescription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private IEnumerable<string> GetHttpMethods(ControllerActionDescriptor action)
|
private IEnumerable<string> GetHttpMethods(ControllerActionDescriptor action)
|
||||||
{
|
{
|
||||||
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
|
if (action.ActionConstraints != null && action.ActionConstraints.Count > 0)
|
||||||
|
|
@ -133,23 +175,93 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetRelativePath(ControllerActionDescriptor action)
|
private RouteTemplate ParseTemplate(ControllerActionDescriptor action)
|
||||||
{
|
{
|
||||||
// This is a placeholder for functionality which will correctly generate the relative path
|
|
||||||
// stub of an action. See: #885
|
|
||||||
if (action.AttributeRouteInfo != null &&
|
if (action.AttributeRouteInfo != null &&
|
||||||
action.AttributeRouteInfo.Template != null)
|
action.AttributeRouteInfo.Template != null)
|
||||||
{
|
{
|
||||||
return action.AttributeRouteInfo.Template;
|
return TemplateParser.Parse(action.AttributeRouteInfo.Template, _constraintResolver);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ApiParameterDescription GetParameter(ParameterDescriptor parameter)
|
private string GetRelativePath(RouteTemplate parsedTemplate)
|
||||||
|
{
|
||||||
|
if (parsedTemplate == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segments = new List<string>();
|
||||||
|
|
||||||
|
foreach (var segment in parsedTemplate.Segments)
|
||||||
|
{
|
||||||
|
var currentSegment = "";
|
||||||
|
foreach (var part in segment.Parts)
|
||||||
|
{
|
||||||
|
if (part.IsLiteral)
|
||||||
|
{
|
||||||
|
currentSegment += part.Text;
|
||||||
|
}
|
||||||
|
else if (part.IsParameter)
|
||||||
|
{
|
||||||
|
currentSegment += "{" + part.Name + "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.Add(currentSegment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("/", segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiParameterDescription GetParameter(
|
||||||
|
ParameterDescriptor parameterDescriptor,
|
||||||
|
TemplatePart templateParameter)
|
||||||
{
|
{
|
||||||
// This is a placeholder based on currently available functionality for parameters. See #886.
|
// This is a placeholder based on currently available functionality for parameters. See #886.
|
||||||
var resourceParameter = new ApiParameterDescription()
|
ApiParameterDescription parameterDescription = null;
|
||||||
|
|
||||||
|
if (templateParameter != null && parameterDescriptor == null)
|
||||||
|
{
|
||||||
|
// The parameter is part of the route template but not part of the ActionDescriptor.
|
||||||
|
|
||||||
|
// For now if a parameter is part of the template we will asume its value comes from the path.
|
||||||
|
// We will be more accurate when we implement #886.
|
||||||
|
parameterDescription = CreateParameterFromTemplate(templateParameter);
|
||||||
|
}
|
||||||
|
else if (templateParameter != null && parameterDescriptor != null)
|
||||||
|
{
|
||||||
|
// The parameter is part of the route template and part of the ActionDescriptor.
|
||||||
|
parameterDescription = CreateParameterFromTemplateAndParameterDescriptor(
|
||||||
|
templateParameter,
|
||||||
|
parameterDescriptor);
|
||||||
|
}
|
||||||
|
else if(templateParameter == null && parameterDescriptor != null)
|
||||||
|
{
|
||||||
|
// The parameter is part of the ActionDescriptor but is not part of the route template.
|
||||||
|
parameterDescription = CreateParameterFromParameterDescriptor(parameterDescriptor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We will never call this method with templateParameter == null && parameterDescriptor == null
|
||||||
|
Contract.Assert(parameterDescriptor != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterDescription.Type != null)
|
||||||
|
{
|
||||||
|
parameterDescription.ModelMetadata = _modelMetadataProvider.GetMetadataForType(
|
||||||
|
modelAccessor: null,
|
||||||
|
modelType: parameterDescription.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameterDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApiParameterDescription CreateParameterFromParameterDescriptor(ParameterDescriptor parameter)
|
||||||
|
{
|
||||||
|
var resourceParameter = new ApiParameterDescription
|
||||||
{
|
{
|
||||||
IsOptional = parameter.IsOptional,
|
IsOptional = parameter.IsOptional,
|
||||||
Name = parameter.Name,
|
Name = parameter.Name,
|
||||||
|
|
@ -158,8 +270,8 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
|
|
||||||
if (parameter.ParameterBindingInfo != null)
|
if (parameter.ParameterBindingInfo != null)
|
||||||
{
|
{
|
||||||
resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
|
|
||||||
resourceParameter.Source = ApiParameterSource.Query;
|
resourceParameter.Source = ApiParameterSource.Query;
|
||||||
|
resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parameter.BodyParameterInfo != null)
|
if (parameter.BodyParameterInfo != null)
|
||||||
|
|
@ -168,16 +280,49 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
resourceParameter.Source = ApiParameterSource.Body;
|
resourceParameter.Source = ApiParameterSource.Body;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceParameter.Type != null)
|
return resourceParameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApiParameterDescription CreateParameterFromTemplateAndParameterDescriptor(
|
||||||
|
TemplatePart templateParameter,
|
||||||
|
ParameterDescriptor parameter)
|
||||||
|
{
|
||||||
|
var resourceParameter = new ApiParameterDescription
|
||||||
{
|
{
|
||||||
resourceParameter.ModelMetadata = _modelMetadataProvider.GetMetadataForType(
|
Source = ApiParameterSource.Path,
|
||||||
modelAccessor: null,
|
IsOptional = parameter.IsOptional && IsOptionalParameter(templateParameter),
|
||||||
modelType: resourceParameter.Type);
|
Name = parameter.Name,
|
||||||
|
ParameterDescriptor = parameter,
|
||||||
|
Constraint = templateParameter.InlineConstraint,
|
||||||
|
DefaultValue = templateParameter.DefaultValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parameter.ParameterBindingInfo != null)
|
||||||
|
{
|
||||||
|
resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
|
||||||
}
|
}
|
||||||
|
|
||||||
return resourceParameter;
|
return resourceParameter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsOptionalParameter(TemplatePart templateParameter)
|
||||||
|
{
|
||||||
|
return templateParameter.IsOptional || templateParameter.DefaultValue != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApiParameterDescription CreateParameterFromTemplate(TemplatePart templateParameter)
|
||||||
|
{
|
||||||
|
return new ApiParameterDescription
|
||||||
|
{
|
||||||
|
Source = ApiParameterSource.Path,
|
||||||
|
IsOptional = IsOptionalParameter(templateParameter),
|
||||||
|
Name = templateParameter.Name,
|
||||||
|
ParameterDescriptor = null,
|
||||||
|
Constraint = templateParameter.InlineConstraint,
|
||||||
|
DefaultValue = templateParameter.DefaultValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
|
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
|
||||||
ControllerActionDescriptor action,
|
ControllerActionDescriptor action,
|
||||||
IApiResponseMetadataProvider[] responseMetadataAttributes,
|
IApiResponseMetadataProvider[] responseMetadataAttributes,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
|
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
using Microsoft.AspNet.Mvc.Routing;
|
using Microsoft.AspNet.Mvc.Routing;
|
||||||
|
using Microsoft.AspNet.Routing;
|
||||||
|
using Microsoft.AspNet.Routing.Constraints;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -151,25 +153,269 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
Assert.Equal(typeof(string), username.Type);
|
Assert.Equal(typeof(string), username.Type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a placeholder based on current functionality - see #885
|
[Theory]
|
||||||
[Fact]
|
[InlineData("api/products/{id}", false, null, null)]
|
||||||
public void GetApiDescription_PopluatesRelativePath()
|
[InlineData("api/products/{id?}", true, null, null)]
|
||||||
|
[InlineData("api/products/{id=5}", true, null, "5")]
|
||||||
|
[InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{id:int=5}", true, null, "5")]
|
||||||
|
[InlineData("api/products/{*id}", false, null, null)]
|
||||||
|
[InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")]
|
||||||
|
public void GetApiDescription_PopulatesParameters_ThatAppearOnlyOnRouteTemplate(
|
||||||
|
string template,
|
||||||
|
bool isOptional,
|
||||||
|
Type constraintType,
|
||||||
|
object defaultValue)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action = CreateActionDescriptor();
|
var action = CreateActionDescriptor();
|
||||||
action.AttributeRouteInfo = new AttributeRouteInfo();
|
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
|
||||||
action.AttributeRouteInfo.Template = "api/Products/{id}";
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var descriptions = GetApiDescriptions(action);
|
var descriptions = GetApiDescriptions(action);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var description = Assert.Single(descriptions);
|
var description = Assert.Single(descriptions);
|
||||||
Assert.Equal("api/Products/{id}", description.RelativePath);
|
|
||||||
|
var parameter = Assert.Single(description.ParameterDescriptions);
|
||||||
|
Assert.Equal(ApiParameterSource.Path, parameter.Source);
|
||||||
|
Assert.Equal(isOptional, parameter.IsOptional);
|
||||||
|
Assert.Equal("id", parameter.Name);
|
||||||
|
Assert.Null(parameter.ParameterDescriptor);
|
||||||
|
|
||||||
|
if (constraintType != null)
|
||||||
|
{
|
||||||
|
Assert.IsType(constraintType, parameter.Constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValue != null)
|
||||||
|
{
|
||||||
|
Assert.Equal(defaultValue, parameter.DefaultValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Null(parameter.DefaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("api/products/{id}", false, null, null)]
|
||||||
|
[InlineData("api/products/{id?}", true, null, null)]
|
||||||
|
[InlineData("api/products/{id=5}", true, null, "5")]
|
||||||
|
[InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{id:int=5}", true, typeof(IntRouteConstraint), "5")]
|
||||||
|
[InlineData("api/products/{*id}", false, null, null)]
|
||||||
|
[InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")]
|
||||||
|
public void GetApiDescription_PopulatesParametersThatAppearOnRouteTemplate_AndHaveAssociatedParameterDescriptor(
|
||||||
|
string template,
|
||||||
|
bool isOptional,
|
||||||
|
Type constraintType,
|
||||||
|
object defaultValue)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = CreateActionDescriptor();
|
||||||
|
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
|
||||||
|
|
||||||
|
var parameterDescriptor = new ParameterDescriptor
|
||||||
|
{
|
||||||
|
Name = "id",
|
||||||
|
IsOptional = true,
|
||||||
|
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int))
|
||||||
|
};
|
||||||
|
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptions = GetApiDescriptions(action);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(descriptions);
|
||||||
|
|
||||||
|
var parameter = Assert.Single(description.ParameterDescriptions);
|
||||||
|
Assert.Equal(ApiParameterSource.Path, parameter.Source);
|
||||||
|
Assert.Equal(isOptional, parameter.IsOptional);
|
||||||
|
Assert.Equal("id", parameter.Name);
|
||||||
|
Assert.Equal(parameterDescriptor, parameter.ParameterDescriptor);
|
||||||
|
|
||||||
|
if (constraintType != null)
|
||||||
|
{
|
||||||
|
Assert.IsType(constraintType, parameter.Constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValue != null)
|
||||||
|
{
|
||||||
|
Assert.Equal(defaultValue, parameter.DefaultValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Null(parameter.DefaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("api/products/{id}", false, null, null)]
|
||||||
|
[InlineData("api/products/{id?}", true, null, null)]
|
||||||
|
[InlineData("api/products/{id=5}", true, null, "5")]
|
||||||
|
[InlineData("api/products/{id:int}", false, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{id:int?}", true, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{id:int=5}", true, typeof(IntRouteConstraint), "5")]
|
||||||
|
[InlineData("api/products/{*id}", false, null, null)]
|
||||||
|
[InlineData("api/products/{*id:int}", false, typeof(IntRouteConstraint), null)]
|
||||||
|
[InlineData("api/products/{*id:int=5}", true, typeof(IntRouteConstraint), "5")]
|
||||||
|
public void GetApiDescription_CreatesDifferentParameters_IfParameterDescriptorIsFromBody(
|
||||||
|
string template,
|
||||||
|
bool isOptional,
|
||||||
|
Type constraintType,
|
||||||
|
object defaultValue)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = CreateActionDescriptor();
|
||||||
|
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
|
||||||
|
|
||||||
|
var parameterDescriptor = new ParameterDescriptor
|
||||||
|
{
|
||||||
|
Name = "id",
|
||||||
|
IsOptional = false,
|
||||||
|
BodyParameterInfo = new BodyParameterInfo(typeof(int))
|
||||||
|
};
|
||||||
|
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptions = GetApiDescriptions(action);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(descriptions);
|
||||||
|
|
||||||
|
var bodyParameter = Assert.Single(description.ParameterDescriptions, p => p.Source == ApiParameterSource.Body);
|
||||||
|
Assert.False(bodyParameter.IsOptional);
|
||||||
|
Assert.Equal("id", bodyParameter.Name);
|
||||||
|
Assert.Equal(parameterDescriptor, bodyParameter.ParameterDescriptor);
|
||||||
|
|
||||||
|
var pathParameter = Assert.Single(description.ParameterDescriptions, p => p.Source == ApiParameterSource.Path);
|
||||||
|
Assert.Equal(isOptional, pathParameter.IsOptional);
|
||||||
|
Assert.Equal("id", pathParameter.Name);
|
||||||
|
Assert.Null(pathParameter.ParameterDescriptor);
|
||||||
|
|
||||||
|
if (constraintType != null)
|
||||||
|
{
|
||||||
|
Assert.IsType(constraintType, pathParameter.Constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultValue != null)
|
||||||
|
{
|
||||||
|
Assert.Equal(defaultValue, pathParameter.DefaultValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Null(pathParameter.DefaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("api/products/{id}", false, false)]
|
||||||
|
[InlineData("api/products/{id}", true, false)]
|
||||||
|
[InlineData("api/products/{id?}", false, false)]
|
||||||
|
[InlineData("api/products/{id?}", true, true)]
|
||||||
|
[InlineData("api/products/{id=5}", false, false)]
|
||||||
|
[InlineData("api/products/{id=5}", true, true)]
|
||||||
|
public void GetApiDescription_ParameterFromPathAndDescriptor_IsOptionalOnly_IfBothAreOptional(
|
||||||
|
string template,
|
||||||
|
bool isDescriptorParameterOptional,
|
||||||
|
bool expectedOptional)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = CreateActionDescriptor();
|
||||||
|
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
|
||||||
|
|
||||||
|
var parameterDescriptor = new ParameterDescriptor
|
||||||
|
{
|
||||||
|
Name = "id",
|
||||||
|
IsOptional = isDescriptorParameterOptional,
|
||||||
|
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int))
|
||||||
|
};
|
||||||
|
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptions = GetApiDescriptions(action);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(descriptions);
|
||||||
|
var parameter = Assert.Single(description.ParameterDescriptions);
|
||||||
|
Assert.Equal(expectedOptional, parameter.IsOptional);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("api/Products/{id}", "api/Products/{id}")]
|
||||||
|
[InlineData("api/Products/{id?}", "api/Products/{id}")]
|
||||||
|
[InlineData("api/Products/{id:int}", "api/Products/{id}")]
|
||||||
|
[InlineData("api/Products/{id:int?}", "api/Products/{id}")]
|
||||||
|
[InlineData("api/Products/{*id}", "api/Products/{id}")]
|
||||||
|
[InlineData("api/Products/{*id:int}", "api/Products/{id}")]
|
||||||
|
[InlineData("api/Products/{id1}-{id2:int}", "api/Products/{id1}-{id2}")]
|
||||||
|
[InlineData("api/{id1}/{id2?}/{id3:int}/{id4:int?}/{*id5:int}", "api/{id1}/{id2}/{id3}/{id4}/{id5}")]
|
||||||
|
public void GetApiDescription_PopulatesRelativePath(string template, string relativePath)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = CreateActionDescriptor();
|
||||||
|
action.AttributeRouteInfo = new AttributeRouteInfo();
|
||||||
|
action.AttributeRouteInfo.Template = template;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptions = GetApiDescriptions(action);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(descriptions);
|
||||||
|
Assert.Equal(relativePath, description.RelativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetApiDescription_PopluatesResponseType_WithProduct()
|
public void GetApiDescription_DetectsMultipleParameters_OnTheSameSegment()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = CreateActionDescriptor();
|
||||||
|
action.AttributeRouteInfo = new AttributeRouteInfo();
|
||||||
|
action.AttributeRouteInfo.Template = "api/Products/{id1}-{id2:int}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptions = GetApiDescriptions(action);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(descriptions);
|
||||||
|
var id1 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id1");
|
||||||
|
Assert.Equal(ApiParameterSource.Path, id1.Source);
|
||||||
|
Assert.Null(id1.Constraint);
|
||||||
|
|
||||||
|
var id2 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id2");
|
||||||
|
Assert.Equal(ApiParameterSource.Path, id2.Source);
|
||||||
|
Assert.IsType<IntRouteConstraint>(id2.Constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiDescription_DetectsMultipleParameters_OnDifferentSegments()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var action = CreateActionDescriptor();
|
||||||
|
action.AttributeRouteInfo = new AttributeRouteInfo();
|
||||||
|
action.AttributeRouteInfo.Template = "api/Products/{id1}-{id2}/{id3:int}/{id4:int?}/{*id5:int}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var descriptions = GetApiDescriptions(action);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(descriptions);
|
||||||
|
|
||||||
|
Assert.Single(description.ParameterDescriptions, p => p.Name == "id1");
|
||||||
|
Assert.Single(description.ParameterDescriptions, p => p.Name == "id2");
|
||||||
|
Assert.Single(description.ParameterDescriptions, p => p.Name == "id3");
|
||||||
|
Assert.Single(description.ParameterDescriptions, p => p.Name == "id4");
|
||||||
|
Assert.Single(description.ParameterDescriptions, p => p.Name == "id5");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiDescription_PopulatesResponseType_WithProduct()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action = CreateActionDescriptor(nameof(ReturnsProduct));
|
var action = CreateActionDescriptor(nameof(ReturnsProduct));
|
||||||
|
|
@ -184,7 +430,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetApiDescription_PopluatesResponseType_WithTaskOfProduct()
|
public void GetApiDescription_PopulatesResponseType_WithTaskOfProduct()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action = CreateActionDescriptor(nameof(ReturnsTaskOfProduct));
|
var action = CreateActionDescriptor(nameof(ReturnsTaskOfProduct));
|
||||||
|
|
@ -205,7 +451,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
[InlineData(nameof(ReturnsTaskOfObject))]
|
[InlineData(nameof(ReturnsTaskOfObject))]
|
||||||
[InlineData(nameof(ReturnsTaskOfActionResult))]
|
[InlineData(nameof(ReturnsTaskOfActionResult))]
|
||||||
[InlineData(nameof(ReturnsTaskOfJsonResult))]
|
[InlineData(nameof(ReturnsTaskOfJsonResult))]
|
||||||
public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenUnknown(string methodName)
|
public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenUnknown(string methodName)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action = CreateActionDescriptor(methodName);
|
var action = CreateActionDescriptor(methodName);
|
||||||
|
|
@ -223,7 +469,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(nameof(ReturnsVoid))]
|
[InlineData(nameof(ReturnsVoid))]
|
||||||
[InlineData(nameof(ReturnsTask))]
|
[InlineData(nameof(ReturnsTask))]
|
||||||
public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenVoid(string methodName)
|
public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenVoid(string methodName)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action = CreateActionDescriptor(methodName);
|
var action = CreateActionDescriptor(methodName);
|
||||||
|
|
@ -247,7 +493,7 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
[InlineData(nameof(ReturnsTask))]
|
[InlineData(nameof(ReturnsTask))]
|
||||||
[InlineData(nameof(ReturnsTaskOfActionResult))]
|
[InlineData(nameof(ReturnsTaskOfActionResult))]
|
||||||
[InlineData(nameof(ReturnsTaskOfJsonResult))]
|
[InlineData(nameof(ReturnsTaskOfJsonResult))]
|
||||||
public void GetApiDescription_PopluatesResponseInformation_WhenSetByFilter(string methodName)
|
public void GetApiDescription_PopulatesResponseInformation_WhenSetByFilter(string methodName)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var action = CreateActionDescriptor(methodName);
|
var action = CreateActionDescriptor(methodName);
|
||||||
|
|
@ -349,13 +595,19 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
return GetApiDescriptions(action, CreateFormatters());
|
return GetApiDescriptions(action, CreateFormatters());
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<ApiDescription> GetApiDescriptions(ActionDescriptor action, List<MockFormatter> formatters)
|
private IReadOnlyList<ApiDescription> GetApiDescriptions(
|
||||||
|
ActionDescriptor action,
|
||||||
|
List<MockFormatter> formatters)
|
||||||
{
|
{
|
||||||
var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action });
|
var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action });
|
||||||
|
|
||||||
var formattersProvider = new Mock<IOutputFormattersProvider>(MockBehavior.Strict);
|
var formattersProvider = new Mock<IOutputFormattersProvider>(MockBehavior.Strict);
|
||||||
formattersProvider.Setup(fp => fp.OutputFormatters).Returns(formatters);
|
formattersProvider.Setup(fp => fp.OutputFormatters).Returns(formatters);
|
||||||
|
|
||||||
|
var constraintResolver = new Mock<IInlineConstraintResolver>();
|
||||||
|
constraintResolver.Setup(c => c.ResolveConstraint("int"))
|
||||||
|
.Returns(new IntRouteConstraint());
|
||||||
|
|
||||||
var modelMetadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
|
var modelMetadataProvider = new Mock<IModelMetadataProvider>(MockBehavior.Strict);
|
||||||
modelMetadataProvider
|
modelMetadataProvider
|
||||||
.Setup(mmp => mmp.GetMetadataForType(null, It.IsAny<Type>()))
|
.Setup(mmp => mmp.GetMetadataForType(null, It.IsAny<Type>()))
|
||||||
|
|
@ -364,7 +616,11 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
return new ModelMetadata(modelMetadataProvider.Object, null, accessor, type, null);
|
return new ModelMetadata(modelMetadataProvider.Object, null, accessor, type, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
var provider = new DefaultApiDescriptionProvider(formattersProvider.Object, modelMetadataProvider.Object);
|
var provider = new DefaultApiDescriptionProvider(
|
||||||
|
formattersProvider.Object,
|
||||||
|
constraintResolver.Object,
|
||||||
|
modelMetadataProvider.Object);
|
||||||
|
|
||||||
provider.Invoke(context, () => { });
|
provider.Invoke(context, () => { });
|
||||||
return context.Results;
|
return context.Results;
|
||||||
}
|
}
|
||||||
|
|
@ -396,7 +652,6 @@ namespace Microsoft.AspNet.Mvc.Description
|
||||||
methodName ?? "ReturnsObject",
|
methodName ?? "ReturnsObject",
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Builder;
|
using Microsoft.AspNet.Builder;
|
||||||
using Microsoft.AspNet.TestHost;
|
using Microsoft.AspNet.TestHost;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Net.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
|
|
@ -139,6 +139,254 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
Assert.Equal(description.GroupName, "SetOnAction");
|
Assert.Equal(description.GroupName, "SetOnAction");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplate_DisplaysFixedRoute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/ApiExplorerRouteAndPathParametersInformation");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal(description.RelativePath, "ApiExplorerRouteAndPathParametersInformation");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplate_DisplaysRouteWithParameters()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/ApiExplorerRouteAndPathParametersInformation/5");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal(description.RelativePath, "ApiExplorerRouteAndPathParametersInformation/{id}");
|
||||||
|
|
||||||
|
var parameter = Assert.Single(description.ParameterDescriptions);
|
||||||
|
Assert.Equal("id", parameter.Name);
|
||||||
|
Assert.False(parameter.IsOptional);
|
||||||
|
Assert.Equal("Path", parameter.Source);
|
||||||
|
Assert.Null(parameter.ConstraintType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplate_StripsInlineConstraintsFromThePath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/Constraint/5";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal("ApiExplorerRouteAndPathParametersInformation/Constraint/{integer}", description.RelativePath);
|
||||||
|
|
||||||
|
var parameter = Assert.Single(description.ParameterDescriptions);
|
||||||
|
Assert.Equal("integer", parameter.Name);
|
||||||
|
Assert.False(parameter.IsOptional);
|
||||||
|
Assert.Equal("Path", parameter.Source);
|
||||||
|
Assert.Equal("IntRouteConstraint", parameter.ConstraintType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplate_StripsCatchAllsFromThePath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/CatchAll/5";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal("ApiExplorerRouteAndPathParametersInformation/CatchAll/{parameter}", description.RelativePath);
|
||||||
|
|
||||||
|
var parameter = Assert.Single(description.ParameterDescriptions);
|
||||||
|
Assert.Equal("parameter", parameter.Name);
|
||||||
|
Assert.False(parameter.IsOptional);
|
||||||
|
Assert.Equal("Path", parameter.Source);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplate_StripsCatchAllsWithConstraintsFromThePath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/CatchAllAndConstraint/5";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal(
|
||||||
|
"ApiExplorerRouteAndPathParametersInformation/CatchAllAndConstraint/{integer}",
|
||||||
|
description.RelativePath);
|
||||||
|
|
||||||
|
var parameter = Assert.Single(description.ParameterDescriptions);
|
||||||
|
Assert.Equal("integer", parameter.Name);
|
||||||
|
Assert.False(parameter.IsOptional);
|
||||||
|
Assert.Equal("Path", parameter.Source);
|
||||||
|
Assert.Equal("IntRouteConstraint", parameter.ConstraintType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplateStripsMultipleConstraints_OnTheSamePathSegment()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
|
||||||
|
var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/"
|
||||||
|
+ "MultipleParametersInSegment/12-01-1987";
|
||||||
|
|
||||||
|
var expectedRelativePath = "ApiExplorerRouteAndPathParametersInformation/"
|
||||||
|
+ "MultipleParametersInSegment/{month}-{day}-{year}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal(expectedRelativePath, description.RelativePath);
|
||||||
|
|
||||||
|
var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month");
|
||||||
|
Assert.False(month.IsOptional);
|
||||||
|
Assert.Equal("Path", month.Source);
|
||||||
|
Assert.Equal("RangeRouteConstraint", month.ConstraintType);
|
||||||
|
|
||||||
|
var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day");
|
||||||
|
Assert.False(day.IsOptional);
|
||||||
|
Assert.Equal("Path", day.Source);
|
||||||
|
Assert.Equal("IntRouteConstraint", day.ConstraintType);
|
||||||
|
|
||||||
|
var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year");
|
||||||
|
Assert.False(year.IsOptional);
|
||||||
|
Assert.Equal("Path", year.Source);
|
||||||
|
Assert.Equal("IntRouteConstraint", year.ConstraintType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplateStripsMultipleConstraints_InMultipleSegments()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/"
|
||||||
|
+ "MultipleParametersInMultipleSegments/12/01/1987";
|
||||||
|
|
||||||
|
var expectedRelativePath = "ApiExplorerRouteAndPathParametersInformation/"
|
||||||
|
+ "MultipleParametersInMultipleSegments/{month}/{day}/{year}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal(expectedRelativePath, description.RelativePath);
|
||||||
|
|
||||||
|
var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month");
|
||||||
|
Assert.False(month.IsOptional);
|
||||||
|
Assert.Equal("Path", month.Source);
|
||||||
|
Assert.Equal("RangeRouteConstraint", month.ConstraintType);
|
||||||
|
|
||||||
|
var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day");
|
||||||
|
Assert.False(day.IsOptional);
|
||||||
|
Assert.Equal("Path", day.Source);
|
||||||
|
Assert.Equal("IntRouteConstraint", day.ConstraintType);
|
||||||
|
|
||||||
|
var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year");
|
||||||
|
Assert.True(year.IsOptional);
|
||||||
|
Assert.Equal("Path", year.Source);
|
||||||
|
Assert.Equal("IntRouteConstraint", year.ConstraintType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_DescribeParameters_FromAllSources()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var url = "http://localhost/ApiExplorerRouteAndPathParametersInformation/MultipleTypesOfParameters/1/2/3";
|
||||||
|
|
||||||
|
var expectedRelativePath = "ApiExplorerRouteAndPathParametersInformation/"
|
||||||
|
+ "MultipleTypesOfParameters/{path}/{pathAndQuery}/{pathAndFromBody}";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync(url);
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal(expectedRelativePath, description.RelativePath);
|
||||||
|
|
||||||
|
var path = Assert.Single(description.ParameterDescriptions, p => p.Name == "path");
|
||||||
|
Assert.Equal("Path", path.Source);
|
||||||
|
|
||||||
|
var pathAndQuery = Assert.Single(description.ParameterDescriptions, p => p.Name == "pathAndQuery");
|
||||||
|
Assert.Equal("Path", pathAndQuery.Source);
|
||||||
|
|
||||||
|
Assert.Single(description.ParameterDescriptions, p => p.Name == "pathAndFromBody" && p.Source == "Body");
|
||||||
|
Assert.Single(description.ParameterDescriptions, p => p.Name == "pathAndFromBody" && p.Source == "Path");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiExplorer_RouteTemplate_MakesParametersOptional()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var server = TestServer.Create(_provider, _app);
|
||||||
|
var client = server.CreateClient();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await client.GetAsync("http://localhost/ApiExplorerRouteAndPathParametersInformation/Optional/");
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var description = Assert.Single(result);
|
||||||
|
Assert.Equal("ApiExplorerRouteAndPathParametersInformation/Optional/{id}", description.RelativePath);
|
||||||
|
|
||||||
|
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "id");
|
||||||
|
Assert.True(id.IsOptional);
|
||||||
|
Assert.Equal("Path", id.Source);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApiExplorer_HttpMethod_All()
|
public async Task ApiExplorer_HttpMethod_All()
|
||||||
{
|
{
|
||||||
|
|
@ -500,6 +748,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
|
||||||
public string Source { get; set; }
|
public string Source { get; set; }
|
||||||
|
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string ConstraintType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to serialize data between client and server
|
// Used to serialize data between client and server
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,8 @@ namespace ApiExplorer
|
||||||
IsOptional = parameter.IsOptional,
|
IsOptional = parameter.IsOptional,
|
||||||
Name = parameter.Name,
|
Name = parameter.Name,
|
||||||
Source = parameter.Source.ToString(),
|
Source = parameter.Source.ToString(),
|
||||||
Type = parameter.Type.FullName,
|
Type = parameter?.Type?.FullName,
|
||||||
|
ConstraintType = parameter?.Constraint?.GetType()?.Name,
|
||||||
};
|
};
|
||||||
|
|
||||||
data.ParameterDescriptions.Add(parameterData);
|
data.ParameterDescriptions.Add(parameterData);
|
||||||
|
|
@ -101,6 +102,8 @@ namespace ApiExplorer
|
||||||
public string Source { get; set; }
|
public string Source { get; set; }
|
||||||
|
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
public string ConstraintType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to serialize data between client and server
|
// Used to serialize data between client and server
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNet.Mvc;
|
||||||
|
|
||||||
|
namespace ApiExplorer
|
||||||
|
{
|
||||||
|
[Route("ApiExplorerRouteAndPathParametersInformation")]
|
||||||
|
public class ApiExplorerRouteAndPathParametersInformationController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public void Get() { }
|
||||||
|
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public void Get(int id) { }
|
||||||
|
|
||||||
|
[HttpGet("Optional/{id?}")]
|
||||||
|
public void GetOptional(int id = 0) { }
|
||||||
|
|
||||||
|
[HttpGet("Constraint/{integer:int}")]
|
||||||
|
public void GetInteger(int integer) { }
|
||||||
|
|
||||||
|
[HttpGet("CatchAll/{*parameter}")]
|
||||||
|
public void GetCatchAll(string parameter) { }
|
||||||
|
|
||||||
|
[HttpGet("MultipleParametersInSegment/{month:range(1,12)}-{day:int}-{year:int}")]
|
||||||
|
public void GetMultipleParametersInSegment(string month, string day, string year) { }
|
||||||
|
|
||||||
|
[HttpGet("MultipleParametersInMultipleSegments/{month:range(1,12)}/{day:int?}/{year:int?}")]
|
||||||
|
public void GetMultipleParametersInMultipleSegments(string month, string day, string year = "") { }
|
||||||
|
|
||||||
|
[HttpGet("MultipleTypesOfParameters/{path}/{pathAndQuery}/{pathAndFromBody}")]
|
||||||
|
public void MultipleTypesOfParameters(string query, string pathAndQuery, [FromBody] string pathAndFromBody) { }
|
||||||
|
|
||||||
|
[HttpGet("CatchAllAndConstraint/{*integer:int}")]
|
||||||
|
public void GetIntegers(string integer) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue