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;
}
private static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute)
public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute)
{
foreach (var declaredAttribute in symbol.GetAttributes())
{

View File

@ -116,6 +116,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
return false;
}
if (!method.ReturnsVoid)
{
return false;
}
if (method.Parameters.Length != disposableDispose.Parameters.Length)
{
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.
using System;
using System.Diagnostics;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
@ -24,6 +25,11 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesDefaultResponseTypeAttribute);
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 members = disposable.GetMembers(nameof(IDisposable.Dispose));
IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null;
@ -47,6 +53,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public IMethodSymbol IDisposableDispose { get; }
public IPropertySymbol StatusCodeActionResultStatusProperty { get; }
public ITypeSymbol ModelStateDictionary { get; }
public INamedTypeSymbol NonActionAttribute { get; }
@ -56,5 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public INamedTypeSymbol ProducesDefaultResponseTypeAttribute { 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 IStatusCodeActionResult = "Microsoft.AspNetCore.Mvc.Infrastructure.IStatusCodeActionResult";
public const string ModelStateDictionary = "Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary";
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 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>();
}
}
private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
{
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>())
{
if (returnStatementSyntax.IsMissing || returnStatementSyntax.Expression.IsMissing)
{
// Ignore malformed return statements.
continue;
}
var responseMetadata = InspectReturnStatementSyntax(
symbolCache,
semanticModel,
@ -248,11 +255,6 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
CancellationToken cancellationToken)
{
var returnExpression = returnStatementSyntax.Expression;
if (returnExpression.IsMissing)
{
return null;
}
var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken);
if (typeInfo.Type.TypeKind == TypeKind.Error)
{
@ -267,25 +269,176 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
if (defaultStatusCodeAttribute != null)
{
var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
if (statusCode == null)
var defaultStatusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
if (defaultStatusCode == null)
{
// Unable to read the status code even though the attribute exists.
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
// an instance of IActionResult. Must be returning the "model".
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;
}
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)
{
return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) &&

View File

@ -46,7 +46,13 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
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;
}
@ -69,7 +75,8 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
private ICollection<ApiResponseType> GetApiResponseTypes(
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
Type type)
Type type,
Type defaultErrorType)
{
var results = new Dictionary<int, ApiResponseType>();
@ -83,48 +90,39 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
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,
Type = metadataAttribute.Type,
};
}
else if (metadataAttribute.Type == typeof(void) &&
type != null &&
(metadataAttribute.StatusCode == StatusCodes.Status200OK || metadataAttribute.StatusCode == StatusCodes.Status201Created))
{
// 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
// 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.Type = type;
}
else if (IsClientError(statusCode) || apiResponseType.IsDefaultResponse)
{
StatusCode = metadataAttribute.StatusCode,
Type = type,
};
}
else if (metadataAttribute.Type != null)
{
apiResponseType = new ApiResponseType
{
StatusCode = metadataAttribute.StatusCode,
Type = metadataAttribute.Type,
};
}
else
{
continue;
// Use the default error type for "default" responses or 4xx client errors if no response type is specified.
apiResponseType.Type = defaultErrorType;
}
}
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
if (results.Count == 0 && type != null)
{
@ -225,5 +223,10 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
return declaredReturnType;
}
private static bool IsClientError(int statusCode)
{
return statusCode >= 400 && statusCode < 500;
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary>
public class ApiBehaviorOptions : IEnumerable<ICompatibilitySwitch>
{
private readonly CompatibilitySwitch<bool> _suppressUseClientErrorFactory;
private readonly CompatibilitySwitch<bool> _suppressMapClientErrors;
private readonly CompatibilitySwitch<bool> _suppressUseValidationProblemDetailsForInvalidModelStateResponses;
private readonly ICompatibilitySwitch[] _switches;
@ -26,11 +26,11 @@ namespace Microsoft.AspNetCore.Mvc
/// </summary>
public ApiBehaviorOptions()
{
_suppressUseClientErrorFactory = new CompatibilitySwitch<bool>(nameof(SuppressUseClientErrorFactory));
_suppressMapClientErrors = new CompatibilitySwitch<bool>(nameof(SuppressMapClientErrors));
_suppressUseValidationProblemDetailsForInvalidModelStateResponses = new CompatibilitySwitch<bool>(nameof(SuppressUseValidationProblemDetailsForInvalidModelStateResponses));
_switches = new[]
{
_suppressUseClientErrorFactory,
_suppressMapClientErrors,
_suppressUseValidationProblemDetailsForInvalidModelStateResponses,
};
}
@ -71,12 +71,16 @@ namespace Microsoft.AspNetCore.Mvc
public bool SuppressConsumesConstraintForFormFileParameters { get; set; }
/// <summary>
/// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/> use <see cref="ClientErrorFactory"/>
/// to transform certain certain client errors.
/// Gets or sets a value that determines if controllers with <see cref="ApiControllerAttribute"/>
/// transform certain certain client errors.
/// <para>
/// When <c>false</c>, <see cref="ClientErrorFactory"/> is used to transform <see cref="IClientErrorActionResult"/> to the value
/// specified by the factory. In the default case, this converts <see cref="StatusCodeResult"/> instances to an <see cref="ObjectResult"/>
/// with <see cref="ProblemDetails"/>.
/// When <c>false</c>, a result filter is added to API controller actions that transforms <see cref="IClientErrorActionResult"/>.
/// By default, <see cref="ClientErrorMapping"/> is used to map <see cref="IClientErrorActionResult"/> to a
/// <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>
/// </summary>
/// <value>
@ -102,11 +106,11 @@ namespace Microsoft.AspNetCore.Mvc
/// higher then this setting will have the value <see langword="true"/> unless explicitly configured.
/// </para>
/// </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.
get => _suppressUseClientErrorFactory.Value;
set => _suppressUseClientErrorFactory.Value = value;
get => _suppressMapClientErrors.Value;
set => _suppressMapClientErrors.Value = value;
}
/// <summary>
@ -148,11 +152,15 @@ namespace Microsoft.AspNetCore.Mvc
}
/// <summary>
/// Gets a map of HTTP status codes to <see cref="IActionResult"/> factories.
/// Configured factories are used when <see cref="SuppressUseClientErrorFactory"/> is <see langword="false"/>.
/// Gets a map of HTTP status codes to <see cref="ClientErrorData"/>. Configured values
/// are used to transform <see cref="IClientErrorActionResult"/> to an <see cref="ObjectResult"/>
/// 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>
public IDictionary<int, Func<ActionContext, IActionResult>> ClientErrorFactory { get; } =
new Dictionary<int, Func<ActionContext, IActionResult>>();
public IDictionary<int, ClientErrorData> ClientErrorMapping { get; } =
new Dictionary<int, ClientErrorData>();
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.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
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>
/// <returns>The created <see cref="StatusCodeResult"/> object for the response.</returns>
[NonAction]
public virtual StatusCodeResult StatusCode(int statusCode)
public virtual StatusCodeResult StatusCode([StatusCodeValue] int statusCode)
=> new StatusCodeResult(statusCode);
/// <summary>
@ -210,10 +211,12 @@ namespace Microsoft.AspNetCore.Mvc
/// <param name="value">The value to set on the <see cref="ObjectResult"/>.</param>
/// <returns>The created <see cref="ObjectResult"/> object for the response.</returns>
[NonAction]
public virtual ObjectResult StatusCode(int statusCode, object value)
public virtual ObjectResult StatusCode([StatusCodeValue] int statusCode, object value)
{
var result = new ObjectResult(value);
result.StatusCode = statusCode;
var result = new ObjectResult(value)
{
StatusCode = statusCode
};
return result;
}

View File

@ -257,6 +257,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();
services.TryAddSingleton<IClientErrorFactory, ProblemDetailsClientErrorFactory>();
//
// 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.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.Logging;
@ -11,7 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
internal class ClientErrorResultFilter : IAlwaysRunResultFilter, IOrderedFilter
{
private readonly IDictionary<int, Func<ActionContext, IActionResult>> _clientErrorFactory;
private readonly IClientErrorFactory _clientErrorFactory;
private readonly ILogger<ClientErrorResultFilter> _logger;
/// <summary>
@ -20,10 +19,10 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
public int Order => -2000;
public ClientErrorResultFilter(
ApiBehaviorOptions apiBehaviorOptions,
IClientErrorFactory clientErrorFactory,
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));
}
@ -38,16 +37,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
throw new ArgumentNullException(nameof(context));
}
if (context.Result is IClientErrorActionResult clientErrorActionResult &&
clientErrorActionResult.StatusCode is int statusCode &&
_clientErrorFactory.TryGetValue(statusCode, out var factory))
if (!(context.Result is IClientErrorActionResult clientError))
{
var result = factory(context);
_logger.TransformingClientError(context.Result.GetType(), result?.GetType(), statusCode);
context.Result = factory(context);
return;
}
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
{
private readonly ProducesErrorResponseTypeAttribute DefaultErrorType = new ProducesErrorResponseTypeAttribute(typeof(ProblemDetails));
private readonly ApiBehaviorOptions _apiBehaviorOptions;
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly ModelStateInvalidFilter _modelStateInvalidFilter;
@ -27,6 +28,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public ApiBehaviorApplicationModelProvider(
IOptions<ApiBehaviorOptions> apiBehaviorOptions,
IModelMetadataProvider modelMetadataProvider,
IClientErrorFactory clientErrorFactory,
ILoggerFactory loggerFactory)
{
_apiBehaviorOptions = apiBehaviorOptions.Value;
@ -45,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
loggerFactory.CreateLogger<ModelStateInvalidFilter>());
_clientErrorResultFilter = new ClientErrorResultFilter(
_apiBehaviorOptions,
clientErrorFactory,
loggerFactory.CreateLogger<ClientErrorResultFilter>());
}
@ -104,6 +106,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
AddMultipartFormDataConsumesAttribute(actionModel);
DiscoverApiConvention(actionModel, conventions);
DiscoverErrorResponseType(actionModel);
}
}
}
@ -158,7 +162,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private void AddClientErrorFilter(ActionModel actionModel)
{
if (_apiBehaviorOptions.SuppressUseClientErrorFactory)
if (_apiBehaviorOptions.SuppressMapClientErrors)
{
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)
{
foreach (var (route, _, _) in ActionAttributeRouteModel.GetAttributeRoutes(actionModel))

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging;
@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (Version < CompatibilityVersion.Version_2_2)
{
dictionary[nameof(ApiBehaviorOptions.SuppressUseClientErrorFactory)] = true;
dictionary[nameof(ApiBehaviorOptions.SuppressMapClientErrors)] = true;
dictionary[nameof(ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses)] = true;
}
@ -48,7 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
options.InvalidModelStateResponseFactory = DefaultFactory;
ConfigureClientErrorFactories(options);
ConfigureClientErrorMapping(options);
}
public override void PostConfigure(string name, ApiBehaviorOptions options)
@ -57,9 +58,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
base.PostConfigure(name, options);
// 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
if (!options.SuppressUseClientErrorFactory &&
if (!options.SuppressMapClientErrors &&
object.ReferenceEquals(options.InvalidModelStateResponseFactory, DefaultFactory))
{
options.InvalidModelStateResponseFactory = ProblemDetailsFactory;
@ -67,77 +68,55 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
// 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,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = Resources.ApiConventions_Title_400,
});
};
AddClientErrorFactory(new ProblemDetails
options.ClientErrorMapping[401] = new ClientErrorData
{
Status = 401,
Type = "https://tools.ietf.org/html/rfc7235#section-3.1",
Link = "https://tools.ietf.org/html/rfc7235#section-3.1",
Title = Resources.ApiConventions_Title_401,
});
};
AddClientErrorFactory(new ProblemDetails
options.ClientErrorMapping[403] = new ClientErrorData
{
Status = 403,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.3",
Title = Resources.ApiConventions_Title_403,
});
};
AddClientErrorFactory(new ProblemDetails
options.ClientErrorMapping[404] = new ClientErrorData
{
Status = 404,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
Title = Resources.ApiConventions_Title_404,
});
};
AddClientErrorFactory(new ProblemDetails
options.ClientErrorMapping[406] = new ClientErrorData
{
Status = 406,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.6",
Title = Resources.ApiConventions_Title_406,
});
};
AddClientErrorFactory(new ProblemDetails
options.ClientErrorMapping[409] = new ClientErrorData
{
Status = 409,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.8",
Title = Resources.ApiConventions_Title_409,
});
};
AddClientErrorFactory(new ProblemDetails
options.ClientErrorMapping[415] = new ClientErrorData
{
Status = 415,
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.13",
Title = Resources.ApiConventions_Title_415,
});
};
AddClientErrorFactory(new ProblemDetails
options.ClientErrorMapping[422] = new ClientErrorData
{
Status = 422,
Type = "https://tools.ietf.org/html/rfc4918#section-11.2",
Link = "https://tools.ietf.org/html/rfc4918#section-11.2",
Title = Resources.ApiConventions_Title_422,
});
void AddClientErrorFactory(ProblemDetails problemDetails)
{
var statusCode = problemDetails.Status.Value;
options.ClientErrorFactory[statusCode] = _ => new ObjectResult(problemDetails)
{
StatusCode = statusCode,
ContentTypes =
{
"application/problem+json",
"application/problem+xml",
},
};
}
};
}
private static IActionResult DefaultInvalidModelStateResponse(ActionContext context)
@ -150,9 +129,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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+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, 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()
{
@ -651,10 +651,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
48,
"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,
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)
@ -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(

View File

@ -174,7 +174,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpointInfo.Defaults,
++conventionalRouteOrder,
endpointInfo,
suppressLinkGeneration: false);
endpointInfo.DataTokens,
suppressLinkGeneration: false,
suppressPathMatching: false);
endpoints.Add(subEndpoint);
}
@ -213,7 +215,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpointInfo.Defaults,
++conventionalRouteOrder,
endpointInfo,
suppressLinkGeneration: false);
endpointInfo.DataTokens,
suppressLinkGeneration: false,
suppressPathMatching: false);
endpoints.Add(endpoint);
}
}
@ -227,7 +231,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
nonInlineDefaults: null,
action.AttributeRouteInfo.Order,
action.AttributeRouteInfo,
suppressLinkGeneration: action.AttributeRouteInfo.SuppressLinkGeneration);
dataTokens: null,
suppressLinkGeneration: action.AttributeRouteInfo.SuppressLinkGeneration,
suppressPathMatching: action.AttributeRouteInfo.SuppressPathMatching);
endpoints.Add(endpoint);
}
}
@ -375,19 +381,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
object nonInlineDefaults,
int order,
object source,
bool suppressLinkGeneration)
RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
bool suppressPathMatching)
{
RequestDelegate requestDelegate = (context) =>
{
var values = context.Features.Get<IRouteValuesFeature>().RouteValues;
var routeData = new RouteData();
foreach (var kvp in values)
{
if (kvp.Value != null)
{
routeData.Values.Add(kvp.Key, kvp.Value);
}
}
var routeData = context.GetRouteData();
var actionContext = new ActionContext(context, routeData, action);
@ -403,7 +403,9 @@ namespace Microsoft.AspNetCore.Mvc.Internal
routeName,
new RouteValueDictionary(action.RouteValues),
source,
suppressLinkGeneration);
dataTokens,
suppressLinkGeneration,
suppressPathMatching);
var endpoint = new RouteEndpoint(
requestDelegate,
@ -420,12 +422,12 @@ namespace Microsoft.AspNetCore.Mvc.Internal
string routeName,
RouteValueDictionary requiredValues,
object source,
bool suppressLinkGeneration)
RouteValueDictionary dataTokens,
bool suppressLinkGeneration,
bool suppressPathMatching)
{
var metadata = new List<object>
{
// REVIEW: Used for debugging. Consider removing before release
source,
action
};
@ -434,6 +436,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
metadata.AddRange(action.EndpointMetadata);
}
if (dataTokens != null)
{
metadata.Add(new DataTokensMetadata(dataTokens));
}
metadata.Add(new RouteValuesAddressMetadata(routeName, requiredValues));
// Add filter descriptors to endpoint metadata
@ -475,6 +482,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
metadata.Add(new SuppressLinkGenerationMetadata());
}
if (suppressPathMatching)
{
metadata.Add(new SuppressMatchingMetadata());
}
var metadataCollection = new EndpointMetadataCollection(metadata);
return metadataCollection;
}
@ -497,10 +509,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
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.Collections.Generic;
using Newtonsoft.Json;
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
/// "about:blank".
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Type { get; set; }
/// <summary>
@ -24,21 +26,39 @@ namespace Microsoft.AspNetCore.Mvc
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
/// see[RFC7231], Section 3.4).
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Title { get; set; }
/// <summary>
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public int? Status { get; set; }
/// <summary>
/// A human-readable explanation specific to this occurrence of the problem.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Detail { get; set; }
/// <summary>
/// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
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
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
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(
HttpContext httpContext,
IRouter route,
@ -23,16 +41,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (route == null)
{
throw new ArgumentNullException(nameof(route));
}
if (routeKey == null)
{
throw new ArgumentNullException(nameof(routeKey));
@ -49,7 +57,9 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var value = obj as string;
if (value != null)
{
var allValues = GetAndCacheAllMatchingValues(routeKey, httpContext);
var actionDescriptors = GetAndValidateActionDescriptors(httpContext);
var allValues = GetAndCacheAllMatchingValues(routeKey, actionDescriptors);
foreach (var existingValue in allValues)
{
if (string.Equals(value, existingValue, StringComparison.OrdinalIgnoreCase))
@ -63,9 +73,36 @@ namespace Microsoft.AspNetCore.Mvc.Routing
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 valuesCollection = _cachedValuesCollection;
@ -77,8 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
var action = actionDescriptors.Items[i];
string value;
if (action.RouteValues.TryGetValue(routeKey, out value) &&
if (action.RouteValues.TryGetValue(routeKey, out var value) &&
!string.IsNullOrEmpty(value))
{
values.Add(value);
@ -92,22 +128,6 @@ namespace Microsoft.AspNetCore.Mvc.Routing
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
{
public RouteValuesCollection(int version, string[] items)

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
/// with the given <paramref name="statusCode"/>.
/// </summary>
/// <param name="statusCode">The HTTP status code of the response.</param>
public StatusCodeResult(int statusCode)
public StatusCodeResult([StatusCodeValue] int 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
// 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
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;
}
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();
WrapperProviderFactories = new List<IWrapperProviderFactory>();
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
}
/// <summary>

View File

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

View File

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

View File

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

View File

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

View File

@ -51,13 +51,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
{
get
{
if (ViewContext == null)
var viewContext = ViewContext;
if (viewContext == null)
{
var message = Resources.FormatViewContextMustBeSet("ViewContext", "Output");
throw new InvalidOperationException(message);
throw new InvalidOperationException(Resources.FormatViewContextMustBeSet(nameof(ViewContext), nameof(Output)));
}
return ViewContext.Writer;
return viewContext.Writer;
}
}
@ -183,8 +183,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// </remarks>
public void StartTagHelperWritingScope(HtmlEncoder encoder)
{
var viewContext = ViewContext;
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 (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
// from HTML helpers) is redirected.
ViewContext.Writer = new ViewBufferTextWriter(buffer, ViewContext.Writer.Encoding);
viewContext.Writer = new ViewBufferTextWriter(buffer, viewContext.Writer.Encoding);
}
/// <summary>
@ -238,7 +239,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
throw new InvalidOperationException(Resources.RazorPage_NestingAttributeWritingScopesNotSupported);
}
_pageWriter = ViewContext.Writer;
var viewContext = ViewContext;
_pageWriter = viewContext.Writer;
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
// 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));
}
_textWriterStack.Push(ViewContext.Writer);
ViewContext.Writer = writer;
var viewContext = ViewContext;
_textWriterStack.Push(viewContext.Writer);
viewContext.Writer = writer;
}
// Internal for unit testing.
protected internal virtual TextWriter PopWriter()
{
ViewContext.Writer = _textWriterStack.Pop();
return ViewContext.Writer;
var viewContext = ViewContext;
var writer = _textWriterStack.Pop();
viewContext.Writer = writer;
return writer;
}
public virtual string Href(string contentPath)
@ -304,9 +309,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
if (_urlHelper == null)
{
var services = ViewContext?.HttpContext.RequestServices;
var viewContext = ViewContext;
var services = viewContext?.HttpContext.RequestServices;
var factory = services.GetRequiredService<IUrlHelperFactory>();
_urlHelper = factory.GetUrlHelper(ViewContext);
_urlHelper = factory.GetUrlHelper(viewContext);
}
return _urlHelper.Content(contentPath);
@ -637,8 +643,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor
/// before <see cref="RazorPageBase.FlushAsync"/> flushes the headers. </remarks>
public virtual HtmlString SetAntiforgeryCookieAndHeader()
{
var antiforgery = ViewContext?.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
antiforgery.SetCookieTokenAndHeader(ViewContext?.HttpContext);
var viewContext = ViewContext;
var antiforgery = viewContext?.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
antiforgery.SetCookieTokenAndHeader(viewContext?.HttpContext);
return HtmlString.Empty;
}

View File

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

View File

@ -37,7 +37,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
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 pageLength = Math.Min(length, page.Length);

View File

@ -19,16 +19,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
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
{
get
{
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;
@ -100,13 +103,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
/// </summary>
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
{
Pages.RemoveAt(i);
pages.RemoveAt(i);
}
finally
{
@ -115,7 +119,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
_charIndex = 0;
CurrentPage = Pages.Count > 0 ? Pages[0] : null;
CurrentPage = pages.Count > 0 ? pages[0] : null;
}
private char[] GetCurrentPage()
@ -148,12 +152,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
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.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
@ -90,55 +91,73 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
}
/// <inheritdoc />
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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;
}
/// <inheritdoc />
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IHtmlContentBuilder AppendHtml(IHtmlContent content)
{
if (content == null)
if (content != null)
{
return this;
AppendValue(new ViewBufferValue(content));
}
AppendValue(new ViewBufferValue(content));
return this;
}
/// <inheritdoc />
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IHtmlContentBuilder AppendHtml(string encoded)
{
if (encoded == null)
if (encoded != null)
{
return this;
AppendValue(new ViewBufferValue(encoded));
}
AppendValue(new ViewBufferValue(encoded));
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)
{
var page = GetCurrentPage();
page.Append(value);
}
// Very common trival method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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;
}

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.CompilerServices;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
{
public class ViewBufferPage
@ -18,6 +20,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
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;
}
}

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()
{
var mvcOptions = new MvcOptions

View File

@ -32,35 +32,25 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
}
[Fact]
public void OnResultExecuting_DoesNothing_IfStatusCodeDoesNotExistInApiBehaviorOptions()
public void OnResultExecuting_DoesNothing_IfTransformedValueIsNull()
{
// Arrange
var actionResult = new NotFoundResult();
var context = GetContext(actionResult);
var filter = GetFilter(new ApiBehaviorOptions());
// Act
filter.OnResultExecuting(context);
// Assert
Assert.Same(actionResult, context.Result);
}
[Fact]
public void OnResultExecuting_DoesNothing_IfResultDoesNotHaveStatusCode()
{
// Arrange
var actionResult = new Mock<IActionResult>()
.As<IClientErrorActionResult>()
.Object;
var context = GetContext(actionResult);
var filter = GetFilter(new ApiBehaviorOptions());
var factory = new Mock<IClientErrorFactory>();
factory
.Setup(f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()))
.Returns((IActionResult)null)
.Verifiable();
var filter = new ClientErrorResultFilter(factory.Object, NullLogger<ClientErrorResultFilter>.Instance);
// Act
filter.OnResultExecuting(context);
// Assert
Assert.Same(actionResult, context.Result);
factory.Verify();
}
[Fact]
@ -78,18 +68,12 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Assert.Same(Result, context.Result);
}
private static ClientErrorResultFilter GetFilter(ApiBehaviorOptions options = null)
private static ClientErrorResultFilter GetFilter()
{
var apiBehaviorOptions = options ?? GetOptions();
var filter = new ClientErrorResultFilter(apiBehaviorOptions, NullLogger<ClientErrorResultFilter>.Instance);
return filter;
}
var factory = Mock.Of<IClientErrorFactory>(
f => f.GetClientError(It.IsAny<ActionContext>(), It.IsAny<IClientErrorActionResult>()) == Result);
private static ApiBehaviorOptions GetOptions()
{
var apiBehaviorOptions = new ApiBehaviorOptions();
apiBehaviorOptions.ClientErrorFactory[404] = _ => Result;
return apiBehaviorOptions;
return new ClientErrorResultFilter(factory, NullLogger<ClientErrorResultFilter>.Instance);
}
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.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
[assembly: Microsoft.AspNetCore.Mvc.ProducesErrorResponseType(typeof(InvalidEnumArgumentException))]
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ApiBehaviorApplicationModelProviderTest
@ -1041,9 +1043,6 @@ Environment.NewLine + "int b";
var actionModel = new ActionModel(
typeof(TestApiConventionController).GetMethod(nameof(TestApiConventionController.Delete)),
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)) };
// 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]
public void OnProvidersExecuting_AddsClientErrorResultFilter()
{
@ -1081,7 +1241,7 @@ Environment.NewLine + "int b";
var context = GetContext(typeof(TestApiController));
var options = new ApiBehaviorOptions
{
SuppressUseClientErrorFactory = true,
SuppressMapClientErrors = true,
InvalidModelStateResponseFactory = _ => null,
};
var provider = GetProvider(options);
@ -1122,7 +1282,11 @@ Environment.NewLine + "int b";
var loggerFactory = NullLoggerFactory.Instance;
modelMetadataProvider = modelMetadataProvider ?? new EmptyModelMetadataProvider();
return new ApiBehaviorApplicationModelProvider(optionsAccessor, modelMetadataProvider, loggerFactory);
return new ApiBehaviorApplicationModelProvider(
optionsAccessor,
modelMetadataProvider,
Mock.Of<IClientErrorFactory>(),
loggerFactory);
}
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.
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@ -28,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
[Fact]
public void Configure_AddsClientErrorFactories()
public void Configure_AddsClientErrorMappings()
{
// Arrange
var expected = new[] { 400, 401, 403, 404, 406, 409, 415, 422, };
@ -41,7 +44,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
optionsSetup.Configure(options);
// Assert
Assert.Equal(expected, options.ClientErrorFactory.Keys);
Assert.Equal(expected, options.ClientErrorMapping.Keys);
}
[Fact]
@ -97,5 +100,64 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// Assert
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)
.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()
.AddSingleton<IInlineConstraintResolver>(new DefaultInlineConstraintResolver(routeOptions.Object))
.AddSingleton<IInlineConstraintResolver>(inlineConstraintResolver)
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

View File

@ -89,6 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var featureCollection = new FeatureCollection();
featureCollection.Set<IEndpointFeature>(endpointFeature);
featureCollection.Set<IRouteValuesFeature>(endpointFeature);
featureCollection.Set<IRoutingFeature>(endpointFeature);
var httpContextMock = new Mock<HttpContext>();
httpContextMock.Setup(m => m.Features).Returns(featureCollection);
@ -180,7 +181,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actionDescriptorCollection = GetActionDescriptorCollection(
new { controller = "TestController", action = "TestAction", area = "TestArea" });
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
var endpoints = dataSource.Endpoints;
@ -686,28 +695,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider = null,
MvcEndpointInvokerFactory mvcEndpointInvokerFactory = null)
@ -719,13 +706,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Array.Empty<IActionDescriptorChangeProvider>());
}
var serviceProviderMock = new Mock<IServiceProvider>();
serviceProviderMock.Setup(m => m.GetService(typeof(IActionDescriptorCollectionProvider))).Returns(actionDescriptorCollectionProvider);
var services = new ServiceCollection();
services.AddSingleton(actionDescriptorCollectionProvider);
var dataSource = new MvcEndpointDataSource(
actionDescriptorCollectionProvider,
mvcEndpointInvokerFactory ?? new MvcEndpointInvokerFactory(new ActionInvokerFactory(Array.Empty<IActionInvokerProvider>())),
serviceProviderMock.Object);
services.BuildServiceProvider());
return dataSource;
}
@ -735,15 +722,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal
string template,
RouteValueDictionary defaults = null,
IDictionary<string, object> constraints = null,
RouteValueDictionary dataTokens = null)
RouteValueDictionary dataTokens = null,
IServiceProvider serviceProvider = null)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddRouting();
if (serviceProvider == null)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddRouting();
var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
serviceCollection.Configure<RouteOptions>(routeOptionsSetup.Configure);
var routeOptionsSetup = new MvcCoreRouteOptionsSetup();
serviceCollection.Configure<RouteOptions>(routeOptionsSetup.Configure);
var serviceProvider = serviceCollection.BuildServiceProvider();
serviceProvider = serviceCollection.BuildServiceProvider();
}
var parameterPolicyFactory = serviceProvider.GetRequiredService<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.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
@ -10,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
@ -17,7 +17,47 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
public class KnownRouteValueConstraintTests
{
#pragma warning disable CS0618 // Type or member is obsolete
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]
[InlineData("area", RouteDirection.IncomingRequest)]
@ -55,8 +95,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
{
// Arrange
var actionDescriptor = CreateActionDescriptor("testArea",
"testController",
"testAction");
"testController",
"testAction");
actionDescriptor.RouteValues.Add("randomKey", "testRandom");
var httpContext = GetHttpContext(actionDescriptor);
var route = Mock.Of<IRouter>();
@ -115,8 +155,8 @@ namespace Microsoft.AspNetCore.Mvc.Routing
public void RouteValue_IsNotAString_MatchFails(RouteDirection direction)
{
var actionDescriptor = CreateActionDescriptor("testArea",
controller: null,
action: null);
controller: null,
action: null);
var httpContext = GetHttpContext(actionDescriptor);
var route = Mock.Of<IRouter>();
var values = new RouteValueDictionary()
@ -157,7 +197,57 @@ namespace Microsoft.AspNetCore.Mvc.Routing
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);
@ -176,12 +266,7 @@ namespace Microsoft.AspNetCore.Mvc.Routing
var descriptorCollectionProvider = new DefaultActionDescriptorCollectionProvider(
new[] { actionProvider.Object },
Enumerable.Empty<IActionDescriptorChangeProvider>());
var context = new Mock<HttpContext>();
context.Setup(o => o.RequestServices
.GetService(typeof(IActionDescriptorCollectionProvider)))
.Returns(descriptorCollectionProvider);
return context.Object;
return descriptorCollectionProvider;
}
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.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
@ -10,6 +11,7 @@ using System.Threading.Tasks;
using BasicWebSite.Models;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -34,37 +36,48 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task ActionsReturnBadRequest_WhenModelStateIsInvalid()
{
// Arrange
var contactModel = new Contact
using (new ActivityReplacer())
{
Name = "Abc",
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 =>
var contactModel = new Contact
{
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);
}
);
Name = "Abc",
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);
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]
@ -97,6 +110,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert
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]
@ -112,8 +129,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
};
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."}},
{"Zip", new string[]{ @"The field Zip must match the regular expression '\d{5}'."}}
{"Name", new[] {"The field Name must be a string with a minimum length of 5 and a maximum length of 30."}},
{"Zip", new[] { @"The field Zip must match the regular expression '\d{5}'."}}
};
var contactString = JsonConvert.SerializeObject(contactModel);
@ -248,14 +265,88 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
[Fact]
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
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);
// 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 =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
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()
{
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
var expectedMediaTypes = new[] { "application/json" };
// Act
var response = await Client.PostAsync(
@ -1236,15 +1236,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(403, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats);
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
});
}
[Fact]
public async Task ApiConvention_ForPostMethodThatMatchesConvention()
{
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act
var response = await Client.PostAsync(
$"ApiExplorerResponseTypeWithApiConventionController/PostTaskOfProduct",
@ -1268,15 +1271,18 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(400, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats);
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
});
}
[Fact]
public async Task ApiConvention_ForPutActionThatMatchesConvention()
{
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act
var response = await Client.PutAsync(
$"ApiExplorerResponseTypeWithApiConventionController/Put",
@ -1300,21 +1306,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(400, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats);
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(404, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats);
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
});
}
[Fact]
public async Task ApiConvention_ForDeleteActionThatMatchesConvention()
{
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act
var response = await Client.DeleteAsync(
$"ApiExplorerResponseTypeWithApiConventionController/DeleteProductAsync");
@ -1337,21 +1346,24 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(400, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats);
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
Assert.Equal(404, responseType.StatusCode);
Assert.Empty(responseType.ResponseFormats);
Assert.Equal(expectedMediaTypes, GetSortedMediaTypes(responseType));
});
}
[Fact]
public async Task ApiConvention_ForActionWtihApiConventionMethod()
{
// Arrange
var expectedMediaTypes = new[] { "application/json", "application/xml", "text/json", "text/xml" };
// Act
var response = await Client.PostAsync(
"ApiExplorerResponseTypeWithApiConventionController/PostItem",
@ -1371,9 +1383,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(typeof(ProblemDetails).FullName, responseType.ResponseType);
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);
}
[Fact(Skip = "https://github.com/aspnet/Routing/issues/721")]
[Fact]
public async Task ActionModelSuppressedForPathMatching_CannotBeRouted()
{
// Arrange & Act

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
throw new StatusCodeMismatchException
{
ExpectedStatusCode = HttpStatusCode.OK,
ExpectedStatusCode = expectedStatusCode,
ActualStatusCode = response.StatusCode,
ResponseContent = responseContent,
};
@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
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>
<Compile Include="..\Microsoft.AspNetCore.Mvc.Formatters.Xml.Test\XmlAssert.cs" />
<Compile Include="..\Microsoft.AspNetCore.Mvc.Core.TestCommon\ActivityReplacer.cs" />
<EmbeddedResource Include="compiler\resources\**\*" />
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

View File

@ -72,6 +72,30 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
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]
public virtual async Task ConventionalRoutedController_ActionIsReachable()
{
@ -1259,6 +1283,19 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
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)
{
return new LinkBuilder(url);

View File

@ -1,6 +1,7 @@
// 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 System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
@ -208,5 +209,85 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"</ArrayOfSerializableErrorWrapper>",
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.
// 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.Http;
using System.Net.Http.Headers;
@ -183,5 +184,85 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
"<key4>key2-error</key4></SerializableErrorWrapper></ArrayOfSerializableErrorWrapper>",
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.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory);
Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
}
[Fact]
@ -72,7 +72,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.False(mvcOptions.EnableEndpointRouting);
Assert.Null(mvcOptions.MaxValidationDepth);
Assert.True(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.True(apiBehaviorOptions.SuppressUseClientErrorFactory);
Assert.True(apiBehaviorOptions.SuppressMapClientErrors);
}
[Fact]
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth);
Assert.False(apiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses);
Assert.False(apiBehaviorOptions.SuppressUseClientErrorFactory);
Assert.False(apiBehaviorOptions.SuppressMapClientErrors);
}
[Fact]
@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTest
Assert.True(mvcOptions.EnableEndpointRouting);
Assert.Equal(32, mvcOptions.MaxValidationDepth);
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.

