Adding Api Explorer

This commit is contained in:
Ryan Nowak 2014-08-27 14:28:26 -07:00
parent 6600e68fc0
commit 3cd6d3e060
58 changed files with 3010 additions and 14 deletions

15
Mvc.sln
View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22115.0
VisualStudioVersion = 14.0.22013.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -80,6 +80,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "XmlSerializerWebSite", "tes
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "UrlHelperWebSite", "test\WebSites\UrlHelperWebSite\UrlHelperWebSite.kproj", "{A192E504-2881-41DC-90D1-B7F1DD1134E8}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ApiExplorerWebSite", "test\WebSites\ApiExplorerWebSite\ApiExplorerWebSite.kproj", "{61061528-071E-424E-965A-07BCC2F02672}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -410,6 +412,16 @@ Global
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{A192E504-2881-41DC-90D1-B7F1DD1134E8}.Release|x86.ActiveCfg = Release|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Debug|x86.ActiveCfg = Debug|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Release|Any CPU.Build.0 = Release|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{61061528-071E-424E-965A-07BCC2F02672}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -448,5 +460,6 @@ Global
{1976AC4A-FEA4-4587-A158-D9F79736D2B6} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{96107AC0-18E2-474D-BAB4-2FFF2185FBCD} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{A192E504-2881-41DC-90D1-B7F1DD1134E8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{61061528-071E-424E-965A-07BCC2F02672} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,22 @@
// 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 Microsoft.AspNet.Mvc.Description;
namespace MvcSample.Web
{
[Route("ApiExplorer")]
public class ApiExplorerController : Controller
{
[Activate]
public IApiDescriptionGroupCollectionProvider Provider { get; set; }
[HttpGet]
public IActionResult All()
{
var descriptions = Provider.ApiDescriptionGroups.Items;
return View(descriptions);
}
}
}

View File

@ -0,0 +1,10 @@
.api-description {
margin: 10px 0px 10px 0px;
padding: 5px;
}
.api-description hr {
border: 0px;
border-top: 1px solid #D44B4B;
margin: 0px;
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNet.Mvc;
using System;
using System.Collections.Generic;
namespace MvcSample.Web.ApiExplorerSamples
{
[ApiExplorerSettings(GroupName = "Admin API")]
[Route("api/Admin/Products")]
public class ProductsAdminController : Controller
{
[HttpPut]
public void AddProduct([FromBody] Product product)
{
}
[HttpPost("{id}")]
public void UpdateProduct([FromBody] Product product)
{
}
[HttpPost("{id}/Stock")]
public void SetQuantityInStock(int id, int quantity)
{
}
[HttpPost("{id}/Price")]
public void SetPrice(int id, decimal price)
{
}
[Produces("application/json", "application/xml")]
[HttpGet("{id}/Orders")]
public IEnumerable<ProductOrderConfirmation> GetOrders(DateTime? fromData = null, DateTime? toDate = null)
{
return null;
}
}
}

View File

@ -0,0 +1,31 @@
using System;
using Microsoft.AspNet.Mvc;
using System.Collections.Generic;
namespace MvcSample.Web.ApiExplorerSamples
{
[ApiExplorerSettings(GroupName = "Public API")]
[Produces("application/json")]
[Route("api/Products")]
public class ProductsController : Controller
{
[HttpGet("{id}")]
public Product GetById(int id)
{
return null;
}
[HttpGet("Search/{name}")]
public IEnumerable<Product> SearchByName(string name)
{
return null;
}
[Produces("application/json", Type = typeof(ProductOrderConfirmation))]
[HttpPut("{id}/Buy")]
public IActionResult Buy(int projectId, int quantity = 1)
{
return null;
}
}
}

View File

@ -0,0 +1,13 @@
using System;
namespace MvcSample.Web.ApiExplorerSamples
{
public class Product
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace MvcSample.Web.ApiExplorerSamples
{
public class ProductOrderConfirmation
{
public Product Product { get; set; }
public decimal PricePerUnit { get; set; }
public int Quantity { get; set; }
public decimal TotalPrice { get; set; }
}
}

View File

@ -0,0 +1,23 @@
@using Microsoft.AspNet.Mvc.Description
@model IReadOnlyList<ApiDescriptionGroup>
@section header
{
<link rel="stylesheet" href="~/Content/api-description.css" />
}
<div style="padding: 50px 0px 0px 0px">
@foreach (var group in Model)
{
<div class="row">
<h1>Group: @group.GroupName</h1>
@foreach (var item in group.Items)
{
await Html.RenderPartialAsync("_ApiDescription", item);
}
</div>
}
</div>

View File

@ -0,0 +1,31 @@
@using Microsoft.AspNet.Mvc.Description
@model ApiDescription
<div class="api-description">
<h4>@(Model.HttpMethod ?? "*") - @(Model.RelativePath ?? "Unknown Url")</h4>
<hr />
<h5>For action: @Model.ActionDescriptor.DisplayName</h5>
<p>Return Type: @(Model.ResponseType?.FullName ?? "Unknown Type")</p>
@if (Model.ParameterDescriptions.Count > 0)
{
<p>Parameters:</p>
<ul>
@foreach (var parameter in Model.ParameterDescriptions)
{
<li>@parameter.Name - @parameter.Type.FullName - @parameter.Source.ToString()</li>
}
</ul>
}
@if (Model.SupportedResponseFormats.Count > 0)
{
<p>Response Formats:</p>
<ul>
@foreach(var response in Model.SupportedResponseFormats)
{
<li>@response.MediaType.RawValue - @response.Formatter.GetType().Name</li>
}
</ul>
}
</div>

View File

@ -11,6 +11,7 @@ namespace Microsoft.AspNet.Mvc
{
public ActionDescriptor()
{
Properties = new Dictionary<object, object>();
RouteValueDefaults = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
}
@ -34,5 +35,10 @@ namespace Microsoft.AspNet.Mvc
/// A friendly name for this action.
/// </summary>
public virtual string DisplayName { get; set; }
/// <summary>
/// Stores arbitrary metadata properties associated with the <see cref="ActionDescriptor"/>.
/// </summary>
public IDictionary<object, object> Properties { get; private set; }
}
}

View File

@ -0,0 +1,43 @@
// 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 Microsoft.AspNet.Mvc
{
/// <summary>
/// Extension methods for <see cref="ActionDescriptor"/>.
/// </summary>
public static class ActionDescriptorExtensions
{
/// <summary>
/// Gets the value of a property from the <see cref="ActionDescriptor.Properties"/> collection
/// using the provided value of <typeparamref name="T"/> as the key.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="actionDescriptor">The action descriptor.</param>
/// <returns>The property or the default value of <typeparamref name="T"/>.</returns>
public static T GetProperty<T>([NotNull] this ActionDescriptor actionDescriptor)
{
object value;
if (actionDescriptor.Properties.TryGetValue(typeof(T), out value))
{
return (T)value;
}
else
{
return default(T);
}
}
/// <summary>
/// Sets the value of an property in the <see cref="ActionDescriptor.Properties"/> collection using
/// the provided value of <typeparamref name="T"/> as the key.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="actionDescriptor">The action descriptor.</param>
/// <param name="value">The value of the property.</param>
public static void SetProperty<T>([NotNull] this ActionDescriptor actionDescriptor, [NotNull] T value)
{
actionDescriptor.Properties[typeof(T)] = value;
}
}
}

View File

@ -0,0 +1,81 @@
// 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 Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Represents an API exposed by this application.
/// </summary>
public class ApiDescription
{
/// <summary>
/// Creates a new instance of <see cref="ApiDescription"/>.
/// </summary>
public ApiDescription()
{
Properties = new Dictionary<object, object>();
ParameterDescriptions = new List<ApiParameterDescription>();
SupportedResponseFormats = new List<ApiResponseFormat>();
}
/// <summary>
/// The <see cref="ActionDescriptor"/> for this api.
/// </summary>
public ActionDescriptor ActionDescriptor { get; set; }
/// <summary>
/// The group name for this api.
/// </summary>
public string GroupName { get; set; }
/// <summary>
/// The supported HTTP method for this api, or null if all HTTP methods are supported.
/// </summary>
public string HttpMethod { get; set; }
/// <summary>
/// The list of <see cref="ApiParameterDescription"/> for this api.
/// </summary>
public List<ApiParameterDescription> ParameterDescriptions { get; private set; }
/// <summary>
/// Stores arbitrary metadata properties associated with the <see cref="ApiDescription"/>.
/// </summary>
public IDictionary<object, object> Properties { get; private set; }
/// <summary>
/// The relative url path template (relative to application root) for this api.
/// </summary>
public string RelativePath { get; set; }
/// <summary>
/// The <see cref="ModelMetadata"/> for the <see cref="ResponseType"/> or null.
/// </summary>
/// <remarks>
/// Will be null if <see cref="ResponseType"/> is null.
/// </remarks>
public ModelMetadata ResponseModelMetadata { get; set; }
/// <summary>
/// The CLR data type of the response or null.
/// </summary>
/// <remarks>
/// Will be null if the action returns no response, or if the response type is unclear. Use
/// <see cref="ProducesAttribute"/> on an action method to specify a response type.
/// </remarks>
public Type ResponseType { get; set; }
/// <summary>
/// A list of possible formats for a response.
/// </summary>
/// <remarks>
/// Will be empty if the action returns no response, or if the response type is unclear. Use
/// <see cref="ProducesAttribute"/> on an action method to specify a response type.
/// </remarks>
public IList<ApiResponseFormat> SupportedResponseFormats { get; private set; }
}
}

View File

@ -0,0 +1,18 @@
// 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 Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Represents data used to build an <see cref="ApiDescription"/>, stored as part of the
/// <see cref="ActionDescriptor.Properties"/>.
/// </summary>
public class ApiDescriptionActionData
{
/// <summary>
/// The <see cref="ApiDescription.GroupName"/> of <see cref="ApiDescription"/> objects for the associated
/// action.
/// </summary>
public string GroupName { get; set; }
}
}

View File

@ -0,0 +1,62 @@
// 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.Linq;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Description
{
/// <inheritdoc />
public class ApiDescriptionGroupCollectionProvider : IApiDescriptionGroupCollectionProvider
{
private readonly IActionDescriptorsCollectionProvider _actionDescriptorCollectionProvider;
private readonly INestedProviderManager<ApiDescriptionProviderContext> _apiDescriptionProvider;
private ApiDescriptionGroupCollection _apiDescriptionGroups;
/// <summary>
/// Creates a new instance of <see cref="ApiDescriptionGroupCollectionProvider"/>.
/// </summary>
/// <param name="actionDescriptorCollectionProvider">
/// The <see cref="IActionDescriptorsCollectionProvider"/>.
/// </param>
/// <param name="apiDescriptionProvider">
/// The <see cref="INestedProviderManager{ApiDescriptionProviderContext}"/>.
/// </param>
public ApiDescriptionGroupCollectionProvider(
IActionDescriptorsCollectionProvider actionDescriptorCollectionProvider,
INestedProviderManager<ApiDescriptionProviderContext> apiDescriptionProvider)
{
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
_apiDescriptionProvider = apiDescriptionProvider;
}
/// <inheritdoc />
public ApiDescriptionGroupCollection ApiDescriptionGroups
{
get
{
var actionDescriptors = _actionDescriptorCollectionProvider.ActionDescriptors;
if (_apiDescriptionGroups == null || _apiDescriptionGroups.Version != actionDescriptors.Version)
{
_apiDescriptionGroups = GetCollection(actionDescriptors);
}
return _apiDescriptionGroups;
}
}
private ApiDescriptionGroupCollection GetCollection(ActionDescriptorsCollection actionDescriptors)
{
var context = new ApiDescriptionProviderContext(actionDescriptors.Items);
_apiDescriptionProvider.Invoke(context);
var groups = context.Results
.GroupBy(d => d.GroupName)
.Select(g => new ApiDescriptionGroup(g.Key, g.ToArray()))
.ToArray();
return new ApiDescriptionGroupCollection(groups, actionDescriptors.Version);
}
}
}

View File

@ -0,0 +1,43 @@
// 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 Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Extension methods for <see cref="ApiDescription"/>.
/// </summary>
public static class ApiDescriptionExtensions
{
/// <summary>
/// Gets the value of a property from the <see cref="ApiDescription.Properties"/> collection
/// using the provided value of <typeparamref name="T"/> as the key.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="apiDescription">The <see cref="ApiDescription"/>.</param>
/// <returns>The property or the default value of <typeparamref name="T"/>.</returns>
public static T GetProperty<T>([NotNull] this ApiDescription apiDescription)
{
object value;
if (apiDescription.Properties.TryGetValue(typeof(T), out value))
{
return (T)value;
}
else
{
return default(T);
}
}
/// <summary>
/// Sets the value of an property in the <see cref="ApiDescription.Properties"/> collection using
/// the provided value of <typeparamref name="T"/> as the key.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="apiDescription">The <see cref="ApiDescription"/>.</param>
/// <param name="value">The value of the property.</param>
public static void SetProperty<T>([NotNull] this ApiDescription apiDescription, [NotNull] T value)
{
apiDescription.Properties[typeof(T)] = value;
}
}
}

View File

@ -0,0 +1,34 @@
// 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;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Represents a group of related apis.
/// </summary>
public class ApiDescriptionGroup
{
/// <summary>
/// Creates a new <see cref="ApiDescriptionGroup"/>.
/// </summary>
/// <param name="groupName">The group name.</param>
/// <param name="items">A collection of <see cref="ApiDescription"/> items for this group.</param>
public ApiDescriptionGroup(string groupName, IReadOnlyList<ApiDescription> items)
{
GroupName = groupName;
Items = items;
}
/// <summary>
/// The group name.
/// </summary>
public string GroupName { get; private set; }
/// <summary>
/// A collection of <see cref="ApiDescription"/> items for this group.
/// </summary>
public IReadOnlyList<ApiDescription> Items { get; private set; }
}
}

View File

@ -0,0 +1,34 @@
// 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;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// A cached collection of <see cref="ApiDescriptionGroup" />.
/// </summary>
public class ApiDescriptionGroupCollection
{
/// <summary>
/// Initializes a new instance of the <see cref="ApiDescriptionGroupCollection"/>.
/// </summary>
/// <param name="items">The list of <see cref="ApiDescriptionGroup"/>.</param>
/// <param name="version">The unique version of discovered groups.</param>
public ApiDescriptionGroupCollection([NotNull] IReadOnlyList<ApiDescriptionGroup> items, int version)
{
Items = items;
Version = version;
}
/// <summary>
/// Returns the list of <see cref="IReadOnlyList{ApiDescriptionGroup}"/>.
/// </summary>
public IReadOnlyList<ApiDescriptionGroup> Items { get; private set; }
/// <summary>
/// Returns the unique version of the current items.
/// </summary>
public int Version { get; private set; }
}
}

View File

@ -0,0 +1,34 @@
// 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;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// A context object for <see cref="ApiDescription"/> providers.
/// </summary>
public class ApiDescriptionProviderContext
{
/// <summary>
/// Creates a new instance of <see cref="ApiDescriptionProviderContext"/>.
/// </summary>
/// <param name="actions">The list of actions.</param>
public ApiDescriptionProviderContext([NotNull] IReadOnlyList<ActionDescriptor> actions)
{
Actions = actions;
Results = new List<ApiDescription>();
}
/// <summary>
/// The list of actions.
/// </summary>
public IReadOnlyList<ActionDescriptor> Actions { get; private set; }
/// <summary>
/// The list of resulting <see cref="ApiDescription"/>.
/// </summary>
public List<ApiDescription> Results { get; private set; }
}
}

View File

