Merge release/2.2

This commit is contained in:
James Newton-King 2018-08-22 20:18:54 +12:00
commit f34634f9a4
No known key found for this signature in database
GPG Key ID: 0A66B2F456BF5526
32 changed files with 914 additions and 124 deletions

View File

@ -48,8 +48,8 @@
<MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreRazorTagHelpersTestingSourcesPackageVersion>
<MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreResponseCachingAbstractionsPackageVersion>
<MicrosoftAspNetCoreResponseCachingPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreResponseCachingPackageVersion>
<MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>
<MicrosoftAspNetCoreRoutingPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreRoutingPackageVersion>
<MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>3.0.0-a-alpha1-master-routing-refactor-16895</MicrosoftAspNetCoreRoutingAbstractionsPackageVersion>
<MicrosoftAspNetCoreRoutingPackageVersion>3.0.0-a-alpha1-master-routing-refactor-16895</MicrosoftAspNetCoreRoutingPackageVersion>
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
<MicrosoftAspNetCoreServerKestrelPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreServerKestrelPackageVersion>
<MicrosoftAspNetCoreSessionPackageVersion>3.0.0-alpha1-10321</MicrosoftAspNetCoreSessionPackageVersion>

View File

@ -2,7 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Microsoft.AspNetCore.Mvc
@ -10,17 +13,31 @@ namespace Microsoft.AspNetCore.Mvc
/// <summary>
/// Options used to configure behavior for types annotated with <see cref="ApiControllerAttribute"/>.
/// </summary>
public class ApiBehaviorOptions
public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
{
private readonly CompatibilitySwitch<bool> _suppressUseClientErrorFactory;
private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses;
private readonly ICompatibilitySwitch[] _switches;
private Func<ActionContext, IActionResult> _invalidModelStateResponseFactory;
/// <summary>
/// Creates a new instance of <see cref="ApiBehaviorOptions"/>.
/// </summary>
public ApiBehaviorOptions()
{
_suppressUseClientErrorFactory = new CompatibilitySwitch<bool>(nameof(SuppressUseClientErrorFactory));
_suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses));
_switches = new[]
{
_suppressUseClientErrorFactory,
_suppressUseValidationProblemDetailsForInvalidModelStateResponses,
};
}
/// <summary>
/// Delegate invoked on actions annotated with <see cref="ApiControllerAttribute"/> to convert invalid
/// <see cref="ModelStateDictionary"/> into an <see cref="IActionResult"/>
/// <para>
/// By default, the delegate produces a <see cref="BadRequestObjectResult"/> that wraps a serialized form
/// of <see cref="ModelStateDictionary"/>.
/// </para>
/// </summary>
public Func<ActionContext, IActionResult> InvalidModelStateResponseFactory
{
@ -52,5 +69,96 @@ namespace Microsoft.AspNetCore.Mvc
/// that are bound from form data.
/// </summary>
public bool SuppressConsumesConstraintForFormFileParameters { get; set; }
/// <summary>
/// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/> use <see cref="ClientErrorFactory"/>
/// to transform certain certain client errors.
/// <para>
/// When <c>false</c>, <see cref="ClientErrorFactory"/> is used to transform <see cref="IClientErrorActionResult"/> to the value
/// specified by the factory. In the default case, this converts <see cref="StatusCodeResult"/> instances to an <see cref="ObjectResult"/>
/// with <see cref="ProblemDetails"/>.
/// </para>
/// </summary>
/// <value>
/// The default value is <see langword="true"/> if the version is
/// <see cref="CompatibilityVersion.Version_2_2"/> or later; <see langword="false"/> otherwise.
/// </value>
/// <remarks>
/// <para>
/// This property is associated with a compatibility switch and can provide a different behavior depending on
/// the configured compatibility version for the application. See <see cref="CompatibilityVersion"/> for
/// guidance and examples of setting the application's compatibility version.
/// </para>
/// <para>
/// Configuring the desired value of the compatibility switch by calling this property's setter will take
/// precedence over the value implied by the application's <see cref="CompatibilityVersion"/>.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_1"/> or
/// lower then this setting will have the value <see langword="false"/> unless explicitly configured.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_2"/> or
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
/// </para>
/// </remarks>
public bool SuppressUseClientErrorFactory
{
// Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property.
get => _suppressUseClientErrorFactory.Value;
set => _suppressUseClientErrorFactory.Value = value;
}
/// <summary>
/// Gets or sets a value that determines if controllers annotated with <see cref="ApiControllerAttribute"/> respond using
/// <see cref="ValidationProblemDetails"/> in <see cref="InvalidModelStateResponseFactory"/>.
/// <para>
/// When <see langword="true"/>, <see cref="SuppressModelStateInvalidFilter"/> returns errors in <see cref="ModelStateDictionary"/>
/// as a <see cref="ValidationProblemDetails"/>. Otherwise, <see cref="SuppressModelStateInvalidFilter"/> returns the errors
/// in the format determined by <see cref="SerializableError"/>.
/// </para>
/// </summary>
/// <value>
/// The default value is <see langword="true"/> if the version is
/// <see cref="CompatibilityVersion.Version_2_2"/> or later; <see langword="false"/> otherwise.
/// </value>
/// <remarks>
/// <para>
/// This property is associated with a compatibility switch and can provide a different behavior depending on
/// the configured compatibility version for the application. See <see cref="CompatibilityVersion"/> for
/// guidance and examples of setting the application's compatibility version.
/// </para>
/// <para>
/// Configuring the desired value of the compatibility switch by calling this property's setter will take
/// precedence over the value implied by the application's <see cref="CompatibilityVersion"/>.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_1"/> or
/// lower then this setting will have the value <see langword="false"/> unless explicitly configured.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_2"/> or
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
/// </para>
/// </remarks>
public bool SuppressUseValidationProblemDetailsForInvalidModelStateResponses
{
get => _suppressUseValidationProblemDetailsForInvalidModelStateResponses.Value;
set => _suppressUseValidationProblemDetailsForInvalidModelStateResponses.Value = value;
}
/// <summary>
/// Gets a map of HTTP status codes to <see cref="IActionResult"/> factories.
/// Configured factories are used when <see cref="SuppressUseClientErrorFactory"/> is <see langword="false"/>.
/// </summary>
public IDictionary<int, Func<ActionContext, IActionResult>> ClientErrorFactory { get; } =
new Dictionary<int, Func<ActionContext, IActionResult>>();
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
{
return ((IEnumerable<ICompatibilitySwitch>)_switches).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => _switches.GetEnumerator();
}
}

View File

@ -148,6 +148,8 @@ namespace Microsoft.Extensions.DependencyInjection
ServiceDescriptor.Transient<IPostConfigureOptions<MvcOptions>, MvcOptionsConfigureCompatibilityOptions>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IPostConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>());
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, MvcCoreRouteOptionsSetup>());

View File

@ -0,0 +1,53 @@
// 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 System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter
{
private readonly IDictionary<int, Func<ActionContext, IActionResult>> _clientErrorFactory;
private readonly ILogger<ClientErrorResultFilter> _logger;
/// <summary>
/// Gets the filter order. Defaults to -2000 so that it runs early.
/// </summary>
public int Order => -2000;
public ClientErrorResultFilter(
ApiBehaviorOptions apiBehaviorOptions,
ILogger<ClientErrorResultFilter> logger)
{
_clientErrorFactory = apiBehaviorOptions?.ClientErrorFactory ?? throw new ArgumentNullException(nameof(apiBehaviorOptions));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
public void OnResultExecuting(ResultExecutingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Result is IClientErrorActionResult clientErrorActionResult &&
clientErrorActionResult.StatusCode is int statusCode &&
_clientErrorFactory.TryGetValue(statusCode, out var factory))
{
var result = factory(context);
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), statusCode);
context.Result = factory(context);
}
}
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
/// <summary>
/// An <see cref="IStatusCodeActionResult"/> that can be transformed to a more descriptive client error.
/// </summary>
public interface IClientErrorActionResult : IStatusCodeActionResult
{
}
}

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private readonly ApiBehaviorOptions _apiBehaviorOptions;
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly ModelStateInvalidFilter _modelStateInvalidFilter;
private readonly ClientErrorResultFilter _clientErrorResultFilter;
private readonly ILogger _logger;
public ApiBehaviorApplicationModelProvider(
@ -42,6 +43,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
_modelStateInvalidFilter = new ModelStateInvalidFilter(
apiBehaviorOptions.Value,
loggerFactory.CreateLogger<ModelStateInvalidFilter>());
_clientErrorResultFilter = new ClientErrorResultFilter(
_apiBehaviorOptions,
loggerFactory.CreateLogger<ClientErrorResultFilter>());
}
/// <remarks>
@ -90,6 +95,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
AddInvalidModelStateFilter(actionModel);
AddClientErrorFilter(actionModel);
InferParameterBindingSources(actionModel);
InferParameterModelPrefixes(actionModel);
@ -149,6 +156,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionModel.Filters.Add(_modelStateInvalidFilter);
}
private void AddClientErrorFilter(ActionModel actionModel)
{
if (_apiBehaviorOptions.SuppressUseClientErrorFactory)
{
return;
}
actionModel.Filters.Add(_clientErrorResultFilter);
}
// Internal for unit testing
internal void InferParameterBindingSources(ActionModel actionModel)
{

View File

@ -2,17 +2,44 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions>
public class ApiBehaviorOptionsSetup :
ConfigureCompatibilityOptions<ApiBehaviorOptions>,
IConfigureOptions<ApiBehaviorOptions>
{
internal static readonly Func<ActionContext, IActionResult> DefaultFactory = DefaultInvalidModelStateResponse;
internal static readonly Func<ActionContext, IActionResult> ProblemDetailsFactory = ProblemDetailsInvalidModelStateResponse;
public ApiBehaviorOptionsSetup()
public ApiBehaviorOptionsSetup(
ILoggerFactory loggerFactory,
IOptions<MvcCompatibilityOptions> compatibilityOptions)
: base(loggerFactory, compatibilityOptions)
{
}
protected override IReadOnlyDictionary<string, object> DefaultValues
{
get
{
var dictionary = new Dictionary<string, object>();
if (Version < CompatibilityVersion.Version_2_2)
{
dictionary[nameof(ApiBehaviorOptions.SuppressUseClientErrorFactory)] = true;
dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true;
}
return dictionary;
}
}
public void Configure(ApiBehaviorOptions options)
{
if (options == null)
@ -20,17 +47,117 @@ namespace Microsoft.AspNetCore.Mvc.Internal
throw new ArgumentNullException(nameof(options));
}
options.InvalidModelStateResponseFactory = GetInvalidModelStateResponse;
options.InvalidModelStateResponseFactory = DefaultFactory;
ConfigureClientErrorFactories(options);
}
IActionResult GetInvalidModelStateResponse(ActionContext context)
public override void PostConfigure(string name, ApiBehaviorOptions options)
{
// Let compatibility switches do their thing.
base.PostConfigure(name, options);
// We want to use problem details factory only if
// (a) it has not been opted out of (SuppressUseClientErrorFactory = true)
// (b) a different factory was configured
if (!options.SuppressUseClientErrorFactory &&
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
{
var result = new BadRequestObjectResult(context.ModelState);
result.ContentTypes.Add("application/json");
result.ContentTypes.Add("application/xml");
return result;
options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
}
}
// Internal for unit testing
internal static void ConfigureClientErrorFactories(ApiBehaviorOptions options)
{
AddClientErrorFactory(new ProblemDetails
{
Status = 400,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = Resources.ApiConventions_Title_400,
});
AddClientErrorFactory(new ProblemDetails
{
Status = 401,
Type = "https://tools.ietf.org/html/rfc7235#section-3.1",
Title = Resources.ApiConventions_Title_401,
});
AddClientErrorFactory(new ProblemDetails
{
Status = 403,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Title = Resources.ApiConventions_Title_403,
});
AddClientErrorFactory(new ProblemDetails
{
Status = 404,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = Resources.ApiConventions_Title_404,
});
AddClientErrorFactory(new ProblemDetails
{
Status = 406,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
Title = Resources.ApiConventions_Title_406,
});
AddClientErrorFactory(new ProblemDetails
{
Status = 409,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Title = Resources.ApiConventions_Title_409,
});
AddClientErrorFactory(new ProblemDetails
{
Status = 415,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
Title = Resources.ApiConventions_Title_415,
});
AddClientErrorFactory(new ProblemDetails
{
Status = 422,
Type = "https://tools.ietf.org/html/rfc4918#section-11.2",
Title = Resources.ApiConventions_Title_422,
});
void AddClientErrorFactory(ProblemDetails problemDetails)
{
var statusCode = problemDetails.Status.Value;
options.ClientErrorFactory[statusCode] = _ => new ObjectResult(problemDetails)
{
StatusCode = statusCode,
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
}
}
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
{
var result = new BadRequestObjectResult(context.ModelState);
result.ContentTypes.Add("application/json");
result.ContentTypes.Add("application/xml");
return result;
}
private static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
{
var result = new BadRequestObjectResult(new ValidationProblemDetails(context.ModelState));
result.ContentTypes.Add("application/problem+json");
result.ContentTypes.Add("application/problem+xml");
return result;
}
}
}

View File

@ -151,6 +151,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private static readonly Action<ILogger, Type, Type, Type, Exception> _notMostEffectiveFilter;
private static readonly Action<ILogger, IEnumerable<IOutputFormatter>, Exception> _registeredOutputFormatters;
private static readonly Action<ILogger, Type, Type, int, Exception> _transformingClientError;
static MvcCoreLoggerExtensions()
{
_actionExecuting = LoggerMessage.Define<string, string>(
@ -648,6 +650,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
LogLevel.Debug,
48,
"Skipped binding parameter '{ParameterName}' since its binding information disallowed it for the current request.");
_transformingClientError = LoggerMessage.Define<Type, Type, int>(
LogLevel.Trace,
new EventId(49, nameof(Infrastructure.ClientErrorResultFilter)),
"Replacing {InitialActionResultType} with status code {StatusCode} with {ReplacedActionResultType} produced from ClientErrorFactory'.");
}
public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters)
@ -1578,6 +1585,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
public static void TransformingClientError(this ILogger logger, Type initialType, Type replacedType, int statusCode)
{
_transformingClientError(logger, initialType, replacedType, statusCode, null);
}
private static void LogFilterExecutionPlan(
ILogger logger,
string filterType,

View File

@ -6,11 +6,11 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.Primitives;
@ -262,7 +262,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return false;
}
private MatcherEndpoint CreateEndpoint(
private RouteEndpoint CreateEndpoint(
ActionDescriptor action,
string routeName,
string template,
@ -271,9 +271,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
object source,
bool suppressLinkGeneration)
{
RequestDelegate invokerDelegate = (context) =>
RequestDelegate requestDelegate = (context) =>
{
var values = context.Features.Get<IEndpointFeature>().Values;
var values = context.Features.Get<IRouteValuesFeature>().RouteValues;
var routeData = new RouteData();
foreach (var kvp in values)
{
@ -299,9 +299,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
source,
suppressLinkGeneration);
var endpoint = new MatcherEndpoint(
next => invokerDelegate,
RoutePatternFactory.Parse(template, defaults, constraints: null),
var endpoint = new RouteEndpoint(
requestDelegate,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
order,
metadataCollection,
action.DisplayName);

View File

@ -95,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc
/// lower then this setting will have the value <see langword="false"/> unless explicitly configured.
/// </para>
/// <para>
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_1"/> or
/// If the application's compatibility version is set to <see cref="CompatibilityVersion.Version_2_2"/> or
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
/// </para>
/// </remarks>

View File

@ -1578,6 +1578,118 @@ namespace Microsoft.AspNetCore.Mvc.Core
internal static string FormatValidationVisitor_ExceededMaxPropertyDepth(object p0, object p1, object p2, object p3)
=> string.Format(CultureInfo.CurrentCulture, GetString("ValidationVisitor_ExceededMaxPropertyDepth"), p0, p1, p2, p3);
/// <summary>
/// Bad Request
/// </summary>
internal static string ApiConventions_Title_400
{
get => GetString("ApiConventions_Title_400");
}
/// <summary>
/// Bad Request
/// </summary>
internal static string FormatApiConventions_Title_400()
=> GetString("ApiConventions_Title_400");
/// <summary>
/// Unauthorized
/// </summary>
internal static string ApiConventions_Title_401
{
get => GetString("ApiConventions_Title_401");
}
/// <summary>
/// Unauthorized
/// </summary>
internal static string FormatApiConventions_Title_401()
=> GetString("ApiConventions_Title_401");
/// <summary>
/// Forbidden
/// </summary>
internal static string ApiConventions_Title_403
{
get => GetString("ApiConventions_Title_403");
}
/// <summary>
/// Forbidden
/// </summary>
internal static string FormatApiConventions_Title_403()
=> GetString("ApiConventions_Title_403");
/// <summary>
/// Not Found
/// </summary>
internal static string ApiConventions_Title_404
{
get => GetString("ApiConventions_Title_404");
}
/// <summary>
/// Not Found
/// </summary>
internal static string FormatApiConventions_Title_404()
=> GetString("ApiConventions_Title_404");
/// <summary>
/// Not Acceptable
/// </summary>
internal static string ApiConventions_Title_406
{
get => GetString("ApiConventions_Title_406");
}
/// <summary>
/// Not Acceptable
/// </summary>
internal static string FormatApiConventions_Title_406()
=> GetString("ApiConventions_Title_406");
/// <summary>
/// Conflict
/// </summary>
internal static string ApiConventions_Title_409
{
get => GetString("ApiConventions_Title_409");
}
/// <summary>
/// Conflict
/// </summary>
internal static string FormatApiConventions_Title_409()
=> GetString("ApiConventions_Title_409");
/// <summary>
/// Unsupported Media Type
/// </summary>
internal static string ApiConventions_Title_415
{
get => GetString("ApiConventions_Title_415");
}
/// <summary>
/// Unsupported Media Type
/// </summary>
internal static string FormatApiConventions_Title_415()
=> GetString("ApiConventions_Title_415");
/// <summary>
/// Unprocessable Entity
/// </summary>
internal static string ApiConventions_Title_422
{
get => GetString("ApiConventions_Title_422");
}
/// <summary>
/// Unprocessable Entity
/// </summary>
internal static string FormatApiConventions_Title_422()
=> GetString("ApiConventions_Title_422");
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -466,4 +466,28 @@
<data name="ValidationVisitor_ExceededMaxPropertyDepth" xml:space="preserve">
<value>{0} exceeded the maximum configured validation depth '{1}' when validating property '{2}' on type '{3}'.</value>
</data>
<data name="ApiConventions_Title_400" xml:space="preserve">
<value>Bad Request</value>
</data>
<data name="ApiConventions_Title_401" xml:space="preserve">
<value>Unauthorized</value>
</data>
<data name="ApiConventions_Title_403" xml:space="preserve">
<value>Forbidden</value>
</data>
<data name="ApiConventions_Title_404" xml:space="preserve">
<value>Not Found</value>
</data>
<data name="ApiConventions_Title_406" xml:space="preserve">
<value>Not Acceptable</value>
</data>
<data name="ApiConventions_Title_409" xml:space="preserve">
<value>Conflict</value>
</data>
<data name="ApiConventions_Title_415" xml:space="preserve">
<value>Unsupported Media Type</value>
</data>
<data name="ApiConventions_Title_422" xml:space="preserve">
<value>Unprocessable Entity</value>
</data>
</root>

View File

@ -135,8 +135,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
private Endpoint CreateRejectionEndpoint()
{
return new MatcherEndpoint(
(next) => (context) =>
return new RouteEndpoint(
(context) =>
{
context.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType;
return Task.CompletedTask;

View File

@ -3,6 +3,7 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc
/// Represents an <see cref="ActionResult"/> that when executed will
/// produce an HTTP response with the given response status code.
/// </summary>
public class StatusCodeResult : ActionResult, IStatusCodeActionResult
public class StatusCodeResult : ActionResult, IClientErrorActionResult
{
/// <summary>
/// Initializes a new instance of the <see cref="StatusCodeResult"/> class

View File

@ -264,6 +264,13 @@ namespace Microsoft.AspNetCore.Mvc
typeof(ApiBehaviorOptionsSetup),
}
},
{
typeof(IPostConfigureOptions<ApiBehaviorOptions>),
new Type[]
{
typeof(ApiBehaviorOptionsSetup),
}
},
{
typeof(IActionConstraintProvider),
new Type[]

View File

@ -0,0 +1,104 @@
// 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.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
public class ClientErrorResultFilterTest
{
private static readonly IActionResult Result = new EmptyResult();
[Fact]
public void OnResultExecuting_DoesNothing_IfActionIsNotClientErrorActionResult()
{
// Arrange
var actionResult = new NotFoundObjectResult(new object());
var context = GetContext(actionResult);
var filter = GetFilter();
// Act
filter.OnResultExecuting(context);
// Assert
Assert.Same(actionResult, context.Result);
}
[Fact]
public void OnResultExecuting_DoesNothing_IfStatusCodeDoesNotExistInApiBehaviorOptions()
{
// Arrange
var actionResult = new NotFoundResult();
var context = GetContext(actionResult);
var filter = GetFilter(new ApiBehaviorOptions());
// Act
filter.OnResultExecuting(context);
// Assert
Assert.Same(actionResult, context.Result);
}
[Fact]
public void OnResultExecuting_DoesNothing_IfResultDoesNotHaveStatusCode()
{
// Arrange
var actionResult = new Mock<IActionResult>()
.As<IClientErrorActionResult>()
.Object;
var context = GetContext(actionResult);
var filter = GetFilter(new ApiBehaviorOptions());
// Act
filter.OnResultExecuting(context);
// Assert
Assert.Same(actionResult, context.Result);
}
[Fact]
public void OnResultExecuting_TransformsClientErrors()
{
// Arrange
var actionResult = new NotFoundResult();
var context = GetContext(actionResult);
var filter = GetFilter();
// Act
filter.OnResultExecuting(context);
// Assert
Assert.Same(Result, context.Result);
}
private static ClientErrorResultFilter GetFilter(ApiBehaviorOptions options = null)
{
var apiBehaviorOptions = options ?? GetOptions();
var filter = new ClientErrorResultFilter(apiBehaviorOptions, NullLogger<ClientErrorResultFilter>.Instance);
return filter;
}
private static ApiBehaviorOptions GetOptions()
{
var apiBehaviorOptions = new ApiBehaviorOptions();
apiBehaviorOptions.ClientErrorFactory[404] = _ => Result;
return apiBehaviorOptions;
}
private static ResultExecutingContext GetContext(IActionResult actionResult)
{
return new ResultExecutingContext(
new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor()),
Array.Empty<IFilterMetadata>(),
actionResult,
new object());
}
}
}

View File

@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions);
Assert.IsType<ModelStateInvalidFilter>(actionModel.Filters.Last());
Assert.Single(actionModel.Filters.OfType<ModelStateInvalidFilter>());
}
[Fact]
@ -54,8 +54,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
provider.OnProvidersExecuting(context);
// Assert
var controllerModel = Assert.Single(context.Result.Controllers);
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), controllerModel.Filters.Select(f => f.GetType()));
var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions);
Assert.Empty(actionModel.Filters.OfType<ModelStateInvalidFilter>());
}
[Fact]
@ -73,11 +73,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Single(context.Result.Controllers).Actions.OrderBy(a => a.ActionName),
action =>
{
Assert.Contains(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
Assert.Single(action.Filters.OfType<ModelStateInvalidFilter>());
},
action =>
{
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
Assert.Empty(action.Filters.OfType<ModelStateInvalidFilter>());
});
}
@ -101,11 +101,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Single(context.Result.Controllers).Actions.OrderBy(a => a.ActionName),
action =>
{
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
Assert.Empty(action.Filters.OfType<ModelStateInvalidFilter>());
},
action =>
{
Assert.DoesNotContain(typeof(ModelStateInvalidFilter), action.Filters.Select(f => f.GetType()));
Assert.Empty(action.Filters.OfType<ModelStateInvalidFilter>());
});
}
@ -1059,6 +1059,41 @@ Environment.NewLine + "int b";
});
}
[Fact]
public void OnProvidersExecuting_AddsClientErrorResultFilter()
{
// Arrange
var context = GetContext(typeof(TestApiController));
var provider = GetProvider();
// Act
provider.OnProvidersExecuting(context);
// Assert
var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions);
Assert.Single(actionModel.Filters.OfType<ClientErrorResultFilter>());
}
[Fact]
public void OnProvidersExecuting_DoesNotAddClientErrorResultFilter_IfFeatureIsDisabled()
{
// Arrange
var context = GetContext(typeof(TestApiController));
var options = new ApiBehaviorOptions
{
SuppressUseClientErrorFactory = true,
InvalidModelStateResponseFactory = _ => null,
};
var provider = GetProvider(options);
// Act
provider.OnProvidersExecuting(context);
// Assert
var actionModel = Assert.Single(Assert.Single(context.Result.Controllers).Actions);
Assert.Empty(actionModel.Filters.OfType<ClientErrorResultFilter>());
}
// A dynamically generated type in an assembly that has an ApiConventionAttribute.
private static TypeBuilder CreateTestControllerType()
{

View File

@ -0,0 +1,101 @@
// 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;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ApiBehaviorOptionsSetupTest
{
[Fact]
public void Configure_AssignsInvalidModelStateResponseFactory()
{
// Arrange
var optionsSetup = new ApiBehaviorOptionsSetup(
NullLoggerFactory.Instance,
Options.Create(new MvcCompatibilityOptions()));
var options = new ApiBehaviorOptions();
// Act
optionsSetup.Configure(options);
// Assert
Assert.Same(ApiBehaviorOptionsSetup.DefaultFactory, options.InvalidModelStateResponseFactory);
}
[Fact]
public void Configure_AddsClientErrorFactories()
{
// Arrange
var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, };
var optionsSetup = new ApiBehaviorOptionsSetup(
NullLoggerFactory.Instance,
Options.Create(new MvcCompatibilityOptions()));
var options = new ApiBehaviorOptions();
// Act
optionsSetup.Configure(options);
// Assert
Assert.Equal(expected, options.ClientErrorFactory.Keys);
}
[Fact]
public void PostConfigure_SetProblemDetailsModelStateResponseFactory()
{
// Arrange
var optionsSetup = new ApiBehaviorOptionsSetup(
NullLoggerFactory.Instance,
Options.Create(new MvcCompatibilityOptions { CompatibilityVersion = CompatibilityVersion.Latest }));
var options = new ApiBehaviorOptions();
// Act
optionsSetup.Configure(options);
optionsSetup.PostConfigure(string.Empty, options);
// Assert
Assert.Same(ApiBehaviorOptionsSetup.ProblemDetailsFactory, options.InvalidModelStateResponseFactory);
}
[Fact]
public void PostConfigure_DoesNotSetProblemDetailsFactoryWithLegacyCompatBehavior()
{
// Arrange
var optionsSetup = new ApiBehaviorOptionsSetup(
NullLoggerFactory.Instance,
Options.Create(new MvcCompatibilityOptions { CompatibilityVersion = CompatibilityVersion.Version_2_1 }));
var options = new ApiBehaviorOptions();
// Act
optionsSetup.Configure(options);
optionsSetup.PostConfigure(string.Empty, options);
// Assert
Assert.Same(ApiBehaviorOptionsSetup.DefaultFactory, options.InvalidModelStateResponseFactory);
}
[Fact]
public void PostConfigure_DoesNotSetProblemDetailsFactory_IfValueWasModified()
{
// Arrange
var optionsSetup = new ApiBehaviorOptionsSetup(
NullLoggerFactory.Instance,
Options.Create(new MvcCompatibilityOptions { CompatibilityVersion = CompatibilityVersion.Latest }));
var options = new ApiBehaviorOptions();
Func<ActionContext, IActionResult> expected = _ => null;
// Act
optionsSetup.Configure(options);
// This is equivalent to user code updating the value via ConfigureOptions
options.InvalidModelStateResponseFactory = expected;
optionsSetup.PostConfigure(string.Empty, options);
// Assert
Assert.Same(expected, options.InvalidModelStateResponseFactory);
}
}
}

