Merge branch 'release' into dev
This commit is contained in:
commit
7fbe0ce307
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -5,5 +5,8 @@ namespace ApiExplorerWebSite
|
|||
{
|
||||
public class Product
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue