Merge remote-tracking branch 'origin/release/2.2'

This commit is contained in:
Pranav K 2018-09-05 16:59:37 -07:00
commit 1dba6dfcc8
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
80 changed files with 3148 additions and 398 deletions

View File

@ -123,7 +123,7 @@ namespace Microsoft.CodeAnalysis
return false; return false;
} }
private static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute) public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute)
{ {
foreach (var declaredAttribute in symbol.GetAttributes()) foreach (var declaredAttribute in symbol.GetAttributes())
{ {

View File

@ -116,6 +116,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
return false; return false;
} }
if (!method.ReturnsVoid)
{
return false;
}
if (method.Parameters.Length != disposableDispose.Parameters.Length) if (method.Parameters.Length != disposableDispose.Parameters.Length)
{ {
return false; return false;

View File

@ -2,6 +2,7 @@
// 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.Diagnostics;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
@ -24,6 +25,11 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesDefaultResponseTypeAttribute); ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesDefaultResponseTypeAttribute);
ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesResponseTypeAttribute); ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesResponseTypeAttribute);
StatusCodeValueAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.StatusCodeValueAttribute);
var statusCodeActionResult = compilation.GetTypeByMetadataName(ApiSymbolNames.IStatusCodeActionResult);
StatusCodeActionResultStatusProperty = (IPropertySymbol)statusCodeActionResult?.GetMembers("StatusCode")[0];
var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable); var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable);
var members = disposable.GetMembers(nameof(IDisposable.Dispose)); var members = disposable.GetMembers(nameof(IDisposable.Dispose));
IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null; IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null;
@ -47,6 +53,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public IMethodSymbol IDisposableDispose { get; } public IMethodSymbol IDisposableDispose { get; }
public IPropertySymbol StatusCodeActionResultStatusProperty { get; }
public ITypeSymbol ModelStateDictionary { get; } public ITypeSymbol ModelStateDictionary { get; }
public INamedTypeSymbol NonActionAttribute { get; } public INamedTypeSymbol NonActionAttribute { get; }
@ -56,5 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public INamedTypeSymbol ProducesDefaultResponseTypeAttribute { get; } public INamedTypeSymbol ProducesDefaultResponseTypeAttribute { get; }
public INamedTypeSymbol ProducesResponseTypeAttribute { get; } public INamedTypeSymbol ProducesResponseTypeAttribute { get; }
public INamedTypeSymbol StatusCodeValueAttribute { get; }
} }
} }

View File

@ -23,6 +23,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public const string IActionResult = "Microsoft.AspNetCore.Mvc.IActionResult"; public const string IActionResult = "Microsoft.AspNetCore.Mvc.IActionResult";
public const string IStatusCodeActionResult = "Microsoft.AspNetCore.Mvc.Infrastructure.IStatusCodeActionResult";
public const string ModelStateDictionary = "Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary"; public const string ModelStateDictionary = "Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary";
public const string NonActionAttribute = "Microsoft.AspNetCore.Mvc.NonActionAttribute"; public const string NonActionAttribute = "Microsoft.AspNetCore.Mvc.NonActionAttribute";
@ -34,5 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute"; public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute";
public const string HttpStatusCodes = "Microsoft.AspNetCore.Http.StatusCodes"; public const string HttpStatusCodes = "Microsoft.AspNetCore.Http.StatusCodes";
public const string StatusCodeValueAttribute = "Microsoft.AspNetCore.Mvc.Infrastructure.StatusCodeValueAttribute";
} }
} }

View File

@ -61,7 +61,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
} }
return Array.Empty<DeclaredApiResponseMetadata>(); return Array.Empty<DeclaredApiResponseMetadata>();
} }
private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method) private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
{ {
var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true) var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true)
@ -222,6 +223,12 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>()) foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
{ {
if (returnStatementSyntax.IsMissing || returnStatementSyntax.Expression.IsMissing)
{
// Ignore malformed return statements.
continue;
}
var responseMetadata = InspectReturnStatementSyntax( var responseMetadata = InspectReturnStatementSyntax(
symbolCache, symbolCache,
semanticModel, semanticModel,
@ -248,11 +255,6 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var returnExpression = returnStatementSyntax.Expression; var returnExpression = returnStatementSyntax.Expression;
if (returnExpression.IsMissing)
{
return null;
}
var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken); var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken);
if (typeInfo.Type.TypeKind == TypeKind.Error) if (typeInfo.Type.TypeKind == TypeKind.Error)
{ {
@ -267,25 +269,176 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
if (defaultStatusCodeAttribute != null) if (defaultStatusCodeAttribute != null)
{ {
var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute); var defaultStatusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
if (statusCode == null) if (defaultStatusCode == null)
{ {
// Unable to read the status code even though the attribute exists. // Unable to read the status code even though the attribute exists.
return null; return null;
} }
return new ActualApiResponseMetadata(returnStatementSyntax, statusCode.Value); return new ActualApiResponseMetadata(returnStatementSyntax, defaultStatusCode.Value);
} }
else if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType))
if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType))
{ {
// Return expression does not have a DefaultStatusCodeAttribute and it is not // Return expression does not have a DefaultStatusCodeAttribute and it is not
// an instance of IActionResult. Must be returning the "model". // an instance of IActionResult. Must be returning the "model".
return new ActualApiResponseMetadata(returnStatementSyntax); return new ActualApiResponseMetadata(returnStatementSyntax);
} }
int statusCode;
switch (returnExpression)
{
case InvocationExpressionSyntax invocation:
// Covers the 'return StatusCode(200)' case.
if (TryGetParameterStatusCode(symbolCache, semanticModel, invocation.Expression, invocation.ArgumentList, cancellationToken, out statusCode))
{
return new ActualApiResponseMetadata(returnStatementSyntax, statusCode);
}
break;
case ObjectCreationExpressionSyntax creation:
// Covers the 'return new ObjectResult(...) { StatusCode = 200 }' case.
if (TryGetInitializerStatusCode(symbolCache, semanticModel, creation.Initializer, cancellationToken, out statusCode))
{
return new ActualApiResponseMetadata(returnStatementSyntax, statusCode);
}
// Covers the 'return new StatusCodeResult(200) case.
if (TryGetParameterStatusCode(symbolCache, semanticModel, creation, creation.ArgumentList, cancellationToken, out statusCode))
{
return new ActualApiResponseMetadata(returnStatementSyntax, statusCode);
}
break;
}
return null; return null;
} }
private static bool TryGetInitializerStatusCode(
in ApiControllerSymbolCache symbolCache,
SemanticModel semanticModel,
InitializerExpressionSyntax initializer,
CancellationToken cancellationToken,
out int statusCode)
{
if (initializer == null)
{
statusCode = default;
return false;
}
for (var i = 0; i < initializer.Expressions.Count; i++)
{
if (!(initializer.Expressions[i] is AssignmentExpressionSyntax assignment))
{
continue;
}
if (assignment.Left is IdentifierNameSyntax identifier)
{
var symbolInfo = semanticModel.GetSymbolInfo(identifier, cancellationToken);
if (symbolInfo.Symbol is IPropertySymbol property && IsInterfaceImplementation(property, symbolCache.StatusCodeActionResultStatusProperty))
{
return TryGetExpressionStatusCode(semanticModel, assignment.Right, cancellationToken, out statusCode);
}
}
}
statusCode = default;
return false;
}
private static bool IsInterfaceImplementation(IPropertySymbol property, IPropertySymbol statusCodeActionResultStatusProperty)
{
if (property.Name != statusCodeActionResultStatusProperty.Name)
{
return false;
}
for (var i = 0; i < property.ExplicitInterfaceImplementations.Length; i++)
{
if (property.ExplicitInterfaceImplementations[i] == statusCodeActionResultStatusProperty)
{
return true;
}
}
var implementedProperty = property.ContainingType.FindImplementationForInterfaceMember(statusCodeActionResultStatusProperty);
return implementedProperty == property;
}
private static bool TryGetParameterStatusCode(
in ApiControllerSymbolCache symbolCache,
SemanticModel semanticModel,
ExpressionSyntax expression,
BaseArgumentListSyntax argumentList,
CancellationToken cancellationToken,
out int statusCode)
{
var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken);
if (!(symbolInfo.Symbol is IMethodSymbol method))
{
statusCode = default;
return false;
}
for (var i = 0; i < method.Parameters.Length; i++)
{
var parameter = method.Parameters[i];
if (!parameter.HasAttribute(symbolCache.StatusCodeValueAttribute))
{
continue;
}
var argument = argumentList.Arguments[parameter.Ordinal];
return TryGetExpressionStatusCode(semanticModel, argument.Expression, cancellationToken, out statusCode);
}
statusCode = default;
return false;
}
private static bool TryGetExpressionStatusCode(
SemanticModel semanticModel,
ExpressionSyntax expression,
CancellationToken cancellationToken,
out int statusCode)
{
if (expression is LiteralExpressionSyntax literal && literal.Token.Value is int literalStatusCode)
{
// Covers the 'return StatusCode(200)' case.
statusCode = literalStatusCode;
return true;
}
if (expression is IdentifierNameSyntax || expression is MemberAccessExpressionSyntax)
{
var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken);
if (symbolInfo.Symbol is IFieldSymbol field && field.HasConstantValue && field.ConstantValue is int constantStatusCode)
{
// Covers the 'return StatusCode(StatusCodes.Status200OK)' case.
// It also covers the 'return StatusCode(StatusCode)' case, where 'StatusCode' is a constant field.
statusCode = constantStatusCode;
return true;
}
if (symbolInfo.Symbol is ILocalSymbol local && local.HasConstantValue && local.ConstantValue is int localStatusCode)
{
// Covers the 'return StatusCode(statusCode)' case, where 'statusCode' is a local constant.
statusCode = localStatusCode;
return true;
}
}
statusCode = default;
return false;
}
private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode) private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode)
{ {
return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) && return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) &&

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

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary> /// </summary>
public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch> public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
{ {
private readonly CompatibilitySwitch<bool> _suppressUseClientErrorFactory; private readonly CompatibilitySwitch<bool> _suppressMapClientErrors;
private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses; private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses;
private readonly ICompatibilitySwitch[] _switches; private readonly ICompatibilitySwitch[] _switches;
@ -26,11 +26,11 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary> /// </summary>
public ApiBehaviorOptions() public ApiBehaviorOptions()
{ {
_suppressUseClientErrorFactory = new CompatibilitySwitch<bool>(nameof(SuppressUseClientErrorFactory)); _suppressMapClientErrors = new CompatibilitySwitch<bool>(nameof(SuppressMapClientErrors));
_suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses)); _suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses));
_switches = new[] _switches = new[]
{ {
_suppressUseClientErrorFactory, _suppressMapClientErrors,
_suppressUseValidationProblemDetailsForInvalidModelStateResponses, _suppressUseValidationProblemDetailsForInvalidModelStateResponses,
}; };
} }
@ -71,12 +71,16 @@ namespace Microsoft.AspNetCore.Mvc
public bool SuppressConsumesConstraintForFormFileParameters { get; set; } public bool SuppressConsumesConstraintForFormFileParameters { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/> use <see cref="ClientErrorFactory"/> /// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/>
/// to transform certain certain client errors. /// transform certain certain client errors.
/// <para> /// <para>
/// When <c>false</c>, <see cref="ClientErrorFactory"/> is used to transform <see cref="IClientErrorActionResult"/> to the value /// When <c>false</c>, a result filter is added to API controller actions that transforms <see cref="IClientErrorActionResult"/>.
/// specified by the factory. In the default case, this converts <see cref="StatusCodeResult"/> instances to an <see cref="ObjectResult"/> /// By default, <see cref="ClientErrorMapping"/> is used to map <see cref="IClientErrorActionResult"/> to a
/// with <see cref="ProblemDetails"/>. /// <see cref="ProblemDetails"/> instance (returned as the value for <see cref="ObjectResult"/>).
/// </para>
/// <para>
/// To customize the output of the filter (for e.g. to return a different error type), register a custom
/// implementation of of <see cref="IClientErrorFactory"/> in the service collection.
/// </para> /// </para>
/// </summary> /// </summary>
/// <value> /// <value>
@ -102,11 +106,11 @@ namespace Microsoft.AspNetCore.Mvc
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured. /// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
/// </para> /// </para>
/// </remarks> /// </remarks>
public bool SuppressUseClientErrorFactory public bool SuppressMapClientErrors
{ {
// Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property. // Note: When compatibility switches are removed in 3.0, this property should be retained as a regular boolean property.
get => _suppressUseClientErrorFactory.Value; get => _suppressMapClientErrors.Value;
set => _suppressUseClientErrorFactory.Value = value; set => _suppressMapClientErrors.Value = value;
} }
/// <summary> /// <summary>
@ -148,11 +152,15 @@ namespace Microsoft.AspNetCore.Mvc
} }
/// <summary> /// <summary>
/// Gets a map of HTTP status codes to <see cref="IActionResult"/> factories. /// Gets a map of HTTP status codes to <see cref="ClientErrorData"/>. Configured values
/// Configured factories are used when <see cref="SuppressUseClientErrorFactory"/> is <see langword="false"/>. /// are used to transform <see cref="IClientErrorActionResult"/> to an <see cref="ObjectResult"/>
/// instance where the <see cref="ObjectResult.Value"/> is <see cref="ProblemDetails"/>.
/// <para>
/// Use of this feature can be disabled by resetting <see cref="SuppressMapClientErrors"/>.
/// </para>
/// </summary> /// </summary>
public IDictionary<int, Func<ActionContext, IActionResult>> ClientErrorFactory { get; } = public IDictionary<int, ClientErrorData> ClientErrorMapping { get; } =
new Dictionary<int, Func<ActionContext, IActionResult>>(); new Dictionary<int, ClientErrorData>();
IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator() IEnumerator<ICompatibilitySwitch> IEnumerable<ICompatibilitySwitch>.GetEnumerator()
{ {

View File

@ -0,0 +1,29 @@
// 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
{
/// <summary>
/// Information for producing client errors. This type is used to configure client errors
/// produced by consumers of <see cref="ApiBehaviorOptions.ClientErrorMapping"/>.
/// </summary>
public class ClientErrorData
{
/// <summary>
/// Gets or sets a link (URI) that describes the client error.
/// </summary>
/// <remarks>
/// By default, this maps to <see cref="ProblemDetails.Type"/>.
/// </remarks>
public string Link { get; set; }
/// <summary>
/// Gets or sets the summary of the client error.
/// </summary>
/// <remarks>
/// By default, this maps to <see cref="ProblemDetails.Title"/> and should not change
/// between multiple occurences of the same error.
/// </remarks>
public string Title { get; set; }
}
}

View File

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@ -200,7 +201,7 @@ namespace Microsoft.AspNetCore.Mvc
/// <param name="statusCode">The status code to set on the response.</param> /// <param name="statusCode">The status code to set on the response.</param>
/// <returns>The created <see cref="StatusCodeResult"/> object for the response.</returns> /// <returns>The created <see cref="StatusCodeResult"/> object for the response.</returns>
[NonAction] [NonAction]
public virtual StatusCodeResult StatusCode(int statusCode) public virtual StatusCodeResult StatusCode([StatusCodeValue] int statusCode)
=> new StatusCodeResult(statusCode); => new StatusCodeResult(statusCode);
/// <summary> /// <summary>
@ -210,10 +211,12 @@ namespace Microsoft.AspNetCore.Mvc
/// <param name="value">The value to set on the <see cref="ObjectResult"/>.</param> /// <param name="value">The value to set on the <see cref="ObjectResult"/>.</param>
/// <returns>The created <see cref="ObjectResult"/> object for the response.</returns> /// <returns>The created <see cref="ObjectResult"/> object for the response.</returns>
[NonAction] [NonAction]
public virtual ObjectResult StatusCode(int statusCode, object value) public virtual ObjectResult StatusCode([StatusCodeValue] int statusCode, object value)
{ {
var result = new ObjectResult(value); var result = new ObjectResult(value)
result.StatusCode = statusCode; {
StatusCode = statusCode
};
return result; return result;
} }

View File

@ -257,6 +257,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>(); services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>(); services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>(); services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
// //
// Route Handlers // Route Handlers

View File

@ -2,7 +2,6 @@
// 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 Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -11,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{ {
internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter
{ {
private readonly IDictionary<int, Func<ActionContext, IActionResult>> _clientErrorFactory; private readonly IClientErrorFactory _clientErrorFactory;
private readonly ILogger<ClientErrorResultFilter> _logger; private readonly ILogger<ClientErrorResultFilter> _logger;
/// <summary> /// <summary>
@ -20,10 +19,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
public int Order => -2000; public int Order => -2000;
public ClientErrorResultFilter( public ClientErrorResultFilter(
ApiBehaviorOptions apiBehaviorOptions, IClientErrorFactory clientErrorFactory,
ILogger<ClientErrorResultFilter> logger) ILogger<ClientErrorResultFilter> logger)
{ {
_clientErrorFactory = apiBehaviorOptions?.ClientErrorFactory ?? throw new ArgumentNullException(nameof(apiBehaviorOptions)); _clientErrorFactory = clientErrorFactory ?? throw new ArgumentNullException(nameof(clientErrorFactory));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@ -38,16 +37,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Result is IClientErrorActionResult clientErrorActionResult && if (!(context.Result is IClientErrorActionResult clientError))
clientErrorActionResult.StatusCode is int statusCode &&
_clientErrorFactory.TryGetValue(statusCode, out var factory))
{ {
var result = factory(context); return;
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), statusCode);
context.Result = factory(context);
} }
var result = _clientErrorFactory.GetClientError(context, clientError);
if (result == null)
{
return;
}
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), clientError.StatusCode);
context.Result = result;
} }
} }
} }

View File

@ -0,0 +1,20 @@
// 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>
/// A factory for producing client errors. This contract is used by controllers annotated
/// with <see cref="ApiControllerAttribute"/> to transform <see cref="IClientErrorActionResult"/>.
/// </summary>
public interface IClientErrorFactory
{
/// <summary>
/// Transforms <paramref name="clientError"/> for the specified <paramref name="actionContext"/>.
/// </summary>
/// <param name="actionContext">The <see cref="ActionContext"/>.</param>
/// <param name="clientError">The <see cref="IClientErrorActionResult"/>.</param>
/// <returns>THe <see cref="IActionResult"/> that would be returned to the client.</returns>
IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError);
}
}

View File

@ -0,0 +1,54 @@
// 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.Diagnostics;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
internal class ProblemDetailsClientErrorFactory : IClientErrorFactory
{
private static readonly string TraceIdentifierKey = "traceId";
private readonly ApiBehaviorOptions _options;
public ProblemDetailsClientErrorFactory(IOptions<ApiBehaviorOptions> options)
{
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
}
public IActionResult GetClientError(ActionContext actionContext, IClientErrorActionResult clientError)
{
var problemDetails = new ProblemDetails
{
Status = clientError.StatusCode,
Type = "about:blank",
};
if (clientError.StatusCode is int statusCode &&
_options.ClientErrorMapping.TryGetValue(statusCode, out var errorData))
{
problemDetails.Title = errorData.Title;
problemDetails.Type = errorData.Link;
SetTraceId(actionContext, problemDetails);
}
return new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status,
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
}
internal static void SetTraceId(ActionContext actionContext, ProblemDetails problemDetails)
{
var traceId = Activity.Current?.Id ?? actionContext.HttpContext.TraceIdentifier;
problemDetails.Extensions[TraceIdentifierKey] = traceId;
}
}
}

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.
using System;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class StatusCodeValueAttribute : Attribute
{
}
}

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;
@ -27,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public ApiBehaviorApplicationModelProvider( public ApiBehaviorApplicationModelProvider(
IOptions<ApiBehaviorOptions> apiBehaviorOptions, IOptions<ApiBehaviorOptions> apiBehaviorOptions,
IModelMetadataProvider modelMetadataProvider, IModelMetadataProvider modelMetadataProvider,
IClientErrorFactory clientErrorFactory,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
{ {
_apiBehaviorOptions = apiBehaviorOptions.Value; _apiBehaviorOptions = apiBehaviorOptions.Value;
@ -45,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
loggerFactory.CreateLogger<ModelStateInvalidFilter>()); loggerFactory.CreateLogger<ModelStateInvalidFilter>());
_clientErrorResultFilter = new ClientErrorResultFilter( _clientErrorResultFilter = new ClientErrorResultFilter(
_apiBehaviorOptions, clientErrorFactory,
loggerFactory.CreateLogger<ClientErrorResultFilter>()); loggerFactory.CreateLogger<ClientErrorResultFilter>());
} }
@ -104,6 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
AddMultipartFormDataConsumesAttribute(actionModel); AddMultipartFormDataConsumesAttribute(actionModel);
DiscoverApiConvention(actionModel, conventions); DiscoverApiConvention(actionModel, conventions);
DiscoverErrorResponseType(actionModel);
} }
} }
} }
@ -158,7 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private void AddClientErrorFilter(ActionModel actionModel) private void AddClientErrorFilter(ActionModel actionModel)
{ {
if (_apiBehaviorOptions.SuppressUseClientErrorFactory) if (_apiBehaviorOptions.SuppressMapClientErrors)
{ {
return; return;
} }
@ -273,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

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core; using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (Version < CompatibilityVersion.Version_2_2) if (Version < CompatibilityVersion.Version_2_2)
{ {
dictionary[nameof(ApiBehaviorOptions.SuppressUseClientErrorFactory)] = true; dictionary[nameof(ApiBehaviorOptions.SuppressMapClientErrors)] = true;
dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true; dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true;
} }
@ -48,7 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
options.InvalidModelStateResponseFactory = DefaultFactory; options.InvalidModelStateResponseFactory = DefaultFactory;
ConfigureClientErrorFactories(options); ConfigureClientErrorMapping(options);
} }
public override void PostConfigure(string name, ApiBehaviorOptions options) public override void PostConfigure(string name, ApiBehaviorOptions options)
@ -57,9 +58,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
base.PostConfigure(name, options); base.PostConfigure(name, options);
// We want to use problem details factory only if // We want to use problem details factory only if
// (a) it has not been opted out of (SuppressUseClientErrorFactory = true) // (a) it has not been opted out of (SuppressMapClientErrors = true)
// (b) a different factory was configured // (b) a different factory was configured
if (!options.SuppressUseClientErrorFactory && if (!options.SuppressMapClientErrors &&
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory)) object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
{ {
options.InvalidModelStateResponseFactory = ProblemDetailsFactory; options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
@ -67,77 +68,55 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
// Internal for unit testing // Internal for unit testing
internal static void ConfigureClientErrorFactories(ApiBehaviorOptions options) internal static void ConfigureClientErrorMapping(ApiBehaviorOptions options)
{ {
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[400] = new ClientErrorData
{ {
Status = 400, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = Resources.ApiConventions_Title_400, Title = Resources.ApiConventions_Title_400,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[401] = new ClientErrorData
{ {
Status = 401, Link = "https://tools.ietf.org/html/rfc7235#section-3.1",
Type = "https://tools.ietf.org/html/rfc7235#section-3.1",
Title = Resources.ApiConventions_Title_401, Title = Resources.ApiConventions_Title_401,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[403] = new ClientErrorData
{ {
Status = 403, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Title = Resources.ApiConventions_Title_403, Title = Resources.ApiConventions_Title_403,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[404] = new ClientErrorData
{ {
Status = 404, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = Resources.ApiConventions_Title_404, Title = Resources.ApiConventions_Title_404,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[406] = new ClientErrorData
{ {
Status = 406, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
Title = Resources.ApiConventions_Title_406, Title = Resources.ApiConventions_Title_406,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[409] = new ClientErrorData
{ {
Status = 409, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Title = Resources.ApiConventions_Title_409, Title = Resources.ApiConventions_Title_409,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[415] = new ClientErrorData
{ {
Status = 415, Link = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
Title = Resources.ApiConventions_Title_415, Title = Resources.ApiConventions_Title_415,
}); };
AddClientErrorFactory(new ProblemDetails options.ClientErrorMapping[422] = new ClientErrorData
{ {
Status = 422, Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
Type = "https://tools.ietf.org/html/rfc4918#section-11.2",
Title = Resources.ApiConventions_Title_422, 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) private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
@ -150,9 +129,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return result; return result;
} }
private static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context) internal static IActionResult ProblemDetailsInvalidModelStateResponse(ActionContext context)
{ {
var result = new BadRequestObjectResult(new ValidationProblemDetails(context.ModelState)); var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status400BadRequest,
};
ProblemDetailsClientErrorFactory.SetTraceId(context, problemDetails);
var result = new BadRequestObjectResult(problemDetails);
result.ContentTypes.Add("application/problem+json"); result.ContentTypes.Add("application/problem+json");
result.ContentTypes.Add("application/problem+xml"); result.ContentTypes.Add("application/problem+xml");

View File

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

View File

@ -174,7 +174,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpointInfo.Defaults, endpointInfo.Defaults,
++conventionalRouteOrder, ++conventionalRouteOrder,
endpointInfo, endpointInfo,
suppressLinkGeneration: false); endpointInfo.DataTokens,
suppressLinkGeneration: false,
suppressPathMatching: false);
endpoints.Add(subEndpoint); endpoints.Add(subEndpoint);
} }
@ -213,7 +215,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpointInfo.Defaults, endpointInfo.Defaults,
++conventionalRouteOrder, ++conventionalRouteOrder,
endpointInfo, endpointInfo,
suppressLinkGeneration: false); endpointInfo.DataTokens,
suppressLinkGeneration: false,
suppressPathMatching: false);
endpoints.Add(endpoint); endpoints.Add(endpoint);
} }
} }
@ -227,7 +231,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
nonInlineDefaults: null, nonInlineDefaults: null,
action.AttributeRouteInfo.Order, action.AttributeRouteInfo.Order,
action.AttributeRouteInfo, action.AttributeRouteInfo,
suppressLinkGeneration: action.AttributeRouteInfo.SuppressLinkGeneration); dataTokens: null,
suppressLinkGeneration: action.AttributeRouteInfo.SuppressLinkGeneration,
suppressPathMatching: action.AttributeRouteInfo.SuppressPathMatching);
endpoints.Add(endpoint); endpoints.Add(endpoint);
} }
} }
@ -375,19 +381,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
object nonInlineDefaults, object nonInlineDefaults,
int order, int order,
object source, object source,
bool suppressLinkGeneration) RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
bool suppressPathMatching)
{ {
RequestDelegate requestDelegate = (context) => RequestDelegate requestDelegate = (context) =>
{ {
var values = context.Features.Get<IRouteValuesFeature>().RouteValues; var routeData = context.GetRouteData();
var routeData = new RouteData();
foreach (var kvp in values)
{
if (kvp.Value != null)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
}
var actionContext = new ActionContext(context, routeData, action); var actionContext = new ActionContext(context, routeData, action);
@ -403,7 +403,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
routeName, routeName,
new RouteValueDictionary(action.RouteValues), new RouteValueDictionary(action.RouteValues),
source, source,
suppressLinkGeneration); dataTokens,
suppressLinkGeneration,
suppressPathMatching);
var endpoint = new RouteEndpoint( var endpoint = new RouteEndpoint(
requestDelegate, requestDelegate,
@ -420,12 +422,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
string routeName, string routeName,
RouteValueDictionary requiredValues, RouteValueDictionary requiredValues,
object source, object source,
bool suppressLinkGeneration) RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
bool suppressPathMatching)
{ {
var metadata = new List<object> var metadata = new List<object>
{ {
// REVIEW: Used for debugging. Consider removing before release
source,
action action
}; };
@ -434,6 +436,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
metadata.AddRange(action.EndpointMetadata); metadata.AddRange(action.EndpointMetadata);
} }
if (dataTokens != null)
{
metadata.Add(new DataTokensMetadata(dataTokens));
}
metadata.Add(new RouteValuesAddressMetadata(routeName, requiredValues)); metadata.Add(new RouteValuesAddressMetadata(routeName, requiredValues));
// Add filter descriptors to endpoint metadata // Add filter descriptors to endpoint metadata
@ -475,6 +482,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
metadata.Add(new SuppressLinkGenerationMetadata()); metadata.Add(new SuppressLinkGenerationMetadata());
} }
if (suppressPathMatching)
{
metadata.Add(new SuppressMatchingMetadata());
}
var metadataCollection = new EndpointMetadataCollection(metadata); var metadataCollection = new EndpointMetadataCollection(metadata);
return metadataCollection; return metadataCollection;
} }
@ -497,10 +509,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
foreach (var kvp in requiredValues) foreach (var kvp in requiredValues)
{ {
defaults[kvp.Key] = kvp.Value; if (kvp.Value != null)
{
defaults[kvp.Key] = kvp.Value;
}
} }
} }
private class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata { }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json;
namespace Microsoft.AspNetCore.Mvc namespace Microsoft.AspNetCore.Mvc
{ {
@ -17,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
/// "about:blank". /// "about:blank".
/// </summary> /// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Type { get; set; } public string Type { get; set; }
/// <summary> /// <summary>
@ -24,21 +26,39 @@ namespace Microsoft.AspNetCore.Mvc
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation; /// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
/// see[RFC7231], Section 3.4). /// see[RFC7231], Section 3.4).
/// </summary> /// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Title { get; set; } public string Title { get; set; }
/// <summary> /// <summary>
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
/// </summary> /// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? Status { get; set; } public int? Status { get; set; }
/// <summary> /// <summary>
/// A human-readable explanation specific to this occurrence of the problem. /// A human-readable explanation specific to this occurrence of the problem.
/// </summary> /// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Detail { get; set; } public string Detail { get; set; }
/// <summary> /// <summary>
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
/// </summary> /// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Instance { get; set; } public string Instance { get; set; }
/// <summary>
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members.
/// <para>
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
/// other members of a problem type.
/// </para>
/// </summary>
/// <remarks>
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters.
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
/// </remarks>
[JsonExtensionData]
public IDictionary<string, object> Extensions { get; } = new Dictionary<string, object>(StringComparer.Ordinal);
} }
} }

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

@ -14,8 +14,26 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{ {
public class KnownRouteValueConstraint : IRouteConstraint public class KnownRouteValueConstraint : IRouteConstraint
{ {
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private RouteValuesCollection _cachedValuesCollection; private RouteValuesCollection _cachedValuesCollection;
[Obsolete("This constructor is obsolete. Use KnownRouteValueConstraint.ctor(IActionDescriptorCollectionProvider) instead.")]
public KnownRouteValueConstraint()
{
// Empty constructor for backwards compatibility
// Services will need to be resolved from HttpContext when this ctor is used
}
public KnownRouteValueConstraint(IActionDescriptorCollectionProvider actionDescriptorCollectionProvider)
{
if (actionDescriptorCollectionProvider == null)
{
throw new ArgumentNullException(nameof(actionDescriptorCollectionProvider));
}
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
}
public bool Match( public bool Match(
HttpContext httpContext, HttpContext httpContext,
IRouter route, IRouter route,
@ -23,16 +41,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
RouteValueDictionary values, RouteValueDictionary values,
RouteDirection routeDirection) RouteDirection routeDirection)
{ {
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null) if (routeKey == null)
{ {
throw new ArgumentNullException(nameof(routeKey)); throw new ArgumentNullException(nameof(routeKey));
@ -49,7 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var value = obj as string; var value = obj as string;
if (value != null) if (value != null)
{ {
var allValues = GetAndCacheAllMatchingValues(routeKey, httpContext); var actionDescriptors = GetAndValidateActionDescriptors(httpContext);
var allValues = GetAndCacheAllMatchingValues(routeKey, actionDescriptors);
foreach (var existingValue in allValues) foreach (var existingValue in allValues)
{ {
if (string.Equals(value, existingValue, StringComparison.OrdinalIgnoreCase)) if (string.Equals(value, existingValue, StringComparison.OrdinalIgnoreCase))
@ -63,9 +73,36 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return false; return false;
} }
private string[] GetAndCacheAllMatchingValues(string routeKey, HttpContext httpContext) private ActionDescriptorCollection GetAndValidateActionDescriptors(HttpContext httpContext)
{
var actionDescriptorsProvider = _actionDescriptorCollectionProvider;
if (actionDescriptorsProvider == null)
{
// Only validate that HttpContext was passed to constraint if it is needed
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var services = httpContext.RequestServices;
actionDescriptorsProvider = services.GetRequiredService<IActionDescriptorCollectionProvider>();
}
var actionDescriptors = actionDescriptorsProvider.ActionDescriptors;
if (actionDescriptors == null)
{
throw new InvalidOperationException(
Resources.FormatPropertyOfTypeCannotBeNull(
nameof(IActionDescriptorCollectionProvider.ActionDescriptors),
actionDescriptorsProvider.GetType()));
}
return actionDescriptors;
}
private string[] GetAndCacheAllMatchingValues(string routeKey, ActionDescriptorCollection actionDescriptors)
{ {
var actionDescriptors = GetAndValidateActionDescriptorCollection(httpContext);
var version = actionDescriptors.Version; var version = actionDescriptors.Version;
var valuesCollection = _cachedValuesCollection; var valuesCollection = _cachedValuesCollection;
@ -77,8 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{ {
var action = actionDescriptors.Items[i]; var action = actionDescriptors.Items[i];
string value; if (action.RouteValues.TryGetValue(routeKey, out var value) &&
if (action.RouteValues.TryGetValue(routeKey, out value) &&
!string.IsNullOrEmpty(value)) !string.IsNullOrEmpty(value))
{ {
values.Add(value); values.Add(value);
@ -92,22 +128,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
return _cachedValuesCollection.Items; return _cachedValuesCollection.Items;
} }
private static ActionDescriptorCollection GetAndValidateActionDescriptorCollection(HttpContext httpContext)
{
var services = httpContext.RequestServices;
var provider = services.GetRequiredService<IActionDescriptorCollectionProvider>();
var descriptors = provider.ActionDescriptors;
if (descriptors == null)
{
throw new InvalidOperationException(
Resources.FormatPropertyOfTypeCannotBeNull("ActionDescriptors",
provider.GetType()));
}
return descriptors;
}
private class RouteValuesCollection private class RouteValuesCollection
{ {
public RouteValuesCollection(int version, string[] items) public RouteValuesCollection(int version, string[] items)

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
/// with the given <paramref name="statusCode"/>. /// with the given <paramref name="statusCode"/>.
/// </summary> /// </summary>
/// <param name="statusCode">The HTTP status code of the response.</param> /// <param name="statusCode">The HTTP status code of the response.</param>
public StatusCodeResult(int statusCode) public StatusCodeResult([StatusCodeValue] int statusCode)
{ {
StatusCode = statusCode; StatusCode = statusCode;
} }

View File

@ -0,0 +1,187 @@
// 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.Globalization;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
/// <summary>
/// Wrapper class for <see cref="Mvc.ProblemDetails"/> to enable it to be serialized by the xml formatters.
/// </summary>
[XmlRoot(nameof(ProblemDetails))]
public class ProblemDetailsWrapper : IXmlSerializable, IUnwrappable
{
/// <summary>
/// Key used to represent dictionary elements with empty keys
/// </summary>
protected static readonly string EmptyKey = SerializableErrorWrapper.EmptyKey;
/// <summary>
/// Initializes a new instance of <see cref="ProblemDetailsWrapper"/>.
/// </summary>
public ProblemDetailsWrapper()
: this(new ProblemDetails())
{
}
/// <summary>
/// Initializes a new instance of <see cref="ProblemDetailsWrapper"/>.
/// </summary>
public ProblemDetailsWrapper(ProblemDetails problemDetails)
{
ProblemDetails = problemDetails;
}
internal ProblemDetails ProblemDetails { get; }
/// <inheritdoc />
public XmlSchema GetSchema() => null;
/// <inheritdoc />
public virtual void ReadXml(XmlReader reader)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}
if (reader.IsEmptyElement)
{
reader.Read();
return;
}
reader.ReadStartElement();
while (reader.NodeType != XmlNodeType.EndElement)
{
var key = XmlConvert.DecodeName(reader.LocalName);
ReadValue(reader, key);
reader.MoveToContent();
}
reader.ReadEndElement();
}
/// <summary>
/// Reads the value for the specified <paramref name="name"/> from the <paramref name="reader"/>.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <param name="name">The name of the node.</param>
protected virtual void ReadValue(XmlReader reader, string name)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}
var value = reader.ReadInnerXml();
switch (name)
{
case nameof(ProblemDetails.Detail):
ProblemDetails.Detail = value;
break;
case nameof(ProblemDetails.Instance):
ProblemDetails.Instance = value;
break;
case nameof(ProblemDetails.Status):
ProblemDetails.Status = string.IsNullOrEmpty(value) ?
(int?)null :
int.Parse(value, CultureInfo.InvariantCulture);
break;
case nameof(ProblemDetails.Title):
ProblemDetails.Title = value;
break;
case nameof(ProblemDetails.Type):
ProblemDetails.Type = value;
break;
default:
if (string.Equals(name, EmptyKey, StringComparison.Ordinal))
{
name = string.Empty;
}
ProblemDetails.Extensions.Add(name, value);
break;
}
}
/// <inheritdoc />
public virtual void WriteXml(XmlWriter writer)
{
if (!string.IsNullOrEmpty(ProblemDetails.Detail))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Detail)),
ProblemDetails.Detail);
}
if (!string.IsNullOrEmpty(ProblemDetails.Instance))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Instance)),
ProblemDetails.Instance);
}
if (ProblemDetails.Status.HasValue)
{
writer.WriteStartElement(XmlConvert.EncodeLocalName(nameof(ProblemDetails.Status)));
writer.WriteValue(ProblemDetails.Status.Value);
writer.WriteEndElement();
}
if (!string.IsNullOrEmpty(ProblemDetails.Title))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Title)),
ProblemDetails.Title);
}
if (!string.IsNullOrEmpty(ProblemDetails.Type))
{
writer.WriteElementString(
XmlConvert.EncodeLocalName(nameof(ProblemDetails.Type)),
ProblemDetails.Type);
}
foreach (var keyValuePair in ProblemDetails.Extensions)
{
var key = keyValuePair.Key;
var value = keyValuePair.Value;
if (string.IsNullOrEmpty(key))
{
key = EmptyKey;
}
writer.WriteStartElement(XmlConvert.EncodeLocalName(key));
if (value != null)
{
writer.WriteValue(value);
}
writer.WriteEndElement();
}
}
object IUnwrappable.Unwrap(Type declaredType)
{
if (declaredType == null)
{
throw new ArgumentNullException(nameof(declaredType));
}
return ProblemDetails;
}
}
}

View File

@ -0,0 +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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Mvc.Formatters.Xml.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{ {
// Element name used when ModelStateEntry's Key is empty. Dash in element name should avoid collisions with // Element name used when ModelStateEntry's Key is empty. Dash in element name should avoid collisions with
// other ModelState entries because the character is not legal in an expression name. // other ModelState entries because the character is not legal in an expression name.
private static readonly string EmptyKey = "MVC-Empty"; internal static readonly string EmptyKey = "MVC-Empty";
// Note: XmlSerializer requires to have default constructor // Note: XmlSerializer requires to have default constructor
public SerializableErrorWrapper() public SerializableErrorWrapper()

View File

@ -0,0 +1,126 @@
// 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.Xml;
using System.Xml.Serialization;
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
/// <summary>
/// Wrapper class for <see cref="ValidationProblemDetails"/> to enable it to be serialized by the xml formatters.
/// </summary>
[XmlRoot(nameof(ValidationProblemDetails))]
public class ValidationProblemDetailsWrapper : ProblemDetailsWrapper, IUnwrappable
{
private static readonly string ErrorKey = "MVC-Errors";
/// <summary>
/// Initializes a new instance of <see cref="ValidationProblemDetailsWrapper"/>.
/// </summary>
public ValidationProblemDetailsWrapper()
: this(new ValidationProblemDetails())
{
}
/// <summary>
/// Initializes a new instance of <see cref="ValidationProblemDetailsWrapper"/> for the specified
/// <paramref name="problemDetails"/>.
/// </summary>
/// <param name="problemDetails">The <see cref="ProblemDetails"/>.</param>
public ValidationProblemDetailsWrapper(ValidationProblemDetails problemDetails)
: base(problemDetails)
{
ProblemDetails = problemDetails;
}
internal new ValidationProblemDetails ProblemDetails { get; }
/// <inheritdoc />
protected override void ReadValue(XmlReader reader, string name)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}
if (string.Equals(name, ErrorKey, StringComparison.Ordinal))
{
reader.Read();
ReadErrorProperty(reader);
}
else
{
base.ReadValue(reader, name);
}
}
private void ReadErrorProperty(XmlReader reader)
{
if (reader.IsEmptyElement)
{
return;
}
while (reader.NodeType != XmlNodeType.EndElement)
{
var key = XmlConvert.DecodeName(reader.LocalName);
var value = reader.ReadInnerXml();
if (string.Equals(EmptyKey, key, StringComparison.Ordinal))
{
key = string.Empty;
}
ProblemDetails.Errors.Add(key, new[] { value });
reader.MoveToContent();
}
}
/// <inheritdoc />
public override void WriteXml(XmlWriter writer)
{
if (writer == null)
{
throw new ArgumentNullException(nameof(writer));
}
base.WriteXml(writer);
if (ProblemDetails.Errors.Count == 0)
{
return;
}
writer.WriteStartElement(XmlConvert.EncodeLocalName(ErrorKey));
foreach (var keyValuePair in ProblemDetails.Errors)
{
var key = keyValuePair.Key;
var value = keyValuePair.Value;
if (string.IsNullOrEmpty(key))
{
key = EmptyKey;
}
writer.WriteStartElement(XmlConvert.EncodeLocalName(key));
if (value != null)
{
writer.WriteValue(value);
}
writer.WriteEndElement();
}
writer.WriteEndElement();
}
object IUnwrappable.Unwrap(Type declaredType)
{
if (declaredType == null)
{
throw new ArgumentNullException(nameof(declaredType));
}
return ProblemDetails;
}
}
}