@ -0,0 +1,25 @@
// 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 Microsoft.AspNet.Mvc.Description;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Controls the visibility and group name for an <see cref="ApiDescription"/> of the associated
/// controller class or action method.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ApiExplorerSettingsAttribute :
Attribute,
IApiDescriptionGroupNameProvider,
IApiDescriptionVisibilityProvider
{
/// <inheritdoc />
public string GroupName { get; set; }
/// <inheritdoc />
public bool IgnoreApi { get; set; }
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.Description
{
public class ApiParameterDescription
{
public bool IsOptional { get; set; }
public ModelMetadata ModelMetadata { get; set; }
public string Name { get; set; }
public ParameterDescriptor ParameterDescriptor { get; set; }
public ApiParameterSource Source { get; set; }
public Type Type { get; set; }
}
}

View File

@ -0,0 +1,12 @@
// 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 Microsoft.AspNet.Mvc.Description
{
// This is a placeholder - see #886
public enum ApiParameterSource
{
Body,
Query,
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Represents a possible format for the body of a response.
/// </summary>
public class ApiResponseFormat
{
/// <summary>
/// The formatter used to output this response.
/// </summary>
public IOutputFormatter Formatter { get; set; }
/// <summary>
/// The media type of the response.
/// </summary>
public MediaTypeHeaderValue MediaType { get; set; }
}
}

View File

@ -0,0 +1,301 @@
// 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 System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Implements a provider of <see cref="ApiDescription"/> for actions represented
/// by <see cref="ReflectedActionDescriptor"/>.
/// </summary>
public class DefaultApiDescriptionProvider : INestedProvider<ApiDescriptionProviderContext>
{
private readonly IOutputFormattersProvider _formattersProvider;
private readonly IModelMetadataProvider _modelMetadataProvider;
/// <summary>
/// Creates a new instance of <see cref="DefaultApiDescriptionProvider"/>.
/// </summary>
/// <param name="formattersProvider">The <see cref="IOutputFormattersProvider"/>.</param>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
public DefaultApiDescriptionProvider(
IOutputFormattersProvider formattersProvider,
IModelMetadataProvider modelMetadataProvider)
{
_formattersProvider = formattersProvider;
_modelMetadataProvider = modelMetadataProvider;
}
/// <inheritdoc />
public int Order { get; private set; }
/// <inheritdoc />
public void Invoke(ApiDescriptionProviderContext context, Action callNext)
{
foreach (var action in context.Actions.OfType<ReflectedActionDescriptor>())
{
var extensionData = action.GetProperty<ApiDescriptionActionData>();
if (extensionData != null)
{
var httpMethods = GetHttpMethods(action);
foreach (var httpMethod in httpMethods)
{
context.Results.Add(CreateApiDescription(action, httpMethod, extensionData.GroupName));
}
}
}
callNext();
}
private ApiDescription CreateApiDescription(
ReflectedActionDescriptor action,
string httpMethod,
string groupName)
{
var apiDescription = new ApiDescription()
{
ActionDescriptor = action,
GroupName = groupName,
HttpMethod = httpMethod,
RelativePath = GetRelativePath(action),
};
if (action.Parameters != null)
{
foreach (var parameter in action.Parameters)
{
apiDescription.ParameterDescriptions.Add(GetParameter(parameter));
}
}
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
// We only provide response info if we can figure out a type that is a user-data type.
// Void /Task object/IActionResult will result in no data.
var declaredReturnType = GetDeclaredReturnType(action);
// Now 'simulate' an action execution. This attempts to figure out to the best of our knowledge
// what the logical data type is using filters.
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType, responseMetadataAttributes);
// We might not be able to figure out a good runtime return type. If that's the case we don't
// provide any information about outputs. The workaround is to attribute the action.
if (runtimeReturnType == typeof(void))
{
// As a special case, if the return type is void - we want to surface that information
// specifically, but nothing else. This can be overridden with a filter/attribute.
apiDescription.ResponseType = runtimeReturnType;
}
else if (runtimeReturnType != null)
{
apiDescription.ResponseType = runtimeReturnType;
apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(
modelAccessor: null,
modelType: runtimeReturnType);
var formats = GetResponseFormats(
action,
responseMetadataAttributes,
declaredReturnType,
runtimeReturnType);
foreach (var format in formats)
{
apiDescription.SupportedResponseFormats.Add(format);
}
}
return apiDescription;
}
private IEnumerable<string> GetHttpMethods(ReflectedActionDescriptor action)
{
if (action.MethodConstraints != null && action.MethodConstraints.Count > 0)
{
return action.MethodConstraints.SelectMany(c => c.HttpMethods);
}
else
{
return new string[] { null };
}
}
private string GetRelativePath(ReflectedActionDescriptor action)
{
// This is a placeholder for functionality which will correctly generate the relative path
// stub of an action. See: #885
if (action.AttributeRouteInfo != null &&
action.AttributeRouteInfo.Template != null)
{
return action.AttributeRouteInfo.Template;
}
return null;
}
private ApiParameterDescription GetParameter(ParameterDescriptor parameter)
{
// This is a placeholder based on currently available functionality for parameters. See #886.
var resourceParameter = new ApiParameterDescription()
{
IsOptional = parameter.IsOptional,
Name = parameter.Name,
ParameterDescriptor = parameter,
};
if (parameter.ParameterBindingInfo != null)
{
resourceParameter.Type = parameter.ParameterBindingInfo.ParameterType;
resourceParameter.Source = ApiParameterSource.Query;
}
if (parameter.BodyParameterInfo != null)
{
resourceParameter.Type = parameter.BodyParameterInfo.ParameterType;
resourceParameter.Source = ApiParameterSource.Body;
}
if (resourceParameter.Type != null)
{
resourceParameter.ModelMetadata = _modelMetadataProvider.GetMetadataForType(
modelAccessor: null,
modelType: resourceParameter.Type);
}
return resourceParameter;
}
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
ReflectedActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
Type declaredType,
Type runtimeType)
{
var results = new List<ApiResponseFormat>();
// Walk through all 'filter' attributes in order, and allow each one to see or override
// the results of the previous ones. This is similar to the execution path for content-negotiation.
var contentTypes = new List<MediaTypeHeaderValue>();
if (responseMetadataAttributes != null)
{
foreach (var metadataAttribute in responseMetadataAttributes)
{
metadataAttribute.SetContentTypes(contentTypes);
}
}
if (contentTypes.Count == 0)
{
contentTypes.Add(null);
}
var formatters = _formattersProvider.OutputFormatters;
foreach (var contentType in contentTypes)
{
foreach (var formatter in formatters)
{
var supportedTypes = formatter.GetSupportedContentTypes(declaredType, runtimeType, contentType);
if (supportedTypes != null)
{
foreach (var supportedType in supportedTypes)
{
results.Add(new ApiResponseFormat()
{
Formatter = formatter,
MediaType = supportedType,
});
}
}
}
}
return results;
}
private Type GetDeclaredReturnType(ReflectedActionDescriptor action)
{
var declaredReturnType = action.MethodInfo.ReturnType;
if (declaredReturnType == typeof(void) ||
declaredReturnType == typeof(Task))
{
return typeof(void);
}
// Unwrap the type if it's a Task<T>. The Task (non-generic) case was already handled.
var unwrappedType = TypeHelper.GetTaskInnerTypeOrNull(declaredReturnType) ?? declaredReturnType;
// If the method is declared to return IActionResult or a derived class, that information
// isn't valuable to the formatter.
if (typeof(IActionResult).IsAssignableFrom(unwrappedType))
{
return null;
}
else
{
return unwrappedType;
}
}
private Type GetRuntimeReturnType(Type declaredReturnType, IApiResponseMetadataProvider[] metadataAttributes)
{
// Walk through all of the filter attributes and allow them to set the type. This will execute them
// in filter-order allowing the desired behavior for overriding.
if (metadataAttributes != null)
{
Type typeSetByAttribute = null;
foreach (var metadataAttribute in metadataAttributes)
{
if (metadataAttribute.Type != null)
{
typeSetByAttribute = metadataAttribute.Type;
}
}
// If one of the filters set a type, then trust it.
if (typeSetByAttribute != null)
{
return typeSetByAttribute;
}
}
// If we get here, then a filter didn't give us an answer, so we need to figure out if we
// want to use the declared return type.
//
// We've already excluded Task, void, and IActionResult at this point.
//
// If the action might return any object, then assume we don't know anything about it.
if (declaredReturnType == typeof(object))
{
return null;
}
return declaredReturnType;
}
private IApiResponseMetadataProvider[] GetResponseMetadataAttributes(ReflectedActionDescriptor action)
{
if (action.FilterDescriptors == null)
{
return null;
}
// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
// for a filter that implements IApiResponseMetadataProvider.
//
// The workaround for that is to implement the metadata interface on the IFilterFactory.
return action.FilterDescriptors
.Select(fd => fd.Filter)
.OfType<IApiResponseMetadataProvider>()
.ToArray();
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Provides access to a collection of <see cref="ApiDescriptionGroup"/>.
/// </summary>
public interface IApiDescriptionGroupCollectionProvider
{
/// <summary>
/// Gets a collection of <see cref="ApiDescriptionGroup"/>.
/// </summary>
ApiDescriptionGroupCollection ApiDescriptionGroups { get; }
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Represents group name metadata for an <see cref="ApiDescription"/>.
/// </summary>
public interface IApiDescriptionGroupNameProvider
{
/// <summary>
/// The group name for the <see cref="ApiDescription"/> of the associated action or controller.
/// </summary>
string GroupName { get; }
}
}

View File

@ -0,0 +1,17 @@
// 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 Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Represents visibility metadata for an <see cref="ApiDescription"/>.
/// </summary>
public interface IApiDescriptionVisibilityProvider
{
/// <summary>
/// If <c>false</c> then no <see cref="ApiDescription"/> objects will be created for the associated controller
/// or action.
/// </summary>
bool IgnoreApi { get; }
}
}

View File

@ -5,21 +5,21 @@ using System;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
namespace Microsoft.AspNet.Mvc.Description
{
/// <summary>
/// Provides a return type and a set of possible content types returned by a successful execution of the action.
/// </summary>
public interface IProducesMetadataProvider
public interface IApiResponseMetadataProvider
{
/// <summary>
/// Optimistic return type of the action.
/// </summary>
Type Type { get; set; }
Type Type { get; }
/// <summary>
/// A collection of allowed content types which can be produced by the action.
/// Configures a collection of allowed content types which can be produced by the action.
/// </summary>
IList<MediaTypeHeaderValue> ContentTypes { get; set; }
void SetContentTypes(IList<MediaTypeHeaderValue> contentTypes);
}
}

View File

@ -3,8 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace Microsoft.AspNet.Mvc
@ -14,7 +13,7 @@ namespace Microsoft.AspNet.Mvc
/// which can be used to select a formatter while executing <see cref="ObjectResult"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ProducesAttribute : ResultFilterAttribute, IProducesMetadataProvider
public class ProducesAttribute : ResultFilterAttribute, IApiResponseMetadataProvider
{
public ProducesAttribute(string contentType, params string[] additionalContentTypes)
{
@ -32,7 +31,7 @@ namespace Microsoft.AspNet.Mvc
if (objectResult != null)
{
objectResult.ContentTypes = ContentTypes;
SetContentTypes(objectResult.ContentTypes);
}
}
@ -48,5 +47,14 @@ namespace Microsoft.AspNet.Mvc
return contentTypes;
}
public void SetContentTypes(IList<MediaTypeHeaderValue> contentTypes)
{
contentTypes.Clear();
foreach (var contentType in ContentTypes)
{
contentTypes.Add(contentType);
}
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
using Microsoft.AspNet.Mvc.Routing;
@ -143,6 +144,7 @@ namespace Microsoft.AspNet.Mvc
foreach (var actionDescriptor in actionDescriptors)
{
AddApiExplorerInfo(actionDescriptor, action, controller);
AddActionFilters(actionDescriptor, action.Filters, controller.Filters, model.Filters);
AddActionConstraints(actionDescriptor, action, controller);
AddControllerRouteConstraints(
@ -347,6 +349,23 @@ namespace Microsoft.AspNet.Mvc
return actionDescriptor;
}
private static void AddApiExplorerInfo(
ReflectedActionDescriptor actionDescriptor,
ReflectedActionModel action,
ReflectedControllerModel controller)
{
var apiExplorerIsVisible = action.ApiExplorerIsVisible ?? controller.ApiExplorerIsVisible ?? false;
if (apiExplorerIsVisible)
{
var apiExplorerActionData = new ApiDescriptionActionData()
{
GroupName = action.ApiExplorerGroupName ?? controller.ApiExplorerGroupName,
};
actionDescriptor.SetProperty(apiExplorerActionData);
}
}
private static void AddActionFilters(
ReflectedActionDescriptor actionDescriptor,
IEnumerable<IFilter> actionFilters,

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
@ -28,6 +29,18 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
AttributeRouteModel = new ReflectedAttributeRouteModel(routeTemplateAttribute);
}
var apiExplorerNameAttribute = Attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiExplorerNameAttribute != null)
{
ApiExplorerGroupName = apiExplorerNameAttribute.GroupName;
}
var apiExplorerVisibilityAttribute = Attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiExplorerVisibilityAttribute != null)
{
ApiExplorerIsVisible = !apiExplorerVisibilityAttribute.IgnoreApi;
}
HttpMethods = new List<string>();
Parameters = new List<ReflectedParameterModel>();
}
@ -47,5 +60,18 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
public List<ReflectedParameterModel> Parameters { get; private set; }
public ReflectedAttributeRouteModel AttributeRouteModel { get; set; }
/// <summary>
/// If <c>true</c>, <see cref="ApiDescription"/> objects will be created for this action. If <c>null</c>
/// then the value of <see cref="ReflectedControllerModel.ApiExplorerIsVisible"/> will be used.
/// </summary>
public bool? ApiExplorerIsVisible { get; set; }
/// <summary>
/// The value for <see cref="ApiDescription.GroupName"/> of <see cref="ApiDescription"/> objects created
/// for actions defined by this controller. If <c>null</c> then the value of
/// <see cref="ReflectedControllerModel.ApiExplorerGroupName"/> will be used.
/// </summary>
public string ApiExplorerGroupName { get; set; }
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Routing;
namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
@ -31,6 +32,18 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
.Select(rtp => new ReflectedAttributeRouteModel(rtp))
.ToList();
var apiExplorerNameAttribute = Attributes.OfType<IApiDescriptionGroupNameProvider>().FirstOrDefault();
if (apiExplorerNameAttribute != null)
{
ApiExplorerGroupName = apiExplorerNameAttribute.GroupName;
}
var apiExplorerVisibilityAttribute = Attributes.OfType<IApiDescriptionVisibilityProvider>().FirstOrDefault();
if (apiExplorerVisibilityAttribute != null)
{
ApiExplorerIsVisible = !apiExplorerVisibilityAttribute.IgnoreApi;
}
ControllerName = controllerType.Name.EndsWith("Controller", StringComparison.Ordinal)
? controllerType.Name.Substring(0, controllerType.Name.Length - "Controller".Length)
: controllerType.Name;
@ -49,5 +62,17 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder
public List<RouteConstraintAttribute> RouteConstraints { get; private set; }
public List<ReflectedAttributeRouteModel> AttributeRoutes { get; private set; }
/// <summary>
/// If <c>true</c>, <see cref="ApiDescription"/> objects will be created for actions defined by this
/// controller.
/// </summary>
public bool? ApiExplorerIsVisible { get; set; }
/// <summary>
/// The value for <see cref="ApiDescription.GroupName"/> of <see cref="ApiDescription"/> objects created
/// for actions defined by this controller.
/// </summary>
public string ApiExplorerGroupName { get; set; }
}
}
}

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
@ -103,6 +104,11 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Singleton<IAntiForgeryAdditionalDataProvider,
DefaultAntiForgeryAdditionalDataProvider>();
yield return describe.Singleton<IApiDescriptionGroupCollectionProvider,
ApiDescriptionGroupCollectionProvider>();
yield return describe.Transient<INestedProvider<ApiDescriptionProviderContext>,
DefaultApiDescriptionProvider>();
yield return
describe.Describe(
typeof(INestedProviderManager<>),

View File

@ -0,0 +1,508 @@
// 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.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Routing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Description
{
public class DefaultApiDescriptionProviderTest
{
[Fact]
public void GetApiDescription_IgnoresNonReflectedActionDescriptor()
{
// Arrange
var action = new ActionDescriptor();
action.SetProperty(new ApiDescriptionActionData());
// Act
var descriptions = GetApiDescriptions(action);
// Assert
Assert.Empty(descriptions);
}
[Fact]
public void GetApiDescription_IgnoresActionWithoutApiExplorerData()
{
// Arrange
var action = new ReflectedActionDescriptor();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
Assert.Empty(descriptions);
}
[Fact]
public void GetApiDescription_PopulatesActionDescriptor()
{
// Arrange
var action = CreateActionDescriptor();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Same(action, description.ActionDescriptor);
}
[Fact]
public void GetApiDescription_PopulatesGroupName()
{
// Arrange
var action = CreateActionDescriptor();
action.GetProperty<ApiDescriptionActionData>().GroupName = "Customers";
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal("Customers", description.GroupName);
}
[Fact]
public void GetApiDescription_HttpMethodIsNullWithoutConstraint()
{
// Arrange
var action = CreateActionDescriptor();
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Null(description.HttpMethod);
}
[Fact]
public void GetApiDescription_CreatesMultipleDescriptionsForMultipleHttpMethods()
{
// Arrange
var action = CreateActionDescriptor();
action.MethodConstraints = new List<HttpMethodConstraint>()
{
new HttpMethodConstraint(new string[] { "PUT", "POST" }),
new HttpMethodConstraint(new string[] { "GET" }),
};
// Act
var descriptions = GetApiDescriptions(action);
// Assert
Assert.Equal(3, descriptions.Count);
Assert.Single(descriptions, d => d.HttpMethod == "PUT");
Assert.Single(descriptions, d => d.HttpMethod == "POST");
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",
IsOptional = true,
ParameterBindingInfo = new ParameterBindingInfo("id", typeof(int)),
},
new ParameterDescriptor()
{
Name = "username",
BodyParameterInfo = new BodyParameterInfo(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.True(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);
}
// This is a placeholder based on current functionality - see #885
[Fact]
public void GetApiDescription_PopluatesRelativePath()
{
// Arrange
var action = CreateActionDescriptor();
action.AttributeRouteInfo = new AttributeRouteInfo();
action.AttributeRouteInfo.Template = "api/Products/{id}";
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal("api/Products/{id}", description.RelativePath);
}
[Fact]
public void GetApiDescription_PopluatesResponseType_WithProduct()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsProduct));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(Product), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
}
[Fact]
public void GetApiDescription_PopluatesResponseType_WithTaskOfProduct()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsTaskOfProduct));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(Product), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
}
[Theory]
[InlineData(nameof(ReturnsObject))]
[InlineData(nameof(ReturnsActionResult))]
[InlineData(nameof(ReturnsJsonResult))]
[InlineData(nameof(ReturnsTaskOfObject))]
[InlineData(nameof(ReturnsTaskOfActionResult))]
[InlineData(nameof(ReturnsTaskOfJsonResult))]
public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenUnknown(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Null(description.ResponseType);
Assert.Null(description.ResponseModelMetadata);
Assert.Empty(description.SupportedResponseFormats);
}
[Theory]
[InlineData(nameof(ReturnsVoid))]
[InlineData(nameof(ReturnsTask))]
public void GetApiDescription_DoesNotPopluatesResponseInformation_WhenVoid(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(void), description.ResponseType);
Assert.Null(description.ResponseModelMetadata);
Assert.Empty(description.SupportedResponseFormats);
}
[Theory]
[InlineData(nameof(ReturnsObject))]
[InlineData(nameof(ReturnsVoid))]
[InlineData(nameof(ReturnsActionResult))]
[InlineData(nameof(ReturnsJsonResult))]
[InlineData(nameof(ReturnsTaskOfObject))]
[InlineData(nameof(ReturnsTask))]
[InlineData(nameof(ReturnsTaskOfActionResult))]
[InlineData(nameof(ReturnsTaskOfJsonResult))]
public void GetApiDescription_PopluatesResponseInformation_WhenSetByFilter(string methodName)
{
// Arrange
var action = CreateActionDescriptor(methodName);
var filter = new ContentTypeAttribute("text/*")
{
Type = typeof(Order)
};
action.FilterDescriptors = new List<FilterDescriptor>();
action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(typeof(Order), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
}
[Fact]
public void GetApiDescription_IncludesResponseFormats()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsProduct));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(4, description.SupportedResponseFormats.Count);
var formats = description.SupportedResponseFormats;
Assert.Single(formats, f => f.MediaType.RawValue == "text/json");
Assert.Single(formats, f => f.MediaType.RawValue == "application/json");
Assert.Single(formats, f => f.MediaType.RawValue == "text/xml");
Assert.Single(formats, f => f.MediaType.RawValue == "application/xml");
}
[Fact]
public void GetApiDescription_IncludesResponseFormats_FilteredByAttribute()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsProduct));
action.FilterDescriptors = new List<FilterDescriptor>();
action.FilterDescriptors.Add(new FilterDescriptor(new ContentTypeAttribute("text/*"), FilterScope.Action));
// Act
var descriptions = GetApiDescriptions(action);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(2, description.SupportedResponseFormats.Count);
var formats = description.SupportedResponseFormats;
Assert.Single(formats, f => f.MediaType.RawValue == "text/json");
Assert.Single(formats, f => f.MediaType.RawValue == "text/xml");
}
[Fact]
public void GetApiDescription_IncludesResponseFormats_FilteredByType()
{
// Arrange
var action = CreateActionDescriptor(nameof(ReturnsObject));
var filter = new ContentTypeAttribute("text/*")
{
Type = typeof(Order)
};
action.FilterDescriptors = new List<FilterDescriptor>();
action.FilterDescriptors.Add(new FilterDescriptor(filter, FilterScope.Action));
var formatters = CreateFormatters();
// This will just format Order
formatters[0].SupportedTypes.Add(typeof(Order));
// This will just format Product
formatters[1].SupportedTypes.Add(typeof(Product));
// Act
var descriptions = GetApiDescriptions(action, formatters);
// Assert
var description = Assert.Single(descriptions);
Assert.Equal(1, description.SupportedResponseFormats.Count);
Assert.Equal(typeof(Order), description.ResponseType);
Assert.NotNull(description.ResponseModelMetadata);
var formats = description.SupportedResponseFormats;
Assert.Single(formats, f => f.MediaType.RawValue == "text/json");
Assert.Same(formatters[0], formats[0].Formatter);
}
private IReadOnlyList<ApiDescription> GetApiDescriptions(ActionDescriptor action)
{
return GetApiDescriptions(action, CreateFormatters());
}
private IReadOnlyList<ApiDescription> GetApiDescriptions(ActionDescriptor action, List<MockFormatter> formatters)
{
var context = new ApiDescriptionProviderContext(new ActionDescriptor[] { action });
var formattersProvider = new Mock<IOutputFormattersProvider>(MockBehavior.Strict);
formattersProvider.Setup(fp => fp.OutputFormatters).Returns(formatters);
var 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 provider = new DefaultApiDescriptionProvider(formattersProvider.Object, modelMetadataProvider.Object);
provider.Invoke(context, () => { });
return context.Results;
}
private List<MockFormatter> CreateFormatters()
{
// Include some default formatters that look reasonable, some tests will override this.
var formatters = new List<MockFormatter>()
{
new MockFormatter(),
new MockFormatter(),
};
formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json"));
formatters[0].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/json"));
formatters[1].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/xml"));
formatters[1].SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/xml"));
return formatters;
}
private ReflectedActionDescriptor CreateActionDescriptor(string methodName = null)
{
var action = new ReflectedActionDescriptor();
action.SetProperty(new ApiDescriptionActionData());
action.MethodInfo = GetType().GetMethod(
methodName ?? "ReturnsObject",
BindingFlags.Instance | BindingFlags.NonPublic);
return action;
}
private object ReturnsObject()
{
return null;
}
private void ReturnsVoid()
{
}
private IActionResult ReturnsActionResult()
{
return null;
}
private JsonResult ReturnsJsonResult()
{
return null;
}
private Task<Product> ReturnsTaskOfProduct()
{
return null;
}
private Task<object> ReturnsTaskOfObject()
{
return null;
}
private Task ReturnsTask()
{
return null;
}
private Task<IActionResult> ReturnsTaskOfActionResult()
{
return null;
}
private Task<JsonResult> ReturnsTaskOfJsonResult()
{
return null;
}
private Product ReturnsProduct()
{
return null;
}
private class Product
{
}
private class Order
{
}
private class MockFormatter : OutputFormatter
{
public List<Type> SupportedTypes { get; } = new List<Type>();
public override Task WriteResponseBodyAsync(OutputFormatterContext context)
{
throw new NotImplementedException();
}
protected override bool CanWriteType(Type declaredType, Type actualType)
{
if (SupportedTypes.Count == 0)
{
return true;
}
else if ((actualType ?? declaredType) == null)
{
return false;
}
else
{
return SupportedTypes.Contains(actualType ?? declaredType);
}
}
}
private class ContentTypeAttribute : Attribute, IFilter, IApiResponseMetadataProvider
{
public ContentTypeAttribute(string mediaType)
{
ContentTypes.Add(MediaTypeHeaderValue.Parse(mediaType));
}
public List<MediaTypeHeaderValue> ContentTypes { get; } = new List<MediaTypeHeaderValue>();
public Type Type { get; set; }
public void SetContentTypes(IList<MediaTypeHeaderValue> contentTypes)
{
contentTypes.Clear();
foreach (var contentType in ContentTypes)
{
contentTypes.Add(contentType);
}
}
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Routing;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.Routing;
using Moq;
using Xunit;
@ -1016,6 +1017,119 @@ namespace Microsoft.AspNet.Mvc.Test
Assert.Equal("stub/{controller}/{action}", action.AttributeRouteInfo.Template);
}
[Fact]
public void ApiExplorer_SetsExtensionData_WhenVisible()
{
// Arrange
var provider = GetProvider(typeof(ApiExplorerVisibleController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
var action = Assert.Single(actions);
Assert.NotNull(action.GetProperty<ApiDescriptionActionData>());
}
[Fact]
public void ApiExplorer_SetsExtensionData_WhenVisible_CanOverrideControllerOnAction()
{
// Arrange
var provider = GetProvider(typeof(ApiExplorerVisibilityOverrideController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
Assert.Equal(2, actions.Count());
var action = Assert.Single(actions, a => a.Name == "Edit");
Assert.NotNull(action.GetProperty<ApiDescriptionActionData>());
action = Assert.Single(actions, a => a.Name == "Create");
Assert.Null(action.GetProperty<ApiDescriptionActionData>());
}
[Theory]
[InlineData(typeof(ApiExplorerNotVisibleController))]
[InlineData(typeof(ApiExplorerExplicitlyNotVisibleController))]
[InlineData(typeof(ApiExplorerExplicitlyNotVisibleOnActionController))]
public void ApiExplorer_DoesNotSetExtensionData_WhenNotVisible(Type controllerType)
{
// Arrange
var provider = GetProvider(controllerType.GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
var action = Assert.Single(actions);
Assert.Null(action.GetProperty<ApiDescriptionActionData>());
}
[Fact]
public void ApiExplorer_SetsName_DefaultToNull()
{
// Arrange
var provider = GetProvider(typeof(ApiExplorerNoNameController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
var action = Assert.Single(actions);
Assert.Null(action.GetProperty<ApiDescriptionActionData>().GroupName);
}
[Fact]
public void ApiExplorer_SetsName_OnController()
{
// Arrange
var provider = GetProvider(typeof(ApiExplorerNameOnControllerController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
var action = Assert.Single(actions);
Assert.Equal("Store", action.GetProperty<ApiDescriptionActionData>().GroupName);
}
[Fact]
public void ApiExplorer_SetsName_OnAction()
{
// Arrange
var provider = GetProvider(typeof(ApiExplorerNameOnActionController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
var action = Assert.Single(actions);
Assert.Equal("Blog", action.GetProperty<ApiDescriptionActionData>().GroupName);
}
[Fact]
public void ApiExplorer_SetsName_CanOverrideControllerOnAction()
{
// Arrange
var provider = GetProvider(typeof(ApiExplorerNameOverrideController).GetTypeInfo());
// Act
var actions = provider.GetDescriptors();
// Assert
Assert.Equal(2, actions.Count());
var action = Assert.Single(actions, a => a.Name == "Edit");
Assert.Equal("Blog", action.GetProperty<ApiDescriptionActionData>().GroupName);
action = Assert.Single(actions, a => a.Name == "Create");
Assert.Equal("Store", action.GetProperty<ApiDescriptionActionData>().GroupName);
}
private ReflectedActionDescriptorProvider GetProvider(
TypeInfo controllerTypeInfo,
IEnumerable<IFilter> filters = null)
@ -1404,5 +1518,65 @@ namespace Microsoft.AspNet.Mvc.Test
public int Id { get; set; }
public int Name { get; set; }
}
private class ApiExplorerNotVisibleController
{
public void Edit() { }
}
[ApiExplorerSettings()]
private class ApiExplorerVisibleController
{
public void Edit() { }
}
[ApiExplorerSettings(IgnoreApi = true)]
private class ApiExplorerExplicitlyNotVisibleController
{
public void Edit() { }
}
private class ApiExplorerExplicitlyNotVisibleOnActionController
{
[ApiExplorerSettings(IgnoreApi = true)]
public void Edit() { }
}
[ApiExplorerSettings(IgnoreApi = true)]
private class ApiExplorerVisibilityOverrideController
{
[ApiExplorerSettings(IgnoreApi = false)]
public void Edit() { }
public void Create() { }
}
[ApiExplorerSettings(GroupName = "Store")]
private class ApiExplorerNameOnControllerController
{
public void Edit() { }
}
private class ApiExplorerNameOnActionController
{
[ApiExplorerSettings(GroupName = "Blog")]
public void Edit() { }
}
[ApiExplorerSettings()]
private class ApiExplorerNoNameController
{
public void Edit() { }
}
[ApiExplorerSettings(GroupName = "Store")]
private class ApiExplorerNameOverrideController
{
[ApiExplorerSettings(GroupName = "Blog")]
public void Edit() { }
public void Create() { }
}
}
}

View File

@ -38,6 +38,34 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
Assert.IsType<MyFilterAttribute>(model.Filters[0]);
}
[Fact]
public void ReflectedActionModel_PopulatesApiExplorerInfo()
{
// Arrange
var actionMethod = typeof(BlogController).GetMethod("Create");
// Act
var model = new ReflectedActionModel(actionMethod);
// Assert
Assert.Equal(false, model.ApiExplorerIsVisible);
Assert.Equal("Blog", model.ApiExplorerGroupName);
}
[Fact]
public void ReflectedActionModel_PopulatesApiExplorerInfo_NoAttribute()
{
// Arrange
var actionMethod = typeof(BlogController).GetMethod("Edit");
// Act
var model = new ReflectedActionModel(actionMethod);
// Assert
Assert.Null(model.ApiExplorerIsVisible);
Assert.Null(model.ApiExplorerGroupName);
}
private class BlogController
{
[MyOther]
@ -46,6 +74,12 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
public void Edit()
{
}
[ApiExplorerSettings(IgnoreApi = true, GroupName = "Blog")]
public void Create()
{
}
}
private class MyFilterAttribute : Attribute, IFilter
@ -56,4 +90,4 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
{
}
}
}
}

View File

@ -20,11 +20,12 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal(5, model.Attributes.Count);
Assert.Equal(6, model.Attributes.Count);
Assert.Single(model.Attributes, a => a is MyOtherAttribute);
Assert.Single(model.Attributes, a => a is MyFilterAttribute);
Assert.Single(model.Attributes, a => a is MyRouteConstraintAttribute);
Assert.Single(model.Attributes, a => a is ApiExplorerSettingsAttribute);
var routes = model.Attributes.OfType<RouteAttribute>().ToList();
Assert.Equal(2, routes.Count());
@ -102,11 +103,54 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
Assert.Single(model.AttributeRoutes, r => r.Template.Equals("Microblog"));
}
[Fact]
public void ReflectedControllerModel_PopulatesApiExplorerInfo()
{
// Arrange
var controllerType = typeof(BlogController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal(true, model.ApiExplorerIsVisible);
Assert.Equal("Blog", model.ApiExplorerGroupName);
}
[Fact]
public void ReflectedControllerModel_PopulatesApiExplorerInfo_Inherited()
{
// Arrange
var controllerType = typeof(DerivedController);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Equal(true, model.ApiExplorerIsVisible);
Assert.Equal("API", model.ApiExplorerGroupName);
}
[Fact]
public void ReflectedControllerModel_PopulatesApiExplorerInfo_NoAttribute()
{
// Arrange
var controllerType = typeof(Store);
// Act
var model = new ReflectedControllerModel(controllerType.GetTypeInfo());
// Assert
Assert.Null(model.ApiExplorerIsVisible);
Assert.Null(model.ApiExplorerGroupName);
}
[MyOther]
[MyFilter]
[MyRouteConstraint]
[Route("Blog")]
[Route("Microblog")]
[ApiExplorerSettings(GroupName = "Blog")]
private class BlogController
{
}
@ -115,6 +159,16 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
{
}
private class DerivedController : BaseController
{
}
[ApiExplorerSettings(GroupName = "API")]
private class BaseController
{
}
private class MyRouteConstraintAttribute : RouteConstraintAttribute
{
public MyRouteConstraintAttribute()
@ -131,4 +185,4 @@ namespace Microsoft.AspNet.Mvc.ReflectedModelBuilder.Test
{
}
}
}
}

View File

@ -0,0 +1,513 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Xunit;
using Newtonsoft.Json;
using System.Net.Http;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class ApiExplorerTest
{
private readonly IServiceProvider _provider = TestHelper.CreateServices("ApiExplorerWebSite");
private readonly Action<IApplicationBuilder> _app = new ApiExplorer.Startup().Configure;
[Fact]
public async Task ApiExplorer_IsVisible_EnabledWithConvention()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerVisbilityEnabledByConvention");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
Assert.Single(result);
}
[Fact]
public async Task ApiExplorer_IsVisible_DisabledWithConvention()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerVisbilityDisabledByConvention");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task ApiExplorer_IsVisible_DisabledWithAttribute()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerVisibilitySetExplicitly/Disabled");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
Assert.Empty(result);
}
[Fact]
public async Task ApiExplorer_IsVisible_EnabledWithAttribute()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerVisibilitySetExplicitly/Enabled");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
Assert.Single(result);
}
[Fact]
public async Task ApiExplorer_GroupName_SetByConvention()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerNameSetByConvention");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal(description.GroupName, "ApiExplorerNameSetByConvention");
}
[Fact]
public async Task ApiExplorer_GroupName_SetByAttributeOnController()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerNameSetExplicitly/SetOnController");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal(description.GroupName, "SetOnController");
}
[Fact]
public async Task ApiExplorer_GroupName_SetByAttributeOnAction()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerNameSetExplicitly/SetOnAction");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal(description.GroupName, "SetOnAction");
}
[Fact]
public async Task ApiExplorer_HttpMethod_All()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerHttpMethod/All");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Null(description.HttpMethod);
}
[Fact]
public async Task ApiExplorer_HttpMethod_Single()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerHttpMethod/Get");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal("GET", description.HttpMethod);
}
// This is hitting one action with two allowed methods (using [AcceptVerbs]). This should
// return two api descriptions.
[Theory]
[InlineData("PUT")]
[InlineData("POST")]
public async Task ApiExplorer_HttpMethod_Single(string httpMethod)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
var request = new HttpRequestMessage(
new HttpMethod(httpMethod),
"http://localhost/ApiExplorerHttpMethod/Single");
// Act
var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
Assert.Equal(2, result.Count);
Assert.Single(result, d => d.HttpMethod == "PUT");
Assert.Single(result, d => d.HttpMethod == "POST");
}
[Theory]
[InlineData("GetVoid")]
[InlineData("GetTask")]
public async Task ApiExplorer_ResponseType_VoidWithoutAttribute(string action)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/ApiExplorerResponseTypeWithoutAttribute/" + action);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal(typeof(void).FullName, description.ResponseType);
}
[Theory]
[InlineData("GetObject")]
[InlineData("GetIActionResult")]
[InlineData("GetDerivedActionResult")]
[InlineData("GetTaskOfObject")]
[InlineData("GetTaskOfIActionResult")]
[InlineData("GetTaskOfDerivedActionResult")]
public async Task ApiExplorer_ResponseType_UnknownWithoutAttribute(string action)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/ApiExplorerResponseTypeWithoutAttribute/" + action);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Null(description.ResponseType);
}
[Theory]
[InlineData("GetProduct", "ApiExplorer.Product")]
[InlineData("GetInt", "System.Int32")]
[InlineData("GetTaskOfProduct", "ApiExplorer.Product")]
[InlineData("GetTaskOfInt", "System.Int32")]
public async Task ApiExplorer_ResponseType_KnownWithoutAttribute(string action, string type)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/ApiExplorerResponseTypeWithoutAttribute/" + action);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal(type, description.ResponseType);
}
[Theory]
[InlineData("GetVoid", "ApiExplorer.Customer")]
[InlineData("GetObject", "ApiExplorer.Product")]
[InlineData("GetIActionResult", "System.String")]
[InlineData("GetProduct", "ApiExplorer.Customer")]
[InlineData("GetTask", "System.Int32")]
public async Task ApiExplorer_ResponseType_KnownWithAttribute(string action, string type)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/ApiExplorerResponseTypeWithAttribute/" + action);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal(type, description.ResponseType);
}
[Theory]
[InlineData("Controller", "ApiExplorer.Product")]
[InlineData("Action", "ApiExplorer.Customer")]
public async Task ApiExplorer_ResponseType_OverrideOnAction(string action, string type)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/ApiExplorerResponseTypeOverrideOnAction/" + action);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
Assert.Equal(type, description.ResponseType);
}
[Fact]
public async Task ApiExplorer_ResponseContentType_Unset()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerResponseContentType/Unset");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var formats = description.SupportedResponseFormats;
Assert.Equal(4, formats.Count);
var textXml = Assert.Single(formats, f => f.MediaType == "text/xml");
Assert.Equal(typeof(XmlDataContractSerializerOutputFormatter).FullName, textXml.FormatterType);
var applicationXml = Assert.Single(formats, f => f.MediaType == "application/xml");
Assert.Equal(typeof(XmlDataContractSerializerOutputFormatter).FullName, applicationXml.FormatterType);
var textJson = Assert.Single(formats, f => f.MediaType == "text/json");
Assert.Equal(typeof(JsonOutputFormatter).FullName, textJson.FormatterType);
var applicationJson = Assert.Single(formats, f => f.MediaType == "application/json");
Assert.Equal(typeof(JsonOutputFormatter).FullName, applicationJson.FormatterType);
}
// uses [Produces("*/*")]
[Fact]
public async Task ApiExplorer_ResponseContentType_AllTypes()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerResponseContentType/AllTypes");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var formats = description.SupportedResponseFormats;
Assert.Equal(4, formats.Count);
var textXml = Assert.Single(formats, f => f.MediaType == "text/xml");
Assert.Equal(typeof(XmlDataContractSerializerOutputFormatter).FullName, textXml.FormatterType);
var applicationXml = Assert.Single(formats, f => f.MediaType == "application/xml");
Assert.Equal(typeof(XmlDataContractSerializerOutputFormatter).FullName, applicationXml.FormatterType);
var textJson = Assert.Single(formats, f => f.MediaType == "text/json");
Assert.Equal(typeof(JsonOutputFormatter).FullName, textJson.FormatterType);
var applicationJson = Assert.Single(formats, f => f.MediaType == "application/json");
Assert.Equal(typeof(JsonOutputFormatter).FullName, applicationJson.FormatterType);
}
[Fact]
public async Task ApiExplorer_ResponseContentType_Range()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerResponseContentType/Range");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var formats = description.SupportedResponseFormats;
Assert.Equal(2, formats.Count);
var textXml = Assert.Single(formats, f => f.MediaType == "text/xml");
Assert.Equal(typeof(XmlDataContractSerializerOutputFormatter).FullName, textXml.FormatterType);
var textJson = Assert.Single(formats, f => f.MediaType == "text/json");
Assert.Equal(typeof(JsonOutputFormatter).FullName, textJson.FormatterType);
}
[Fact]
public async Task ApiExplorer_ResponseContentType_Specific()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerResponseContentType/Specific");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var formats = description.SupportedResponseFormats;
Assert.Equal(1, formats.Count);
var applicationJson = Assert.Single(formats, f => f.MediaType == "application/json");
Assert.Equal(typeof(JsonOutputFormatter).FullName, applicationJson.FormatterType);
}
[Fact]
public async Task ApiExplorer_ResponseContentType_NoMatch()
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync("http://localhost/ApiExplorerResponseContentType/NoMatch");
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var formats = description.SupportedResponseFormats;
Assert.Empty(formats);
}
[Theory]
[InlineData("Controller", "text/xml", "Microsoft.AspNet.Mvc.XmlDataContractSerializerOutputFormatter")]
[InlineData("Action", "application/json", "Microsoft.AspNet.Mvc.JsonOutputFormatter")]
public async Task ApiExplorer_ResponseContentType_OverrideOnAction(
string action,
string contentType,
string formatterType)
{
// Arrange
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var response = await client.GetAsync(
"http://localhost/ApiExplorerResponseContentTypeOverrideOnAction/" + action);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(body);
// Assert
var description = Assert.Single(result);
var format = Assert.Single(description.SupportedResponseFormats);
Assert.Equal(contentType, format.MediaType);
Assert.Equal(formatterType, format.FormatterType);
}
// Used to serialize data between client and server
private class ApiExplorerData
{
public string GroupName { get; set; }
public string HttpMethod { get; set; }
public List<ApiExplorerParameterData> ParameterDescriptions { get; } = new List<ApiExplorerParameterData>();
public string RelativePath { get; set; }
public string ResponseType { get; set; }
public List<ApiExplorerResponseData> SupportedResponseFormats { get; } = new List<ApiExplorerResponseData>();
}
// Used to serialize data between client and server
private class ApiExplorerParameterData
{
public bool IsOptional { get; set; }
public string Name { get; set; }
public string Source { get; set; }
public string Type { get; set; }
}
// Used to serialize data between client and server
private class ApiExplorerResponseData
{
public string MediaType { get; set; }
public string FormatterType { get; set; }
}
}
}

