Merge branch 'release' into dev

This commit is contained in:
Ryan Nowak 2015-01-22 14:02:17 -08:00
commit 7fbe0ce307
29 changed files with 1981 additions and 339 deletions

View File

@ -12,13 +12,17 @@ namespace MvcSample.Web.ApiExplorerSamples
public class ProductsAdminController : Controller
{
[HttpPut]
public void AddProduct([FromBody] Product product)
[Produces("application/json", Type = typeof(Product))]
public IActionResult AddProduct([FromBody] Product product)
{
return null;
}
[HttpPost("{id}")]
public void UpdateProduct([FromBody] Product product)
[HttpPost("{id?}")]
[Produces("application/json", Type = typeof(Product))]
public IActionResult UpdateProduct(UpdateProductDTO dto)
{
return null;
}
[HttpPost("{id}/Stock")]

View File

@ -29,5 +29,12 @@ namespace MvcSample.Web.ApiExplorerSamples
{
return null;
}
[Produces("application/json", Type = typeof(ProductOrderConfirmation))]
[HttpPut("{order.acountId:int}/PlaceOrder")]
public IActionResult PlaceOrder(Order order)
{
return null;
}
}
}

View File

@ -0,0 +1,27 @@
// 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;
using System.Collections.Generic;
namespace MvcSample.Web.ApiExplorerSamples
{
public class Order
{
[FromRoute]
public int AccountId { get; set; }
[FromBody]
public List<OrderItem> Items { get; set; }
[FromQuery]
public bool? IncludeWarranty { get; set; }
public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
}
}

View File

@ -5,6 +5,8 @@ namespace MvcSample.Web.ApiExplorerSamples
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }

View File

@ -0,0 +1,14 @@
// 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.
namespace MvcSample.Web.ApiExplorerSamples
{
public class ProductChangeDTO
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
}

View File

@ -0,0 +1,20 @@
// 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 MvcSample.Web.ApiExplorerSamples
{
public class UpdateProductDTO
{
public int Id { get; set; }
[FromBody]
public Product Product { get; set; }
[FromHeader(Name = "Admin-User")]
public string AdminId { get; set; }
public string Comments { get; set; }
}
}

View File

@ -2,38 +2,58 @@
@model ApiDescription
<div class="api-description">
<h4>@(Model.HttpMethod ?? "*") - @(Model.RelativePath ?? "Unknown Url")</h4>
<h4>
<span title="@Model.ActionDescriptor.DisplayName">
@(Model.HttpMethod ?? "*") - @(Model.RelativePath ?? "Unknown Url")
</span>
</h4>
<hr />
<h5>For action: @Model.ActionDescriptor.DisplayName</h5>
<p>Return Type: @(Model.ResponseType?.FullName ?? "Unknown Type")</p>
<h5>Parameters:</h5>
@if (Model.ParameterDescriptions.Count > 0)
{
<p>Parameters:</p>
<ul>
@foreach (var parameter in Model.ParameterDescriptions)
{
<li>
@parameter.Name - @(parameter?.Type?.FullName ?? "Unknown") - @parameter.Source.ToString()
@if (parameter.Constraints != null && parameter.Constraints.Any())
{
var constraints = parameter.Constraints;
Write("-Constraint:" + string.Join(", ", constraints.Select(c => c.GetType()?.Name?.Replace("RouteConstraint", ""))));
}
- Default value: @(parameter?.DefaultValue ?? " none")
</li>
}
</ul>
<table>
<thead>
<tr>
<td>Name</td>
<td>Data Type</td>
<td>Source</td>
</tr>
</thead>
<tbody>
@foreach (var parameter in Model.ParameterDescriptions)
{
<tr>
<td>@parameter.Name</td>
<td>@(parameter.Type?.FullName ?? "Unknown Type")</td>
<td>@parameter.Source.Id</td>
</tr>
}
</tbody>
</table>
}
<h5>Response Formats</h5>
@if (Model.SupportedResponseFormats.Count > 0)
{
<p>Response Formats:</p>
<ul>
@foreach (var response in Model.SupportedResponseFormats)
{
<li>@response.MediaType.ToString() - @response.Formatter.GetType().Name</li>
}
</ul>
<table>
<thead>
<tr>
<td>Data Type</td>
<td>Media Type</td>
<td>Formatter</td>
</tr>
</thead>
<tbody>
@foreach (var response in Model.SupportedResponseFormats)
{
<tr>
<td>@Model.ResponseType.FullName</td>
<td>@response.MediaType.ToString()</td>
<td>@response.Formatter.GetType().Name</td>
</tr>
}
</tbody>
</table>
}
</div>

View File

@ -3,8 +3,30 @@
padding: 5px;
}
.api-description h5 {
margin-top: 20px;
}
.api-description hr {
border: 0px;
border-top: 1px solid #D44B4B;
margin: 0px;
margin-bottom: 10px;
}
.api-description table {
width: 100%;
}
.api-description thead {
border-bottom: 1px solid black;
}
.api-description td {
border-bottom: 1px solid #CCCCCC;
color: #666666;
padding: 5px;
}
.api-description tr:last-child td {
border-bottom: none;
}

View File

@ -290,6 +290,13 @@ namespace Microsoft.AspNet.Mvc
var apiExplorerIsVisible = action.ApiExplorer?.IsVisible ?? controller.ApiExplorer?.IsVisible ?? false;
if (apiExplorerIsVisible)
{
if (!IsAttributeRoutedAction(actionDescriptor))
{
// ApiExplorer is only supported on attribute routed actions.
throw new InvalidOperationException(Resources.FormatApiExplorer_UnsupportedAction(
actionDescriptor.DisplayName));
}
var apiExplorerActionData = new ApiDescriptionActionData()
{
GroupName = action.ApiExplorer?.GroupName ?? controller.ApiExplorer?.GroupName,

View File

@ -2,28 +2,38 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// A metadata description of an input to an API.
/// </summary>
public class ApiParameterDescription
{
public bool IsOptional { get; set; }
/// <summary>
/// Gets or sets the <see cref="ModelMetadata"/>.
/// </summary>
public ModelMetadata ModelMetadata { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
public ParameterDescriptor ParameterDescriptor { get; set; }
/// <summary>
/// Gets or sets the <see cref="ApiParameterRouteInfo"/>.
/// </summary>
public ApiParameterRouteInfo RouteInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="ApiParameterSource"/>.
/// </summary>
public ApiParameterSource Source { get; set; }
public IEnumerable<IRouteConstraint> Constraints { get; set; }
public object DefaultValue { get; set; }
/// <summary>
/// Gets or sets the parameter type.
/// </summary>
public Type Type { get; set; }
}
}

View File

@ -0,0 +1,41 @@
// 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 System.Collections.Generic;
using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// A metadata description of routing information for an <see cref="ApiParameterDescription"/>.
/// </summary>
public class ApiParameterRouteInfo
{
/// <summary>
/// Gets or sets the set of <see cref="IRouteConstraint"/> objects for the parameter.
/// </summary>
/// <remarks>
/// Route constraints are only applied when a value is bound from a URL's path. See
/// <see cref="ApiParameterDescription.Source"/> for the data source considered.
/// </remarks>
public IEnumerable<IRouteConstraint> Constraints { get; set; }
/// <summary>
/// Gets or sets the default value for the parameter.
/// </summary>
public object DefaultValue { get; set; }
/// <summary>
/// Gets a value indicating whether not a parameter is considered optional by routing.
/// </summary>
/// <remarks>
/// An optional parameter is considered optional by the routing system. This does not imply
/// that the parameter is considered optional by the action.
///
/// If the parameter uses <see cref="ApiParameterSource.ModelBinding"/> for the value of
/// <see cref="ApiParameterDescription.Source"/> then the value may also come from the
/// URL query string or form data.
/// </remarks>
public bool IsOptional { get; set; }
}
}

View File

@ -1,13 +1,130 @@
// 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 System;
using System.Diagnostics;
using Microsoft.AspNet.Mvc.Core;
namespace Microsoft.AspNet.Mvc.Description
{
// This is a placeholder - see #886
public enum ApiParameterSource
/// <summary>
/// A metadata description of the source of an <see cref="ApiParameterDescription"/> for an HTTP request.
/// </summary>
[DebuggerDisplay("Source: {DisplayName}")]
public class ApiParameterSource : IEquatable<ApiParameterSource>
{
Body,
Query,
Path
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request body.
/// </summary>
public static readonly ApiParameterSource Body = new ApiParameterSource(
"Body",
Resources.ApiParameterSource_Body);
/// <summary>
/// An <see cref="ApiParameterSource"/> for a custom model binder (unknown data source).
/// </summary>
public static readonly ApiParameterSource Custom = new ApiParameterSource(
"Custom",
Resources.ApiParameterSource_Custom);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request form-data.
/// </summary>
public static readonly ApiParameterSource Form = new ApiParameterSource(
"Form",
Resources.ApiParameterSource_Form);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request headers.
/// </summary>
public static readonly ApiParameterSource Header = new ApiParameterSource(
"Header",
Resources.ApiParameterSource_Header);
/// <summary>
/// An <see cref="ApiParameterSource"/> for a parameter that should be hidden. Used when
/// a parameter cannot be set with user input.
/// </summary>
public static readonly ApiParameterSource Hidden = new ApiParameterSource(
"Hidden",
Resources.ApiParameterSource_Hidden);
/// <summary>
/// An <see cref="ApiParameterSource"/> for model binding. Includes form-data, query-string
/// and headers from the request.
/// </summary>
public static readonly ApiParameterSource ModelBinding = new ApiParameterSource(
"ModelBinding",
Resources.ApiParameterSource_ModelBinding);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request url path.
/// </summary>
public static readonly ApiParameterSource Path = new ApiParameterSource(
"Path",
Resources.ApiParameterSource_Path);
/// <summary>
/// An <see cref="ApiParameterSource"/> for the request query-string.
/// </summary>
public static readonly ApiParameterSource Query = new ApiParameterSource(
"Query",
Resources.ApiParameterSource_Query);
/// <summary>
/// Creates a new <see cref="ApiParameterSource"/>.
/// </summary>
/// <param name="id">The id. Used for comparison.</param>
/// <param name="displayName"> The display name.</param>
public ApiParameterSource([NotNull] string id, string displayName)
{
Id = id;
DisplayName = displayName;
}
/// <summary>
/// Gets the display name.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Gets the id.
/// </summary>
public string Id { get; }
/// <inheritdoc />
public bool Equals(ApiParameterSource other)
{
return other == null ? false : string.Equals(other.Id, Id, StringComparison.Ordinal);
}
/// <inheritdoc />
public override bool Equals(object obj)
{
return Equals(obj as ApiParameterSource);
}
/// <inheritdoc />
public override int GetHashCode()
{
return Id.GetHashCode();
}
/// <inheritdoc />
public static bool operator ==(ApiParameterSource s1, ApiParameterSource s2)
{
if (object.ReferenceEquals(s1, null))
{
return object.ReferenceEquals(s2, null); ;
}
return s1.Equals(s2);
}
/// <inheritdoc />
public static bool operator !=(ApiParameterSource s1, ApiParameterSource s2)
{
return !(s1 == s2);
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -35,8 +34,8 @@ namespace Microsoft.AspNet.Mvc.Description
IModelMetadataProvider modelMetadataProvider)
{
_formattersProvider = formattersProvider;
_modelMetadataProvider = modelMetadataProvider;
_constraintResolver = constraintResolver;
_modelMetadataProvider = modelMetadataProvider;
}
/// <inheritdoc />
@ -81,7 +80,8 @@ namespace Microsoft.AspNet.Mvc.Description
var templateParameters = parsedTemplate?.Parameters?.ToList() ?? new List<TemplatePart>();
GetParameters(apiDescription, action.Parameters, templateParameters);
var parameterContext = new ApiParameterContext(_modelMetadataProvider, action, templateParameters);
apiDescription.ParameterDescriptions.AddRange(GetParameters(parameterContext));
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
@ -124,43 +124,90 @@ namespace Microsoft.AspNet.Mvc.Description
return apiDescription;
}
private void GetParameters(
ApiDescription apiDescription,
IList<ParameterDescriptor> parameterDescriptors,
IList<TemplatePart> templateParameters)
private IList<ApiParameterDescription> GetParameters(ApiParameterContext context)
{
if (parameterDescriptors != null)
// First, get parameters from the model-binding/parameter-binding side of the world.
if (context.ActionDescriptor.Parameters != null)
{
foreach (var parameter in parameterDescriptors)
foreach (var actionParameter in context.ActionDescriptor.Parameters)
{
// 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.BinderMetadata as IFormatterBinderMetadata == null)
{
templateParameter = templateParameters
.FirstOrDefault(p => p.Name.Equals(parameter.Name, StringComparison.OrdinalIgnoreCase));
var visitor = new PseudoModelBindingVisitor(context, actionParameter);
visitor.WalkParameter();
}
}
if (templateParameter != null)
for (var i = context.Results.Count - 1; i >= 0; i--)
{
// Remove any 'hidden' parameters. These are things that can't come from user input,
// so they aren't worth showing.
if (context.Results[i].Source == ApiParameterSource.Hidden)
{
context.Results.RemoveAt(i);
}
}
// Next, we want to join up any route parameters with those discovered from the action's parameters.
var routeParameters = new Dictionary<string, ApiParameterRouteInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var routeParameter in context.RouteParameters)
{
routeParameters.Add(routeParameter.Name, CreateRouteInfo(routeParameter));
}
foreach (var parameter in context.Results)
{
if (parameter.Source == ApiParameterSource.Path ||
parameter.Source == ApiParameterSource.ModelBinding ||
parameter.Source == ApiParameterSource.Custom)
{
ApiParameterRouteInfo routeInfo;
if (routeParameters.TryGetValue(parameter.Name, out routeInfo))
{
parameter.RouteInfo = routeInfo;
routeParameters.Remove(parameter.Name);
if (parameter.Source == ApiParameterSource.ModelBinding &&
!parameter.RouteInfo.IsOptional)
{
templateParameters.Remove(templateParameter);
// If we didn't see any information about the parameter, but we have
// a route parameter that matches, let's switch it to path.
parameter.Source = ApiParameterSource.Path;
}
}
apiDescription.ParameterDescriptions.Add(GetParameter(parameter, templateParameter));
}
}
if (templateParameters.Count > 0)
// Lastly, create a parameter representation for each route parameter that did not find
// a partner.
foreach (var routeParameter in routeParameters)
{
// Process parameters that only appear on the path template if any.
foreach (var templateParameter in templateParameters)
context.Results.Add(new ApiParameterDescription()
{
var parameterDescription =
GetParameter(parameterDescriptor: null, templateParameter: templateParameter);
apiDescription.ParameterDescriptions.Add(parameterDescription);
Name = routeParameter.Key,
RouteInfo = routeParameter.Value,
Source = ApiParameterSource.Path,
});
}
return context.Results;
}
private ApiParameterRouteInfo CreateRouteInfo(TemplatePart routeParameter)
{
var constraints = new List<IRouteConstraint>();
if (routeParameter.InlineConstraints != null)
{
foreach (var constraint in routeParameter.InlineConstraints)
{
constraints.Add(_constraintResolver.ResolveConstraint(constraint.Constraint));
}
}
return new ApiParameterRouteInfo()
{
Constraints = constraints,
DefaultValue = routeParameter.DefaultValue,
IsOptional = routeParameter.IsOptional || routeParameter.DefaultValue != null,
};
}
private IEnumerable<string> GetHttpMethods(ControllerActionDescriptor action)
@ -216,117 +263,6 @@ namespace Microsoft.AspNet.Mvc.Description
return string.Join("/", segments);
}
private ApiParameterDescription GetParameter(
ParameterDescriptor parameterDescriptor,
TemplatePart templateParameter)
{
// This is a placeholder based on currently available functionality for parameters. See #886.
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
Debug.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
{
Name = parameter.Name,
ParameterDescriptor = parameter,
Type = parameter.ParameterType,
};
if (parameter.BinderMetadata as IFormatterBinderMetadata != null)
{
resourceParameter.Source = ApiParameterSource.Body;
}
else
{
resourceParameter.Source = ApiParameterSource.Query;
}
return resourceParameter;
}
private ApiParameterDescription CreateParameterFromTemplateAndParameterDescriptor(
TemplatePart templateParameter,
ParameterDescriptor parameter)
{
var resourceParameter = new ApiParameterDescription
{
Source = ApiParameterSource.Path,
IsOptional = IsOptionalParameter(templateParameter),
Name = parameter.Name,
ParameterDescriptor = parameter,
Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints),
DefaultValue = templateParameter.DefaultValue,
Type = parameter.ParameterType,
};
return resourceParameter;
}
private static IEnumerable<IRouteConstraint> GetConstraints(
IInlineConstraintResolver constraintResolver,
IEnumerable<InlineConstraint> constraints)
{
return
constraints
.Select(c => constraintResolver.ResolveConstraint(c.Constraint))
.Where(c => c != null)
.ToArray();
}
private static bool IsOptionalParameter(TemplatePart templateParameter)
{
return templateParameter.IsOptional || templateParameter.DefaultValue != null;
}
private ApiParameterDescription CreateParameterFromTemplate(TemplatePart templateParameter)
{
return new ApiParameterDescription
{
Source = ApiParameterSource.Path,
IsOptional = IsOptionalParameter(templateParameter),
Name = templateParameter.Name,
ParameterDescriptor = null,
Constraints = GetConstraints(_constraintResolver, templateParameter.InlineConstraints),
DefaultValue = templateParameter.DefaultValue,
};
}
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
ControllerActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
@ -442,7 +378,7 @@ namespace Microsoft.AspNet.Mvc.Description
}
// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
// for a filter that implements IApiResponseMetadataProvider.
// while searching for a filter that implements IApiResponseMetadataProvider.
//
// The workaround for that is to implement the metadata interface on the IFilterFactory.
return action.FilterDescriptors
@ -450,5 +386,333 @@ namespace Microsoft.AspNet.Mvc.Description
.OfType<IApiResponseMetadataProvider>()
.ToArray();
}
private class ApiParameterContext
{
public ApiParameterContext(
IModelMetadataProvider metadataProvider,
ControllerActionDescriptor actionDescriptor,
IReadOnlyList<TemplatePart> routeParameters)
{
MetadataProvider = metadataProvider;
ActionDescriptor = actionDescriptor;
RouteParameters = routeParameters;
Results = new List<ApiParameterDescription>();
}
public ControllerActionDescriptor ActionDescriptor { get; }
public IModelMetadataProvider MetadataProvider { get; }
public IList<ApiParameterDescription> Results { get; }
public IReadOnlyList<TemplatePart> RouteParameters { get; }
}
private class PseudoModelBindingVisitor
{
public PseudoModelBindingVisitor(ApiParameterContext context, ParameterDescriptor parameter)
{
Context = context;
Parameter = parameter;
Visited = new HashSet<PropertyKey>();
}
public ApiParameterContext Context { get; }
public ParameterDescriptor Parameter { get; }
// Avoid infinite recursion by tracking properties.
private HashSet<PropertyKey> Visited { get; }
public void WalkParameter()
{
var modelMetadata = Context.MetadataProvider.GetMetadataForParameter(
modelAccessor: null,
methodInfo: Context.ActionDescriptor.MethodInfo,
parameterName: Parameter.Name);
var binderMetadata = Parameter.BinderMetadata;
if (binderMetadata != null)
{
modelMetadata.BinderMetadata = binderMetadata;
}
var nameProvider = binderMetadata as IModelNameProvider;
if (nameProvider != null && nameProvider.Name != null)
{
modelMetadata.BinderModelName = nameProvider.Name;
}
// Attempt to find a binding source for the parameter
//
// The default is ModelBinding (aka all default value providers)
var source = ApiParameterSource.ModelBinding;
if (!Visit(modelMetadata, source, containerName: string.Empty))
{
// If we get here, then it means we didn't find a match for any of the model. This means that it's
// likely 'model-bound' in the traditional MVC sense (formdata + query string + route data) and
// doesn't use any IBinderMetadata.
//
// Add a single 'default' parameter description for the model.
Context.Results.Add(CreateResult(modelMetadata, source, containerName: string.Empty));
}
}
/// <summary>
/// Visits a node in a model, and attempts to create <see cref="ApiParameterDescription"/> for any
/// model properties where we can definitely compute an answer.
/// </summary>
/// <param name="modelMetadata">The metadata for the model.</param>
/// <param name="ambientSource">The <see cref="ApiParameterSource"/> from the ambient context.</param>
/// <param name="containerName">The current name prefix (to prepend to property names).</param>
/// <returns>
/// <c>true</c> if the set of <see cref="ApiParameterDescription"/> objects were created for the model.
/// <c>false</c> if no <see cref="ApiParameterDescription"/> objects were created for the model.
/// </returns>
/// <remarks>
/// Its the reponsibility of this method to create a parameter description for ALL of the current model
/// or NONE of it. If a parameter description is created for ANY sub-properties of the model, then a parameter
/// description will be created for ALL of them.
/// </remarks>
private bool Visit(ModelMetadata modelMetadata, ApiParameterSource ambientSource, string containerName)
{
ApiParameterSource source;
if (GetSource(modelMetadata, out source))
{
// We have a definite answer for this model. This is a greedy source like
// [FromBody] so there's no need to consider properties.
Context.Results.Add(CreateResult(modelMetadata, source, containerName));
return true;
}
// For any property which is a leaf node, we don't want to keep traversing:
//
// 1) Collections - while it's possible to have binder attributes on the inside of a collection,
// it hardly seems useful, and would result in some very wierd binding.
//
// 2) Simple Types - These are generally part of the .net framework - primitives, or types which have a
// type converter from string.
//
// 3) Types with no properties. Obviously nothing to explore there.
//
if (modelMetadata.IsCollectionType ||
!modelMetadata.IsComplexType ||
!modelMetadata.Properties.Any())
{
if (source == null || source == ambientSource)
{
// If it's a leaf node, and we have no new source then we don't know how to bind this.
// Return without creating any parameters, so that this can be included in the parent model.
return false;
}
else
{
// We found a new source, and this model has no properties. This is probabaly
// a simple type with an attribute like [FromQuery].
Context.Results.Add(CreateResult(modelMetadata, source, containerName));
return true;
}
}
// This will come from composite model binding - so investigate what's going on with each property.
//
// Basically once we find something that we know how to bind, we want to treat all properties at that
// level (and higher levels) as separate parameters.
//
// Ex:
//
// public IActionResult PlaceOrder(OrderDTO order) {...}
//
// public class OrderDTO
// {
// public int AccountId { get; set; }
//
// [FromBody]
// public Order { get; set; }
// }
//
// This should result in two parameters:
//
// AccountId - source: Any
// Order - source: Body
//
var propertyCount = 0;
var unboundProperties = new HashSet<ModelMetadata>();
// We don't want to append the **parameter** name when building a model name.
var newContainerName = containerName;
if (modelMetadata.ContainerType != null)
{
newContainerName = GetName(containerName, modelMetadata);
}
foreach (var propertyMetadata in modelMetadata.Properties)
{
propertyCount++;
var key = new PropertyKey(propertyMetadata, source);
if (Visited.Add(key))
{
if (!Visit(propertyMetadata, source ?? ambientSource, newContainerName))
{
unboundProperties.Add(propertyMetadata);
}
}
else
{
unboundProperties.Add(propertyMetadata);
}
}
if (unboundProperties.Count == propertyCount)
{
if (source == null || source == ambientSource)
{
// No properties were bound and we didn't find a new source, let the caller handle it.
return false;
}
else
{
// We found a new source, and didn't create a result for any of the properties yet,
// so create a result for the current object.
Context.Results.Add(CreateResult(modelMetadata, source, containerName));
return true;
}
}
else
{
// This model was only partially bound, so create a result for all the other properties
foreach (var property in unboundProperties)
{
// Create a 'default' description for each property
Context.Results.Add(CreateResult(property, source ?? ambientSource, newContainerName));
}
return true;
}
}
private ApiParameterDescription CreateResult(
ModelMetadata metadata,
ApiParameterSource source,
string containerName)
{
return new ApiParameterDescription()
{
ModelMetadata = metadata,
Name = GetName(containerName, metadata),
Source = source,
Type = metadata.ModelType,
};
}
private static string GetName(string containerName, ModelMetadata metadata)
{
if (!string.IsNullOrEmpty(metadata.BinderModelName))
{
// Name was explicitly provided
return metadata.BinderModelName;
}
else
{
return ModelBindingHelper.CreatePropertyModelName(containerName, metadata.PropertyName);
}
}
// This isn't extensible right now.
//
// Returns true if the source is greedy (means to stop exploring the model)
// Returns false if the source in unknown or known but not greedy (like [FromQuery])
private static bool GetSource(ModelMetadata metadata, out ApiParameterSource source)
{
if (metadata.BinderMetadata == null)
{
// There's nothing we can figure out.
source = null;
return false;
}
if (metadata.BinderMetadata is IFormatterBinderMetadata)
{
source = ApiParameterSource.Body;
return true;
}
else if (metadata.BinderMetadata is IHeaderBinderMetadata)
{
source = ApiParameterSource.Header;
return true;
}
else if (metadata.BinderMetadata is IServiceActivatorBinderMetadata)
{
source = ApiParameterSource.Hidden;
return true;
}
else if (metadata.BinderMetadata is IRouteDataValueProviderMetadata)
{
source = ApiParameterSource.Path;
return false;
}
else if (metadata.BinderMetadata is IQueryValueProviderMetadata)
{
source = ApiParameterSource.Query;
return false;
}
else if (metadata.BinderMetadata is IFormDataValueProviderMetadata)
{
source = ApiParameterSource.Form;
return false;
}
var binderTypeMetadata = metadata.BinderMetadata as IBinderTypeProviderMetadata;
if (binderTypeMetadata != null && binderTypeMetadata.BinderType != null)
{
// This provides it's own model binder, so we can't really make a good
// estimate of where it comes from.
source = ApiParameterSource.Custom;
return true;
}
// We're out of cases we know how to handle.
source = null;
return false;
}
private struct PropertyKey
{
public readonly Type ContainerType;
public readonly string PropertyName;
public readonly ApiParameterSource Source;
public PropertyKey(ModelMetadata metadata, ApiParameterSource source)
{
ContainerType = metadata.ContainerType;
PropertyName = metadata.PropertyName;
Source = source;
}
}
private class PropertyKeyEqualityComparer : IEqualityComparer<PropertyKey>
{
public bool Equals(PropertyKey x, PropertyKey y)
{
return
x.ContainerType == y.ContainerType &&
x.PropertyName == y.PropertyName &&
x.Source == y.Source;
}
public int GetHashCode(PropertyKey obj)
{
return obj.ContainerType.GetHashCode() ^ obj.PropertyName.GetHashCode() ^ obj.Source.GetHashCode();
}
}
}
}
}

View File

@ -246,7 +246,7 @@ namespace Microsoft.AspNet.Mvc
StringComparison.OrdinalIgnoreCase);
}
private static string CreatePropertyModelName(string prefix, string propertyName)
public static string CreatePropertyModelName(string prefix, string propertyName)
{
if (string.IsNullOrEmpty(prefix))
{

View File

@ -1562,6 +1562,150 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("ResponseCache_SpecifyDuration"), p0, p1);
}
/// <summary>
/// Body
/// </summary>
internal static string ApiParameterSource_Body
{
get { return GetString("ApiParameterSource_Body"); }
}
/// <summary>
/// Body
/// </summary>
internal static string FormatApiParameterSource_Body()
{
return GetString("ApiParameterSource_Body");
}
/// <summary>
/// Custom
/// </summary>
internal static string ApiParameterSource_Custom
{
get { return GetString("ApiParameterSource_Custom"); }
}
/// <summary>
/// Custom
/// </summary>
internal static string FormatApiParameterSource_Custom()
{
return GetString("ApiParameterSource_Custom");
}
/// <summary>
/// Header
/// </summary>
internal static string ApiParameterSource_Header
{
get { return GetString("ApiParameterSource_Header"); }
}
/// <summary>
/// Header
/// </summary>
internal static string FormatApiParameterSource_Header()
{
return GetString("ApiParameterSource_Header");
}
/// <summary>
/// Hidden
/// </summary>
internal static string ApiParameterSource_Hidden
{
get { return GetString("ApiParameterSource_Hidden"); }
}
/// <summary>
/// Hidden
/// </summary>
internal static string FormatApiParameterSource_Hidden()
{
return GetString("ApiParameterSource_Hidden");
}
/// <summary>
/// ModelBinding
/// </summary>
internal static string ApiParameterSource_ModelBinding
{
get { return GetString("ApiParameterSource_ModelBinding"); }
}
/// <summary>
/// ModelBinding
/// </summary>
internal static string FormatApiParameterSource_ModelBinding()
{
return GetString("ApiParameterSource_ModelBinding");
}
/// <summary>
/// Path
/// </summary>
internal static string ApiParameterSource_Path
{
get { return GetString("ApiParameterSource_Path"); }
}
/// <summary>
/// Path
/// </summary>
internal static string FormatApiParameterSource_Path()
{
return GetString("ApiParameterSource_Path");
}
/// <summary>
/// Query
/// </summary>
internal static string ApiParameterSource_Query
{
get { return GetString("ApiParameterSource_Query"); }
}
/// <summary>
/// Query
/// </summary>
internal static string FormatApiParameterSource_Query()
{
return GetString("ApiParameterSource_Query");
}
/// <summary>
/// Form
/// </summary>
internal static string ApiParameterSource_Form
{
get { return GetString("ApiParameterSource_Form"); }
}
/// <summary>
/// Form
/// </summary>
internal static string FormatApiParameterSource_Form()
{
return GetString("ApiParameterSource_Form");
}
/// <summary>
/// The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.
/// </summary>
internal static string ApiExplorer_UnsupportedAction
{
get { return GetString("ApiExplorer_UnsupportedAction"); }
}
/// <summary>
/// The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.
/// </summary>
internal static string FormatApiExplorer_UnsupportedAction(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ApiExplorer_UnsupportedAction"), p0);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -418,4 +418,31 @@
<data name="ResponseCache_SpecifyDuration" xml:space="preserve">
<value>If the '{0}' property is not set to true, '{1}' property must be specified.</value>
</data>
<data name="ApiParameterSource_Body" xml:space="preserve">
<value>Body</value>
</data>
<data name="ApiParameterSource_Custom" xml:space="preserve">
<value>Custom</value>
</data>
<data name="ApiParameterSource_Header" xml:space="preserve">
<value>Header</value>
</data>
<data name="ApiParameterSource_Hidden" xml:space="preserve">
<value>Hidden</value>
</data>
<data name="ApiParameterSource_ModelBinding" xml:space="preserve">
<value>ModelBinding</value>
</data>
<data name="ApiParameterSource_Path" xml:space="preserve">
<value>Path</value>
</data>
<data name="ApiParameterSource_Query" xml:space="preserve">
<value>Query</value>
</data>
<data name="ApiParameterSource_Form" xml:space="preserve">
<value>Form</value>
</data>
<data name="ApiExplorer_UnsupportedAction" xml:space="preserve">
<value>The action '{0}' has ApiExplorer enabled, but is using conventional routing. Only actions which use attribute routing support ApiExplorer.</value>
</data>
</root>

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
@ -24,5 +26,18 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string));
}
internal static bool IsCollectionType(Type type)
{
if (type == typeof(string))
{
// Even though string implements IEnumerable, we don't really think of it
// as a collection for the purposes of model binding.
return false;
}
// We only need to look for IEnumerable, because IEnumerable<T> extends it.
return typeof(IEnumerable).IsAssignableFrom(type);
}
}
}

View File

@ -137,6 +137,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </remarks>
public virtual bool HideSurroundingHtml { get; set; }
public virtual bool IsCollectionType
{
get { return TypeHelper.IsCollectionType(ModelType); }
}
public virtual bool IsComplexType
{
get { return !TypeHelper.HasStringConverter(ModelType); }

View File

@ -1092,6 +1092,24 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal("Store", action.GetProperty<ApiDescriptionActionData>().GroupName);
}
[Theory]
[InlineData("A", typeof(ApiExplorerEnabledConventionalRoutedController))]
[InlineData("A", typeof(ApiExplorerEnabledActionConventionalRoutedController))]
public void ApiExplorer_ThrowsForContentionalRouting(string actionName, Type type)
{
var expected = string.Format(
"The action '{0}.{1}' has ApiExplorer enabled, but is using conventional routing. " +
"Only actions which use attribute routing support ApiExplorer.",
type.FullName, actionName);
// Arrange
var provider = GetProvider(type.GetTypeInfo());
// Act & Assert
var ex = Assert.Throws<InvalidOperationException>(() => provider.GetDescriptors());
Assert.Equal(expected, ex.Message);
}
// Verifies the sequence of conventions running
[Fact]
public void ApplyConventions_RunsInOrderOfDecreasingScope()
@ -1743,29 +1761,34 @@ namespace Microsoft.AspNet.Mvc.Test
public int Name { get; set; }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
private class ApiExplorerNotVisibleController
{
public void Edit() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
[ApiExplorerSettings()]
private class ApiExplorerVisibleController
{
public void Edit() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
[ApiExplorerSettings(IgnoreApi = true)]
private class ApiExplorerExplicitlyNotVisibleController
{
public void Edit() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
private class ApiExplorerExplicitlyNotVisibleOnActionController
{
[ApiExplorerSettings(IgnoreApi = true)]
public void Edit() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
[ApiExplorerSettings(IgnoreApi = true)]
private class ApiExplorerVisibilityOverrideController
{
@ -1775,25 +1798,28 @@ namespace Microsoft.AspNet.Mvc.Test
public void Create() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
[ApiExplorerSettings(GroupName = "Store")]
private class ApiExplorerNameOnControllerController
{
public void Edit() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
private class ApiExplorerNameOnActionController
{
[ApiExplorerSettings(GroupName = "Blog")]
public void Edit() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
[ApiExplorerSettings()]
private class ApiExplorerNoNameController
{
public void Edit() { }
}
[Route("AttributeRouting/IsRequired/ForApiExplorer")]
[ApiExplorerSettings(GroupName = "Store")]
private class ApiExplorerNameOverrideController
{
@ -1851,5 +1877,22 @@ namespace Microsoft.AspNet.Mvc.Test
private class ConstraintAttribute : Attribute, IActionConstraintMetadata
{
}
[ApiExplorerSettings(GroupName = "Default")]
private class ApiExplorerEnabledConventionalRoutedController : Controller
{
public void A()
{
}
}
[ApiExplorerSettings(IgnoreApi = true)]
private class ApiExplorerEnabledActionConventionalRoutedController : Controller
{
[ApiExplorerSettings(GroupName = "Default")]
public void A()
{
}
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -110,49 +111,6 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Single(descriptions, d => d.HttpMethod == "GET");
}
// This is a test for the placeholder behavior - see #886
[Fact]
public void GetApiDescription_PopulatesParameters()
{
// Arrange
var action = CreateActionDescriptor();
action.Parameters = new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "id",
ParameterType = typeof(int),
},
new ParameterDescriptor()
{
BinderMetadata = new FromBodyAttribute(),
Name = "username",
ParameterType = typeof(string),
}
};
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(2, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "id");
Assert.NotNull(id.ModelMetadata);
Assert.False(id.IsOptional);
Assert.Same(action.Parameters[0], id.ParameterDescriptor);
Assert.Equal(ApiParameterSource.Query, id.Source);
Assert.Equal(typeof(int), id.Type);
var username = Assert.Single(description.ParameterDescriptions, p => p.Name == "username");
Assert.NotNull(username.ModelMetadata);
Assert.False(username.IsOptional);
Assert.Same(action.Parameters[1], username.ParameterDescriptor);
Assert.Equal(ApiParameterSource.Body, username.Source);
Assert.Equal(typeof(string), username.Type);
}
[Theory]
[InlineData("api/products/{id}", false, null, null)]
[InlineData("api/products/{id?}", true, null, null)]
@ -181,22 +139,21 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal(ApiParameterSource.Path, parameter.Source);
Assert.Equal(isOptional, parameter.IsOptional);
Assert.Equal(isOptional, parameter.RouteInfo.IsOptional);
Assert.Equal("id", parameter.Name);
Assert.Null(parameter.ParameterDescriptor);
if (constraintType != null)
{
Assert.IsType(constraintType, Assert.Single(parameter.Constraints));
Assert.IsType(constraintType, Assert.Single(parameter.RouteInfo.Constraints));
}
if (defaultValue != null)
{
Assert.Equal(defaultValue, parameter.DefaultValue);
Assert.Equal(defaultValue, parameter.RouteInfo.DefaultValue);
}
else
{
Assert.Null(parameter.DefaultValue);
Assert.Null(parameter.RouteInfo.DefaultValue);
}
}
@ -217,15 +174,10 @@ namespace Microsoft.AspNet.Mvc.Description
object defaultValue)
{
// Arrange
var action = CreateActionDescriptor();
var action = CreateActionDescriptor(nameof(FromRouting));
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var parameterDescriptor = new ParameterDescriptor
{
Name = "id",
ParameterType = typeof(int),
};
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
var parameterDescriptor = action.Parameters[0];
// Act
var descriptions = GetApiDescriptions(action);
@ -235,82 +187,76 @@ namespace Microsoft.AspNet.Mvc.Description
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal(ApiParameterSource.Path, parameter.Source);
Assert.Equal(isOptional, parameter.IsOptional);
Assert.Equal(isOptional, parameter.RouteInfo.IsOptional);
Assert.Equal("id", parameter.Name);
Assert.Equal(parameterDescriptor, parameter.ParameterDescriptor);
if (constraintType != null)
{
Assert.IsType(constraintType, Assert.Single(parameter.Constraints));
Assert.IsType(constraintType, Assert.Single(parameter.RouteInfo.Constraints));
}
if (defaultValue != null)
{
Assert.Equal(defaultValue, parameter.DefaultValue);
Assert.Equal(defaultValue, parameter.RouteInfo.DefaultValue);
}
else
{
Assert.Null(parameter.DefaultValue);
Assert.Null(parameter.RouteInfo.DefaultValue);
}
}
// Only a parameter which comes from a route or model binding or unknown should
// include route info.
[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(
[InlineData("api/products/{id}", nameof(FromBody), "Body")]
[InlineData("api/products/{id}", nameof(FromHeader), "Header")]
public void GetApiDescription_ParameterDescription_DoesNotIncludeRouteInfo(
string template,
bool isOptional,
Type constraintType,
object defaultValue)
string methodName,
string source)
{
// Arrange
var action = CreateActionDescriptor();
var action = CreateActionDescriptor(methodName);
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var parameterDescriptor = new ParameterDescriptor
{
BinderMetadata = new FromBodyAttribute(),
Name = "id",
ParameterType = typeof(int),
};
action.Parameters = new List<ParameterDescriptor> { parameterDescriptor };
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameters = description.ParameterDescriptions;
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 id = Assert.Single(parameters, p => p.Source == new ApiParameterSource(source, displayName: null));
Assert.Null(id.RouteInfo);
}
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);
// Only a parameter which comes from a route or model binding or unknown should
// include route info. If the source is model binding, we also check if it's an optional
// parameter, and only change the source if it's a match.
[Theory]
[InlineData("api/products/{id}", nameof(FromRouting), "Path")]
[InlineData("api/products/{id}", nameof(FromModelBinding), "Path")]
[InlineData("api/products/{id?}", nameof(FromModelBinding), "ModelBinding")]
[InlineData("api/products/{id=5}", nameof(FromModelBinding), "ModelBinding")]
[InlineData("api/products/{id}", nameof(FromCustom), "Custom")]
public void GetApiDescription_ParameterDescription_IncludesRouteInfo(
string template,
string methodName,
string source)
{
// Arrange
var action = CreateActionDescriptor(methodName);
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
if (constraintType != null)
{
Assert.IsType(constraintType, Assert.Single(pathParameter.Constraints));
}
// Act
var descriptions = GetApiDescriptions(action);
if (defaultValue != null)
{
Assert.Equal(defaultValue, pathParameter.DefaultValue);
}
else
{
Assert.Null(pathParameter.DefaultValue);
}
// Assert
var description = Assert.Single(descriptions);
var parameters = description.ParameterDescriptions;
var id = Assert.Single(parameters, p => p.Source == new ApiParameterSource(source, displayName: null));
Assert.NotNull(id.RouteInfo);
}
[Theory]
@ -322,23 +268,16 @@ namespace Microsoft.AspNet.Mvc.Description
bool expectedOptional)
{
// Arrange
var action = CreateActionDescriptor();
var action = CreateActionDescriptor(nameof(FromRouting));
action.AttributeRouteInfo = new AttributeRouteInfo { Template = template };
var parameterDescriptor = new ParameterDescriptor
{
Name = "id",
ParameterType = 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);
Assert.Equal(expectedOptional, parameter.RouteInfo.IsOptional);
}
[Theory]
@ -380,11 +319,11 @@ namespace Microsoft.AspNet.Mvc.Description
var description = Assert.Single(descriptions);
var id1 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id1");
Assert.Equal(ApiParameterSource.Path, id1.Source);
Assert.Empty(id1.Constraints);
Assert.Empty(id1.RouteInfo.Constraints);
var id2 = Assert.Single(description.ParameterDescriptions, p => p.Name == "id2");
Assert.Equal(ApiParameterSource.Path, id2.Source);
Assert.IsType<IntRouteConstraint>(Assert.Single(id2.Constraints));
Assert.IsType<IntRouteConstraint>(Assert.Single(id2.RouteInfo.Constraints));
}
[Fact]
@ -584,6 +523,378 @@ namespace Microsoft.AspNet.Mvc.Description
Assert.Same(formatters[0], formats[0].Formatter);
}
[Fact]
public void GetApiDescription_ParameterDescription_ModelBoundParameter()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProduct));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.ModelBinding, parameter.Source);
}
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromRouteData()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsId_Route));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("id", parameter.Name);
Assert.Same(ApiParameterSource.Path, parameter.Source);
}
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromQueryString()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsId_Query));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("id", parameter.Name);
Assert.Same(ApiParameterSource.Query, parameter.Source);
}
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromBody()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProduct_Body));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.Body, parameter.Source);
}
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromForm()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProduct_Form));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.Form, parameter.Source);
}
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromHeader()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsId_Header));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("id", parameter.Name);
Assert.Same(ApiParameterSource.Header, parameter.Source);
}
// 'Hidden' parameters are hidden (not returned).
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromServices()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsFormatters_Services));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Empty(description.ParameterDescriptions);
}
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromCustomModelBinder()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProduct_Custom));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.Custom, parameter.Source);
}
[Fact]
public void GetApiDescription_ParameterDescription_SourceFromDefault_ModelBinderAttribute_WithoutBinderType()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProduct_Default));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("product", parameter.Name);
Assert.Same(ApiParameterSource.ModelBinding, parameter.Source);
}
[Fact]
public void GetApiDescription_ParameterDescription_ComplexDTO()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProductChangeDTO));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(4, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var product = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product");
Assert.Same(ApiParameterSource.Body, product.Source);
Assert.Equal(typeof(Product), product.Type);
var userId = Assert.Single(description.ParameterDescriptions, p => p.Name == "UserId");
Assert.Same(ApiParameterSource.Header, userId.Source);
Assert.Equal(typeof(string), userId.Type);
var comments = Assert.Single(description.ParameterDescriptions, p => p.Name == "Comments");
Assert.Same(ApiParameterSource.ModelBinding, comments.Source);
Assert.Equal(typeof(string), comments.Type);
}
// The method under test uses an attribute on the parameter to set a 'default' source
[Fact]
public void GetApiDescription_ParameterDescription_ComplexDTO_AmbientValueProviderMetadata()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsProductChangeDTO_Query));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(4, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var product = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product");
Assert.Same(ApiParameterSource.Body, product.Source);
Assert.Equal(typeof(Product), product.Type);
var userId = Assert.Single(description.ParameterDescriptions, p => p.Name == "UserId");
Assert.Same(ApiParameterSource.Header, userId.Source);
Assert.Equal(typeof(string), userId.Type);
var comments = Assert.Single(description.ParameterDescriptions, p => p.Name == "Comments");
Assert.Same(ApiParameterSource.Query, comments.Source);
Assert.Equal(typeof(string), comments.Type);
}
[Fact]
public void GetApiDescription_ParameterDescription_ComplexDTO_AnotherLevel()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsOrderDTO));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(4, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var quantity = Assert.Single(description.ParameterDescriptions, p => p.Name == "Quantity");
Assert.Same(ApiParameterSource.ModelBinding, quantity.Source);
Assert.Equal(typeof(int), quantity.Type);
var productId = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product.Id");
Assert.Same(ApiParameterSource.ModelBinding, productId.Source);
Assert.Equal(typeof(int), productId.Type);
var price = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product.Price");
Assert.Same(ApiParameterSource.Query, price.Source);
Assert.Equal(typeof(decimal), price.Type);
}
// The method under test uses an attribute on the parameter to set a 'default' source
[Fact]
public void GetApiDescription_ParameterDescription_ComplexDTO_AnotherLevel_AmbientValueProviderMetadata()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsOrderDTO_Query));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(3, description.ParameterDescriptions.Count);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Path, id.Source);
Assert.Equal(typeof(int), id.Type);
var quantity = Assert.Single(description.ParameterDescriptions, p => p.Name == "Quantity");
Assert.Same(ApiParameterSource.Query, quantity.Source);
Assert.Equal(typeof(int), quantity.Type);
var product = Assert.Single(description.ParameterDescriptions, p => p.Name == "Product");
Assert.Same(ApiParameterSource.Query, product.Source);
Assert.Equal(typeof(OrderProductDTO), product.Type);
}
[Fact]
public void GetApiDescription_ParameterDescription_BreaksCycles()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsCycle));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var c = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.Query, c.Source);
Assert.Equal("C.C", c.Name);
Assert.Equal(typeof(Cycle1), c.Type);
}
[Fact]
public void GetApiDescription_ParameterDescription_DTOWithCollection()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsHasCollection));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var products = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.Query, products.Source);
Assert.Equal("Products", products.Name);
Assert.Equal(typeof(Product[]), products.Type);
}
// If a property/parameter is a collection, we automatically treat it as a leaf-node.
[Fact]
public void GetApiDescription_ParameterDescription_DTOWithCollection_ElementsWithBinderMetadataIgnored()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsHasCollection_Complex));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var c = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.ModelBinding, c.Source);
Assert.Equal("c", c.Name);
Assert.Equal(typeof(HasCollection_Complex), c.Type);
}
[Fact]
public void GetApiDescription_ParameterDescription_RedundentMetadataMergedWithParent()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsRedundentMetadata));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var r = Assert.Single(description.ParameterDescriptions);
Assert.Same(ApiParameterSource.Query, r.Source);
Assert.Equal("r", r.Name);
Assert.Equal(typeof(RedundentMetadata), r.Type);
}
[Fact]
public void GetApiDescription_ParameterDescription_RedundentMetadata_WithParameterMetadata()
{
// Arrange
var action = CreateActionDescriptor(nameof(AcceptsPerson));
var parameterDescriptor = action.Parameters.Single();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
var name = Assert.Single(description.ParameterDescriptions, p => p.Name == "Name");
Assert.Same(ApiParameterSource.Header, name.Source);
Assert.Equal(typeof(string), name.Type);
var id = Assert.Single(description.ParameterDescriptions, p => p.Name == "Id");
Assert.Same(ApiParameterSource.Form, id.Source);
Assert.Equal(typeof(int), id.Type);
}
private IReadOnlyList<ApiDescription> GetApiDescriptions(ActionDescriptor action)
{
return GetApiDescriptions(action, CreateFormatters());
@ -602,18 +913,12 @@ namespace Microsoft.AspNet.Mvc.Description
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>()))
.Returns((Func<object> accessor, Type type) =>
{
return new ModelMetadata(modelMetadataProvider.Object, null, accessor, type, null);
});
var modelMetadataProvider = new DataAnnotationsModelMetadataProvider();
var provider = new DefaultApiDescriptionProvider(
formattersProvider.Object,
constraintResolver.Object,
modelMetadataProvider.Object);
modelMetadataProvider);
provider.Invoke(context, () => { });
return context.Results;
@ -646,6 +951,18 @@ namespace Microsoft.AspNet.Mvc.Description
methodName ?? "ReturnsObject",
BindingFlags.Instance | BindingFlags.NonPublic);
action.Parameters = new List<ParameterDescriptor>();
foreach (var parameter in action.MethodInfo.GetParameters())
{
action.Parameters.Add(new ParameterDescriptor()
{
BinderMetadata = parameter.GetCustomAttributes().OfType<IBinderMetadata>().FirstOrDefault(),
Name = parameter.Name,
ParameterType = parameter.ParameterType,
});
}
return action;
}
@ -699,12 +1016,198 @@ namespace Microsoft.AspNet.Mvc.Description
return null;
}
private void AcceptsProduct(Product product)
{
}
private void AcceptsProduct_Body([FromBody] Product product)
{
}
private void AcceptsProduct_Form([FromForm] Product product)
{
}
// This will show up as source = model binding
private void AcceptsProduct_Default([ModelBinder] Product product)
{
}
// This will show up as source = unknown
private void AcceptsProduct_Custom([ModelBinder(BinderType = typeof(BodyModelBinder))] Product product)
{
}
private void AcceptsId_Route([FromRoute] int id)
{
}
private void AcceptsId_Query([FromQuery] int id)
{
}
private void AcceptsId_Header([FromHeader] int id)
{
}
private void AcceptsFormatters_Services([FromServices] IOutputFormattersProvider formatters)
{
}
private void AcceptsProductChangeDTO(ProductChangeDTO dto)
{
}
private void AcceptsProductChangeDTO_Query([FromQuery] ProductChangeDTO dto)
{
}
private void AcceptsOrderDTO(OrderDTO dto)
{
}
private void AcceptsOrderDTO_Query([FromQuery] OrderDTO dto)
{
}
private void AcceptsCycle(Cycle1 c)
{
}
private void AcceptsHasCollection(HasCollection c)
{
}
private void AcceptsHasCollection_Complex(HasCollection_Complex c)
{
}
private void AcceptsRedundentMetadata([FromQuery] RedundentMetadata r)
{
}
private void AcceptsPerson([FromForm] Person person)
{
}
private void FromRouting([FromRoute] int id)
{
}
private void FromModelBinding(int id)
{
}
private void FromCustom([ModelBinder(BinderType = typeof(BodyModelBinder))] int id)
{
}
private void FromHeader([FromHeader] int id)
{
}
private void FromBody([FromBody] int id)
{
}
private class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
private class Order
{
public int OrderId { get; set; }
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
private class ProductChangeDTO
{
[FromRoute]
public int Id { get; set; }
[FromBody]
public Product Product { get; set; }
[FromHeader]
public string UserId { get; set; }
public string Comments { get; set; }
}
private class OrderDTO
{
[FromRoute]
public int Id { get; set; }
public int Quantity { get; set; }
public OrderProductDTO Product { get; set; }
}
private class OrderProductDTO
{
public int Id { get; set; }
[FromQuery]
public decimal Price { get; set; }
}
private class Cycle1
{
public Cycle2 C { get; set; }
}
private class Cycle2
{
[FromQuery]
public Cycle1 C { get; set; }
}
private class HasCollection
{
[FromQuery]
public Product[] Products { get; set; }
}
private class HasCollection_Complex
{
public Child[] Items { get; set; }
}
private class Child
{
[FromQuery]
public int Id { get; set; }
public string Name { get; set; }
}
private class RedundentMetadata
{
[FromQuery]
public int Id { get; set; }
[FromQuery]
public string Name { get; set; }
}
public class Person
{
[FromHeader(Name = "Name")]
public string Name { get; set; }
[FromForm]
public int Id { get; set; }
}
private class MockFormatter : OutputFormatter

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.TestHost;
using Newtonsoft.Json;
using Xunit;
@ -176,9 +177,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("id", parameter.Name);
Assert.False(parameter.IsOptional);
Assert.False(parameter.RouteInfo.IsOptional);
Assert.Equal("Path", parameter.Source);
Assert.Empty(parameter.ConstraintTypes);
Assert.Empty(parameter.RouteInfo.ConstraintTypes);
}
[Fact]
@ -201,9 +202,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("integer", parameter.Name);
Assert.False(parameter.IsOptional);
Assert.False(parameter.RouteInfo.IsOptional);
Assert.Equal("Path", parameter.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(parameter.ConstraintTypes));
Assert.Equal("IntRouteConstraint", Assert.Single(parameter.RouteInfo.ConstraintTypes));
}
[Fact]
@ -226,7 +227,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("parameter", parameter.Name);
Assert.False(parameter.IsOptional);
Assert.False(parameter.RouteInfo.IsOptional);
Assert.Equal("Path", parameter.Source);
}
@ -252,9 +253,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var parameter = Assert.Single(description.ParameterDescriptions);
Assert.Equal("integer", parameter.Name);
Assert.False(parameter.IsOptional);
Assert.False(parameter.RouteInfo.IsOptional);
Assert.Equal("Path", parameter.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(parameter.ConstraintTypes));
Assert.Equal("IntRouteConstraint", Assert.Single(parameter.RouteInfo.ConstraintTypes));
}
[Fact]
@ -281,19 +282,19 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expectedRelativePath, description.RelativePath);
var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month");
Assert.False(month.IsOptional);
Assert.False(month.RouteInfo.IsOptional);
Assert.Equal("Path", month.Source);
Assert.Equal("RangeRouteConstraint", Assert.Single(month.ConstraintTypes));
Assert.Equal("RangeRouteConstraint", Assert.Single(month.RouteInfo.ConstraintTypes));
var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day");
Assert.False(day.IsOptional);
Assert.False(day.RouteInfo.IsOptional);
Assert.Equal("Path", day.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(day.ConstraintTypes));
Assert.Equal("IntRouteConstraint", Assert.Single(day.RouteInfo.ConstraintTypes));
var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year");
Assert.False(year.IsOptional);
Assert.False(year.RouteInfo.IsOptional);
Assert.Equal("Path", year.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(year.ConstraintTypes));
Assert.Equal("IntRouteConstraint", Assert.Single(year.RouteInfo.ConstraintTypes));
}
[Fact]
@ -319,19 +320,19 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expectedRelativePath, description.RelativePath);
var month = Assert.Single(description.ParameterDescriptions, p => p.Name == "month");
Assert.False(month.IsOptional);
Assert.False(month.RouteInfo.IsOptional);
Assert.Equal("Path", month.Source);
Assert.Equal("RangeRouteConstraint", Assert.Single(month.ConstraintTypes));
Assert.Equal("RangeRouteConstraint", Assert.Single(month.RouteInfo.ConstraintTypes));
var day = Assert.Single(description.ParameterDescriptions, p => p.Name == "day");
Assert.True(day.IsOptional);
Assert.Equal("Path", day.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(day.ConstraintTypes));
Assert.True(day.RouteInfo.IsOptional);
Assert.Equal("ModelBinding", day.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(day.RouteInfo.ConstraintTypes));
var year = Assert.Single(description.ParameterDescriptions, p => p.Name == "year");
Assert.True(year.IsOptional);
Assert.Equal("Path", year.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(year.ConstraintTypes));
Assert.True(year.RouteInfo.IsOptional);
Assert.Equal("ModelBinding", year.Source);
Assert.Equal("IntRouteConstraint", Assert.Single(year.RouteInfo.ConstraintTypes));
}
[Fact]
@ -383,8 +384,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
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);
Assert.True(id.RouteInfo.IsOptional);
Assert.Equal("ModelBinding", id.Source);
}
[Fact]
@ -722,6 +723,158 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(formatterType, format.FormatterType);
}
[Fact]
public async Task ApiExplorer_Parameters_SimpleTypes_Default()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerParameters/SimpleParameters");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var parameters = description.ParameterDescriptions;
Assert.Equal(2, parameters.Count);
var i = Assert.Single(parameters, p => p.Name == "i");
Assert.Equal(ApiParameterSource.ModelBinding.Id, i.Source);
Assert.Equal(typeof(int).FullName, i.Type);
var s = Assert.Single(parameters, p => p.Name == "s");
Assert.Equal(ApiParameterSource.ModelBinding.Id, s.Source);
Assert.Equal(typeof(string).FullName, s.Type);
}
[Fact]
public async Task ApiExplorer_Parameters_SimpleTypes_BinderMetadataOnParameters()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerParameters/SimpleParametersWithBinderMetadata");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var parameters = description.ParameterDescriptions;
Assert.Equal(2, parameters.Count);
var i = Assert.Single(parameters, p => p.Name == "i");
Assert.Equal(ApiParameterSource.Query.Id, i.Source);
Assert.Equal(typeof(int).FullName, i.Type);
var s = Assert.Single(parameters, p => p.Name == "s");
Assert.Equal(ApiParameterSource.Path.Id, s.Source);
Assert.Equal(typeof(string).FullName, s.Type);
}
[Fact]
public async Task ApiExplorer_ParametersSimpleModel()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerParameters/SimpleModel");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var parameters = description.ParameterDescriptions;
Assert.Equal(1, parameters.Count);
var product = Assert.Single(parameters, p => p.Name == "product");
Assert.Equal(ApiParameterSource.ModelBinding.Id, product.Source);
Assert.Equal(typeof(ApiExplorerWebSite.Product).FullName, product.Type);
}
[Fact]
public async Task ApiExplorer_Parameters_SimpleTypes_SimpleModel_FromBody()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerParameters/SimpleModelFromBody/5");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var parameters = description.ParameterDescriptions;
Assert.Equal(2, parameters.Count);
var id = Assert.Single(parameters, p => p.Name == "id");
Assert.Equal(ApiParameterSource.Path.Id, id.Source);
Assert.Equal(typeof(int).FullName, id.Type);
var product = Assert.Single(parameters, p => p.Name == "product");
Assert.Equal(ApiParameterSource.Body.Id, product.Source);
Assert.Equal(typeof(ApiExplorerWebSite.Product).FullName, product.Type);
}
[Fact]
public async Task ApiExplorer_Parameters_SimpleTypes_ComplexModel()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerParameters/ComplexModel");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var parameters = description.ParameterDescriptions;
Assert.Equal(6, parameters.Count);
var customerId = Assert.Single(parameters, p => p.Name == "CustomerId");
Assert.Equal(ApiParameterSource.Query.Id, customerId.Source);
Assert.Equal(typeof(string).FullName, customerId.Type);
var referrer = Assert.Single(parameters, p => p.Name == "Referrer");
Assert.Equal(ApiParameterSource.Header.Id, referrer.Source);
Assert.Equal(typeof(string).FullName, referrer.Type);
var quantity = Assert.Single(parameters, p => p.Name == "Details.Quantity");
Assert.Equal(ApiParameterSource.Form.Id, quantity.Source);
Assert.Equal(typeof(int).FullName, quantity.Type);
var product = Assert.Single(parameters, p => p.Name == "Details.Product");
Assert.Equal(ApiParameterSource.Form.Id, product.Source);
Assert.Equal(typeof(ApiExplorerWebSite.Product).FullName, product.Type);
var shippingInstructions = Assert.Single(parameters, p => p.Name == "Comments.ShippingInstructions");
Assert.Equal(ApiParameterSource.Query.Id, shippingInstructions.Source);
Assert.Equal(typeof(string).FullName, shippingInstructions.Type);
var feedback = Assert.Single(parameters, p => p.Name == "Comments.Feedback");
Assert.Equal(ApiParameterSource.Form.Id, feedback.Source);
Assert.Equal(typeof(string).FullName, feedback.Type);
}
// Used to serialize data between client and server
private class ApiExplorerData
{
@ -741,15 +894,23 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Used to serialize data between client and server
private class ApiExplorerParameterData
{
public bool IsOptional { get; set; }
public string Name { get; set; }
public ApiExplorerParameterRouteInfo RouteInfo { get; set; }
public string Source { get; set; }
public string Type { get; set; }
}
// Used to serialize data between client and server
private class ApiExplorerParameterRouteInfo
{
public string[] ConstraintTypes { get; set; }
public object DefaultValue { get; set; }
public bool IsOptional { get; set; }
}
// Used to serialize data between client and server

View File

@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
#if ASPNET50
using Moq;
@ -84,6 +87,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
Assert.False(metadata.HideSurroundingHtml);
Assert.True(metadata.HtmlEncode);
Assert.False(metadata.IsComplexType);
Assert.False(metadata.IsCollectionType);
Assert.False(metadata.IsNullableValueType);
Assert.False(metadata.IsReadOnly);
Assert.False(metadata.IsRequired);
@ -128,7 +132,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var provider = new EmptyModelMetadataProvider();
// Act
var modelMetadata = new ModelMetadata(provider, null, null, type, null);
var modelMetadata = new ModelMetadata(
provider,
containerType: null,
modelAccessor: null,
modelType: type,
propertyName: null);
// Assert
Assert.False(modelMetadata.IsComplexType);
@ -145,12 +154,72 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var provider = new EmptyModelMetadataProvider();
// Act
var modelMetadata = new ModelMetadata(provider, null, null, type, null);
var modelMetadata = new ModelMetadata(
provider,
containerType: null,
modelAccessor: null,
modelType: type,
propertyName: null);
// Assert
Assert.True(modelMetadata.IsComplexType);
}
[Theory]
[InlineData(typeof(object))]
[InlineData(typeof(int))]
[InlineData(typeof(NonCollectionType))]
[InlineData(typeof(string))]
public void IsCollectionType_NonCollectionTypes(Type type)
{
// Arrange
var provider = new EmptyModelMetadataProvider();
// Act
var modelMetadata = new ModelMetadata(
provider,
containerType: null,
modelAccessor: null,
modelType: type,
propertyName: null);
// Assert
Assert.False(modelMetadata.IsCollectionType);
}
[Theory]
[InlineData(typeof(int[]))]
[InlineData(typeof(List<string>))]
[InlineData(typeof(DerivedList))]
[InlineData(typeof(IEnumerable))]
[InlineData(typeof(IEnumerable<string>))]
[InlineData(typeof(Collection<int>))]
[InlineData(typeof(Dictionary<object, object>))]
public void IsCollectionType_CollectionTypes(Type type)
{
// Arrange
var provider = new EmptyModelMetadataProvider();
// Act
var modelMetadata = new ModelMetadata(
provider,
containerType: null,
modelAccessor: null,
modelType: type,
propertyName: null);
// Assert
Assert.True(modelMetadata.IsCollectionType);
}
private class NonCollectionType
{
}
private class DerivedList : List<int>
{
}
// IsNullableValueType
[Fact]

View File

@ -1,6 +1,7 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc;
@ -9,11 +10,11 @@ using Microsoft.AspNet.Mvc.Description;
namespace ApiExplorerWebSite
{
/// <summary>
/// An action filter that looks up and serializes Api Explorer data for the action.
/// A resource filter that looks up and serializes Api Explorer data for the action.
///
/// This replaces the 'actual' output of the action.
/// </summary>
public class ApiExplorerDataFilter : ActionFilterAttribute
public class ApiExplorerDataFilter : IResourceFilter
{
private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider;
@ -22,7 +23,7 @@ namespace ApiExplorerWebSite
_descriptionProvider = descriptionProvider;
}
public override void OnActionExecuted(ActionExecutedContext context)
public void OnResourceExecuting(ResourceExecutingContext context)
{
var descriptions = new List<ApiExplorerData>();
foreach (var group in _descriptionProvider.ApiDescriptionGroups.Items)
@ -39,6 +40,11 @@ namespace ApiExplorerWebSite
context.Result = new JsonResult(descriptions);
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
throw new NotImplementedException();
}
private ApiExplorerData CreateSerializableData(ApiDescription description)
{
var data = new ApiExplorerData()
@ -53,13 +59,21 @@ namespace ApiExplorerWebSite
{
var parameterData = new ApiExplorerParameterData()
{
IsOptional = parameter.IsOptional,
Name = parameter.Name,
Source = parameter.Source.ToString(),
Type = parameter?.Type?.FullName,
ConstraintTypes = parameter?.Constraints?.Select(c => c.GetType().Name).ToArray(),
Source = parameter.Source.Id,
Type = parameter.Type?.FullName,
};
if (parameter.RouteInfo != null)
{
parameterData.RouteInfo = new ApiExplorerParameterRouteInfo()
{
ConstraintTypes = parameter.RouteInfo.Constraints?.Select(c => c.GetType().Name).ToArray(),
DefaultValue = parameter.RouteInfo.DefaultValue,
IsOptional = parameter.RouteInfo.IsOptional,
};
}
data.ParameterDescriptions.Add(parameterData);
}
@ -96,15 +110,23 @@ namespace ApiExplorerWebSite
// Used to serialize data between client and server
private class ApiExplorerParameterData
{
public bool IsOptional { get; set; }
public string Name { get; set; }
public ApiExplorerParameterRouteInfo RouteInfo { get; set; }
public string Source { get; set; }
public string Type { get; set; }
}
// Used to serialize data between client and server
private class ApiExplorerParameterRouteInfo
{
public string[] ConstraintTypes { get; set; }
public object DefaultValue { get; set; }
public bool IsOptional { get; set; }
}
// Used to serialize data between client and server

View File

@ -0,0 +1,32 @@
// 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 ApiExplorerWebSite.Controllers
{
[Route("ApiExplorerParameters/[action]")]
public class ApiExplorerParametersController : Controller
{
public void SimpleParameters(int i, string s)
{
}
public void SimpleParametersWithBinderMetadata([FromQuery] int i, [FromRoute] string s)
{
}
public void SimpleModel(Product product)
{
}
[Route("{id}")]
public void SimpleModelFromBody(int id, [FromBody] Product product)
{
}
public void ComplexModel([FromQuery] OrderDTO order)
{
}
}
}

View File

@ -0,0 +1,15 @@
// 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 ApiExplorerWebSite
{
public class CustomerCommentsDTO
{
[FromQuery]
public string ShippingInstructions { get; set; }
public string Feedback { get; set; }
}
}

View File

@ -0,0 +1,9 @@
// 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.
namespace ApiExplorerWebSite
{
public interface IOrderRepository
{
}
}

View File

@ -0,0 +1,23 @@
// 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 ApiExplorerWebSite
{
public class OrderDTO
{
[FromServices]
public IOrderRepository Repository { get; set; }
public string CustomerId { get; set; }
[FromHeader(Name = "Referrer")]
public string ReferrerId { get; set; }
public OrderDetailsDTO Details { get; set; }
[FromForm]
public CustomerCommentsDTO Comments { get; set; }
}
}

View File

@ -0,0 +1,16 @@
// 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 ApiExplorerWebSite
{
public class OrderDetailsDTO
{
[FromForm]
public int Quantity { get; set; }
[FromForm]
public Product Product { get; set; }
}
}

View File

@ -5,5 +5,8 @@ namespace ApiExplorerWebSite
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
}