View File

@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata<RouteValuesAddressMetadata>();
Assert.NotNull(routeValuesAddressMetadata);
@ -77,14 +77,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
[Fact]
public void Endpoints_InvokeReturnedEndpoint_ActionInvokerProviderCalled()
public async Task Endpoints_InvokeReturnedEndpoint_ActionInvokerProviderCalled()
{
// Arrange
var featureCollection = new FeatureCollection();
featureCollection.Set<IEndpointFeature>(new EndpointFeature
var endpointFeature = new EndpointFeature
{
Values = new RouteValueDictionary()
});
RouteValues = new RouteValueDictionary()
};
var featureCollection = new FeatureCollection();
featureCollection.Set<IEndpointFeature>(endpointFeature);
featureCollection.Set<IRouteValuesFeature>(endpointFeature);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
@ -122,11 +125,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
var invokerDelegate = matcherEndpoint.Invoker((next) => Task.CompletedTask);
invokerDelegate(httpContextMock.Object);
await matcherEndpoint.RequestDelegate(httpContextMock.Object);
Assert.True(actionInvokerCalled);
}
@ -138,7 +139,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var featureCollection = new FeatureCollection();
featureCollection.Set<IEndpointFeature>(new EndpointFeature
{
Values = new RouteValueDictionary()
RouteValues = new RouteValueDictionary()
});
var httpContextMock = new Mock<HttpContext>();
@ -199,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var inspectors = finalEndpointTemplates
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText)))
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText)))
.ToArray();
// Assert
@ -226,7 +227,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var inspectors = finalEndpointTemplates
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText)))
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText)))
.ToArray();
// Assert
@ -250,8 +251,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
Assert.Collection(endpoints,
(e) => Assert.Equal("TestController", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText));
(e) => Assert.Equal("TestController", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText));
}
[Fact]
@ -278,8 +279,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
Assert.Collection(endpoints1,
(e) => Assert.Equal("TestController", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText));
(e) => Assert.Equal("TestController", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText));
Assert.Same(endpoints1, endpoints2);
actionDescriptorCollectionProviderMock.VerifyGet(m => m.ActionDescriptors, Times.Once);
@ -320,8 +321,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var endpoints = dataSource.Endpoints;
Assert.Collection(endpoints,
(e) => Assert.Equal("TestController", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText));
(e) => Assert.Equal("TestController", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText));
actionDescriptorCollectionProviderMock
.Setup(m => m.ActionDescriptors)
@ -337,7 +338,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.NotSame(endpoints, newEndpoints);
Assert.Collection(newEndpoints,
(e) => Assert.Equal("NewTestController/NewTestAction", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText));
(e) => Assert.Equal("NewTestController/NewTestAction", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText));
}
[Fact]
@ -359,8 +360,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
Assert.Collection(endpoints,
(e) => Assert.Equal("TestController/TestAction1", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction2", Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText));
(e) => Assert.Equal("TestController/TestAction1", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText),
(e) => Assert.Equal("TestController/TestAction2", Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText));
}
[Theory]
@ -383,7 +384,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var endpoints = dataSource.Endpoints;
var inspectors = finalEndpointTemplates
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).RoutePattern.RawText)))
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<RouteEndpoint>(e).RoutePattern.RawText)))
.ToArray();
// Assert
@ -405,7 +406,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
var routeValuesAddressNameMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
Assert.NotNull(routeValuesAddressNameMetadata);
Assert.Equal(string.Empty, routeValuesAddressNameMetadata.Name);
@ -430,7 +431,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpoints,
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
Assert.NotNull(routeValuesAddressMetadata);
Assert.Equal("namedRoute", routeValuesAddressMetadata.Name);
@ -438,7 +439,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
var routeValuesAddressMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteValuesAddressMetadata>();
Assert.NotNull(routeValuesAddressMetadata);
Assert.Equal("namedRoute", routeValuesAddressMetadata.Name);
@ -469,25 +470,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpoints,
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
Assert.Equal("Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText);
Assert.Equal(1, matcherEndpoint.Order);
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
Assert.Equal("named/Home/Index/{id?}", matcherEndpoint.RoutePattern.RawText);
Assert.Equal(2, matcherEndpoint.Order);
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
Assert.Equal("Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText);
Assert.Equal(1, matcherEndpoint.Order);
},
(ep) =>
{
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(ep);
Assert.Equal("named/Products/Details/{id?}", matcherEndpoint.RoutePattern.RawText);
Assert.Equal(2, matcherEndpoint.Order);
});
@ -589,7 +590,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText);
AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
}
@ -611,7 +612,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText);
AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
}
@ -634,7 +635,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.RoutePattern.RawText);
AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
}
@ -656,7 +657,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText);
AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
}

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
@ -369,11 +370,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return httpContext;
}
private static MatcherEndpoint CreateEndpoint(ActionDescriptor action)
private static RouteEndpoint CreateEndpoint(ActionDescriptor action)
{
var metadata = new List<object>() { action, };
return new MatcherEndpoint(
(r) => null,
return new RouteEndpoint(
(context) => Task.CompletedTask,
RoutePatternFactory.Parse("/"),
0,
new EndpointMetadataCollection(metadata),

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
@ -212,7 +213,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
Assert.Equal(expected, actual);
}
private static MatcherEndpoint CreateEndpoint(string template, ConsumesMetadata consumesMetadata)
private static RouteEndpoint CreateEndpoint(string template, ConsumesMetadata consumesMetadata)
{
var metadata = new List<object>();
if (consumesMetadata != null)
@ -220,8 +221,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
metadata.Add(consumesMetadata);
}
return new MatcherEndpoint(
(next) => null,
return new RouteEndpoint(
(context) => Task.CompletedTask,
RoutePatternFactory.Parse(template),
0,
new EndpointMetadataCollection(metadata),

View File

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Matching;
using Microsoft.AspNetCore.Routing.Patterns;
@ -60,8 +62,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
// Set the endpoint feature and current context just as a normal request to MVC app would be
var endpointFeature = new EndpointFeature();
urlHelper.ActionContext.HttpContext.Features.Set<IEndpointFeature>(endpointFeature);
urlHelper.ActionContext.HttpContext.Features.Set<IRouteValuesFeature>(endpointFeature);
endpointFeature.Endpoint = endpoint1;
endpointFeature.Values = new RouteValueDictionary
endpointFeature.RouteValues = new RouteValueDictionary
{
["controller"] = "Orders",
["action"] = "GetById",
@ -123,7 +126,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
protected override IUrlHelper CreateUrlHelper(string appRoot, string host, string protocol)
{
return CreateUrlHelper(Enumerable.Empty<MatcherEndpoint>(), appRoot, host, protocol);
return CreateUrlHelper(Enumerable.Empty<RouteEndpoint>(), appRoot, host, protocol);
}
protected override IUrlHelper CreateUrlHelperWithDefaultRoutes(string appRoot, string host, string protocol)
@ -139,8 +142,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
string template)
{
var endpoints = GetDefaultEndpoints();
endpoints.Add(new MatcherEndpoint(
next => httpContext => Task.CompletedTask,
endpoints.Add(new RouteEndpoint(
httpContext => Task.CompletedTask,
RoutePatternFactory.Parse(template),
0,
EndpointMetadataCollection.Empty,
@ -153,10 +156,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var httpContext = actionContext.HttpContext;
httpContext.Features.Set<IEndpointFeature>(new EndpointFeature()
{
Endpoint = new MatcherEndpoint(
next => cntxt => Task.CompletedTask,
RoutePatternFactory.Parse("/"),
0,
Endpoint = new Endpoint(
context => Task.CompletedTask,
EndpointMetadataCollection.Empty,
null)
});
@ -187,7 +188,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return CreateUrlHelper(actionContext);
}
private IUrlHelper CreateUrlHelper(IEnumerable<MatcherEndpoint> endpoints, ActionContext actionContext = null)
private IUrlHelper CreateUrlHelper(IEnumerable<RouteEndpoint> endpoints, ActionContext actionContext = null)
{
var serviceProvider = CreateServices(endpoints);
var httpContext = CreateHttpContext(serviceProvider, null, null, "http");
@ -196,7 +197,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
}
private IUrlHelper CreateUrlHelper(
IEnumerable<MatcherEndpoint> endpoints,
IEnumerable<RouteEndpoint> endpoints,
string appRoot,
string host,
string protocol)
@ -207,9 +208,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return CreateUrlHelper(actionContext);
}
private List<MatcherEndpoint> GetDefaultEndpoints()
private List<RouteEndpoint> GetDefaultEndpoints()
{
var endpoints = new List<MatcherEndpoint>();
var endpoints = new List<RouteEndpoint>();
endpoints.Add(
CreateEndpoint(
"home/newaction/{id}",
@ -278,7 +279,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return endpoints;
}
private MatcherEndpoint CreateEndpoint(
private RouteEndpoint CreateEndpoint(
string template,
object defaults = null,
object requiredValues = null,
@ -292,9 +293,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
new RouteValuesAddressMetadata(routeName, new RouteValueDictionary(requiredValues)));
}
return new MatcherEndpoint(
next => (httpContext) => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, constraints: null),
return new RouteEndpoint(
(httpContext) => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
order,
metadataCollection,
null);
@ -315,11 +316,11 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return services.BuildServiceProvider();
}
private MatcherEndpoint GetEndpoint(string name, string template, RouteValueDictionary defaults)
private RouteEndpoint GetEndpoint(string name, string template, RouteValueDictionary defaults)
{
return new MatcherEndpoint(
next => c => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, constraints: null),
return new RouteEndpoint(
c => Task.CompletedTask,
RoutePatternFactory.Parse(template, defaults, parameterPolicies: null),
0,
EndpointMetadataCollection.Empty,
null);

View File

@ -8,6 +8,7 @@ using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BasicWebSite.Models;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json;
using Xunit;
@ -18,9 +19,16 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public ApiBehaviorTest(MvcTestFixture<BasicWebSite.Startup> fixture)
{
Client = fixture.CreateDefaultClient();
var factory = fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
CustomInvalidModelStateClient = factory.CreateDefaultClient();
}
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
builder.UseStartup<BasicWebSite.StartupWithCustomInvalidModelStateFactory>();
public HttpClient Client { get; }
public HttpClient CustomInvalidModelStateClient { get; }
[Fact]
public async Task ActionsReturnBadRequest_WhenModelStateIsInvalid()
@ -39,11 +47,11 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var response = await Client.PostAsJsonAsync("/contact", contactModel);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
var actual = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(await response.Content.ReadAsStringAsync());
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
Assert.Equal("application/problem+json", response.Content.Headers.ContentType.MediaType);
var problemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(await response.Content.ReadAsStringAsync());
Assert.Collection(
actual.OrderBy(kvp => kvp.Key),
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("Name", kvp.Key);
@ -110,10 +118,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var contactString = JsonConvert.SerializeObject(contactModel);
// Act
var response = await Client.PostAsJsonAsync("/contact/PostWithVnd", contactModel);
var response = await CustomInvalidModelStateClient.PostAsJsonAsync("/contact/PostWithVnd", contactModel);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
Assert.Equal("application/vnd.error+json", response.Content.Headers.ContentType.MediaType);
var content = await response.Content.ReadAsStringAsync();
var actual = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(content);
@ -236,5 +244,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var result = await response.Content.ReadAsStringAsync();
Assert.Equal(expected, result);
}
[Fact]
public async Task ClientErrorResultFilterExecutesForStatusCodeResults()
{
// Act
var response = await Client.GetAsync("/contact/ActionReturningStatusCodeResult");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
Assert.Equal(404, problemDetails.Status);
}
}
}

View File

@ -31,6 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
// Assert
Assert.False(mvcOptions.AllowCombiningAuthorizeFilters);
@ -41,6 +42,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.False(razorPagesOptions.AllowAreas);
Assert.False(mvcOptions.EnableEndpointRouting);
Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory);
}
[Fact]
@ -57,6 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
// Assert
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
@ -67,6 +71,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(razorPagesOptions.AllowAreas);
Assert.False(mvcOptions.EnableEndpointRouting);
Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory);
}
[Fact]
@ -83,6 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
// Assert
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
@ -93,6 +100,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(razorPagesOptions.AllowAreas);
Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory);
}
[Fact]
@ -109,6 +118,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
var mvcOptions = services.GetRequiredService<IOptions<MvcOptions>>().Value;
var jsonOptions = services.GetRequiredService<IOptions<MvcJsonOptions>>().Value;
var razorPagesOptions = services.GetRequiredService<IOptions<RazorPagesOptions>>().Value;
var apiBehaviorOptions = services.GetRequiredService<IOptions<ApiBehaviorOptions>>().Value;
// Assert
Assert.True(mvcOptions.AllowCombiningAuthorizeFilters);
@ -119,6 +129,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(razorPagesOptions.AllowAreas);
Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory);
}
// This just does the minimum needed to be able to resolve these options.