View File

@ -469,14 +469,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
// Assert - 1
Assert.Equal(0, buffer.Length);
Assert.Equal(1, buffer.Pages.Count);
Assert.Single(buffer.Pages);
// Act - 2
buffer.Append("efgh");
// Assert - 2
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));
}
}

View File

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

View File

@ -33,6 +33,18 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
[Fact]
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 = "")
{
// 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();
}
[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
{
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.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
@ -24,12 +25,19 @@ namespace RoutingWebSite
{
app.UseMvc(routes =>
{
routes.MapRoute(
"DataTokensRoute",
"DataTokensRoute/{controller}/{action}",
defaults: null,
constraints: new { controller = "DataTokens" },
dataTokens: new { hasDataTokens = true });
routes.MapAreaRoute(
"flightRoute",
"adminRoute",
"{area:exists}/{controller}/{action}",
new { controller = "Home", action = "Index" },
new { area = "Travel" });
"flightRoute",
"adminRoute",
"{area:exists}/{controller}/{action}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { area = "Travel" });
routes.MapRoute(
"ActionAsMethod",
@ -40,6 +48,11 @@ namespace RoutingWebSite
"RouteWithOptionalSegment",
"{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.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
@ -24,12 +25,19 @@ namespace RoutingWebSite
{
app.UseMvc(routes =>
{
routes.MapRoute(
"DataTokensRoute",
"DataTokensRoute/{controller}/{action}",
defaults: null,
constraints: new { controller = "DataTokens" },
dataTokens: new { hasDataTokens = true });
routes.MapAreaRoute(
"flightRoute",
"adminRoute",
"{area:exists}/{controller}/{action}",
new { controller = "Home", action = "Index" },
new { area = "Travel" });
"flightRoute",
"adminRoute",
"{area:exists}/{controller}/{action}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { area = "Travel" });
routes.MapRoute(
"ActionAsMethod",
@ -40,6 +48,11 @@ namespace RoutingWebSite
"RouteWithOptionalSegment",
"{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());
}
}
}
}