View File

@ -44,5 +44,24 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
return null; return null;
} }
internal static IList<IWrapperProviderFactory> GetDefaultProviderFactories()
{
var wrapperProviderFactories = new List<IWrapperProviderFactory>();
wrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
wrapperProviderFactories.Add(new WrapperProviderFactory(
typeof(ProblemDetails),
typeof(ProblemDetailsWrapper),
value => new ProblemDetailsWrapper((ProblemDetails)value)));
wrapperProviderFactories.Add(new WrapperProviderFactory(
typeof(ValidationProblemDetails),
typeof(ValidationProblemDetailsWrapper),
value => new ValidationProblemDetailsWrapper((ValidationProblemDetails)value)));
return wrapperProviderFactories;
}
} }
} }

View File

@ -0,0 +1,50 @@
// 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;
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
internal class WrapperProviderFactory : IWrapperProviderFactory
{
public WrapperProviderFactory(Type declaredType, Type wrappingType, Func<object, object> wrapper)
{
DeclaredType = declaredType;
WrappingType = wrappingType;
Wrapper = wrapper;
}
public Type DeclaredType { get; }
public Type WrappingType { get; }
public Func<object, object> Wrapper { get; }
public IWrapperProvider GetProvider(WrapperProviderContext context)
{
if (context.DeclaredType == DeclaredType)
{
return new WrapperProvider(this);
}
return null;
}
private class WrapperProvider : IWrapperProvider
{
private readonly WrapperProviderFactory _wrapperFactory;
public WrapperProvider(WrapperProviderFactory wrapperFactory)
{
_wrapperFactory = wrapperFactory;
}
public Type WrappingType => _wrapperFactory.WrappingType;
public object Wrap(object original)
{
return _wrapperFactory.Wrapper(original);
}
}
}
}

View File

@ -46,8 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
_serializerSettings = new DataContractSerializerSettings(); _serializerSettings = new DataContractSerializerSettings();
WrapperProviderFactories = new List<IWrapperProviderFactory>(); WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
} }
/// <summary> /// <summary>

View File

@ -76,9 +76,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
_serializerSettings = new DataContractSerializerSettings(); _serializerSettings = new DataContractSerializerSettings();
WrapperProviderFactories = new List<IWrapperProviderFactory>(); WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories)); WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories));
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
_logger = loggerFactory?.CreateLogger(GetType()); _logger = loggerFactory?.CreateLogger(GetType());
} }

View File

@ -43,8 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml); SupportedMediaTypes.Add(MediaTypeHeaderValues.TextXml);
SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax); SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationAnyXmlSyntax);
WrapperProviderFactories = new List<IWrapperProviderFactory>(); WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
} }
/// <summary> /// <summary>

View File

@ -73,9 +73,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
WriterSettings = writerSettings; WriterSettings = writerSettings;
WrapperProviderFactories = new List<IWrapperProviderFactory>(); WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories)); WrapperProviderFactories.Add(new EnumerableWrapperProviderFactory(WrapperProviderFactories));
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
_logger = loggerFactory?.CreateLogger(GetType()); _logger = loggerFactory?.CreateLogger(GetType());
} }

View File

@ -196,11 +196,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
else if (required) else if (required)
{ {
// If the section is not found, and it is not optional, throw an error. // If the section is not found, and it is not optional, throw an error.
var message = Resources.FormatSectionNotDefined( var viewContext = ViewContext;
ViewContext.ExecutingFilePath, throw new InvalidOperationException(
sectionName, Resources.FormatSectionNotDefined(
ViewContext.View.Path); viewContext.ExecutingFilePath,
throw new InvalidOperationException(message); sectionName,
viewContext.View.Path));
} }
else else
{ {

View File

@ -51,13 +51,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
{ {
get get
{ {
if (ViewContext == null) var viewContext = ViewContext;
if (viewContext == null)
{ {
var message = Resources.FormatViewContextMustBeSet("ViewContext", "Output"); throw new InvalidOperationException(Resources.FormatViewContextMustBeSet(nameof(ViewContext), nameof(Output)));
throw new InvalidOperationException(message);
} }
return ViewContext.Writer; return viewContext.Writer;
} }
} }
@ -183,8 +183,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// </remarks> /// </remarks>
public void StartTagHelperWritingScope(HtmlEncoder encoder) public void StartTagHelperWritingScope(HtmlEncoder encoder)
{ {
var viewContext = ViewContext;
var buffer = new ViewBuffer(BufferScope, Path, ViewBuffer.TagHelperPageSize); var buffer = new ViewBuffer(BufferScope, Path, ViewBuffer.TagHelperPageSize);
TagHelperScopes.Push(new TagHelperScopeInfo(buffer, HtmlEncoder, ViewContext.Writer)); TagHelperScopes.Push(new TagHelperScopeInfo(buffer, HtmlEncoder, viewContext.Writer));
// If passed an HtmlEncoder, override the property. // If passed an HtmlEncoder, override the property.
if (encoder != null) if (encoder != null)
@ -194,7 +195,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// We need to replace the ViewContext's Writer to ensure that all content (including content written // We need to replace the ViewContext's Writer to ensure that all content (including content written
// from HTML helpers) is redirected. // from HTML helpers) is redirected.
ViewContext.Writer = new ViewBufferTextWriter(buffer, ViewContext.Writer.Encoding); viewContext.Writer = new ViewBufferTextWriter(buffer, viewContext.Writer.Encoding);
} }
/// <summary> /// <summary>
@ -238,7 +239,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
throw new InvalidOperationException(Resources.RazorPage_NestingAttributeWritingScopesNotSupported); throw new InvalidOperationException(Resources.RazorPage_NestingAttributeWritingScopesNotSupported);
} }
_pageWriter = ViewContext.Writer; var viewContext = ViewContext;
_pageWriter = viewContext.Writer;
if (_valueBuffer == null) if (_valueBuffer == null)
{ {
@ -247,7 +249,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// We need to replace the ViewContext's Writer to ensure that all content (including content written // We need to replace the ViewContext's Writer to ensure that all content (including content written
// from HTML helpers) is redirected. // from HTML helpers) is redirected.
ViewContext.Writer = _valueBuffer; viewContext.Writer = _valueBuffer;
} }
@ -284,15 +286,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor
throw new ArgumentNullException(nameof(writer)); throw new ArgumentNullException(nameof(writer));
} }
_textWriterStack.Push(ViewContext.Writer); var viewContext = ViewContext;
ViewContext.Writer = writer; _textWriterStack.Push(viewContext.Writer);
viewContext.Writer = writer;
} }
// Internal for unit testing. // Internal for unit testing.
protected internal virtual TextWriter PopWriter() protected internal virtual TextWriter PopWriter()
{ {
ViewContext.Writer = _textWriterStack.Pop(); var viewContext = ViewContext;
return ViewContext.Writer; var writer = _textWriterStack.Pop();
viewContext.Writer = writer;
return writer;
} }
public virtual string Href(string contentPath) public virtual string Href(string contentPath)
@ -304,9 +309,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
if (_urlHelper == null) if (_urlHelper == null)
{ {
var services = ViewContext?.HttpContext.RequestServices; var viewContext = ViewContext;
var services = viewContext?.HttpContext.RequestServices;
var factory = services.GetRequiredService<IUrlHelperFactory>(); var factory = services.GetRequiredService<IUrlHelperFactory>();
_urlHelper = factory.GetUrlHelper(ViewContext); _urlHelper = factory.GetUrlHelper(viewContext);
} }
return _urlHelper.Content(contentPath); return _urlHelper.Content(contentPath);
@ -637,8 +643,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// before <see cref="RazorPageBase.FlushAsync"/> flushes the headers. </remarks> /// before <see cref="RazorPageBase.FlushAsync"/> flushes the headers. </remarks>
public virtual HtmlString SetAntiforgeryCookieAndHeader() public virtual HtmlString SetAntiforgeryCookieAndHeader()
{ {
var antiforgery = ViewContext?.HttpContext.RequestServices.GetRequiredService<IAntiforgery>(); var viewContext = ViewContext;
antiforgery.SetCookieTokenAndHeader(ViewContext?.HttpContext); var antiforgery = viewContext?.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
antiforgery.SetCookieTokenAndHeader(viewContext?.HttpContext);
return HtmlString.Empty; return HtmlString.Empty;
} }

View File

@ -53,7 +53,7 @@
Include="$([System.IO.Path]::ChangeExtension('%(_ContentRootProjectReferences.ResolvedFrom)', '.deps.json'))" /> Include="$([System.IO.Path]::ChangeExtension('%(_ContentRootProjectReferences.ResolvedFrom)', '.deps.json'))" />
</ItemGroup> </ItemGroup>
<Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutputPath)" Condition="Exists('%(DepsFilePaths.FullPath)')" /> <Copy SourceFiles="%(DepsFilePaths.FullPath)" DestinationFolder="$(OutDir)" Condition="Exists('%(DepsFilePaths.FullPath)')" />
</Target> </Target>
</Project> </Project>

View File

@ -37,7 +37,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
var pages = _charBuffer.Pages; var pages = _charBuffer.Pages;
for (var i = 0; i < pages.Count; i++) var count = pages.Count;
for (var i = 0; i < count; i++)
{ {
var page = pages[i]; var page = pages[i];
var pageLength = Math.Min(length, page.Length); var pageLength = Math.Min(length, page.Length);

View File

@ -19,16 +19,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
public ICharBufferSource BufferSource { get; } public ICharBufferSource BufferSource { get; }
public IList<char[]> Pages { get; } = new List<char[]>(); // Strongly typed rather than IList for performance
public List<char[]> Pages { get; } = new List<char[]>();
public int Length public int Length
{ {
get get
{ {
var length = _charIndex; var length = _charIndex;
for (var i = 0; i < Pages.Count - 1; i++) var pages = Pages;
var fullPages = pages.Count - 1;
for (var i = 0; i < fullPages; i++)
{ {
length += Pages[i].Length; length += pages[i].Length;
} }
return length; return length;
@ -100,13 +103,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
/// </summary> /// </summary>
public void Clear() public void Clear()
{ {
for (var i = Pages.Count - 1; i > 0; i--) var pages = Pages;
for (var i = pages.Count - 1; i > 0; i--)
{ {
var page = Pages[i]; var page = pages[i];
try try
{ {
Pages.RemoveAt(i); pages.RemoveAt(i);
} }
finally finally
{ {
@ -115,7 +119,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
_charIndex = 0; _charIndex = 0;
CurrentPage = Pages.Count > 0 ? Pages[0] : null; CurrentPage = pages.Count > 0 ? pages[0] : null;
} }
private char[] GetCurrentPage() private char[] GetCurrentPage()
@ -148,12 +152,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
public void Dispose() public void Dispose()
{ {
for (var i = 0; i < Pages.Count; i++) var pages = Pages;
var count = pages.Count;
for (var i = 0; i < count; i++)
{ {
BufferSource.Return(Pages[i]); BufferSource.Return(pages[i]);
} }
Pages.Clear(); pages.Clear();
} }
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
@ -90,55 +91,73 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
} }
/// <inheritdoc /> /// <inheritdoc />
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IHtmlContentBuilder Append(string unencoded) public IHtmlContentBuilder Append(string unencoded)
{ {
if (unencoded == null) if (unencoded != null)
{ {
return this; // Text that needs encoding is the uncommon case in views, which is why it
// creates a wrapper and pre-encoded text does not.
AppendValue(new ViewBufferValue(new EncodingWrapper(unencoded)));
} }
// Text that needs encoding is the uncommon case in views, which is why it
// creates a wrapper and pre-encoded text does not.
AppendValue(new ViewBufferValue(new EncodingWrapper(unencoded)));
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IHtmlContentBuilder AppendHtml(IHtmlContent content) public IHtmlContentBuilder AppendHtml(IHtmlContent content)
{ {
if (content == null) if (content != null)
{ {
return this; AppendValue(new ViewBufferValue(content));
} }
AppendValue(new ViewBufferValue(content));
return this; return this;
} }
/// <inheritdoc /> /// <inheritdoc />
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IHtmlContentBuilder AppendHtml(string encoded) public IHtmlContentBuilder AppendHtml(string encoded)
{ {
if (encoded == null) if (encoded != null)
{ {
return this; AppendValue(new ViewBufferValue(encoded));
} }
AppendValue(new ViewBufferValue(encoded));
return this; return this;
} }
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void AppendValue(ViewBufferValue value) private void AppendValue(ViewBufferValue value)
{ {
var page = GetCurrentPage(); var page = GetCurrentPage();
page.Append(value); page.Append(value);
} }
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ViewBufferPage GetCurrentPage() private ViewBufferPage GetCurrentPage()
{ {
if (_currentPage == null || _currentPage.IsFull) var currentPage = _currentPage;
if (currentPage == null || currentPage.IsFull)
{ {
AddPage(new ViewBufferPage(_bufferScope.GetPage(_pageSize))); // Uncommon slow-path
return AppendNewPage();
} }
return currentPage;
}
// Slow path for above, don't inline
[MethodImpl(MethodImplOptions.NoInlining)]
private ViewBufferPage AppendNewPage()
{
AddPage(new ViewBufferPage(_bufferScope.GetPage(_pageSize)));
return _currentPage; return _currentPage;
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{ {
public class ViewBufferPage public class ViewBufferPage
@ -18,6 +20,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
public bool IsFull => Count == Capacity; public bool IsFull => Count == Capacity;
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(ViewBufferValue value) => Buffer[Count++] = value; public void Append(ViewBufferValue value) => Buffer[Count++] = value;
} }
} }

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

@ -32,35 +32,25 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
} }
[Fact] [Fact]
public void OnResultExecuting_DoesNothing_IfStatusCodeDoesNotExistInApiBehaviorOptions() public void OnResultExecuting_DoesNothing_IfTransformedValueIsNull()
{ {
// Arrange // Arrange
var actionResult = new NotFoundResult(); var actionResult = new NotFoundResult();
var context = GetContext(actionResult); var context = GetContext(actionResult);
var filter = GetFilter(new ApiBehaviorOptions()); var factory = new Mock<IClientErrorFactory>();
factory
// Act .Setup(f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()))
filter.OnResultExecuting(context); .Returns((IActionResult)null)
.Verifiable();
// Assert
Assert.Same(actionResult, context.Result); var filter = new ClientErrorResultFilter(factory.Object, NullLogger<ClientErrorResultFilter>.Instance);
}
[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 // Act
filter.OnResultExecuting(context); filter.OnResultExecuting(context);
// Assert // Assert
Assert.Same(actionResult, context.Result); Assert.Same(actionResult, context.Result);
factory.Verify();
} }
[Fact] [Fact]
@ -78,18 +68,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Assert.Same(Result, context.Result); Assert.Same(Result, context.Result);
} }
private static ClientErrorResultFilter GetFilter(ApiBehaviorOptions options = null) private static ClientErrorResultFilter GetFilter()
{ {
var apiBehaviorOptions = options ?? GetOptions(); var factory = Mock.Of<IClientErrorFactory>(
var filter = new ClientErrorResultFilter(apiBehaviorOptions, NullLogger<ClientErrorResultFilter>.Instance); f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()) == Result);
return filter;
}
private static ApiBehaviorOptions GetOptions() return new ClientErrorResultFilter(factory, NullLogger<ClientErrorResultFilter>.Instance);
{
var apiBehaviorOptions = new ApiBehaviorOptions();
apiBehaviorOptions.ClientErrorFactory[404] = _ => Result;
return apiBehaviorOptions;
} }
private static ResultExecutingContext GetContext(IActionResult actionResult) private static ResultExecutingContext GetContext(IActionResult actionResult)

View File

@ -0,0 +1,129 @@
// 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.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
public class ProblemDetalsClientErrorFactoryTest
{
[Fact]
public void GetClientError_ReturnsProblemDetails_IfNoMappingWasFound()
{
// Arrange
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[405] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal(415, problemDetails.Status);
Assert.Equal("about:blank", problemDetails.Type);
Assert.Null(problemDetails.Title);
Assert.Null(problemDetails.Detail);
Assert.Null(problemDetails.Instance);
}
[Fact]
public void GetClientError_ReturnsProblemDetails()
{
// Arrange
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal(415, problemDetails.Status);
Assert.Equal("Some link", problemDetails.Type);
Assert.Equal("Summary", problemDetails.Title);
Assert.Null(problemDetails.Detail);
Assert.Null(problemDetails.Instance);
}
[Fact]
public void GetClientError_UsesActivityId_ToSetTraceId()
{
// Arrange
using (new ActivityReplacer())
{
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal(Activity.Current.Id, problemDetails.Extensions["traceId"]);
}
}
[Fact]
public void GetClientError_UsesHttpContext_ToSetTraceIdIfActivityIdIsNotSet()
{
// Arrange
var clientError = new UnsupportedMediaTypeResult();
var factory = new ProblemDetailsClientErrorFactory(Options.Create(new ApiBehaviorOptions
{
ClientErrorMapping =
{
[415] = new ClientErrorData { Link = "Some link", Title = "Summary" },
},
}));
// Act
var result = factory.GetClientError(GetActionContext(), clientError);
// Assert
var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, objectResult.ContentTypes);
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal("42", problemDetails.Extensions["traceId"]);
}
private static ActionContext GetActionContext()
{
return new ActionContext
{
HttpContext = new DefaultHttpContext
{
TraceIdentifier = "42",
}
};
}
}
}

View File

@ -12,13 +12,15 @@ 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;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
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
@ -1041,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
@ -1059,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()
{ {
@ -1081,7 +1241,7 @@ Environment.NewLine + "int b";
var context = GetContext(typeof(TestApiController)); var context = GetContext(typeof(TestApiController));
var options = new ApiBehaviorOptions var options = new ApiBehaviorOptions
{ {
SuppressUseClientErrorFactory = true, SuppressMapClientErrors = true,
InvalidModelStateResponseFactory = _ => null, InvalidModelStateResponseFactory = _ => null,
}; };
var provider = GetProvider(options); var provider = GetProvider(options);
@ -1122,7 +1282,11 @@ Environment.NewLine + "int b";
var loggerFactory = NullLoggerFactory.Instance; var loggerFactory = NullLoggerFactory.Instance;
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider(); modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
return new ApiBehaviorApplicationModelProvider(optionsAccessor, modelMetadataProvider, loggerFactory); return new ApiBehaviorApplicationModelProvider(
optionsAccessor,
modelMetadataProvider,
Mock.Of<IClientErrorFactory>(),
loggerFactory);
} }
private static ApplicationModelProviderContext GetContext( private static ApplicationModelProviderContext GetContext(

View File

@ -2,6 +2,9 @@
// 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.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -28,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
[Fact] [Fact]
public void Configure_AddsClientErrorFactories() public void Configure_AddsClientErrorMappings()
{ {
// Arrange // Arrange
var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, }; var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, };
@ -41,7 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
optionsSetup.Configure(options); optionsSetup.Configure(options);
// Assert // Assert
Assert.Equal(expected, options.ClientErrorFactory.Keys); Assert.Equal(expected, options.ClientErrorMapping.Keys);
} }
[Fact] [Fact]
@ -97,5 +100,64 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert // Assert
Assert.Same(expected, options.InvalidModelStateResponseFactory); Assert.Same(expected, options.InvalidModelStateResponseFactory);
} }
[Fact]
public void ProblemDetailsInvalidModelStateResponse_ReturnsBadRequestWithProblemDetails()
{
// Arrange
var actionContext = new ActionContext
{
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
};
// Act
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
Assert.Equal(new[] { "application/problem+json", "application/problem+xml" }, badRequest.ContentTypes.OrderBy(c => c));
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
Assert.Equal(400, problemDetails.Status);
}
[Fact]
public void ProblemDetailsInvalidModelStateResponse_SetsTraceId()
{
// Arrange
using (new ActivityReplacer())
{
var actionContext = new ActionContext
{
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
};
// Act
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
Assert.Equal(Activity.Current.Id, problemDetails.Extensions["traceId"]);
}
}
[Fact]
public void ProblemDetailsInvalidModelStateResponse_SetsTraceIdFromRequest_IfActivityIsNull()
{
// Arrange
var actionContext = new ActionContext
{
HttpContext = new DefaultHttpContext { TraceIdentifier = "42" },
};
// Act
var result = ApiBehaviorOptionsSetup.ProblemDetailsInvalidModelStateResponse(actionContext);
// Assert
var badRequest = Assert.IsType<BadRequestObjectResult>(result);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequest.Value);
Assert.Equal("42", problemDetails.Extensions["traceId"]);
}
} }
} }

View File

@ -175,8 +175,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
.SetupGet(o => o.Value) .SetupGet(o => o.Value)
.Returns(new RouteOptions()); .Returns(new RouteOptions());
#pragma warning disable CS0618 // Type or member is obsolete
var inlineConstraintResolver = new DefaultInlineConstraintResolver(routeOptions.Object);
#pragma warning restore CS0618 // Type or member is obsolete
var services = new ServiceCollection() var services = new ServiceCollection()
.AddSingleton<IInlineConstraintResolver>(new DefaultInlineConstraintResolver(routeOptions.Object)) .AddSingleton<IInlineConstraintResolver>(inlineConstraintResolver)
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>(); .AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>(); services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

View File

