Skip to content

Add support for setting error type via ProducesResponseType attribute #30236

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions src/Mvc/Mvc.ApiExplorer/src/ApiResponseTypeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,21 @@ private ICollection<ApiResponseType> GetApiResponseTypes(

if (apiResponseType.Type == typeof(void))
{
if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created))
var statusOkWithType = type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created);
var hasDefaultErrorTypeOverride = metadataAttribute is ProducesResponseTypeAttribute producesResponseTypeAttribute && producesResponseTypeAttribute.OverrideDefaultErrorType;
var errorOrDefaultResponse = IsClientError(statusCode) || apiResponseType.IsDefaultResponse;
// ProducesResponseTypeAttribute's constructor defaults to setting "Type" to void when no value is specified.
// In this event, use the action's return type for 200 or 201 status codes. This lets you decorate an action with a
// [ProducesResponseType(201)] instead of [ProducesResponseType(201, typeof(Person)] when typeof(Person) can be inferred
// from the return type.
if (statusOkWithType)
{
// ProducesResponseTypeAttribute's constructor defaults to setting "Type" to void when no value is specified.
// In this event, use the action's return type for 200 or 201 status codes. This lets you decorate an action with a
// [ProducesResponseType(201)] instead of [ProducesResponseType(201, typeof(Person)] when typeof(Person) can be inferred
// from the return type.
apiResponseType.Type = type;
}
else if (IsClientError(statusCode) || apiResponseType.IsDefaultResponse)
// Use the default error type for "default" responses or 4xx client errors if no response type is specified.
// Unless falling back to the default error type is explicitly disabled.
else if (errorOrDefaultResponse && !hasDefaultErrorTypeOverride)
{
// Use the default error type for "default" responses or 4xx client errors if no response type is specified.
apiResponseType.Type = defaultErrorType;
}
}
Expand Down
27 changes: 26 additions & 1 deletion src/Mvc/Mvc.Core/src/ProducesResponseTypeAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class ProducesResponseTypeAttribute : Attribute, IApiResponseMetadataProv
{
/// <summary>
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
/// </summary>
/// </summary>
/// <param name="statusCode">The HTTP response status code.</param>
public ProducesResponseTypeAttribute(int statusCode)
: this(typeof(void), statusCode)
Expand All @@ -31,6 +31,18 @@ public ProducesResponseTypeAttribute(Type type, int statusCode)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
StatusCode = statusCode;
OverrideDefaultErrorType = false;
}

/// <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">The HTTP response status code.</param>
/// <param name="overrideDefaultErrorType">Whether or not to override default error type</param>
public ProducesResponseTypeAttribute(Type type, int statusCode, bool overrideDefaultErrorType) : this(type, statusCode)
{
OverrideDefaultErrorType = overrideDefaultErrorType;
}

/// <summary>
Expand All @@ -43,6 +55,19 @@ public ProducesResponseTypeAttribute(Type type, int statusCode)
/// </summary>
public int StatusCode { get; set; }

/// <summary>
/// Gets or sets whether or not the default error type should be used when one
/// is expliclty provided as a type in the <see cref="ProducesResponseTypeAttribute"/>.
Comment on lines +59 to +60
Copy link
Member

@SteveSandersonMS SteveSandersonMS Feb 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading this correctly, the flag means the opposite of what the sentence above implies. That is, true means we use the explicitly-provided one, and false means we use the global one. Also typo in "explicitly".

Suggested change
/// Gets or sets whether or not the default error type should be used when one
/// is expliclty provided as a type in the <see cref="ProducesResponseTypeAttribute"/>.
/// Gets or sets whether or not the default error type should be overridden when one
/// is explicitly provided as a type in the <see cref="ProducesResponseTypeAttribute"/>.

///
/// When <see langword="true"/>, the ApiExplorer will use the provided <see cref="Type"/>
/// as the default type for error objects.
///
/// When <see langword="false"/>, the ApiExplorer will use the globally configured default
/// error type.
/// </summary>
/// <value></value>
public bool OverrideDefaultErrorType { get; set; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to alternative ideas for names here. My creativity puttered and this was the most explicit name that I could come up with.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll play around with this to get a sense for the bug. Declaring the error behavior here is too far disconnected from the implementation that configures one to make sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another approach is to change the below:

var defaultErrorType = options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails);

to read from an option other than SuppressMapClientErrors.


/// <inheritdoc />
void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,9 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.ValidationSta
Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.ValidationVisitor(Microsoft.AspNetCore.Mvc.ActionContext! actionContext, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IModelValidatorProvider! validatorProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache! validatorCache, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider! metadataProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationStateDictionary? validationState) -> void
Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.ValidatorProvider.get -> Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IModelValidatorProvider!
Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache.GetValidators(Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata! metadata, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IModelValidatorProvider! validatorProvider) -> System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IModelValidator!>!
~Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.ProducesResponseTypeAttribute(System.Type type, int statusCode, bool overrideDefaultErrorType) -> void
Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.OverrideDefaultErrorType.get -> bool
Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute.OverrideDefaultErrorType.set -> void
Microsoft.AspNetCore.Mvc.Routing.DynamicRouteValueTransformer.State.get -> object?
Microsoft.AspNetCore.Mvc.Routing.DynamicRouteValueTransformer.State.set -> void
Microsoft.AspNetCore.Mvc.Routing.HttpMethodAttribute.HttpMethodAttribute(System.Collections.Generic.IEnumerable<string!>! httpMethods) -> void
Expand Down
23 changes: 23 additions & 0 deletions src/Mvc/test/Mvc.FunctionalTests/ApiExplorerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,29 @@ public async Task ApiConvention_ForActionWithApiConventionMethod()
});
}

[Theory]
[InlineData("ActionWithVoidTypeAndOverrideDisabled", typeof(ProblemDetails))]
[InlineData("ActionWithVoidType", typeof(void))]
public async Task ApiAction_ForActionWithVoidResponseType(string path, Type type)
{
// Act
var response = await Client.GetAsync($"http://localhost/ApiExplorerVoid/{path}");

var responseBody = await response.EnsureSuccessStatusCode().Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ApiExplorerData>>(responseBody);

// Assert
var description = Assert.Single(result);
Assert.Collection(
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.Equal(type.FullName, responseType.ResponseType);
Assert.Equal(401, responseType.StatusCode);
Assert.False(responseType.IsDefaultResponse);
});
}

private IEnumerable<string> GetSortedMediaTypes(ApiExplorerResponseType apiResponseType)
{
return apiResponseType.ResponseFormats
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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;

namespace ApiExplorerWebSite
{
[Route("ApiExplorerVoid/[action]")]
[ApiController]
public class ApiExplorerVoidController : Controller
{
[ProducesResponseType(typeof(void), 401, true)]
public IActionResult ActionWithVoidType() => Ok();

[ProducesResponseType(typeof(void), 401)]
public IActionResult ActionWithVoidTypeAndOverrideDisabled() => Ok();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Extra line

}
}