[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:
jacalvar 2014-10-07 15:17:11 -07:00
parent a633ef4f97
commit 3f54492930
9 changed files with 748 additions and 47 deletions

View File

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

View File

@ -13,7 +13,11 @@
<ul>
@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>
}
@ -22,7 +26,7 @@
{
<p>Response Formats:</p>
<ul>
@foreach(var response in Model.SupportedResponseFormats)
@foreach (var response in Model.SupportedResponseFormats)
{
<li>@response.MediaType.RawValue - @response.Formatter.GetType().Name</li>
}

View File

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

View File

@ -8,5 +8,6 @@ namespace Microsoft.AspNet.Mvc.Description
{
Body,
Query,
Path
}
}

View File

@ -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;
/// <summary>
/// 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>
public DefaultApiDescriptionProvider(
IOutputFormattersProvider formattersProvider,
IInlineConstraintResolver constraintResolver,
IModelMetadataProvider modelMetadataProvider)
{
_formattersProvider = formattersProvider;
_modelMetadataProvider = modelMetadataProvider;
_constraintResolver = constraintResolver;
}
/// <inheritdoc />
@ -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<TemplatePart>();
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<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)
{
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<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.
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<ApiResponseFormat> GetResponseFormats(
ControllerActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
@ -220,7 +365,7 @@ namespace Microsoft.AspNet.Mvc.Description
}
}
}
}
}
return results;
}

View File

@ -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> { 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]
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
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<ApiDescription> GetApiDescriptions(ActionDescriptor action, List<MockFormatter> formatters)
private IReadOnlyList<ApiDescription> GetApiDescriptions(
ActionDescriptor action,
List<MockFormatter> formatters)
{
var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action });
var formattersProvider = new Mock<IOutputFormattersProvider>(MockBehavior.Strict);
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);
modelMetadataProvider
.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);
});
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;
}

View File

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

View File

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

View File

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