@ -89,6 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var featureCollection = new FeatureCollection(); var featureCollection = new FeatureCollection();
featureCollection.Set<IEndpointFeature>(endpointFeature); featureCollection.Set<IEndpointFeature>(endpointFeature);
featureCollection.Set<IRouteValuesFeature>(endpointFeature); featureCollection.Set<IRouteValuesFeature>(endpointFeature);
featureCollection.Set<IRoutingFeature>(endpointFeature);
var httpContextMock = new Mock<HttpContext>(); var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(m => m.Features).Returns(featureCollection); httpContextMock.Setup(m => m.Features).Returns(featureCollection);
@ -180,7 +181,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actionDescriptorCollection = GetActionDescriptorCollection( var actionDescriptorCollection = GetActionDescriptorCollection(
new { controller = "TestController", action = "TestAction", area = "TestArea" }); new { controller = "TestController", action = "TestAction", area = "TestArea" });
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection); var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
var services = new ServiceCollection();
services.AddRouting();
services.AddSingleton(actionDescriptorCollection);
var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
services.Configure<RouteOptions>(routeOptionsSetup.Configure);
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute, serviceProvider: services.BuildServiceProvider()));
// Act // Act
var endpoints = dataSource.Endpoints; var endpoints = dataSource.Endpoints;
@ -686,28 +695,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults); AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
} }
[Fact]
public void RequiredValues_HavingNull_AndNotPresentInDefaultValues_IsAddedToDefaultValues()
{
// Arrange
var requiredValues = new RouteValueDictionary(
new { area = (string)null, controller = "Foo", action = "Bar", page = (string)null });
var expectedDefaults = requiredValues;
var actionDescriptorCollection = GetActionDescriptorCollection(requiredValues: requiredValues);
var dataSource = CreateMvcEndpointDataSource(actionDescriptorCollection);
dataSource.ConventionalEndpointInfos.Add(
CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}"));
// Act
var endpoints = dataSource.Endpoints;
// Assert
var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.RoutePattern.RawText);
AssertIsSubset(expectedDefaults, matcherEndpoint.RoutePattern.Defaults);
}
private MvcEndpointDataSource CreateMvcEndpointDataSource( private MvcEndpointDataSource CreateMvcEndpointDataSource(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null,
MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null) MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null)
@ -719,13 +706,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Array.Empty<IActionDescriptorChangeProvider>()); Array.Empty<IActionDescriptorChangeProvider>());
} }
var serviceProviderMock = new Mock<IServiceProvider>(); var services = new ServiceCollection();
serviceProviderMock.Setup(m => m.GetService(typeof(IActionDescriptorCollectionProvider))).Returns(actionDescriptorCollectionProvider); services.AddSingleton(actionDescriptorCollectionProvider);
var dataSource = new MvcEndpointDataSource( var dataSource = new MvcEndpointDataSource(
actionDescriptorCollectionProvider, actionDescriptorCollectionProvider,
mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())), mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
serviceProviderMock.Object); services.BuildServiceProvider());
return dataSource; return dataSource;
} }
@ -735,15 +722,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal
string template, string template,
RouteValueDictionary defaults = null, RouteValueDictionary defaults = null,
IDictionary<string, object> constraints = null, IDictionary<string, object> constraints = null,
RouteValueDictionary dataTokens = null) RouteValueDictionary dataTokens = null,
IServiceProvider serviceProvider = null)
{ {
var serviceCollection = new ServiceCollection(); if (serviceProvider == null)
serviceCollection.AddRouting(); {
var serviceCollection = new ServiceCollection();
serviceCollection.AddRouting();
var routeOptionsSetup = new MvcCoreRouteOptionsSetup(); var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
serviceCollection.Configure<RouteOptions>(routeOptionsSetup.Configure); serviceCollection.Configure<RouteOptions>(routeOptionsSetup.Configure);
var serviceProvider = serviceCollection.BuildServiceProvider(); serviceProvider = serviceCollection.BuildServiceProvider();
}
var parameterPolicyFactory = serviceProvider.GetRequiredService<ParameterPolicyFactory>(); var parameterPolicyFactory = serviceProvider.GetRequiredService<ParameterPolicyFactory>();
return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, parameterPolicyFactory); return new MvcEndpointInfo(name, template, defaults, constraints, dataTokens, parameterPolicyFactory);

View File

@ -2,7 +2,6 @@
// 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.Linq; using System.Linq;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Abstractions;
@ -10,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using Xunit; using Xunit;
@ -17,7 +17,47 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{ {
public class KnownRouteValueConstraintTests public class KnownRouteValueConstraintTests
{ {
#pragma warning disable CS0618 // Type or member is obsolete
private readonly IRouteConstraint _constraint = new KnownRouteValueConstraint(); private readonly IRouteConstraint _constraint = new KnownRouteValueConstraint();
#pragma warning restore CS0618 // Type or member is obsolete
[Fact]
public void ResolveFromServices_InjectsServiceProvider_HttpContextNotNeeded()
{
// Arrange
var actionDescriptor = CreateActionDescriptor("testArea",
"testController",
"testAction");
actionDescriptor.RouteValues.Add("randomKey", "testRandom");
var descriptorCollectionProvider = CreateActionDesciprtorCollectionProvider(actionDescriptor);
var services = new ServiceCollection();
services.AddRouting();
services.AddSingleton(descriptorCollectionProvider);
var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
services.Configure<RouteOptions>(routeOptionsSetup.Configure);
var serviceProvider = services.BuildServiceProvider();
var inlineConstraintResolver = serviceProvider.GetRequiredService<IInlineConstraintResolver>();
var constraint = inlineConstraintResolver.ResolveConstraint("exists");
var values = new RouteValueDictionary()
{
{ "area", "testArea" },
{ "controller", "testController" },
{ "action", "testAction" },
{ "randomKey", "testRandom" }
};
// Act
var knownRouteValueConstraint = Assert.IsType<KnownRouteValueConstraint>(constraint);
var match = knownRouteValueConstraint.Match(httpContext: null, route: null, "area", values, RouteDirection.IncomingRequest);
// Assert
Assert.True(match);
}
[Theory] [Theory]
[InlineData("area", RouteDirection.IncomingRequest)] [InlineData("area", RouteDirection.IncomingRequest)]
@ -55,8 +95,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{ {
// Arrange // Arrange
var actionDescriptor = CreateActionDescriptor("testArea", var actionDescriptor = CreateActionDescriptor("testArea",
"testController", "testController",
"testAction"); "testAction");
actionDescriptor.RouteValues.Add("randomKey", "testRandom"); actionDescriptor.RouteValues.Add("randomKey", "testRandom");
var httpContext = GetHttpContext(actionDescriptor); var httpContext = GetHttpContext(actionDescriptor);
var route = Mock.Of<IRouter>(); var route = Mock.Of<IRouter>();
@ -115,8 +155,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public void RouteValue_IsNotAString_MatchFails(RouteDirection direction) public void RouteValue_IsNotAString_MatchFails(RouteDirection direction)
{ {
var actionDescriptor = CreateActionDescriptor("testArea", var actionDescriptor = CreateActionDescriptor("testArea",
controller: null, controller: null,
action: null); action: null);
var httpContext = GetHttpContext(actionDescriptor); var httpContext = GetHttpContext(actionDescriptor);
var route = Mock.Of<IRouter>(); var route = Mock.Of<IRouter>();
var values = new RouteValueDictionary() var values = new RouteValueDictionary()
@ -157,7 +197,57 @@ namespace Microsoft.AspNetCore.Mvc.Routing
ex.Message); ex.Message);
} }
private static HttpContext GetHttpContext(ActionDescriptor actionDescriptor) [Theory]
[InlineData("area", RouteDirection.IncomingRequest)]
[InlineData("controller", RouteDirection.IncomingRequest)]
[InlineData("action", RouteDirection.IncomingRequest)]
[InlineData("randomKey", RouteDirection.IncomingRequest)]
[InlineData("area", RouteDirection.UrlGeneration)]
[InlineData("controller", RouteDirection.UrlGeneration)]
[InlineData("action", RouteDirection.UrlGeneration)]
[InlineData("randomKey", RouteDirection.UrlGeneration)]
public void ServiceInjected_RouteKey_Exists_MatchSucceeds(string keyName, RouteDirection direction)
{
// Arrange
var actionDescriptor = CreateActionDescriptor("testArea",
"testController",
"testAction");
actionDescriptor.RouteValues.Add("randomKey", "testRandom");
var provider = CreateActionDesciprtorCollectionProvider(actionDescriptor);
var constraint = new KnownRouteValueConstraint(provider);
var values = new RouteValueDictionary()
{
{ "area", "testArea" },
{ "controller", "testController" },
{ "action", "testAction" },
{ "randomKey", "testRandom" }
};
// Act
var match = constraint.Match(httpContext: null, route: null, keyName, values, direction);
// Assert
Assert.True(match);
}
private static HttpContext GetHttpContext(ActionDescriptor actionDescriptor, bool setupRequestServices = true)
{
var descriptorCollectionProvider = CreateActionDesciprtorCollectionProvider(actionDescriptor);
var context = new Mock<HttpContext>();
if (setupRequestServices)
{
context.Setup(o => o.RequestServices
.GetService(typeof(IActionDescriptorCollectionProvider)))
.Returns(descriptorCollectionProvider);
}
return context.Object;
}
private static IActionDescriptorCollectionProvider CreateActionDesciprtorCollectionProvider(ActionDescriptor actionDescriptor)
{ {
var actionProvider = new Mock<IActionDescriptorProvider>(MockBehavior.Strict); var actionProvider = new Mock<IActionDescriptorProvider>(MockBehavior.Strict);
@ -176,12 +266,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var descriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider( var descriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
new[] { actionProvider.Object }, new[] { actionProvider.Object },
Enumerable.Empty<IActionDescriptorChangeProvider>()); Enumerable.Empty<IActionDescriptorChangeProvider>());
return descriptorCollectionProvider;
var context = new Mock<HttpContext>();
context.Setup(o => o.RequestServices
.GetService(typeof(IActionDescriptorCollectionProvider)))
.Returns(descriptorCollectionProvider);
return context.Object;
} }
private static ActionDescriptor CreateActionDescriptor(string area, string controller, string action) private static ActionDescriptor CreateActionDescriptor(string area, string controller, string action)

View File

@ -0,0 +1,25 @@
// 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.Diagnostics;
namespace Microsoft.AspNetCore.Mvc
{
public class ActivityReplacer : IDisposable
{
private readonly Activity _activity;
public ActivityReplacer()
{
_activity = new Activity("Test");
_activity.Start();
}
public void Dispose()
{
Debug.Assert(Activity.Current == _activity);
_activity.Stop();
}
}
}

View File

@ -0,0 +1,100 @@
// 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.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
public class ProblemDetailsWrapperTest
{
[Fact]
public void ReadXml_ReadsProblemDetailsXml()
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>403</Status>" +
"<Instance>Some instance</Instance>" +
"<key1>Test Value 1</key1>" +
"<_x005B_key2_x005D_>Test Value 2</_x005B_key2_x005D_>" +
"<MVC-Empty>Test Value 3</MVC-Empty>" +
"</ProblemDetails>";
var serializer = new DataContractSerializer(typeof(ProblemDetailsWrapper));
// Act
var value = serializer.ReadObject(
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
// Assert
var problemDetails = Assert.IsType<ProblemDetailsWrapper>(value).ProblemDetails;
Assert.Equal("Some title", problemDetails.Title);
Assert.Equal("Some instance", problemDetails.Instance);
Assert.Equal(403, problemDetails.Status);
Assert.Collection(
problemDetails.Extensions.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Empty(kvp.Key);
Assert.Equal("Test Value 3", kvp.Value);
},
kvp =>
{
Assert.Equal("[key2]", kvp.Key);
Assert.Equal("Test Value 2", kvp.Value);
},
kvp =>
{
Assert.Equal("key1", kvp.Key);
Assert.Equal("Test Value 1", kvp.Value);
});
}
[Fact]
public void WriteXml_WritesValidXml()
{
// Arrange
var problemDetails = new ProblemDetails
{
Title = "Some title",
Detail = "Some detail",
Extensions =
{
["key1"] = "Test Value 1",
["[Key2]"] = "Test Value 2",
[""] = "Test Value 3",
},
};
var wrapper = new ProblemDetailsWrapper(problemDetails);
var outputStream = new MemoryStream();
var expectedContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ProblemDetails>" +
"<Detail>Some detail</Detail>" +
"<Title>Some title</Title>" +
"<key1>Test Value 1</key1>" +
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
"<MVC-Empty>Test Value 3</MVC-Empty>" +
"</ProblemDetails>";
// Act
using (var xmlWriter = XmlWriter.Create(outputStream))
{
var dataContractSerializer = new DataContractSerializer(wrapper.GetType());
dataContractSerializer.WriteObject(xmlWriter, wrapper);
}
outputStream.Position = 0;
var res = new StreamReader(outputStream, Encoding.UTF8).ReadToEnd();
// Assert
Assert.Equal(expectedContent, res);
}
}
}

View File

@ -0,0 +1,226 @@
// 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.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
public class ValidationProblemDetailsWrapperTest
{
[Fact]
public void ReadXml_ReadsValidationProblemDetailsXml()
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>400</Status>" +
"<Instance>Some instance</Instance>" +
"<key1>Test Value 1</key1>" +
"<_x005B_key2_x005D_>Test Value 2</_x005B_key2_x005D_>" +
"<MVC-Errors>" +
"<error1>Test error 1 Test error 2</error1>" +
"<_x005B_error2_x005D_>Test error 3</_x005B_error2_x005D_>" +
"<MVC-Empty>Test error 4</MVC-Empty>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
// Act
var value = serializer.ReadObject(
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
// Assert
var problemDetails = Assert.IsType<ValidationProblemDetailsWrapper>(value).ProblemDetails;
Assert.Equal("Some title", problemDetails.Title);
Assert.Equal("Some instance", problemDetails.Instance);
Assert.Equal(400, problemDetails.Status);
Assert.Collection(
problemDetails.Extensions.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("[key2]", kvp.Key);
Assert.Equal("Test Value 2", kvp.Value);
},
kvp =>
{
Assert.Equal("key1", kvp.Key);
Assert.Equal("Test Value 1", kvp.Value);
});
Assert.Collection(
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Empty(kvp.Key);
Assert.Equal(new[] { "Test error 4" }, kvp.Value);
},
kvp =>
{
Assert.Equal("[error2]", kvp.Key);
Assert.Equal(new[] { "Test error 3" }, kvp.Value);
},
kvp =>
{
Assert.Equal("error1", kvp.Key);
Assert.Equal(new[] { "Test error 1 Test error 2" }, kvp.Value);
});
}
[Fact]
public void ReadXml_ReadsValidationProblemDetailsXml_WithNoErrors()
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>400</Status>" +
"<Instance>Some instance</Instance>" +
"<key1>Test Value 1</key1>" +
"<_x005B_key2_x005D_>Test Value 2</_x005B_key2_x005D_>" +
"</ValidationProblemDetails>";
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
// Act
var value = serializer.ReadObject(
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
// Assert
var problemDetails = Assert.IsType<ValidationProblemDetailsWrapper>(value).ProblemDetails;
Assert.Equal("Some title", problemDetails.Title);
Assert.Equal("Some instance", problemDetails.Instance);
Assert.Equal(400, problemDetails.Status);
Assert.Collection(
problemDetails.Extensions,
kvp =>
{
Assert.Equal("key1", kvp.Key);
Assert.Equal("Test Value 1", kvp.Value);
},
kvp =>
{
Assert.Equal("[key2]", kvp.Key);
Assert.Equal("Test Value 2", kvp.Value);
});
Assert.Empty(problemDetails.Errors);
}
[Fact]
public void ReadXml_ReadsValidationProblemDetailsXml_WithEmptyErrorsElement()
{
// Arrange
var xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Title>Some title</Title>" +
"<Status>400</Status>" +
"<MVC-Errors />" +
"</ValidationProblemDetails>";
var serializer = new DataContractSerializer(typeof(ValidationProblemDetailsWrapper));
// Act
var value = serializer.ReadObject(
new MemoryStream(Encoding.UTF8.GetBytes(xml)));
// Assert
var problemDetails = Assert.IsType<ValidationProblemDetailsWrapper>(value).ProblemDetails;
Assert.Equal("Some title", problemDetails.Title);
Assert.Equal(400, problemDetails.Status);
Assert.Empty(problemDetails.Errors);
}
[Fact]
public void WriteXml_WritesValidXml()
{
// Arrange
var problemDetails = new ValidationProblemDetails
{
Title = "Some title",
Detail = "Some detail",
Extensions =
{
["key1"] = "Test Value 1",
["[Key2]"] = "Test Value 2"
},
Errors =
{
{ "error1", new[] {"Test error 1", "Test error 2" } },
{ "[error2]", new[] {"Test error 3" } },
{ "", new[] { "Test error 4" } },
}
};
var wrapper = new ValidationProblemDetailsWrapper(problemDetails);
var outputStream = new MemoryStream();
var expectedContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Detail>Some detail</Detail>" +
"<Title>Some title</Title>" +
"<key1>Test Value 1</key1>" +
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
"<MVC-Errors>" +
"<error1>Test error 1 Test error 2</error1>" +
"<_x005B_error2_x005D_>Test error 3</_x005B_error2_x005D_>" +
"<MVC-Empty>Test error 4</MVC-Empty>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
// Act
using (var xmlWriter = XmlWriter.Create(outputStream))
{
var dataContractSerializer = new DataContractSerializer(wrapper.GetType());
dataContractSerializer.WriteObject(xmlWriter, wrapper);
}
outputStream.Position = 0;
var res = new StreamReader(outputStream, Encoding.UTF8).ReadToEnd();
// Assert
Assert.Equal(expectedContent, res);
}
[Fact]
public void WriteXml_WithNoValidationErrors()
{
// Arrange
var problemDetails = new ValidationProblemDetails
{
Title = "Some title",
Detail = "Some detail",
Extensions =
{
["key1"] = "Test Value 1",
["[Key2]"] = "Test Value 2"
},
};
var wrapper = new ValidationProblemDetailsWrapper(problemDetails);
var outputStream = new MemoryStream();
var expectedContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<ValidationProblemDetails>" +
"<Detail>Some detail</Detail>" +
"<Title>Some title</Title>" +
"<key1>Test Value 1</key1>" +
"<_x005B_Key2_x005D_>Test Value 2</_x005B_Key2_x005D_>" +
"</ValidationProblemDetails>";
// Act
using (var xmlWriter = XmlWriter.Create(outputStream))
{
var dataContractSerializer = new DataContractSerializer(wrapper.GetType());
dataContractSerializer.WriteObject(xmlWriter, wrapper);
}
outputStream.Position = 0;
var res = new StreamReader(outputStream, Encoding.UTF8).ReadToEnd();
// Assert
Assert.Equal(expectedContent, res);
}
}
}

View File

@ -0,0 +1,34 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
public class WrapperProviderFactoryExtensionsTest
{
[Fact]
public void GetDefaultProviderFactories_GetsFactoriesUsedByInputAndOutputFormatters()
{
// Act
var factoryProviders = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
// Assert
Assert.Collection(
factoryProviders,
factory => Assert.IsType<SerializableErrorWrapperProviderFactory>(factory),
factory =>
{
var wrapperProviderFactory = Assert.IsType<WrapperProviderFactory>(factory);
Assert.Equal(typeof(ProblemDetails), wrapperProviderFactory.DeclaredType);
Assert.Equal(typeof(ProblemDetailsWrapper), wrapperProviderFactory.WrappingType);
},
factory =>
{
var wrapperProviderFactory = Assert.IsType<WrapperProviderFactory>(factory);
Assert.Equal(typeof(ValidationProblemDetails), wrapperProviderFactory.DeclaredType);
Assert.Equal(typeof(ValidationProblemDetailsWrapper), wrapperProviderFactory.WrappingType);
});
}
}
}

View File

@ -0,0 +1,63 @@
// 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 Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
{
public class WrapperProviderFactoryTest
{
[Fact]
public void GetProvider_ReturnsNull_IfTypeDoesNotMatch()
{
// Arrange
var provider = new WrapperProviderFactory(
typeof(ProblemDetails),
typeof(ProblemDetailsWrapper),
_ => null);
var context = new WrapperProviderContext(typeof(SerializableError), isSerialization: true);
// Act
var result = provider.GetProvider(context);
// Assert
Assert.Null(result);
}
[Fact]
public void GetProvider_ReturnsNull_IfTypeIsSubtype()
{
// Arrange
var provider = new WrapperProviderFactory(
typeof(ProblemDetails),
typeof(ProblemDetailsWrapper),
_ => null);
var context = new WrapperProviderContext(typeof(ValidationProblemDetails), isSerialization: true);
// Act
var result = provider.GetProvider(context);
// Assert
Assert.Null(result);
}
[Fact]
public void GetProvider_ReturnsValue_IfTypeMatches()
{
// Arrange
var expected = new object();
var providerFactory = new WrapperProviderFactory(
typeof(ProblemDetails),
typeof(ProblemDetailsWrapper),
_ => expected);
var context = new WrapperProviderContext(typeof(ProblemDetails), isSerialization: true);
// Act
var provider = providerFactory.GetProvider(context);
var result = provider.Wrap(new ProblemDetails());
// Assert
Assert.Same(expected, result);
}
}
}

View File

@ -2,6 +2,7 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@ -10,6 +11,7 @@ using System.Threading.Tasks;
using BasicWebSite.Models; using BasicWebSite.Models;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -34,37 +36,48 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ActionsReturnBadRequest_WhenModelStateIsInvalid() public async Task ActionsReturnBadRequest_WhenModelStateIsInvalid()
{ {
// Arrange // Arrange
var contactModel = new Contact using (new ActivityReplacer())
{ {
Name = "Abc", var contactModel = new Contact
City = "Redmond",
State = "WA",
Zip = "Invalid",
};
var contactString = JsonConvert.SerializeObject(contactModel);
// Act
var response = await Client.PostAsJsonAsync("/contact", contactModel);
// Assert
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(
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
{ {
Assert.Equal("Name", kvp.Key); Name = "Abc",
var error = Assert.Single(kvp.Value); City = "Redmond",
Assert.Equal("The field Name must be a string with a minimum length of 5 and a maximum length of 30.", error); State = "WA",
}, Zip = "Invalid",
kvp => };
{ var contactString = JsonConvert.SerializeObject(contactModel);
Assert.Equal("Zip", kvp.Key);
var error = Assert.Single(kvp.Value); // Act
Assert.Equal("The field Zip must match the regular expression '\\d{5}'.", error); var response = await Client.PostAsJsonAsync("/contact", contactModel);
}
); // Assert
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(
problemDetails.Errors.OrderBy(kvp => kvp.Key),
kvp =>
{
Assert.Equal("Name", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Name must be a string with a minimum length of 5 and a maximum length of 30.", error);
},
kvp =>
{
Assert.Equal("Zip", kvp.Key);
var error = Assert.Single(kvp.Value);
Assert.Equal("The field Zip must match the regular expression '\\d{5}'.", error);
}
);
Assert.Collection(
problemDetails.Extensions,
kvp =>
{
Assert.Equal("traceId", kvp.Key);
Assert.Equal(Activity.Current.Id, kvp.Value);
});
}
} }
[Fact] [Fact]
@ -97,6 +110,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert // Assert
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType); await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
var content = await response.Content.ReadAsStringAsync();
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
Assert.Equal((int)HttpStatusCode.UnsupportedMediaType, problemDetails.Status);
Assert.Equal("Unsupported Media Type", problemDetails.Title);
} }
[Fact] [Fact]
@ -112,8 +129,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
}; };
var expected = new Dictionary<string, string[]> var expected = new Dictionary<string, string[]>
{ {
{"Name", new string[] {"The field Name must be a string with a minimum length of 5 and a maximum length of 30."}}, {"Name", new[] {"The field Name must be a string with a minimum length of 5 and a maximum length of 30."}},
{"Zip", new string[]{ @"The field Zip must match the regular expression '\d{5}'."}} {"Zip", new[] { @"The field Zip must match the regular expression '\d{5}'."}}
}; };
var contactString = JsonConvert.SerializeObject(contactModel); var contactString = JsonConvert.SerializeObject(contactModel);
@ -248,14 +265,88 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
[Fact] [Fact]
public async Task ClientErrorResultFilterExecutesForStatusCodeResults() public async Task ClientErrorResultFilterExecutesForStatusCodeResults()
{ {
using (new ActivityReplacer())
{
// 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);
Assert.Collection(
problemDetails.Extensions,
kvp =>
{
Assert.Equal("traceId", kvp.Key);
Assert.Equal(Activity.Current.Id, kvp.Value);
});
}
}
[Fact]
public async Task SerializingProblemDetails_IgnoresNullValuedProperties()
{
// Arrange
var expected = new[] { "status", "title", "traceId", "type" };
// Act // Act
var response = await Client.GetAsync("/contact/ActionReturningStatusCodeResult"); var response = await Client.GetAsync("/contact/ActionReturningStatusCodeResult");
// Assert // Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound); await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
var problemDetails = JsonConvert.DeserializeObject<ProblemDetails>(content);
Assert.Equal(404, problemDetails.Status); // Verify that null-valued properties on ProblemDetails are not serialized.
var json = JObject.Parse(content);
Assert.Equal(expected, json.Properties().OrderBy(p => p.Name).Select(p => p.Name));
}
[Fact]
public async Task SerializingProblemDetails_WithAllValuesSpecified()
{
// Arrange
var expected = new[] { "detail", "instance", "status", "title", "tracking-id", "type" };
// Act
var response = await Client.GetAsync("/contact/ActionReturningProblemDetails");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
var json = JObject.Parse(content);
Assert.Equal(expected, json.Properties().OrderBy(p => p.Name).Select(p => p.Name));
}
[Fact]
public async Task SerializingValidationProblemDetails_WithExtensionData()
{
// Act
var response = await Client.GetAsync("/contact/ActionReturningValidationProblemDetails");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
var validationProblemDetails = JsonConvert.DeserializeObject<ValidationProblemDetails>(content);
Assert.Equal("Error", validationProblemDetails.Title);
Assert.Equal(400, validationProblemDetails.Status);
Assert.Collection(
validationProblemDetails.Extensions,
kvp =>
{
Assert.Equal("tracking-id", kvp.Key);
Assert.Equal("27", kvp.Value);
});
Assert.Collection(
validationProblemDetails.Errors,
kvp =>
{
Assert.Equal("Error1", kvp.Key);
Assert.Equal(new[] { "Error Message" }, kvp.Value);
});
} }
} }
} }

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));
}); });
} }

View File

@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("From Header - HelloWorld", body); Assert.Equal("From Header - HelloWorld", body);
} }
[Fact(Skip = "https://github.com/aspnet/Routing/issues/721")] [Fact]
public async Task ActionModelSuppressedForPathMatching_CannotBeRouted() public async Task ActionModelSuppressedForPathMatching_CannotBeRouted()
{ {
// Arrange & Act // Arrange & Act

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
throw new StatusCodeMismatchException throw new StatusCodeMismatchException
{ {
ExpectedStatusCode = HttpStatusCode.OK, ExpectedStatusCode = expectedStatusCode,
ActualStatusCode = response.StatusCode, ActualStatusCode = response.StatusCode,
ResponseContent = responseContent, ResponseContent = responseContent,
}; };
@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{ {
get get
{ {
return $"Excepted status code 200. Actual {ActualStatusCode}. Response Content:" + Environment.NewLine + ResponseContent; return $"Excepted status code {ExpectedStatusCode}. Actual {ActualStatusCode}. Response Content:" + Environment.NewLine + ResponseContent;
} }
} }
} }

View File

@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<Compile Include="..\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test\XmlAssert.cs" /> <Compile Include="..\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test\XmlAssert.cs" />
<Compile Include="..\Microsoft.AspNetCore.Mvc.Core.TestCommon\ActivityReplacer.cs" />
<EmbeddedResource Include="compiler\resources\**\*" /> <EmbeddedResource Include="compiler\resources\**\*" />
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>

View File

@ -72,6 +72,30 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public string[] Routers { get; set; } public string[] Routers { get; set; }
} }
[Fact]
public async Task DataTokens_ReturnsDataTokensForRoute()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/DataTokensRoute/DataTokens/Index");
// Assert
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
Assert.Single(result, kvp => kvp.Key == "hasDataTokens" && ((bool)kvp.Value) == true);
}
[Fact]
public async Task DataTokens_ReturnsNoDataTokensForRoute()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/DataTokens/Index");
// Assert
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Dictionary<string, object>>(body);
Assert.Empty(result);
}
[Fact] [Fact]
public virtual async Task ConventionalRoutedController_ActionIsReachable() public virtual async Task ConventionalRoutedController_ActionIsReachable()
{ {
@ -1259,6 +1283,19 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("/Home/Contact", contactLink.GetAttribute("href")); Assert.Equal("/Home/Contact", contactLink.GetAttribute("href"));
} }
[Fact]
public async Task CanRunMiddlewareAfterRouting()
{
// Act
var response = await Client.GetAsync("/afterrouting");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello from middleware after routing", content);
}
protected static LinkBuilder LinkFrom(string url) protected static LinkBuilder LinkFrom(string url)
{ {
return new LinkBuilder(url); return new LinkBuilder(url);

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -208,5 +209,85 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"</ArrayOfSerializableErrorWrapper>", "</ArrayOfSerializableErrorWrapper>",
result); result);
} }
[Fact]
public async Task ProblemDetails_IsSerialized()
{
// Arrange
using (new ActivityReplacer())
{
var expected = "<ProblemDetails>" +
"<Status>404</Status>" +
"<Title>Not Found</Title>" +
"<Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"</ProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningClientErrorStatusCodeResult");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]
public async Task ProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ProblemDetails><Instance>instance</Instance><Status>404</Status><Title>title</Title>
<Correlation>correlation</Correlation><Accounts>Account1 Account2</Accounts></ProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningProblemDetails");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
[Fact]
public async Task ValidationProblemDetails_IsSerialized()
{
// Arrange
using (new ActivityReplacer())
{
var expected = "<ValidationProblemDetails>" +
"<Status>400</Status>" +
"<Title>One or more validation errors occurred.</Title>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"<MVC-Errors>" +
"<State>The State field is required.</State>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationProblem");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]
public async Task ValidationProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ValidationProblemDetails><Detail>some detail</Detail><Status>400</Status><Title>One or more validation errors occurred.</Title>
<Type>some type</Type><CorrelationId>correlation</CorrelationId><MVC-Errors><Error1>ErrorValue</Error1></MVC-Errors></ValidationProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlDataContractApi/ActionReturningValidationDetailsWithMetadata");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -183,5 +184,85 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"<key4>key2-error</key4></SerializableErrorWrapper></ArrayOfSerializableErrorWrapper>", "<key4>key2-error</key4></SerializableErrorWrapper></ArrayOfSerializableErrorWrapper>",
result); result);
} }
[Fact]
public async Task ProblemDetails_IsSerialized()
{
// Arrange
using (new ActivityReplacer())
{
var expected = "<ProblemDetails>" +
"<Status>404</Status>" +
"<Title>Not Found</Title>" +
"<Type>https://tools.ietf.org/html/rfc7231#section-6.5.4</Type>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"</ProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningClientErrorStatusCodeResult");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]
public async Task ProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ProblemDetails><Instance>instance</Instance><Status>404</Status><Title>title</Title>
<Correlation>correlation</Correlation><Accounts>Account1 Account2</Accounts></ProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningProblemDetails");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.NotFound);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
[Fact]
public async Task ValidationProblemDetails_IsSerialized()
{
// Arrange
using (new ActivityReplacer())
{
var expected = "<ValidationProblemDetails>" +
"<Status>400</Status>" +
"<Title>One or more validation errors occurred.</Title>" +
$"<traceId>{Activity.Current.Id}</traceId>" +
"<MVC-Errors>" +
"<State>The State field is required.</State>" +
"</MVC-Errors>" +
"</ValidationProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationProblem");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
}
[Fact]
public async Task ValidationProblemDetails_WithExtensionMembers_IsSerialized()
{
// Arrange
var expected = @"<ValidationProblemDetails><Detail>some detail</Detail><Status>400</Status><Title>One or more validation errors occurred.</Title>
<Type>some type</Type><CorrelationId>correlation</CorrelationId><MVC-Errors><Error1>ErrorValue</Error1></MVC-Errors></ValidationProblemDetails>";
// Act
var response = await Client.GetAsync("/api/XmlSerializerApi/ActionReturningValidationDetailsWithMetadata");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.BadRequest);
var content = await response.Content.ReadAsStringAsync();
XmlAssert.Equal(expected, content);
}
} }
} }

View File

@ -1,3 +1,4 @@
{ {
"shadowCopy": false "shadowCopy": false,
"longRunningTestSeconds": 60
} }

View File

@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.False(mvcOptions.EnableEndpointRouting); Assert.False(mvcOptions.EnableEndpointRouting);
Assert.Null(mvcOptions.MaxValidationDepth); Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
} }
[Fact] [Fact]
@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.False(mvcOptions.EnableEndpointRouting); Assert.False(mvcOptions.EnableEndpointRouting);
Assert.Null(mvcOptions.MaxValidationDepth); Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
} }
[Fact] [Fact]
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(mvcOptions.EnableEndpointRouting); Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth); Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
} }
[Fact] [Fact]
@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(mvcOptions.EnableEndpointRouting); Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth); Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses); Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory); Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
} }
// This just does the minimum needed to be able to resolve these options. // This just does the minimum needed to be able to resolve these options.

View File

@ -469,14 +469,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// Assert - 1 // Assert - 1
Assert.Equal(0, buffer.Length); Assert.Equal(0, buffer.Length);
Assert.Equal(1, buffer.Pages.Count); Assert.Single(buffer.Pages);
// Act - 2 // Act - 2
buffer.Append("efgh"); buffer.Append("efgh");
// Assert - 2 // Assert - 2
Assert.Equal(4, buffer.Length); Assert.Equal(4, buffer.Length);
Assert.Equal(1, buffer.Pages.Count); Assert.Single(buffer.Pages);
Assert.Equal(new[] { 'e', 'f', 'g', 'h' }, buffer.Pages[0].Take(buffer.Length)); Assert.Equal(new[] { 'e', 'f', 'g', 'h' }, buffer.Pages[0].Take(buffer.Length));
} }
} }

View File

@ -1045,7 +1045,7 @@ namespace Microsoft.AspNetCore.Mvc
serviceCollection.AddRouting(); serviceCollection.AddRouting();
serviceCollection.AddSingleton<IInlineConstraintResolver>( serviceCollection.AddSingleton<IInlineConstraintResolver>(
provider => new DefaultInlineConstraintResolver(provider.GetRequiredService<IOptions<RouteOptions>>())); provider => new DefaultInlineConstraintResolver(provider.GetRequiredService<IOptions<RouteOptions>>(), provider));
if (localizerFactory != null) if (localizerFactory != null)
{ {

View File

@ -33,6 +33,18 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
[Fact] [Fact]
public Task CodeFixAddsFullyQualifiedProducesResponseType() => RunTest(); public Task CodeFixAddsFullyQualifiedProducesResponseType() => RunTest();
[Fact]
public Task CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants() => RunTest();
[Fact]
public Task CodeFixAddsStatusCodesFromMethodParameters() => RunTest();
[Fact]
public Task CodeFixAddsStatusCodesFromConstructorParameters() => RunTest();
[Fact]
public Task CodeFixAddsStatusCodesFromObjectInitializer() => RunTest();
private async Task RunTest([CallerMemberName] string testMethod = "") private async Task RunTest([CallerMemberName] string testMethod = "")
{ {
// Arrange // Arrange

View File

@ -0,0 +1,17 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsNumericLiteralForNonExistingStatusCodeConstantsController : ControllerBase
{
public IActionResult GetItem(int id)
{
if (id == 0)
{
return StatusCode(345);
}
return Ok(new object());
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsNumericLiteralForNonExistingStatusCodeConstantsController : ControllerBase
{
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(345)]
[ProducesDefaultResponseType]
public IActionResult GetItem(int id)
{
if (id == 0)
{
return StatusCode(345);
}
return Ok(new object());
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesFromConstructorParametersController : ControllerBase
{
private const int FieldStatusCode = 201;
public IActionResult GetItem(int id)
{
if (id == 0)
{
return new StatusCodeResult(422);
}
if (id == 1)
{
return new StatusCodeResult(StatusCodes.Status202Accepted);
}
if (id == 2)
{
const int localStatusCode = 204;
return new StatusCodeResult(localStatusCode);
}
if (id == 3)
{
return new StatusCodeResult(FieldStatusCode);
}
return Ok(new object());
}
}
}

View File

@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesFromConstructorParametersController : ControllerBase
{
private const int FieldStatusCode = 201;
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
[ProducesDefaultResponseType]
public IActionResult GetItem(int id)
{
if (id == 0)
{
return new StatusCodeResult(422);
}
if (id == 1)
{
return new StatusCodeResult(StatusCodes.Status202Accepted);
}
if (id == 2)
{
const int localStatusCode = 204;
return new StatusCodeResult(localStatusCode);
}
if (id == 3)
{
return new StatusCodeResult(FieldStatusCode);
}
return Ok(new object());
}
}
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesFromMethodParametersController : ControllerBase
{
private const int FieldStatusCode = 201;
public IActionResult GetItem(int id)
{
if (id == 0)
{
return StatusCode(422);
}
if (id == 1)
{
return StatusCode(StatusCodes.Status202Accepted);
}
if (id == 2)
{
const int localStatusCode = 204;
return StatusCode(localStatusCode);
}
if (id == 3)
{
return StatusCode(FieldStatusCode);
}
return Ok(new object());
}
}
}

View File

@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesFromMethodParametersController : ControllerBase
{
private const int FieldStatusCode = 201;
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
[ProducesDefaultResponseType]
public IActionResult GetItem(int id)
{
if (id == 0)
{
return StatusCode(422);
}
if (id == 1)
{
return StatusCode(StatusCodes.Status202Accepted);
}
if (id == 2)
{
const int localStatusCode = 204;
return StatusCode(localStatusCode);
}
if (id == 3)
{
return StatusCode(FieldStatusCode);
}
return Ok(new object());
}
}
}

View File

@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesFromObjectInitializerController : ControllerBase
{
private const int FieldStatusCode = 201;
public IActionResult GetItem(int id)
{
if (id == 0)
{
return new ObjectResult(new object())
{
StatusCode = 422
};
}
if (id == 1)
{
return new ObjectResult(new object())
{
StatusCode = StatusCodes.Status202Accepted
};
}
if (id == 2)
{
const int localStatusCode = 204;
return new ObjectResult(new object())
{
StatusCode = localStatusCode
};
}
if (id == 3)
{
return new ObjectResult(new object())
{
ContentTypes = { "application/json" },
StatusCode = FieldStatusCode
};
}
return Ok(new object());
}
}
}

View File

@ -0,0 +1,57 @@
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesFromObjectInitializerController : ControllerBase
{
private const int FieldStatusCode = 201;
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
[ProducesDefaultResponseType]
public IActionResult GetItem(int id)
{
if (id == 0)
{
return new ObjectResult(new object())
{
StatusCode = 422
};
}
if (id == 1)
{
return new ObjectResult(new object())
{
StatusCode = StatusCodes.Status202Accepted
};
}
if (id == 2)
{
const int localStatusCode = 204;
return new ObjectResult(new object())
{
StatusCode = localStatusCode
};
}
if (id == 3)
{
return new ObjectResult(new object())
{
ContentTypes = { "application/json" },
StatusCode = FieldStatusCode
};
}
return Ok(new object());
}
}
}

View File

@ -91,6 +91,41 @@ namespace BasicWebSite
return NotFound(); return NotFound();
} }
[HttpGet("[action]")]
public ActionResult<int> ActionReturningProblemDetails()
{
return NotFound(new ProblemDetails
{
Title = "Not Found",
Type = "Type",
Detail = "Detail",
Status = 404,
Instance = "Instance",
Extensions =
{
["tracking-id"] = 27,
},
});
}
[HttpGet("[action]")]
public ActionResult<int> ActionReturningValidationProblemDetails()
{
return BadRequest(new ValidationProblemDetails
{
Title = "Error",
Status = 400,
Extensions =
{
["tracking-id"] = "27",
},
Errors =
{
{ "Error1", new[] { "Error Message" } },
},
});
}
private class TestModelBinder : IModelBinder private class TestModelBinder : IModelBinder
{ {
public Task BindModelAsync(ModelBindingContext bindingContext) public Task BindModelAsync(ModelBindingContext bindingContext)

View File

@ -0,0 +1,15 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc;
namespace RoutingWebSite
{
public class DataTokensController : Controller
{
public object Index()
{
return RouteData.DataTokens;
}
}
}

View File

@ -2,6 +2,7 @@
// 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -24,12 +25,19 @@ namespace RoutingWebSite
{ {
app.UseMvc(routes => app.UseMvc(routes =>
{ {
routes.MapRoute(
"DataTokensRoute",
"DataTokensRoute/{controller}/{action}",
defaults: null,
constraints: new { controller = "DataTokens" },
dataTokens: new { hasDataTokens = true });
routes.MapAreaRoute( routes.MapAreaRoute(
"flightRoute", "flightRoute",
"adminRoute", "adminRoute",
"{area:exists}/{controller}/{action}", "{area:exists}/{controller}/{action}",
new { controller = "Home", action = "Index" }, defaults: new { controller = "Home", action = "Index" },
new { area = "Travel" }); constraints: new { area = "Travel" });
routes.MapRoute( routes.MapRoute(
"ActionAsMethod", "ActionAsMethod",
@ -40,6 +48,11 @@ namespace RoutingWebSite
"RouteWithOptionalSegment", "RouteWithOptionalSegment",
"{controller}/{action}/{path?}"); "{controller}/{action}/{path?}");
}); });
app.Map("/afterrouting", b => b.Run(c =>
{
return c.Response.WriteAsync("Hello from middleware after routing");
}));
} }
} }
} }

View File

@ -2,6 +2,7 @@
// 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 Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -24,12 +25,19 @@ namespace RoutingWebSite
{ {
app.UseMvc(routes => app.UseMvc(routes =>
{ {
routes.MapRoute(
"DataTokensRoute",
"DataTokensRoute/{controller}/{action}",
defaults: null,
constraints: new { controller = "DataTokens" },
dataTokens: new { hasDataTokens = true });
routes.MapAreaRoute( routes.MapAreaRoute(
"flightRoute", "flightRoute",
"adminRoute", "adminRoute",
"{area:exists}/{controller}/{action}", "{area:exists}/{controller}/{action}",
new { controller = "Home", action = "Index" }, defaults: new { controller = "Home", action = "Index" },
new { area = "Travel" }); constraints: new { area = "Travel" });
routes.MapRoute( routes.MapRoute(
"ActionAsMethod", "ActionAsMethod",
@ -40,6 +48,11 @@ namespace RoutingWebSite
"RouteWithOptionalSegment", "RouteWithOptionalSegment",
"{controller}/{action}/{path?}"); "{controller}/{action}/{path?}");
}); });
app.Map("/afterrouting", b => b.Run(c =>
{
return c.Response.WriteAsync("Hello from middleware after routing");
}));
} }
} }
} }

View File

@ -0,0 +1,55 @@
// 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;
using XmlFormattersWebSite.Models;
namespace XmlFormattersWebSite
{
[ApiController]
[Route("api/[controller]/[action]")]
public abstract class XmlApiControllerBase : ControllerBase
{
[HttpGet]
public ActionResult<Person> ActionReturningClientErrorStatusCodeResult()
=> NotFound();
[HttpGet]
public ActionResult<Person> ActionReturningProblemDetails()
{
return NotFound(new ProblemDetails
{
Instance = "instance",
Title = "title",
Extensions =
{
["Correlation"] = "correlation",
["Accounts"] = new[] { "Account1", "Account2" },
},
});
}
[HttpGet]
public ActionResult<Person> ActionReturningValidationProblem([FromQuery] Address address)
=> throw new NotImplementedException();
[HttpGet]
public ActionResult<Person> ActionReturningValidationDetailsWithMetadata()
{
return new BadRequestObjectResult(new ValidationProblemDetails
{
Detail = "some detail",
Type = "some type",
Extensions =
{
["CorrelationId"] = "correlation",
},
Errors =
{
["Error1"] = new[] { "ErrorValue"},
},
});
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace XmlFormattersWebSite
{
[SetupOutputFormatters]
public class XmlDataContractApiController : XmlApiControllerBase
{
private class SetupOutputFormattersAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.Result is ObjectResult objectResult))
{
return;
}
// Both kinds of Xml serializers are configured for this application and use custom content-types to do formatter
// selection. The globally configured formatters rely on custom content-type to perform conneg which does not play
// well the ProblemDetails returning filters that defaults to using application/xml. We'll explicitly select the formatter for this controller.
objectResult.Formatters.Add(new XmlDataContractSerializerOutputFormatter());
}
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace XmlFormattersWebSite
{
[SetupOutputFormatters]
public class XmlSerializerApiController : XmlApiControllerBase
{
private class SetupOutputFormattersAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
if (!(context.Result is ObjectResult objectResult))
{
return;
}
// Both kinds of Xml serializers are configured for this application and use custom content-types to do formatter
// selection. The globally configured formatters rely on custom content-type to perform conneg which does not play
// well the ProblemDetails returning filters that defaults to using application/xml. We'll explicitly select the formatter for this controller.
objectResult.Formatters.Add(new XmlSerializerOutputFormatter());
}
}
}
}