View File

@ -6,6 +6,7 @@
"ActivatorWebSite": "",
"AddServicesWebSite": "",
"AntiForgeryWebSite": "",
"ApiExplorerWebSite": "",
"BasicWebSite": "",
"CompositeViewEngine": "",
"ConnegWebsite": "",

View File

@ -0,0 +1,114 @@
// 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.Mvc;
using Microsoft.AspNet.Mvc.Description;
namespace ApiExplorer
{
/// <summary>
/// An action 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
{
private readonly IApiDescriptionGroupCollectionProvider _descriptionProvider;
public ApiExplorerDataFilter(IApiDescriptionGroupCollectionProvider descriptionProvider)
{
_descriptionProvider = descriptionProvider;
}
public override void OnActionExecuted(ActionExecutedContext context)
{
var descriptions = new List<ApiExplorerData>();
foreach (var group in _descriptionProvider.ApiDescriptionGroups.Items)
{
foreach (var description in group.Items)
{
if (context.ActionDescriptor == description.ActionDescriptor)
{
descriptions.Add(CreateSerializableData(description));
}
}
}
context.Result = new JsonResult(descriptions);
}
private ApiExplorerData CreateSerializableData(ApiDescription description)
{
var data = new ApiExplorerData()
{
GroupName = description.GroupName,
HttpMethod = description.HttpMethod,
RelativePath = description.RelativePath,
ResponseType = description.ResponseType?.FullName,
};
foreach (var parameter in description.ParameterDescriptions)
{
var parameterData = new ApiExplorerParameterData()
{
IsOptional = parameter.IsOptional,
Name = parameter.Name,
Source = parameter.Source.ToString(),
Type = parameter.Type.FullName,
};
data.ParameterDescriptions.Add(parameterData);
}
foreach (var response in description.SupportedResponseFormats)
{
var responseData = new ApiExplorerResponseData()
{
FormatterType = response.Formatter.GetType().FullName,
MediaType = response.MediaType.RawValue,
};
data.SupportedResponseFormats.Add(responseData);
}
return data;
}
// Used to serialize data between client and server
private class ApiExplorerData
{
public string GroupName { get; set; }
public string HttpMethod { get; set; }
public List<ApiExplorerParameterData> ParameterDescriptions { get; } = new List<ApiExplorerParameterData>();
public string RelativePath { get; set; }
public string ResponseType { get; set; }
public List<ApiExplorerResponseData> SupportedResponseFormats { get; } = new List<ApiExplorerResponseData>();
}
// Used to serialize data between client and server
private class ApiExplorerParameterData
{
public bool IsOptional { get; set; }
public string Name { get; set; }
public string Source { get; set; }
public string Type { get; set; }
}
// Used to serialize data between client and server
private class ApiExplorerResponseData
{
public string MediaType { get; set; }
public string FormatterType { get; set; }
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Reflection;
using Microsoft.AspNet.Mvc.ReflectedModelBuilder;
namespace ApiExplorer
{
// Disables ApiExplorer for a specific controller type.
// This is part of the test that validates that ApiExplorer can be configured via
// convention
public class ApiExplorerVisibilityDisabledConvention : IReflectedApplicationModelConvention
{
private readonly TypeInfo _type;
public ApiExplorerVisibilityDisabledConvention(Type type)
{
_type = type.GetTypeInfo();
}
public void OnModelCreated(ReflectedApplicationModel model)
{
foreach (var controller in model.Controllers)
{
if (controller.ControllerType == _type)
{
controller.ApiExplorerIsVisible = false;
}
}
}
}
}

View File

@ -0,0 +1,25 @@
// 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.ReflectedModelBuilder;
namespace ApiExplorer
{
// Enables ApiExplorer for controllers that haven't explicitly configured it.
// This is part of the test that validates that ApiExplorer can be configured via
// convention
public class ApiExplorerVisibilityEnabledConvention : IReflectedApplicationModelConvention
{
public void OnModelCreated(ReflectedApplicationModel model)
{
foreach (var controller in model.Controllers)
{
if (controller.ApiExplorerIsVisible == null)
{
controller.ApiExplorerIsVisible = true;
controller.ApiExplorerGroupName = controller.ControllerName;
}
}
}
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>61061528-071e-424e-965a-07bcc2f02672</ProjectGuid>
<OutputType>Web</OutputType>
<RootNamespace>ApiExplorer</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Console'">
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Web'">
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>7591</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,30 @@
using Microsoft.AspNet.Mvc;
using System;
namespace ApiExplorer
{
[Route("ApiExplorerHttpMethod")]
public class ApiExplorerHttpMethodController : Controller
{
[Route("All")]
public void All()
{
}
[HttpGet("Get")]
public void Get()
{
}
[AcceptVerbs("PUT", "POST", Route = "Single")]
public void PutOrPost()
{
}
[HttpGet("MultipleActions")]
[HttpPut("MultipleActions")]
public void MultipleActions()
{
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ApiExplorer
{
[Route("ApiExplorerNameSetByConvention")]
public class ApiExplorerNameSetByConventionController : Controller
{
[HttpGet]
public void Get()
{
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ApiExplorer
{
[ApiExplorerSettings(GroupName = "SetOnController")]
[Route("ApiExplorerNameSetExplicitly")]
public class ApiExplorerNameSetExplicitlyController : Controller
{
[HttpGet("SetOnController")]
public void SetOnController()
{
}
[ApiExplorerSettings(GroupName = "SetOnAction")]
[HttpGet("SetOnAction")]
public void SetOnAction()
{
}
}
}

View File

@ -0,0 +1,46 @@
// 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.Threading.Tasks;
namespace ApiExplorer
{
[Route("ApiExplorerResponseContentType/[Action]")]
public class ApiExplorerResponseContentTypeController : Controller
{
[HttpGet]
public Product Unset()
{
return null;
}
[HttpGet]
[Produces("*/*")]
public Product AllTypes()
{
return null;
}
[HttpGet]
[Produces("text/*")]
public Product Range()
{
return null;
}
[HttpGet]
[Produces("application/json")]
public Product Specific()
{
return null;
}
[HttpGet]
[Produces("application/hal+json")]
public Product NoMatch()
{
return null;
}
}
}

View File

@ -0,0 +1,26 @@
// 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.Threading.Tasks;
namespace ApiExplorer
{
[Produces("text/xml")]
[Route("ApiExplorerResponseContentTypeOverrideOnAction")]
public class ApiExplorerResponseContentTypeOverrideOnActionController : Controller
{
[HttpGet("Controller")]
public Product GetController()
{
return null;
}
[HttpGet("Action")]
[Produces("application/json")]
public Product GetAction()
{
return null;
}
}
}

View File

@ -0,0 +1,25 @@
// 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.Threading.Tasks;
namespace ApiExplorer
{
[Produces("*/*", Type = typeof(Product))]
[Route("ApiExplorerResponseTypeOverrideOnAction")]
public class ApiExplorerResponseTypeOverrideOnActionController : Controller
{
[HttpGet("Controller")]
public void GetController()
{
}
[HttpGet("Action")]
[ProducesType(typeof(Customer))]
public object GetAction()
{
return null;
}
}
}

View File

@ -0,0 +1,46 @@
// 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.Threading.Tasks;
namespace ApiExplorer
{
[Route("ApiExplorerResponseTypeWithAttribute/[Action]")]
public class ApiExplorerResponseTypeWithAttributeController : Controller
{
[HttpGet]
[ProducesType(typeof(Customer))]
public void GetVoid()
{
}
[HttpGet]
[Produces("*/*", Type = typeof(Product))]
public object GetObject()
{
return null;
}
[HttpGet]
[Produces("application/json", Type = typeof(string))]
public IActionResult GetIActionResult()
{
return new EmptyResult();
}
[HttpGet]
[Produces("application/json", Type = typeof(int))]
public Task GetTask()
{
return null;
}
[HttpGet]
[ProducesType(typeof(Customer))] // It's possible to lie about what type you return
public Product GetProduct()
{
return null;
}
}
}

View File

@ -0,0 +1,83 @@
// 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.Threading.Tasks;
namespace ApiExplorer
{
[Route("ApiExplorerResponseTypeWithoutAttribute/[Action]")]
public class ApiExplorerResponseTypeWithoutAttributeController : Controller
{
[HttpGet]
public void GetVoid()
{
}
[HttpGet]
public object GetObject()
{
return null;
}
[HttpGet]
public IActionResult GetIActionResult()
{
return new EmptyResult();
}
[HttpGet]
public ObjectResult GetDerivedActionResult()
{
return new ObjectResult(null);
}
[HttpGet]
public Product GetProduct()
{
return null;
}
[HttpGet]
public int GetInt()
{
return 0;
}
[HttpGet]
public Task GetTask()
{
return Task.FromResult<object>(null);
}
[HttpGet]
public Task<object> GetTaskOfObject()
{
return Task.FromResult<object>(null);
}
[HttpGet]
public Task<IActionResult> GetTaskOfIActionResult()
{
return Task.FromResult<IActionResult>(new EmptyResult());
}
[HttpGet]
public Task<ObjectResult> GetTaskOfDerivedActionResult()
{
return Task.FromResult<ObjectResult>(new ObjectResult(null));
}
[HttpGet]
public Task<Product> GetTaskOfProduct()
{
return Task.FromResult<Product>(null);
}
[HttpGet]
public Task<int> GetTaskOfInt()
{
return Task.FromResult<int>(0);
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ApiExplorer
{
[Route("ApiExplorerVisbilityDisabledByConvention")]
public class ApiExplorerVisbilityDisabledByConventionController : Controller
{
[HttpGet]
public void Get()
{
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ApiExplorer
{
[Route("ApiExplorerVisbilityEnabledByConvention")]
public class ApiExplorerVisbilityEnabledByConventionController : Controller
{
[HttpGet]
public void Get()
{
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ApiExplorer
{
[ApiExplorerSettings(IgnoreApi = true)]
[Route("ApiExplorerVisibilitySetExplicitly")]
public class ApiExplorerVisibilitySetExplicitlyController : Controller
{
[ApiExplorerSettings(IgnoreApi = false)]
[HttpGet("Enabled")]
public void Enabled()
{
}
[HttpGet("Disabled")]
public void Disabled()
{
}
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace ApiExplorer
{
public class Customer
{
}
}

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace ApiExplorer
{
public class Product
{
}
}

View File

@ -0,0 +1,25 @@
// 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 Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Description;
using Microsoft.AspNet.Mvc.HeaderValueAbstractions;
namespace ApiExplorer
{
public class ProducesTypeAttribute : ResultFilterAttribute, IApiResponseMetadataProvider
{
public ProducesTypeAttribute(Type type)
{
Type = type;
}
public Type Type { get; private set; }
public void SetContentTypes(IList<MediaTypeHeaderValue> contentTypes)
{
}
}
}

View File

@ -0,0 +1,44 @@
// 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.Builder;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
namespace ApiExplorer
{
public class Startup
{
public void Configure(IApplicationBuilder app)
{
var configuration = app.GetTestConfiguration();
app.UseServices(services =>
{
services.AddMvc(configuration);
services.AddSingleton<ApiExplorerDataFilter>();
services.SetupOptions<MvcOptions>(options =>
{
options.Filters.AddService(typeof(ApiExplorerDataFilter));
options.ApplicationModelConventions.Add(new ApiExplorerVisibilityEnabledConvention());
options.ApplicationModelConventions.Add(new ApiExplorerVisibilityDisabledConvention(
typeof(ApiExplorerVisbilityDisabledByConventionController)));
options.OutputFormatters.Clear();
options.OutputFormatters.Add(new JsonOutputFormatter(
JsonOutputFormatter.CreateDefaultSettings(),
indent: false));
options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
});
});
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller}/{action}");
});
}
}
}

View File

@ -0,0 +1,11 @@
{
"dependencies": {
"Microsoft.AspNet.Mvc": "",
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Mvc.TestConfiguration": ""
},
"frameworks": {
"aspnet50": { },
"aspnetcore50": { }
}
}