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 30, 2016
1 parent fb81a5e commit 3670144
Show file tree
Hide file tree
Showing 14 changed files with 627 additions and 164 deletions.
19 changes: 1 addition & 18 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 All @@ -76,6 +59,6 @@ public class ApiDescription
/// Will be empty 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 IList<ApiResponseFormat> SupportedResponseFormats { get; } = new List<ApiResponseFormat>();
public IList<ApiResponseType> SupportedResponseTypes { get; } = new List<ApiResponseType>();
}
}
10 changes: 5 additions & 5 deletions src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseFormat.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 Microsoft.AspNetCore.Mvc.Formatters;

namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
/// <summary>
/// Represents a possible format for the body of a response.
/// Possible format for an <see cref="ApiResponseType"/>.
/// </summary>
public class ApiResponseFormat
{
/// <summary>
/// The formatter used to output this response.
/// Gets or sets the formatter used to output this response.
/// </summary>
public IOutputFormatter Formatter { get; set; }

/// <summary>
/// The media type of the response.
/// Gets or sets the media type of the response.
/// </summary>
public string MediaType { get; set; }
}
}
}
43 changes: 43 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.ApiExplorer/ApiResponseType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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 Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
/// <summary>
/// Possible type of the response body which is formatted by <see cref="ApiResponseFormats"/>.
/// </summary>
public class ApiResponseType
{
/// <summary>
/// Gets or sets the response formats supported by this type.
/// </summary>
public IList<ApiResponseFormat> ApiResponseFormats { get; set; } = new List<ApiResponseFormat>();

/// <summary>
/// Gets or sets <see cref="ModelBinding.ModelMetadata"/> for the <see cref="Type"/> or null.
/// </summary>
/// <remarks>
/// Will be null if <see cref="Type"/> is null or void.
/// </remarks>
public ModelMetadata ModelMetadata { 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
/// <see cref="ProducesAttribute"/> or <see cref="ProducesResponseTypeAttribute"/> on an action method
/// to specify a response type.
/// </remarks>
public Type Type { get; set; }

/// <summary>
/// Gets or sets the HTTP response status code.
/// </summary>
public int StatusCode { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Reflection;
#endif
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Formatters;
Expand Down Expand Up @@ -112,37 +113,20 @@ private ApiDescription CreateApiDescription(
// 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);
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType);

// 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))
var apiResponseTypes = GetApiResponseTypes(action, responseMetadataAttributes, runtimeReturnType);
foreach (var apiResponseType in apiResponseTypes)
{
// 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);

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

// 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 @@ -364,13 +348,24 @@ private IReadOnlyList<ApiRequestFormat> GetRequestFormats(
return results;
}

private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
private IReadOnlyList<ApiResponseType> GetApiResponseTypes(
ControllerActionDescriptor action,
IApiResponseMetadataProvider[] responseMetadataAttributes,
Type type)
{
var results = new List<ApiResponseFormat>();
var results = new List<ApiResponseType>();

// Build list of all possible return types (and status codes) for an action.
var objectTypes = new Dictionary<int, Type>();

if (type != null && type != typeof(void))
{
// This return type can be overriden by any response metadata
// attributes later if the user wishes to.
objectTypes[StatusCodes.Status200OK] = type;
}

// Get the content type that the action explicitly set to support.
// 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();
Expand All @@ -379,6 +374,11 @@ private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
foreach (var metadataAttribute in responseMetadataAttributes)
{
metadataAttribute.SetContentTypes(contentTypes);

if (metadataAttribute.Type != null)
{
objectTypes[metadataAttribute.StatusCode] = metadataAttribute.Type;
}
}
}

Expand All @@ -387,28 +387,53 @@ private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
contentTypes.Add((string)null);
}

foreach (var contentType in contentTypes)
var responseTypeMetadataProviders = _outputFormatters.OfType<IApiResponseTypeMetadataProvider>();

foreach (var objectType in objectTypes)
{
foreach (var formatter in _outputFormatters)
if (objectType.Value == typeof(void))
{
var responseFormatMetadataProvider = formatter as IApiResponseFormatMetadataProvider;
if (responseFormatMetadataProvider != null)
results.Add(new ApiResponseType()
{
var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(contentType, type);
StatusCode = objectType.Key,
Type = objectType.Value
});

if (supportedTypes != null)
continue;
}

var apiResponseType = new ApiResponseType()
{
Type = objectType.Value,
StatusCode = objectType.Key,
ModelMetadata = _modelMetadataProvider.GetMetadataForType(objectType.Value)
};

foreach (var contentType in contentTypes)
{
foreach (var responseTypeMetadataProvider in responseTypeMetadataProviders)
{
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
contentType,
objectType.Value);

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

foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
{
apiResponseType.ApiResponseFormats.Add(new ApiResponseFormat()
{
results.Add(new ApiResponseFormat()
{
Formatter = formatter,
MediaType = supportedType,
});
}
Formatter = (IOutputFormatter)responseTypeMetadataProvider,
MediaType = formatterSupportedContentType,
});
}
}
}

results.Add(apiResponseType);
}

return results;
Expand Down Expand Up @@ -445,28 +470,8 @@ private static Type GetTaskInnerTypeOrNull(Type type)
return genericType?.GenericTypeArguments[0];
}

private Type GetRuntimeReturnType(Type declaredReturnType, IApiResponseMetadataProvider[] metadataAttributes)
private Type GetRuntimeReturnType(Type declaredReturnType)
{
// 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.
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
/// <summary>
/// Provides a return type and a set of possible content types returned by a successful execution of the action.
/// Provides a return type, status code and a set of possible content types returned by a
/// successful execution of the action.
/// </summary>
public interface IApiResponseMetadataProvider
{
/// <summary>
/// Optimistic return type of the action.
/// Gets the optimistic return type of the action.
/// </summary>
Type Type { get; }

/// <summary>
/// Gets the HTTP status code of the response.
/// </summary>
int StatusCode { get; }

/// <summary>
/// Configures a collection of allowed content types which can be produced by the action.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
/// An <see cref="Formatters.IOutputFormatter"/> should implement this interface to expose metadata information
/// to an <c>IApiDescriptionProvider</c>.
/// </remarks>
public interface IApiResponseFormatMetadataProvider
public interface IApiResponseTypeMetadataProvider
{
/// <summary>
/// Gets a filtered list of content types which are supported by the <see cref="Formatters.IOutputFormatter"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
/// <summary>
/// Writes an object to the output stream.
/// </summary>
public abstract class OutputFormatter : IOutputFormatter, IApiResponseFormatMetadataProvider
public abstract class OutputFormatter : IOutputFormatter, IApiResponseTypeMetadataProvider
{
/// <summary>
/// Gets the mutable collection of media type elements supported by
Expand Down
4 changes: 3 additions & 1 deletion src/Microsoft.AspNetCore.Mvc.Core/ProducesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
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.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc
Expand Down Expand Up @@ -64,6 +64,8 @@ public ProducesAttribute(string contentType, params string[] additionalContentTy

public MediaTypeCollection ContentTypes { get; set; }

public int StatusCode => StatusCodes.Status200OK;

public override void OnResultExecuting(ResultExecutingContext context)
{
if (context == null)
Expand Down
Loading

0 comments on commit 3670144

Please sign in to comment.