Introduce ProducesErrorResponseTypeAttribute

Fixes https://github.com/aspnet/Mvc/issues/8288
This commit is contained in:
Pranav K 2018-08-22 13:17:42 -07:00
parent 927e7c8bfc
commit f90a47c5af
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
6 changed files with 531 additions and 56 deletions

View File

@ -46,7 +46,13 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
responseMetadataAttributes = apiConventionResult.ResponseMetadataProviders; responseMetadataAttributes = apiConventionResult.ResponseMetadataProviders;
} }
var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, runtimeReturnType); var defaultErrorType = typeof(void);
if (action.Properties.TryGetValue(typeof(ProducesErrorResponseTypeAttribute), out result))
{
defaultErrorType = ((ProducesErrorResponseTypeAttribute)result).Type;
}
var apiResponseTypes = GetApiResponseTypes(responseMetadataAttributes, runtimeReturnType, defaultErrorType);
return apiResponseTypes; return apiResponseTypes;
} }
@ -69,7 +75,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
private ICollection<ApiResponseType> GetApiResponseTypes( private ICollection<ApiResponseType> GetApiResponseTypes(
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes, IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
Type type) Type type,
Type defaultErrorType)
{ {
var results = new Dictionary<int, ApiResponseType>(); var results = new Dictionary<int, ApiResponseType>();
@ -83,48 +90,39 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{ {
metadataAttribute.SetContentTypes(contentTypes); metadataAttribute.SetContentTypes(contentTypes);
ApiResponseType apiResponseType; var statusCode = metadataAttribute.StatusCode;
if (metadataAttribute is IApiDefaultResponseMetadataProvider) var apiResponseType = new ApiResponseType
{ {
apiResponseType = new ApiResponseType Type = metadataAttribute.Type,
StatusCode = statusCode,
IsDefaultResponse = metadataAttribute is IApiDefaultResponseMetadataProvider,
};
if (apiResponseType.Type == typeof(void))
{
if (type != null && (statusCode == StatusCodes.Status200OK || statusCode == StatusCodes.Status201Created))
{ {
IsDefaultResponse = true, // ProducesResponseTypeAttribute's constructor defaults to setting "Type" to void when no value is specified.
Type = metadataAttribute.Type, // 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.
else if (metadataAttribute.Type == typeof(void) && apiResponseType.Type = type;
type != null && }
(metadataAttribute.StatusCode == StatusCodes.Status200OK || metadataAttribute.StatusCode == StatusCodes.Status201Created)) else if (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.
apiResponseType = new ApiResponseType
{ {
StatusCode = metadataAttribute.StatusCode, // Use the default error type for "default" responses or 4xx client errors if no response type is specified.
Type = type, apiResponseType.Type = defaultErrorType;
}; }
}
else if (metadataAttribute.Type != null)
{
apiResponseType = new ApiResponseType
{
StatusCode = metadataAttribute.StatusCode,
Type = metadataAttribute.Type,
};
}
else
{
continue;
} }
results[apiResponseType.StatusCode] = apiResponseType; if (apiResponseType.Type != null)
{
results[apiResponseType.StatusCode] = apiResponseType;
}
} }
} }
// Set the default status only when no status has already been set explicitly // Set the default status only when no status has already been set explicitly
if (results.Count == 0 && type != null) if (results.Count == 0 && type != null)
{ {
@ -225,5 +223,10 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
return declaredReturnType; return declaredReturnType;
} }
private static bool IsClientError(int statusCode)
{
return statusCode >= 400 && statusCode < 500;
}
} }
} }

View File

@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
public class ApiBehaviorApplicationModelProvider : IApplicationModelProvider public class ApiBehaviorApplicationModelProvider : IApplicationModelProvider
{ {
private readonly ProducesErrorResponseTypeAttribute DefaultErrorType = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));
private readonly ApiBehaviorOptions _apiBehaviorOptions; private readonly ApiBehaviorOptions _apiBehaviorOptions;
private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly ModelStateInvalidFilter _modelStateInvalidFilter; private readonly ModelStateInvalidFilter _modelStateInvalidFilter;
@ -105,6 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
AddMultipartFormDataConsumesAttribute(actionModel); AddMultipartFormDataConsumesAttribute(actionModel);
DiscoverApiConvention(actionModel, conventions); DiscoverApiConvention(actionModel, conventions);
DiscoverErrorResponseType(actionModel);
} }
} }
} }
@ -274,6 +277,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
} }
internal void DiscoverErrorResponseType(ActionModel actionModel)
{
var errorTypeAttribute =
actionModel.Attributes.OfType<ProducesErrorResponseTypeAttribute>().FirstOrDefault() ??
actionModel.Controller.Attributes.OfType<ProducesErrorResponseTypeAttribute>().FirstOrDefault() ??
actionModel.Controller.ControllerType.Assembly.GetCustomAttribute<ProducesErrorResponseTypeAttribute>();
if (!_apiBehaviorOptions.SuppressMapClientErrors)
{
// If ClientErrorFactory is being used and the application does not supply a error response type, assume ProblemDetails.
errorTypeAttribute = errorTypeAttribute ?? DefaultErrorType;
}
if (errorTypeAttribute != null)
{
actionModel.Properties[typeof(ProducesErrorResponseTypeAttribute)] = errorTypeAttribute;
}
}
private bool ParameterExistsInAnyRoute(ActionModel actionModel, string parameterName) private bool ParameterExistsInAnyRoute(ActionModel actionModel, string parameterName)
{ {
foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(actionModel)) foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(actionModel))

View File

@ -0,0 +1,38 @@
// 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.Infrastructure;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// Specifies the type returned by default by controllers annotated with <see cref="ApiControllerAttribute"/>.
/// <para>
/// <see cref="Type"/> specifies the error model type associated with a <see cref="ProducesResponseTypeAttribute"/>
/// for a client error (HTTP Status Code 4xx) when no value is provided. When no value is specified, MVC assumes the
/// client error type to be <see cref="ProblemDetails"/>, if mapping client errors (<see cref="ApiBehaviorOptions.ClientErrorMapping"/>)
/// is used.
/// </para>
/// <para>
/// Use this <see cref="Attribute"/> to configure the default error type if your application uses a custom error type to respond.
/// </para>
/// </summary>
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ProducesErrorResponseTypeAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of <see cref="ProducesErrorResponseTypeAttribute"/>.
/// </summary>
/// <param name="type">The error type.</param>
public ProducesErrorResponseTypeAttribute(Type type)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
}
/// <summary>
/// Gets the default error type.
/// </summary>
public Type Type { get; }
}
}

View File

@ -309,6 +309,247 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
}); });
} }
[Fact]
public void GetApiResponseTypes_UsesErrorType_ForClientErrors()
{
// Arrange
var errorType = typeof(InvalidTimeZoneException);
var actionDescriptor = GetControllerActionDescriptor(
typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
{
new ProducesResponseTypeAttribute(200),
new ProducesResponseTypeAttribute(404),
new ProducesResponseTypeAttribute(415),
});
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType);
var provider = GetProvider();
// Act
var result = provider.GetApiResponseTypes(actionDescriptor);
// Assert
Assert.Collection(
result.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.Equal(200, responseType.StatusCode);
Assert.Equal(typeof(BaseModel), responseType.Type);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(404, responseType.StatusCode);
Assert.Equal(errorType, responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(415, responseType.StatusCode);
Assert.Equal(errorType, responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
});
}
[Fact]
public void GetApiResponseTypes_UsesErrorType_ForDefaultResponse()
{
// Arrange
var errorType = typeof(ProblemDetails);
var actionDescriptor = GetControllerActionDescriptor(
typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
{
new ProducesResponseTypeAttribute(200),
new ProducesDefaultResponseTypeAttribute(),
});
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType);
var provider = GetProvider();
// Act
var result = provider.GetApiResponseTypes(actionDescriptor);
// Assert
Assert.Collection(
result.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.Equal(errorType, responseType.Type);
Assert.True(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(200, responseType.StatusCode);
Assert.Equal(typeof(BaseModel), responseType.Type);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
});
}
[Fact]
public void GetApiResponseTypes_DoesNotUseErrorType_IfSpecified()
{
// Arrange
var errorType = typeof(InvalidTimeZoneException);
var actionDescriptor = GetControllerActionDescriptor(
typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
{
new ProducesResponseTypeAttribute(200),
new ProducesResponseTypeAttribute(typeof(DivideByZeroException), 415),
new ProducesDefaultResponseTypeAttribute(typeof(DivideByZeroException)),
});
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(errorType);
var provider = GetProvider();
// Act
var result = provider.GetApiResponseTypes(actionDescriptor);
// Assert
Assert.Collection(
result.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.Equal(typeof(DivideByZeroException), responseType.Type);
Assert.True(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(200, responseType.StatusCode);
Assert.Equal(typeof(BaseModel), responseType.Type);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(415, responseType.StatusCode);
Assert.Equal(typeof(DivideByZeroException), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
});
}
[Fact]
public void GetApiResponseTypes_DoesNotUseErrorType_ForNonClientErrors()
{
// Arrange
var actionDescriptor = GetControllerActionDescriptor(
typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
{
new ProducesResponseTypeAttribute(201),
new ProducesResponseTypeAttribute(300),
new ProducesResponseTypeAttribute(500),
});
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(InvalidTimeZoneException));
var provider = GetProvider();
// Act
var result = provider.GetApiResponseTypes(actionDescriptor);
// Assert
Assert.Collection(
result.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.Equal(201, responseType.StatusCode);
Assert.Equal(typeof(BaseModel), responseType.Type);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(300, responseType.StatusCode);
Assert.Equal(typeof(void), responseType.Type);
Assert.Empty(responseType.ApiResponseFormats);
},
responseType =>
{
Assert.Equal(500, responseType.StatusCode);
Assert.Equal(typeof(void), responseType.Type);
Assert.Empty(responseType.ApiResponseFormats);
});
}
[Fact]
public void GetApiResponseTypes_AllowsUsingVoid()
{
// Arrange
var actionDescriptor = GetControllerActionDescriptor(
typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
{
new ProducesResponseTypeAttribute(typeof(InvalidCastException), 400),
new ProducesResponseTypeAttribute(415),
new ProducesDefaultResponseTypeAttribute(),
});
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(void));
var provider = GetProvider();
// Act
var result = provider.GetApiResponseTypes(actionDescriptor);
// Assert
Assert.Collection(
result.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.True(responseType.IsDefaultResponse);
Assert.Equal(typeof(void), responseType.Type);
Assert.Empty(responseType.ApiResponseFormats);
},
responseType =>
{
Assert.Equal(400, responseType.StatusCode);
Assert.Equal(typeof(InvalidCastException), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(415, responseType.StatusCode);
Assert.Equal(typeof(void), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Empty(responseType.ApiResponseFormats);
});
}
private static ApiResponseTypeProvider GetProvider() private static ApiResponseTypeProvider GetProvider()
{ {
var mvcOptions = new MvcOptions var mvcOptions = new MvcOptions

View File

@ -12,7 +12,6 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@ -20,6 +19,8 @@ using Microsoft.Extensions.Options;
using Moq; using Moq;
using Xunit; using Xunit;
[assembly: Microsoft.AspNetCore.Mvc.ProducesErrorResponseType(typeof(InvalidEnumArgumentException))]
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
{ {
public class ApiBehaviorApplicationModelProviderTest public class ApiBehaviorApplicationModelProviderTest
@ -1042,9 +1043,6 @@ Environment.NewLine + "int b";
var actionModel = new ActionModel( var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)), typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
Array.Empty<object>()); Array.Empty<object>());
actionModel.Filters.Add(new AuthorizeFilter());
actionModel.Filters.Add(new ServiceFilterAttribute(typeof(object)));
actionModel.Filters.Add(new ConsumesAttribute("application/xml"));
var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) }; var attributes = new[] { new ApiConventionTypeAttribute(typeof(DefaultApiConventions)) };
// Act // Act
@ -1060,6 +1058,167 @@ Environment.NewLine + "int b";
}); });
} }
[Fact]
public void DiscoverErrorResponseType_SetsProblemDetails_IfActionHasNoAttributes()
{
// Arrange
var expected = typeof(ProblemDetails);
var controllerModel = new ControllerModel(typeof(object).GetTypeInfo(), new[] { new object() });
var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
Array.Empty<object>())
{
Controller = controllerModel,
};
var provider = GetProvider();
// Act
provider.DiscoverErrorResponseType(actionModel);
// Assert
Assert.Collection(
actionModel.Properties,
kvp =>
{
Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key);
var value = Assert.IsType<ProducesErrorResponseTypeAttribute>(kvp.Value);
Assert.Equal(expected, value.Type);
});
}
[Fact]
public void DiscoverErrorResponseType_DoesNotSetDefaultProblemDetailsResponse_IfSuppressMapClientErrorsIsSet()
{
// Arrange
var expected = typeof(ProblemDetails);
var controllerModel = new ControllerModel(typeof(object).GetTypeInfo(), new[] { new object() });
var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
Array.Empty<object>())
{
Controller = controllerModel,
};
var provider = GetProvider(new ApiBehaviorOptions
{
InvalidModelStateResponseFactory = _ => null,
SuppressMapClientErrors = true,
});
// Act
provider.DiscoverErrorResponseType(actionModel);
// Assert
Assert.Empty(actionModel.Properties);
}
[Fact]
public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnControllerAsssembly()
{
// Arrange
var expected = typeof(InvalidEnumArgumentException);
var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new object() });
var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
Array.Empty<object>())
{
Controller = controllerModel,
};
var provider = GetProvider();
// Act
provider.DiscoverErrorResponseType(actionModel);
// Assert
Assert.Collection(
actionModel.Properties,
kvp =>
{
Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key);
var value = Assert.IsType<ProducesErrorResponseTypeAttribute>(kvp.Value);
Assert.Equal(expected, value.Type);
});
}
[Fact]
public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnController()
{
// Arrange
var expected = typeof(InvalidTimeZoneException);
var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new ProducesErrorResponseTypeAttribute(expected) });
var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
Array.Empty<object>())
{
Controller = controllerModel,
};
var provider = GetProvider();
// Act
provider.DiscoverErrorResponseType(actionModel);
// Assert
Assert.Collection(
actionModel.Properties,
kvp =>
{
Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key);
var value = Assert.IsType<ProducesErrorResponseTypeAttribute>(kvp.Value);
Assert.Equal(expected, value.Type);
});
}
[Fact]
public void DiscoverErrorResponseType_UsesValueFromApiErrorTypeAttribute_SpecifiedOnAction()
{
// Arrange
var expected = typeof(InvalidTimeZoneException);
var controllerModel = new ControllerModel(typeof(TestApiConventionController).GetTypeInfo(), new[] { new ProducesErrorResponseTypeAttribute(typeof(Guid)) });
var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
new[] { new ProducesErrorResponseTypeAttribute(expected) })
{
Controller = controllerModel,
};
var provider = GetProvider();
// Act
provider.DiscoverErrorResponseType(actionModel);
// Assert
Assert.Collection(
actionModel.Properties,
kvp =>
{
Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key);
var value = Assert.IsType<ProducesErrorResponseTypeAttribute>(kvp.Value);
Assert.Equal(expected, value.Type);
});
}
[Fact]
public void DiscoverErrorResponseType_AllowsVoidsType()
{
// Arrange
var expected = typeof(void);
var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
new[] { new ProducesErrorResponseTypeAttribute(expected) });
var provider = GetProvider();
// Act
provider.DiscoverErrorResponseType(actionModel);
// Assert
Assert.Collection(
actionModel.Properties,
kvp =>
{
Assert.Equal(typeof(ProducesErrorResponseTypeAttribute), kvp.Key);
var value = Assert.IsType<ProducesErrorResponseTypeAttribute>(kvp.Value);
Assert.Equal(expected, value.Type);
});
}
[Fact] [Fact]
public void OnProvidersExecuting_AddsClientErrorResultFilter() public void OnProvidersExecuting_AddsClientErrorResultFilter()
{ {

View File

@ -1180,9 +1180,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(404, responseType.StatusCode); Assert.Equal(404, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}); });
} }
@ -1215,7 +1215,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ApiConvention_ForMethodWithResponseTypeAttributes() public async Task ApiConvention_ForMethodWithResponseTypeAttributes()
{ {
// Arrange // Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" }; var expectedMediaTypes = new[] { "application/json" };
// Act // Act
var response = await Client.PostAsync( var response = await Client.PostAsync(
@ -1236,15 +1236,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(403, responseType.StatusCode); Assert.Equal(403, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}); });
} }
[Fact] [Fact]
public async Task ApiConvention_ForPostMethodThatMatchesConvention() public async Task ApiConvention_ForPostMethodThatMatchesConvention()
{ {
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act // Act
var response = await Client.PostAsync( var response = await Client.PostAsync(
$"ApiExplorerResponseTypeWithApiConventionController/PostTaskOfProduct", $"ApiExplorerResponseTypeWithApiConventionController/PostTaskOfProduct",
@ -1268,15 +1271,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(400, responseType.StatusCode); Assert.Equal(400, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}); });
} }
[Fact] [Fact]
public async Task ApiConvention_ForPutActionThatMatchesConvention() public async Task ApiConvention_ForPutActionThatMatchesConvention()
{ {
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act // Act
var response = await Client.PutAsync( var response = await Client.PutAsync(
$"ApiExplorerResponseTypeWithApiConventionController/Put", $"ApiExplorerResponseTypeWithApiConventionController/Put",
@ -1300,21 +1306,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(400, responseType.StatusCode); Assert.Equal(400, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(404, responseType.StatusCode); Assert.Equal(404, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}); });
} }
[Fact] [Fact]
public async Task ApiConvention_ForDeleteActionThatMatchesConvention() public async Task ApiConvention_ForDeleteActionThatMatchesConvention()
{ {
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act // Act
var response = await Client.DeleteAsync( var response = await Client.DeleteAsync(
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync"); $"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync");
@ -1337,21 +1346,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(400, responseType.StatusCode); Assert.Equal(400, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(404, responseType.StatusCode); Assert.Equal(404, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}); });
} }
[Fact] [Fact]
public async Task ApiConvention_ForActionWtihApiConventionMethod() public async Task ApiConvention_ForActionWtihApiConventionMethod()
{ {
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act // Act
var response = await Client.PostAsync( var response = await Client.PostAsync(
"ApiExplorerResponseTypeWithApiConventionController/PostItem", "ApiExplorerResponseTypeWithApiConventionController/PostItem",
@ -1371,9 +1383,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}, },
responseType => responseType =>
{ {
Assert.Equal(typeof(void).FullName, responseType.ResponseType); Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(409, responseType.StatusCode); Assert.Equal(409, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats); Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
}); });
} }