View File

@ -1,9 +1,6 @@
// 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 System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BasicWebSite.Models;
@ -88,6 +85,12 @@ namespace BasicWebSite
return foo;
}
[HttpGet("[action]")]
public ActionResult<int> ActionReturningStatusCodeResult()
{
return NotFound();
}
private class TestModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)

View File

@ -1,8 +1,8 @@
// 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.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
namespace BasicWebSite
{

View File

@ -1,7 +1,6 @@
// 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.Linq;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
@ -33,22 +32,6 @@ namespace BasicWebSite
.SetCompatibilityVersion(CompatibilityVersion.Latest)
.AddXmlDataContractSerializerFormatters();
services.Configure<ApiBehaviorOptions>(options =>
{
var previous = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var result = (BadRequestObjectResult)previous(context);
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute))
{
result.ContentTypes.Clear();
result.ContentTypes.Add("application/vnd.error+json");
}
return result;
};
});
services.ConfigureBaseWebSiteAuthPolicies();
services.AddTransient<IAuthorizationHandler, ManagerHandler>();

View File

@ -0,0 +1,52 @@
// 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.Linq;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace BasicWebSite
{
public class StartupWithCustomInvalidModelStateFactory
{
// Set up application services
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Api", _ => { });
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Latest);
services.Configure<ApiBehaviorOptions>(options =>
{
var previous = options.InvalidModelStateResponseFactory;
options.InvalidModelStateResponseFactory = context =>
{
var result = (BadRequestObjectResult)previous(context);
if (context.ActionDescriptor.FilterDescriptors.Any(f => f.Filter is VndErrorAttribute))
{
result.ContentTypes.Clear();
result.ContentTypes.Add("application/vnd.error+json");
}
return result;
};
});
services.ConfigureBaseWebSiteAuthPolicies();
services.AddLogging();
services.AddSingleton<ContactsRepository>();
}
public void Configure(IApplicationBuilder app)
{
app.UseDeveloperExceptionPage();
app.UseMvc();
}
}
}

View File

@ -1,8 +1,8 @@
// 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.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
namespace RoutingWebSite
{

View File

@ -1,8 +1,8 @@
// 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.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
namespace VersioningWebSite
{