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 10, 2016
1 parent 5612ca8 commit 19d4b9a
Show file tree
Hide file tree
Showing 11 changed files with 558 additions and 114 deletions.
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
24 changes: 24 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,27 @@ 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 or void.
/// </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>
/// Http response status code
/// </summary>
public int StatusCode { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,35 +114,22 @@ private ApiDescription CreateApiDescription(

// 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 responseFormats = GetResponseFormats(action, responseMetadataAttributes, runtimeReturnType);
foreach (var responseFormat in responseFormats)
{
// 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.SupportedResponseFormats.Add(responseFormat);
}

// 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 @@ -371,6 +358,23 @@ private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
{
var results = new List<ApiResponseFormat>();

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

// If the action returns a type, then automatically consider it as a possible api response format with
// a status code 200 OK.
// NOTE: In some cases this might sound incorrect, for example in the case where the action is returning
// 'void' or 'Task'. We want to keep it this way to enable users to override the type via the response
// metadata attributes. So an exmple is that action returns void but there is a Produces attribute which
// sets the response type to be 'Produces'
if (type != null)
{
// This return type can be overriden by any response metadata
// attributes later if the user wishes to.
responseTypes[200] = 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 +383,11 @@ private IReadOnlyList<ApiResponseFormat> GetResponseFormats(
foreach (var metadataAttribute in responseMetadataAttributes)
{
metadataAttribute.SetContentTypes(contentTypes);

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

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

foreach (var contentType in contentTypes)
var responseFormatMetadataProviders = _outputFormatters.OfType<IApiResponseFormatMetadataProvider>();

foreach (var responseType in responseTypes)
{
foreach (var formatter in _outputFormatters)
if (responseType.Value == typeof(void))
{
var responseFormatMetadataProvider = formatter as IApiResponseFormatMetadataProvider;
if (responseFormatMetadataProvider != null)
results.Add(new ApiResponseFormat()
{
var supportedTypes = responseFormatMetadataProvider.GetSupportedContentTypes(contentType, type);
StatusCode = responseType.Key,
ResponseType = responseType.Value
});

if (supportedTypes != null)
continue;
}

var responseTypeModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType.Value);

foreach (var contentType in contentTypes)
{
foreach (var responseFormatMetadataProvider in responseFormatMetadataProviders)
{
var formatterSupportedContentTypes = responseFormatMetadataProvider.GetSupportedContentTypes(
contentType,
responseType.Value);

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

foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
{
results.Add(new ApiResponseFormat()
{
results.Add(new ApiResponseFormat()
{
Formatter = formatter,
MediaType = supportedType,
});
}
Formatter = (IOutputFormatter)responseFormatMetadataProvider,
MediaType = formatterSupportedContentType,
ResponseType = responseType.Value,
StatusCode = responseType.Key,
ResponseModelMetadata = responseTypeModelMetadata
});
}
}
}
Expand Down Expand Up @@ -445,28 +475,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 Expand Up @@ -731,5 +741,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 @@ -7,7 +7,8 @@
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
{
Expand All @@ -16,6 +17,11 @@ public interface IApiResponseMetadataProvider
/// </summary>
Type Type { get; }

/// <summary>
/// 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
10 changes: 9 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 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.Extensions.Primitives;
using Microsoft.Net.Http.Headers;

namespace Microsoft.AspNetCore.Mvc
Expand Down Expand Up @@ -64,6 +64,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 Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;

namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Specifies the the type of the value and status code 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, IFilterMetadata
{
/// <summary>
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> of object that is going to be written in the response.</param>
/// <param name="statusCode">Http response status code</param>
public ProducesResponseTypeAttribute(Type type, int statusCode)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

Type = type;
StatusCode = statusCode;
}

/// <summary>
/// The type of the value returned by an action.
/// </summary>
public Type Type { get; set; }

/// <summary>
/// Http status code response.
/// </summary>
public int StatusCode { get; set; }

public void SetContentTypes(MediaTypeCollection contentTypes)
{
// Users are supposed to use the 'Produces' attribute to set the content types that an aciton can support.
}
}
}
Loading

0 comments on commit 19d4b9a

Please sign in to comment.