Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/Mvc/Mvc.Core/src/ApiBehaviorOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ public Func<ActionContext, IActionResult> InvalidModelStateResponseFactory
/// </value>
public bool SuppressMapClientErrors { get; set; }

/// <summary>
/// Gets or sets a value that determines if the status code pages middleware should be skipped
/// for controllers annotated with <see cref="ApiControllerAttribute"/>.
/// </summary>
public bool SkipStatusCodePages { get; set; }

/// <summary>
/// Gets a map of HTTP status codes to <see cref="ClientErrorData"/>. Configured values
/// are used to transform <see cref="IClientErrorActionResult"/> to an <see cref="ObjectResult"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,46 +8,49 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Http.Metadata;

namespace Microsoft.AspNetCore.Mvc.ApplicationModels;

internal sealed class ApiBehaviorApplicationModelProvider : IApplicationModelProvider
{
private readonly ApiBehaviorOptions _options;

public ApiBehaviorApplicationModelProvider(
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
IModelMetadataProvider modelMetadataProvider,
IServiceProvider serviceProvider)
{
var options = apiBehaviorOptions.Value;
_options = apiBehaviorOptions.Value;

ActionModelConventions = new List<IActionModelConvention>()
{
new ApiVisibilityConvention(),
};

if (!options.SuppressMapClientErrors)
if (!_options.SuppressMapClientErrors)
{
ActionModelConventions.Add(new ClientErrorResultFilterConvention());
}

if (!options.SuppressModelStateInvalidFilter)
if (!_options.SuppressModelStateInvalidFilter)
{
ActionModelConventions.Add(new InvalidModelStateFilterConvention());
}

if (!options.SuppressConsumesConstraintForFormFileParameters)
if (!_options.SuppressConsumesConstraintForFormFileParameters)
{
ActionModelConventions.Add(new ConsumesConstraintForFormFileParameterConvention());
}

var defaultErrorType = options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails);
var defaultErrorType = _options.SuppressMapClientErrors ? typeof(void) : typeof(ProblemDetails);
var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType);
ActionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute));

if (!options.SuppressInferBindingSourcesForParameters)
if (!_options.SuppressInferBindingSourcesForParameters)
{
var serviceProviderIsService = serviceProvider.GetService<IServiceProviderIsService>();
var convention = options.DisableImplicitFromServicesParameters || serviceProviderIsService is null ?
var convention = _options.DisableImplicitFromServicesParameters || serviceProviderIsService is null ?
new InferParameterBindingInfoConvention(modelMetadataProvider) :
new InferParameterBindingInfoConvention(modelMetadataProvider, serviceProviderIsService);
ActionModelConventions.Add(convention);
Expand Down Expand Up @@ -80,6 +83,14 @@ public void OnProvidersExecuting(ApplicationModelProviderContext context)
// Ensure ApiController is set up correctly
EnsureActionIsAttributeRouted(action);

if (_options.SkipStatusCodePages)
{
foreach (var selector in action.Selectors)
{
selector.EndpointMetadata.Add(SkipStatusCodePagesMetadata.Instance);
}
}

foreach (var convention in ActionModelConventions)
{
convention.Apply(action);
Expand Down Expand Up @@ -126,3 +137,9 @@ private static bool IsApiController(ControllerModel controller)
return assemblyAttributes.OfType<IApiBehaviorMetadata>().Any();
}
}

internal sealed class SkipStatusCodePagesMetadata : ISkipStatusCodePagesMetadata
{
public static readonly SkipStatusCodePagesMetadata Instance = new();
private SkipStatusCodePagesMetadata() { }
}
2 changes: 2 additions & 0 deletions src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@
*REMOVED*static Microsoft.Extensions.DependencyInjection.MvcCoreMvcBuilderExtensions.SetCompatibilityVersion(this Microsoft.Extensions.DependencyInjection.IMvcBuilder! builder, Microsoft.AspNetCore.Mvc.CompatibilityVersion version) -> Microsoft.Extensions.DependencyInjection.IMvcBuilder!
*REMOVED*static Microsoft.Extensions.DependencyInjection.MvcCoreMvcCoreBuilderExtensions.SetCompatibilityVersion(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder! builder, Microsoft.AspNetCore.Mvc.CompatibilityVersion version) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder!
*REMOVED*virtual Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions<TOptions>.PostConfigure(string? name, TOptions! options) -> void
Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.SkipStatusCodePages.get -> bool
Microsoft.AspNetCore.Mvc.ApiBehaviorOptions.SkipStatusCodePages.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,61 @@ public void OnProvidersExecuting_AppliesConventionsForIResult()
Assert.Empty(actionModel.Selectors[0].EndpointMetadata);
}

