parent
a73d073eea
commit
cb88e906b2
|
|
@ -38,12 +38,12 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType);
|
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType);
|
||||||
|
|
||||||
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
|
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
|
||||||
if (responseMetadataAttributes.Count == 0 &&
|
if (!HasSignificantMetadataProvider(responseMetadataAttributes) &&
|
||||||
action.Properties.TryGetValue(typeof(ApiConventionResult), out var result))
|
action.Properties.TryGetValue(typeof(ApiConventionResult), out var result))
|
||||||
{
|
{
|
||||||
// Action does not have any conventions. Use conventions on it if present.
|
// Action does not have any conventions. Use conventions on it if present.
|
||||||
var apiConventionResult = (ApiConventionResult)result;
|
var apiConventionResult = (ApiConventionResult)result;
|
||||||
responseMetadataAttributes = apiConventionResult.ResponseMetadataProviders;
|
responseMetadataAttributes.AddRange(apiConventionResult.ResponseMetadataProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultErrorType = typeof(void);
|
var defaultErrorType = typeof(void);
|
||||||
|
|
@ -56,11 +56,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
return apiResponseTypes;
|
return apiResponseTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<IApiResponseMetadataProvider> GetResponseMetadataAttributes(ControllerActionDescriptor action)
|
private static List<IApiResponseMetadataProvider> GetResponseMetadataAttributes(ControllerActionDescriptor action)
|
||||||
{
|
{
|
||||||
if (action.FilterDescriptors == null)
|
if (action.FilterDescriptors == null)
|
||||||
{
|
{
|
||||||
return Array.Empty<IApiResponseMetadataProvider>();
|
return new List<IApiResponseMetadataProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
|
// This technique for enumerating filters will intentionally ignore any filter that is an IFilterFactory
|
||||||
|
|
@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
return action.FilterDescriptors
|
return action.FilterDescriptors
|
||||||
.Select(fd => fd.Filter)
|
.Select(fd => fd.Filter)
|
||||||
.OfType<IApiResponseMetadataProvider>()
|
.OfType<IApiResponseMetadataProvider>()
|
||||||
.ToArray();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ICollection<ApiResponseType> GetApiResponseTypes(
|
private ICollection<ApiResponseType> GetApiResponseTypes(
|
||||||
|
|
@ -188,7 +188,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap the type if it's a Task<T>. The Task (non-generic) case was already handled.
|
// Unwrap the type if it's a Task<T>. The Task (non-generic) case was already handled.
|
||||||
Type unwrappedType = declaredReturnType;
|
var unwrappedType = declaredReturnType;
|
||||||
if (declaredReturnType.IsGenericType &&
|
if (declaredReturnType.IsGenericType &&
|
||||||
declaredReturnType.GetGenericTypeDefinition() == typeof(Task<>))
|
declaredReturnType.GetGenericTypeDefinition() == typeof(Task<>))
|
||||||
{
|
{
|
||||||
|
|
@ -228,5 +228,24 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
{
|
{
|
||||||
return statusCode >= 400 && statusCode < 500;
|
return statusCode >= 400 && statusCode < 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasSignificantMetadataProvider(IReadOnlyList<IApiResponseMetadataProvider> providers)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < providers.Count; i++)
|
||||||
|
{
|
||||||
|
var provider = providers[i];
|
||||||
|
|
||||||
|
if (provider is ProducesAttribute producesAttribute && producesAttribute.Type is null)
|
||||||
|
{
|
||||||
|
// ProducesAttribute that does not specify type is considered not significant.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other IApiResponseMetadataProvider is considered significant
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
|
|
||||||
private static void DiscoverApiConvention(ActionModel action)
|
private static void DiscoverApiConvention(ActionModel action)
|
||||||
{
|
{
|
||||||
if (action.Filters.OfType<IApiResponseMetadataProvider>().Any())
|
|
||||||
{
|
|
||||||
// If an action already has providers, don't discover any from conventions.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var controller = action.Controller;
|
var controller = action.Controller;
|
||||||
var apiConventionAttributes = controller.Attributes.OfType<ApiConventionTypeAttribute>().ToArray();
|
var apiConventionAttributes = controller.Attributes.OfType<ApiConventionTypeAttribute>().ToArray();
|
||||||
if (apiConventionAttributes.Length == 0)
|
if (apiConventionAttributes.Length == 0)
|
||||||
|
|
@ -550,6 +550,119 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiResponseTypes_CombinesProducesAttributeAndConventions()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel));
|
||||||
|
actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/json"), FilterScope.Controller));
|
||||||
|
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
|
||||||
|
{
|
||||||
|
new ProducesResponseTypeAttribute(200),
|
||||||
|
new ProducesResponseTypeAttribute(400),
|
||||||
|
new ProducesDefaultResponseTypeAttribute(),
|
||||||
|
});
|
||||||
|
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));
|
||||||
|
|
||||||
|
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(ProblemDetails), responseType.Type);
|
||||||
|
Assert.Collection(
|
||||||
|
responseType.ApiResponseFormats,
|
||||||
|
format => Assert.Equal("application/json", format.MediaType));
|
||||||
|
},
|
||||||
|
responseType =>
|
||||||
|
{
|
||||||
|
Assert.Equal(200, responseType.StatusCode);
|
||||||
|
Assert.Equal(typeof(DerivedModel), responseType.Type);
|
||||||
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
|
Assert.Collection(
|
||||||
|
responseType.ApiResponseFormats,
|
||||||
|
format => Assert.Equal("application/json", format.MediaType));
|
||||||
|
},
|
||||||
|
responseType =>
|
||||||
|
{
|
||||||
|
Assert.Equal(400, responseType.StatusCode);
|
||||||
|
Assert.Equal(typeof(ProblemDetails), responseType.Type);
|
||||||
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
|
Assert.Collection(
|
||||||
|
responseType.ApiResponseFormats,
|
||||||
|
format => Assert.Equal("application/json", format.MediaType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiResponseTypes_DoesNotCombineProducesAttributeThatSpecifiesType()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel));
|
||||||
|
actionDescriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute("application/json") { Type = typeof(string) }, FilterScope.Controller));
|
||||||
|
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
|
||||||
|
{
|
||||||
|
new ProducesResponseTypeAttribute(200),
|
||||||
|
new ProducesResponseTypeAttribute(400),
|
||||||
|
new ProducesDefaultResponseTypeAttribute(),
|
||||||
|
});
|
||||||
|
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));
|
||||||
|
|
||||||
|
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(string), responseType.Type);
|
||||||
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
|
Assert.Collection(
|
||||||
|
responseType.ApiResponseFormats,
|
||||||
|
format => Assert.Equal("application/json", format.MediaType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetApiResponseTypes_DoesNotCombineProducesResponseTypeAttributeThatSpecifiesStatusCode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var actionDescriptor = GetControllerActionDescriptor(typeof(TestController), nameof(TestController.PutModel));
|
||||||
|
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
|
||||||
|
{
|
||||||
|
new ProducesResponseTypeAttribute(200),
|
||||||
|
});
|
||||||
|
actionDescriptor.Properties[typeof(ProducesErrorResponseTypeAttribute)] = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));
|
||||||
|
|
||||||
|
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(DerivedModel), responseType.Type);
|
||||||
|
Assert.False(responseType.IsDefaultResponse);
|
||||||
|
Assert.Collection(
|
||||||
|
responseType.ApiResponseFormats,
|
||||||
|
format => Assert.Equal("application/json", format.MediaType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static ApiResponseTypeProvider GetProvider()
|
private static ApiResponseTypeProvider GetProvider()
|
||||||
{
|
{
|
||||||
var mvcOptions = new MvcOptions
|
var mvcOptions = new MvcOptions
|
||||||
|
|
|
||||||
|
|
@ -15,38 +15,6 @@ namespace Microsoft.AspNetCore.Mvc.ApplicationModels
|
||||||
{
|
{
|
||||||
public class ApiConventionApplicationModelConventionTest
|
public class ApiConventionApplicationModelConventionTest
|
||||||
{
|
{
|
||||||
[Fact]
|
|
||||||
public void Apply_DoesNotAddConventionItem_IfActionHasProducesResponseTypeAttribute()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var actionModel = GetActionModel(nameof(TestController.Delete));
|
|
||||||
actionModel.Filters.Add(new ProducesResponseTypeAttribute(200));
|
|
||||||
|
|
||||||
var convention = GetConvention();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
convention.Apply(actionModel);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.DoesNotContain(typeof(ApiConventionResult), actionModel.Properties.Keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Apply_DoesNotAddConventionItem_IfActionHasProducesAttribute()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var actionModel = GetActionModel(nameof(TestController.Delete));
|
|
||||||
actionModel.Filters.Add(new ProducesAttribute(typeof(object)));
|
|
||||||
|
|
||||||
var convention = GetConvention();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
convention.Apply(actionModel);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.DoesNotContain(typeof(ApiConventionResult), actionModel.Properties.Keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Apply_DoesNotAddConventionItem_IfNoConventionMatches()
|
public void Apply_DoesNotAddConventionItem_IfNoConventionMatches()
|
||||||
{
|
{
|
||||||
|
|
@ -2,14 +2,12 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
using Microsoft.AspNetCore.Mvc.ApplicationModels;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModel
|
namespace Microsoft.AspNetCore.Mvc.Test.ApplicationModels
|
||||||
{
|
{
|
||||||
public class RouteTokenTransformerConventionTest
|
public class RouteTokenTransformerConventionTest
|
||||||
{
|
{
|
||||||
|
|
@ -1277,6 +1277,41 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApiConvention_ForPostActionWithProducesAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMediaTypes = new[] { "application/json", "text/json", };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var response = await Client.PostAsync(
|
||||||
|
$"ApiExplorerResponseTypeWithApiConventionController/PostWithProduces",
|
||||||
|
new StringContent(string.Empty));
|
||||||
|
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.True(responseType.IsDefaultResponse);
|
||||||
|
},
|
||||||
|
responseType =>
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
|
||||||
|
Assert.Equal(201, responseType.StatusCode);
|
||||||
|
Assert.Empty(responseType.ResponseFormats);
|
||||||
|
},
|
||||||
|
responseType =>
|
||||||
|
{
|
||||||
|
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
|
||||||
|
Assert.Equal(400, responseType.StatusCode);
|
||||||
|
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApiConvention_ForPutActionThatMatchesConvention()
|
public async Task ApiConvention_ForPutActionThatMatchesConvention()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ namespace ApiExplorerWebSite
|
||||||
[ProducesResponseType(403)]
|
[ProducesResponseType(403)]
|
||||||
public IActionResult PostWithConventions() => null;
|
public IActionResult PostWithConventions() => null;
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Produces("application/json", "text/json")]
|
||||||
|
public IActionResult PostWithProduces(Product p) => null;
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public Task<IActionResult> PostTaskOfProduct(Product p) => null;
|
public Task<IActionResult> PostTaskOfProduct(Product p) => null;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue