Merge remote-tracking branch 'origin/release/2.2'
This commit is contained in:
commit
1dba6dfcc8
|
|
@ -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())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) &&
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")]
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,8 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
_serializerSettings = new DataContractSerializerSettings();
|
||||
|
||||
WrapperProviderFactories = new List<IWrapperProviderFactory>();
|
||||
WrapperProviderFactories.Add(new SerializableErrorWrapperProviderFactory());
|
||||
WrapperProviderFactories = WrapperProviderFactoriesExtensions.GetDefaultProviderFactories();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"shadowCopy": false
|
||||
"shadowCopy": false,
|
||||
"longRunningTestSeconds": 60
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue