Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Added new attribute ProducesResponseTypeAttribute to enable ApiExplor…
Browse files Browse the repository at this point in the history
…er to expose response type and StatusCode.

[Fixes #4101] StatusCode Metadata
  • Loading branch information
kichalla committed Mar 9, 2016
1 parent 5612ca8 commit 1f9048d
Show file tree
Hide file tree
Showing 9 changed files with 263 additions and 78 deletions.
26 changes: 26 additions & 0 deletions samples/MvcSandbox/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,40 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;

namespace MvcSandbox.Controllers
{
public class HomeController : Controller
{
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionProvider;

public HomeController(IApiDescriptionGroupCollectionProvider apiDescriptionProvider)
{
_apiDescriptionProvider = apiDescriptionProvider;
}

[ApiExplorerSettings(IgnoreApi = true)]
public IActionResult Index()
{
var apiDescriptions = _apiDescriptionProvider.ApiDescriptionGroups;
return View();
}

[Produces("application/json", "text/xml", Type = typeof(Customer))]
public IActionResult Foo()
{
return Ok("hello");
}
}

public class Customer
{
public int Id { get; set; }
}

public class Error
{
public string Message { get; set; }
}
}
17 changes: 0 additions & 17 deletions src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,6 @@ public class ApiDescription
/// </summary>
public string RelativePath { get; set; }

/// <summary>
/// Gets or sets <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>
/// Gets or sets 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
/// <c>ProducesAttribute</c> on an action method to specify a response type.
/// </remarks>
public Type ResponseType { get; set; }

/// <summary>
/// Gets the list of possible formats for a response.
/// </summary>
Expand Down
21 changes: 21 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseFormat.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. 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.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
Expand All @@ -19,5 +21,24 @@ public class ApiResponseFormat
/// The media type of the response.
/// </summary>
public string MediaType { get; set; }

/// <summary>
/// Gets or sets <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>
/// Gets or sets 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
/// <c>ProducesAttribute</c> on an action method to specify a response type.
/// </remarks>
public Type ResponseType { get; set; }

public int StatusCode { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,33 +116,39 @@ private ApiDescription CreateApiDescription(
// 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;
//// 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(runtimeReturnType);
// apiDescription.ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(runtimeReturnType);

var formats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
foreach (var format in formats)
{
apiDescription.SupportedResponseFormats.Add(format);
}
// var formats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
// foreach (var format in formats)
// {
// apiDescription.SupportedResponseFormats.Add(format);
// }
//}

var responseFormats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
foreach (var format in responseFormats)
{
apiDescription.SupportedResponseFormats.Add(format);
}

// It would be possible here to configure an action with multiple body parameters, in which case you
// could end up with duplicate data.
foreach (var parameter in apiDescription.ParameterDescriptions.Where(p => p.Source == BindingSource.Body))
{
var formats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type);
foreach (var format in formats)
var requestFormats = GetRequestFormats(action, requestMetadataAttributes, parameter.Type);
foreach (var format in requestFormats)
{
apiDescription.SupportedRequestFormats.Add(format);
}
Expand Down Expand Up @@ -367,47 +373,109 @@ private IReadOnlyList<ApiRequestFormat> GetRequestFormats(
private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
ControllerActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
Type type)
Type returnType)
{
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 MediaTypeCollection();
var actionSupportedContentTypes = new MediaTypeCollection();
if (responseMetadataAttributes != null)
{
foreach (var metadataAttribute in responseMetadataAttributes)
{
metadataAttribute.SetContentTypes(contentTypes);
metadataAttribute.SetContentTypes(actionSupportedContentTypes);
}
}

if (contentTypes.Count == 0)
if (actionSupportedContentTypes.Count == 0)
{
contentTypes.Add((string)null);
actionSupportedContentTypes.Add((string)null);
}

foreach (var contentType in contentTypes)
var responseTypes = new List<ResponseType>();

// Go through rest of metadata attributes to add their response types too
if (responseMetadataAttributes != null)
{
foreach (var formatter in _outputFormatters)
foreach (var respMetadata in responseMetadataAttributes)
{
var responseFormatMetadataProvider = formatter as IApiResponseFormatMetadataProvider;
if (responseFormatMetadataProvider != null)
if (respMetadata.Type != null)
{
var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(contentType, type);
responseTypes.Add(new ResponseType(respMetadata.Type, respMetadata.StatusCode));
}
}
}

if (supportedTypes != null)
// If the action returns a model type, then automatically consider it as a possible
// api response format.
if (responseTypes.Count == 0)
{
if (returnType != null)
{
responseTypes.Add(new ResponseType(returnType, 200));
}
}

//if (responseMetadataAttributes == null || responseMetadataAttributes.Length == 0)
//{
// if (returnType != null)
// {
// responseTypes.Add(new ResponseType(returnType, 200));
// }
//}
//else
//{
// foreach (var respMetadata in responseMetadataAttributes)
// {
// if (respMetadata.Type != null)
// {
// responseTypes.Add(new ResponseType(respMetadata.Type, respMetadata.StatusCode));
// }
// }
//}

var responseFormatMetadataProviders = _outputFormatters.OfType<IApiResponseFormatMetadataProvider>();

foreach (var responseType in responseTypes)
{
if (responseType.Type == typeof(void))
{
results.Add(new ApiResponseFormat()
{
ResponseType = responseType.Type
});

continue;
}

foreach (var actionSupportedContentType in actionSupportedContentTypes)
{
foreach (var responseFormatMetadataProvider in responseFormatMetadataProviders)
{

var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(
actionSupportedContentType,
responseType.Type);

if (supportedTypes == null)
{
foreach (var supportedType in supportedTypes)
continue;
}

foreach (var supportedType in supportedTypes)
{
results.Add(new ApiResponseFormat()
{
results.Add(new ApiResponseFormat()
{
Formatter = formatter,
MediaType = supportedType,
});
}
Formatter = (IOutputFormatter)responseFormatMetadataProvider,
MediaType = supportedType,
ResponseType = responseType.Type,
StatusCode = responseType.StatusCode,
ResponseModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType.Type)
});
}
}

}
}

Expand Down Expand Up @@ -731,5 +799,18 @@ public int GetHashCode(PropertyKey obj)
}
}
}

private class ResponseType
{
public ResponseType(Type type, int statusCode)
{
Type = type;
StatusCode = statusCode;
}

public Type Type { get; }

public int StatusCode { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface IApiResponseMetadataProvider
/// </summary>
Type Type { get; }

int StatusCode { get; }

/// <summary>
/// Configures a collection of allowed content types which can be produced by the action.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
Expand Down Expand Up @@ -64,6 +65,14 @@ public ProducesAttribute(string contentType, params string[] additionalContentTy

public MediaTypeCollection ContentTypes { get; set; }

public int StatusCode
{
get
{
return (int)HttpStatusCode.OK;
}
}

public override void OnResultExecuting(ResultExecutingContext context)
{
if (context == null)
Expand Down
49 changes: 49 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Core/ProducesResponseTypeAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) .NET Foundation. 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.Net;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Formatters.Internal;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Specifies the allowed content types and the type of the value returned by the action
/// which can be used to select a formatter while executing <see cref="ObjectResult"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class ProducesResponseTypeAttribute : Attribute, IApiResponseMetadataProvider
{
/// <summary>
/// Initializes an instance of <see cref="ProducesAttribute"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> of object that is going to be written in the response.</param>
/// <param name="statusCode"></param>
public ProducesResponseTypeAttribute(Type type, int statusCode)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

Type = type;
StatusCode = statusCode;
}

public Type Type { get; set; }

public int StatusCode { get; set; }

public void SetContentTypes(MediaTypeCollection contentTypes)
{
// do not do anything here
}
}
}
Loading

0 comments on commit 1f9048d

Please sign in to comment.