[Fact]
public void OnProvidersExecuting_AddsSkipStatusCodePagesMetadata_WhenOptionIsTrueAndControllerIsApiController()
{
// Arrange
var (controllerModel, actionModel) = CreateControllerModel(typeof(TestApiController), isApiController: true);
var context = CreateContext(controllerModel);
var provider = GetProvider(new ApiBehaviorOptions
{
SkipStatusCodePages = true,
});

// Act
provider.OnProvidersExecuting(context);

// Assert
var selector = Assert.Single(actionModel.Selectors);
var metadata = Assert.Single(selector.EndpointMetadata.OfType<SkipStatusCodePagesMetadata>());
Assert.Same(SkipStatusCodePagesMetadata.Instance, metadata);
}

[Fact]
public void OnProvidersExecuting_DoesNotAddSkipStatusCodePagesMetadata_WhenOptionIsTrueAndControllerIsNotApiController()
{
// Arrange
var (controllerModel, actionModel) = CreateControllerModel(typeof(TestMvcController), isApiController: false);
var context = CreateContext(controllerModel);
var provider = GetProvider(new ApiBehaviorOptions
{
SkipStatusCodePages = true,
});

// Act
provider.OnProvidersExecuting(context);

// Assert
var selector = Assert.Single(actionModel.Selectors);
Assert.Empty(selector.EndpointMetadata);
}

[Fact]
public void OnProvidersExecuting_DoesNotAddSkipStatusCodePagesMetadata_WhenOptionIsFalseAndControllerIsApiController()
{
// Arrange
var (controllerModel, actionModel) = CreateControllerModel(typeof(TestApiController), isApiController: true);
var context = CreateContext(controllerModel);
var provider = GetProvider(new ApiBehaviorOptions());

// Act
provider.OnProvidersExecuting(context);

// Assert
var selector = Assert.Single(actionModel.Selectors);
Assert.Empty(selector.EndpointMetadata);
}

[Fact]
public void Constructor_SetsUpConventions()
{
Expand Down Expand Up @@ -221,9 +276,41 @@ private static ApiBehaviorApplicationModelProvider GetProvider(
Mock.Of<IServiceProvider>());
}

private static ApplicationModelProviderContext CreateContext(ControllerModel controllerModel)
{
var context = new ApplicationModelProviderContext(new[] { controllerModel.ControllerType });
context.Result.Controllers.Add(controllerModel);
return context;
}

private static (ControllerModel ControllerModel, ActionModel ActionModel) CreateControllerModel(
Type controllerType,
bool isApiController)
{
var controllerAttributes = isApiController ? new object[] { new ApiControllerAttribute() } : Array.Empty<object>();
var controllerModel = new ControllerModel(controllerType.GetTypeInfo(), controllerAttributes)
{
Selectors = { new SelectorModel { AttributeRouteModel = new AttributeRouteModel() } },
};

var actionModel = new ActionModel(controllerType.GetMethod(nameof(TestApiController.TestAction)), Array.Empty<object>())
{
Controller = controllerModel,
Selectors = { new SelectorModel { AttributeRouteModel = new AttributeRouteModel() } },
};
controllerModel.Actions.Add(actionModel);

return (controllerModel, actionModel);
}

private class TestApiController : ControllerBase
{
public IActionResult TestAction(object value) => null;
public IResult TestActionWithIResult(object value) => null;
}

private class TestMvcController : ControllerBase
{
public IActionResult TestAction(object value) => null;
}
}
Loading