diff --git a/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs b/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs
index fd64e2b472..538791b98d 100644
--- a/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs
+++ b/samples/MvcSample.Web/Controllers/ApiExplorerSamples/ProductsController.cs
@@ -9,7 +9,7 @@ namespace MvcSample.Web.ApiExplorerSamples
[Route("api/Products")]
public class ProductsController : Controller
{
- [HttpGet("{id}")]
+ [HttpGet("{id:int}")]
public Product GetById(int id)
{
return null;
@@ -22,7 +22,7 @@ namespace MvcSample.Web.ApiExplorerSamples
}
[Produces("application/json", Type = typeof(ProductOrderConfirmation))]
- [HttpPut("{id}/Buy")]
+ [HttpPut("{id:int}/Buy")]
public IActionResult Buy(int projectId, int quantity = 1)
{
return null;
diff --git a/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml b/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml
index b0e24064b4..f3e73a79b2 100644
--- a/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml
+++ b/samples/MvcSample.Web/Views/ApiExplorer/_ApiDescription.cshtml
@@ -13,7 +13,11 @@
@foreach (var parameter in Model.ParameterDescriptions)
{
- - @parameter.Name - @parameter.Type.FullName - @parameter.Source.ToString()
+ -
+ @parameter.Name - @(parameter?.Type?.FullName ?? "Unknown") - @parameter.Source.ToString()
+ - Constraint: @(parameter?.Constraint?.GetType()?.Name?.Replace("RouteConstraint", "") ?? " none")
+ - Default value: @(parameter?.DefaultValue ?? " none")
+
}
}
@@ -22,7 +26,7 @@
{
Response Formats:
- @foreach(var response in Model.SupportedResponseFormats)
+ @foreach (var response in Model.SupportedResponseFormats)
{
- @response.MediaType.RawValue - @response.Formatter.GetType().Name
}
diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs
index a9dba0415a..80a85b576a 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterDescription.cs
@@ -3,6 +3,7 @@
using System;
using Microsoft.AspNet.Mvc.ModelBinding;
+using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc.Description
{
@@ -18,6 +19,10 @@ namespace Microsoft.AspNet.Mvc.Description
public ApiParameterSource Source { get; set; }
+ public IRouteConstraint Constraint { get; set; }
+
+ public object DefaultValue { get; set; }
+
public Type Type { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs
index 81376af670..ce4121f295 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Description/ApiParameterSource.cs
@@ -8,5 +8,6 @@ namespace Microsoft.AspNet.Mvc.Description
{
Body,
Query,
+ Path
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs
index 8f53217742..ad8e03f8d1 100644
--- a/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs
+++ b/src/Microsoft.AspNet.Mvc.Core/Description/DefaultApiDescriptionProvider.cs
@@ -3,10 +3,13 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.Contracts;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
+using Microsoft.AspNet.Routing;
+using Microsoft.AspNet.Routing.Template;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Description
@@ -19,6 +22,7 @@ namespace Microsoft.AspNet.Mvc.Description
{
private readonly IOutputFormattersProvider _formattersProvider;
private readonly IModelMetadataProvider _modelMetadataProvider;
+ private readonly IInlineConstraintResolver _constraintResolver;
///
/// Creates a new instance of .
@@ -27,10 +31,12 @@ namespace Microsoft.AspNet.Mvc.Description
/// The .
public DefaultApiDescriptionProvider(
IOutputFormattersProvider formattersProvider,
+ IInlineConstraintResolver constraintResolver,
IModelMetadataProvider modelMetadataProvider)
{
_formattersProvider = formattersProvider;
_modelMetadataProvider = modelMetadataProvider;
+ _constraintResolver = constraintResolver;
}
///
@@ -60,25 +66,23 @@ namespace Microsoft.AspNet.Mvc.Description
}
private ApiDescription CreateApiDescription(
- ControllerActionDescriptor action,
- string httpMethod,
+ ControllerActionDescriptor action,
+ string httpMethod,
string groupName)
{
+ var parsedTemplate = ParseTemplate(action);
+
var apiDescription = new ApiDescription()
{
ActionDescriptor = action,
GroupName = groupName,
HttpMethod = httpMethod,
- RelativePath = GetRelativePath(action),
+ RelativePath = GetRelativePath(parsedTemplate),
};
- if (action.Parameters != null)
- {
- foreach (var parameter in action.Parameters)
- {
- apiDescription.ParameterDescriptions.Add(GetParameter(parameter));
- }
- }
+ var templateParameters = parsedTemplate?.Parameters?.ToList() ?? new List();
+
+ GetParameters(apiDescription, action.Parameters, templateParameters);
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
@@ -103,13 +107,13 @@ namespace Microsoft.AspNet.Mvc.Description
apiDescription.ResponseType = runtimeReturnType;
apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(
- modelAccessor: null,
+ modelAccessor: null,
modelType: runtimeReturnType);
var formats = GetResponseFormats(
- action,
- responseMetadataAttributes,
- declaredReturnType,
+ action,
+ responseMetadataAttributes,
+ declaredReturnType,
runtimeReturnType);
foreach (var format in formats)
@@ -121,6 +125,44 @@ namespace Microsoft.AspNet.Mvc.Description
return apiDescription;
}
+ private void GetParameters(
+ ApiDescription apiDescription,
+ IList parameterDescriptors,
+ IList 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 GetHttpMethods(ControllerActionDescriptor action)
{
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 &&
action.AttributeRouteInfo.Template != null)
{
- return action.AttributeRouteInfo.Template;
+ return TemplateParser.Parse(action.AttributeRouteInfo.Template, _constraintResolver);
}
return null;
}
- private ApiParameterDescription GetParameter(ParameterDescriptor parameter)
+ private string GetRelativePath(RouteTemplate parsedTemplate)
+ {
+ if (parsedTemplate == null)
+ {
+ return null;
+ }
+
+ var segments = new List();
+
+ 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.
- 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,
Name = parameter.Name,
@@ -158,8 +270,8 @@ namespace Microsoft.AspNet.Mvc.Description
if (parameter.ParameterBindingInfo != null)
{
- resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
resourceParameter.Source = ApiParameterSource.Query;
+ resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
}
if (parameter.BodyParameterInfo != null)
@@ -168,16 +280,49 @@ namespace Microsoft.AspNet.Mvc.Description
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(
- modelAccessor: null,
- modelType: resourceParameter.Type);
+ Source = ApiParameterSource.Path,
+ IsOptional = parameter.IsOptional && IsOptionalParameter(templateParameter),
+ Name = parameter.Name,
+ ParameterDescriptor = parameter,
+ Constraint = templateParameter.InlineConstraint,
+ DefaultValue = templateParameter.DefaultValue,
+ };
+
+ if (parameter.ParameterBindingInfo != null)
+ {
+ resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
}
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 GetResponseFormats(
ControllerActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
@@ -220,7 +365,7 @@ namespace Microsoft.AspNet.Mvc.Description
}
}
}
- }
+ }
return results;
}
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs
index c674364e84..514431a1e0 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Description/DefaultApiDescriptionProviderTest.cs
@@ -8,6 +8,8 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Routing;
+using Microsoft.AspNet.Routing;
+using Microsoft.AspNet.Routing.Constraints;
using Moq;
using Xunit;
@@ -151,25 +153,269 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Equal(typeof(string), username.Type);
}
- // This is a placeholder based on current functionality - see #885
- [Fact]
- public void GetApiDescription_PopluatesRelativePath()
+ [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, 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
var action = CreateActionDescriptor();
- action.AttributeRouteInfo = new AttributeRouteInfo();
- action.AttributeRouteInfo.Template = "api/Products/{id}";
+ action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
// Act
var descriptions = GetApiDescriptions(action);
// Assert
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 };
+
+ // 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 };
+
+ // 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 };
+
+ // 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]
- 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(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
var action = CreateActionDescriptor(nameof(ReturnsProduct));
@@ -184,7 +430,7 @@ namespace Microsoft.AspNet.Mvc.Description
}
[Fact]
- public void GetApiDescription_PopluatesResponseType_WithTaskOfProduct()
+ public void GetApiDescription_PopulatesResponseType_WithTaskOfProduct()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsTaskOfProduct));
@@ -205,7 +451,7 @@ namespace Microsoft.AspNet.Mvc.Description
[InlineData(nameof(ReturnsTaskOfObject))]
[InlineData(nameof(ReturnsTaskOfActionResult))]
[InlineData(nameof(ReturnsTaskOfJsonResult))]
- public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenUnknown(string methodName)
+ public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenUnknown(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
@@ -223,7 +469,7 @@ namespace Microsoft.AspNet.Mvc.Description
[Theory]
[InlineData(nameof(ReturnsVoid))]
[InlineData(nameof(ReturnsTask))]
- public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenVoid(string methodName)
+ public void GetApiDescription_DoesNotPopulatesResponseInformation_WhenVoid(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
@@ -247,7 +493,7 @@ namespace Microsoft.AspNet.Mvc.Description
[InlineData(nameof(ReturnsTask))]
[InlineData(nameof(ReturnsTaskOfActionResult))]
[InlineData(nameof(ReturnsTaskOfJsonResult))]
- public void GetApiDescription_PopluatesResponseInformation_WhenSetByFilter(string methodName)
+ public void GetApiDescription_PopulatesResponseInformation_WhenSetByFilter(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
@@ -323,7 +569,7 @@ namespace Microsoft.AspNet.Mvc.Description
action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action));
var formatters = CreateFormatters();
-
+
// This will just format Order
formatters[0].SupportedTypes.Add(typeof(Order));
@@ -349,13 +595,19 @@ namespace Microsoft.AspNet.Mvc.Description
return GetApiDescriptions(action, CreateFormatters());
}
- private IReadOnlyList GetApiDescriptions(ActionDescriptor action, List formatters)
+ private IReadOnlyList GetApiDescriptions(
+ ActionDescriptor action,
+ List formatters)
{
var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action });
var formattersProvider = new Mock(MockBehavior.Strict);
formattersProvider.Setup(fp => fp.OutputFormatters).Returns(formatters);
+ var constraintResolver = new Mock();
+ constraintResolver.Setup(c => c.ResolveConstraint("int"))
+ .Returns(new IntRouteConstraint());
+
var modelMetadataProvider = new Mock(MockBehavior.Strict);
modelMetadataProvider
.Setup(mmp => mmp.GetMetadataForType(null, It.IsAny()))
@@ -364,7 +616,11 @@ namespace Microsoft.AspNet.Mvc.Description
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, () => { });
return context.Results;
}
@@ -396,7 +652,6 @@ namespace Microsoft.AspNet.Mvc.Description
methodName ?? "ReturnsObject",
BindingFlags.Instance | BindingFlags.NonPublic);
-
return action;
}
diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs
index cf40035607..0c26d7d42a 100644
--- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs
+++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ApiExplorerTest.cs
@@ -3,12 +3,12 @@
using System;
using System.Collections.Generic;
+using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Xunit;
using Newtonsoft.Json;
-using System.Net.Http;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
@@ -139,6 +139,254 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
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>(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>(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>(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>(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>(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>(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>(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>(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>(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]
public async Task ApiExplorer_HttpMethod_All()
{
@@ -500,6 +748,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
public string Source { get; set; }
public string Type { get; set; }
+
+ public string ConstraintType { 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 49e28d566f..8c23c32e1e 100644
--- a/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
+++ b/test/WebSites/ApiExplorerWebSite/ApiExplorerDataFilter.cs
@@ -55,7 +55,8 @@ namespace ApiExplorer
IsOptional = parameter.IsOptional,
Name = parameter.Name,
Source = parameter.Source.ToString(),
- Type = parameter.Type.FullName,
+ Type = parameter?.Type?.FullName,
+ ConstraintType = parameter?.Constraint?.GetType()?.Name,
};
data.ParameterDescriptions.Add(parameterData);
@@ -101,6 +102,8 @@ namespace ApiExplorer
public string Source { get; set; }
public string Type { get; set; }
+
+ public string ConstraintType { get; set; }
}
// Used to serialize data between client and server
diff --git a/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerRouteAndPathParametersInformationController.cs b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerRouteAndPathParametersInformationController.cs
new file mode 100644
index 0000000000..16036e0b65
--- /dev/null
+++ b/test/WebSites/ApiExplorerWebSite/Controllers/ApiExplorerRouteAndPathParametersInformationController.cs
@@ -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) { }
+ }
+}
\ No newline at end of file