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

# Conflicts:
#	build/dependencies.props
This commit is contained in:
Pranav K 2018-08-10 11:17:26 -07:00
commit 050993ed92
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
12 changed files with 153 additions and 34 deletions

View File

@ -40,9 +40,12 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
} }
var documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); var documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);
var addUsingDirective = false;
foreach (var statusCode in statusCodes.OrderBy(s => s)) foreach (var statusCode in statusCodes.OrderBy(s => s))
{ {
documentEditor.AddAttribute(context.MethodSyntax, CreateProducesResponseTypeAttribute(statusCode)); documentEditor.AddAttribute(context.MethodSyntax, CreateProducesResponseTypeAttribute(context, statusCode, out var addUsing));
addUsingDirective |= addUsing;
} }
if (!declaredResponseMetadata.Any(m => m.IsDefault && m.AttributeSource == context.Method)) if (!declaredResponseMetadata.Any(m => m.IsDefault && m.AttributeSource == context.Method))
@ -64,7 +67,23 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
documentEditor.RemoveNode(attributeSyntax); documentEditor.RemoveNode(attributeSyntax);
} }
return documentEditor.GetChangedDocument(); var document = documentEditor.GetChangedDocument();
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is CompilationUnitSyntax compilationUnit && addUsingDirective)
{
const string @namespace = "Microsoft.AspNetCore.Http";
var declaredUsings = new HashSet<string>(compilationUnit.Usings.Select(x => x.Name.ToString()));
if (!declaredUsings.Contains(@namespace))
{
root = compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(@namespace)));
}
}
return document.WithSyntaxRoot(root);
} }
private async Task<CodeActionContext> CreateCodeActionContext(CancellationToken cancellationToken) private async Task<CodeActionContext> CreateCodeActionContext(CancellationToken cancellationToken)
@ -75,12 +94,37 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
var methodSyntax = methodReturnStatement.FirstAncestorOrSelf<MethodDeclarationSyntax>(); var methodSyntax = methodReturnStatement.FirstAncestorOrSelf<MethodDeclarationSyntax>();
var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken); var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken);
var statusCodesType = semanticModel.Compilation.GetTypeByMetadataName(ApiSymbolNames.HttpStatusCodes);
var statusCodeConstants = GetStatusCodeConstants(statusCodesType);
var symbolCache = new ApiControllerSymbolCache(semanticModel.Compilation); var symbolCache = new ApiControllerSymbolCache(semanticModel.Compilation);
var codeActionContext = new CodeActionContext(semanticModel, symbolCache, method, methodSyntax, cancellationToken); var codeActionContext = new CodeActionContext(semanticModel, symbolCache, method, methodSyntax, statusCodeConstants, cancellationToken);
return codeActionContext; return codeActionContext;
} }
private static Dictionary<int, string> GetStatusCodeConstants(INamespaceOrTypeSymbol statusCodesType)
{
var statusCodeConstants = new Dictionary<int, string>();
if (statusCodesType != null)
{
foreach (var member in statusCodesType.GetMembers())
{
if (member is IFieldSymbol field &&
field.Type.SpecialType == SpecialType.System_Int32 &&
field.Name.StartsWith("Status") &&
field.HasConstantValue &&
field.ConstantValue is int statusCode)
{
statusCodeConstants[statusCode] = field.Name;
}
}
}
return statusCodeConstants;
}
private ICollection<int> CalculateStatusCodesToApply(CodeActionContext context, IList<DeclaredApiResponseMetadata> declaredResponseMetadata) private ICollection<int> CalculateStatusCodesToApply(CodeActionContext context, IList<DeclaredApiResponseMetadata> declaredResponseMetadata)
{ {
if (!SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata)) if (!SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata))
@ -105,14 +149,31 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
return statusCodes; return statusCodes;
} }
private static AttributeSyntax CreateProducesResponseTypeAttribute(int statusCode) private static AttributeSyntax CreateProducesResponseTypeAttribute(CodeActionContext context, int statusCode, out bool addUsingDirective)
{ {
var statusCodeSyntax = CreateStatusCodeSyntax(context, statusCode, out addUsingDirective);
return SyntaxFactory.Attribute( return SyntaxFactory.Attribute(
SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute) SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute)
.WithAdditionalAnnotations(Simplifier.Annotation), .WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.AttributeArgumentList().AddArguments( SyntaxFactory.AttributeArgumentList().AddArguments(
SyntaxFactory.AttributeArgument( SyntaxFactory.AttributeArgument(statusCodeSyntax)));
SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(statusCode))))); }
private static ExpressionSyntax CreateStatusCodeSyntax(CodeActionContext context, int statusCode, out bool addUsingDirective)
{
if (context.StatusCodeConstants.TryGetValue(statusCode, out var constantName))
{
addUsingDirective = true;
return SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.ParseTypeName(ApiSymbolNames.HttpStatusCodes)
.WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.IdentifierName(constantName));
}
addUsingDirective = false;
return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(statusCode));
} }
private static AttributeSyntax CreateProducesDefaultResponseTypeAttribute() private static AttributeSyntax CreateProducesDefaultResponseTypeAttribute()
@ -124,22 +185,25 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
private readonly struct CodeActionContext private readonly struct CodeActionContext
{ {
public CodeActionContext( public CodeActionContext(SemanticModel semanticModel,
SemanticModel semanticModel,
ApiControllerSymbolCache symbolCache, ApiControllerSymbolCache symbolCache,
IMethodSymbol method, IMethodSymbol method,
MethodDeclarationSyntax methodSyntax, MethodDeclarationSyntax methodSyntax,
Dictionary<int, string> statusCodeConstants,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
SemanticModel = semanticModel; SemanticModel = semanticModel;
SymbolCache = symbolCache; SymbolCache = symbolCache;
Method = method; Method = method;
MethodSyntax = methodSyntax; MethodSyntax = methodSyntax;
StatusCodeConstants = statusCodeConstants;
CancellationToken = cancellationToken; CancellationToken = cancellationToken;
} }
public MethodDeclarationSyntax MethodSyntax { get; } public MethodDeclarationSyntax MethodSyntax { get; }
public Dictionary<int, string> StatusCodeConstants { get; }
public IMethodSymbol Method { get; } public IMethodSymbol Method { get; }
public SemanticModel SemanticModel { get; } public SemanticModel SemanticModel { get; }

View File

@ -32,5 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public const string ProducesDefaultResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute"; public const string ProducesDefaultResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute";
public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute"; public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute";
public const string HttpStatusCodes = "Microsoft.AspNetCore.Http.StatusCodes";
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
@ -10,8 +11,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <see cref="ActionContext.ModelState"/> and short-circuits the pipeline /// <see cref="ActionContext.ModelState"/> and short-circuits the pipeline
/// with an Unsupported Media Type (415) response. /// with an Unsupported Media Type (415) response.
/// </summary> /// </summary>
public class UnsupportedContentTypeFilter : IActionFilter public class UnsupportedContentTypeFilter : IActionFilter, IOrderedFilter
{ {
/// <summary>
/// Gets or sets the filter order. <see cref="IOrderedFilter.Order"/>.
/// <para>
/// Defaults to <c>-3000</c> to ensure it executes before <see cref="ModelStateInvalidFilter"/>.
/// </para>
/// </summary>
public int Order { get; set; } = -3000;
/// <inheritdoc /> /// <inheritdoc />
public void OnActionExecuting(ActionExecutingContext context) public void OnActionExecuting(ActionExecutingContext context)
{ {
@ -32,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
foreach (var kvp in modelState) foreach (var kvp in modelState)
{ {
var errors = kvp.Value.Errors; var errors = kvp.Value.Errors;
for (int i = 0; i < errors.Count; i++) for (var i = 0; i < errors.Count; i++)
{ {
var error = errors[i]; var error = errors[i];
if (error.Exception is UnsupportedContentTypeException) if (error.Exception is UnsupportedContentTypeException)

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BasicWebSite.Models; using BasicWebSite.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -58,6 +59,38 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
); );
} }
[Fact]
public async Task ActionsReturnUnsupportedMediaType_WhenMediaTypeIsNotSupported()
{
// Arrange
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/contact")
{
Content = new StringContent("some content", Encoding.UTF8, "text/css"),
};
// Act
var response = await Client.SendAsync(requestMessage);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
}
[Fact]
public async Task ActionsReturnUnsupportedMediaType_WhenEncodingIsUnsupported()
{
// Arrange
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/contact")
{
Content = new StringContent("some content", Encoding.UTF7, "application/json"),
};
// Act
var response = await Client.SendAsync(requestMessage);
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.UnsupportedMediaType);
}
[Fact] [Fact]
public async Task ActionsReturnBadRequest_UsesProblemDescriptionProviderAndApiConventionsToConfigureErrorResponse() public async Task ActionsReturnBadRequest_UsesProblemDescriptionProviderAndApiConventionsToConfigureErrorResponse()
{ {

View File

@ -330,7 +330,7 @@ Hello from page";
var response = await Client.GetStringAsync("/Accounts/PageWithLinks"); var response = await Client.GetStringAsync("/Accounts/PageWithLinks");
// Assert // Assert
Assert.Equal(expected, response.Trim()); Assert.Equal(expected, response.Trim(), ignoreLineEndingDifferences: true);
} }
[Fact] [Fact]
@ -346,7 +346,7 @@ Hello from page";
var response = await Client.GetStringAsync("/Accounts/RelativeLinks"); var response = await Client.GetStringAsync("/Accounts/RelativeLinks");
// Assert // Assert
Assert.Equal(expected, response.Trim()); Assert.Equal(expected, response.Trim(), ignoreLineEndingDifferences: true);
} }
[Fact] [Fact]
@ -369,7 +369,7 @@ Hello from /Pages/Shared/";
var response = await Client.GetStringAsync("/Accounts/Manage/RenderPartials"); var response = await Client.GetStringAsync("/Accounts/Manage/RenderPartials");
// Assert // Assert
Assert.Equal(expected, response.Trim()); Assert.Equal(expected, response.Trim(), ignoreLineEndingDifferences: true);
} }
[Fact] [Fact]

View File

@ -1,4 +1,5 @@
 using Microsoft.AspNetCore.Http;
[assembly: Microsoft.AspNetCore.Mvc.ApiConventionType(typeof(Microsoft.AspNetCore.Mvc.DefaultApiConventions))] [assembly: Microsoft.AspNetCore.Mvc.ApiConventionType(typeof(Microsoft.AspNetCore.Mvc.DefaultApiConventions))]
namespace TestApp._OUTPUT_ namespace TestApp._OUTPUT_
@ -17,9 +18,9 @@ namespace TestApp._OUTPUT_
{ {
public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController
{ {
[Microsoft.AspNetCore.Mvc.ProducesResponseType(202)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status202Accepted)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(400)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status400BadRequest)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(404)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status404NotFound)]
[Microsoft.AspNetCore.Mvc.ProducesDefaultResponseType] [Microsoft.AspNetCore.Mvc.ProducesDefaultResponseType]
public object GetItem(int id) public object GetItem(int id)
{ {

View File

@ -1,10 +1,12 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
{ {
[ApiController] [ApiController]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase public class CodeFixAddsMissingStatusCodes : ControllerBase
{ {
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetItem(int id) public IActionResult GetItem(int id)
{ {
if (id == 0) if (id == 0)

View File

@ -1,12 +1,14 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{ {
[ApiController] [ApiController]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase public class CodeFixAddsMissingStatusCodes : ControllerBase
{ {
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(200)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(400)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public IActionResult GetItem(int id) public IActionResult GetItem(int id)
{ {

View File

@ -1,11 +1,13 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{ {
[ApiController] [ApiController]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesController : ControllerBase public class CodeFixAddsStatusCodesController : ControllerBase
{ {
[ProducesResponseType(200)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public IActionResult GetItem(int id) public IActionResult GetItem(int id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
[assembly: ApiConventionType(typeof(DefaultApiConventions))] [assembly: ApiConventionType(typeof(DefaultApiConventions))]
@ -8,9 +9,9 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixAddsSuccessStatusCode : ControllerBase public class CodeFixAddsSuccessStatusCode : ControllerBase
{ {
[ProducesResponseType(201)] [ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(400)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public ActionResult<object> GetItem(string id) public ActionResult<object> GetItem(string id)
{ {

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
[assembly: ApiConventionType(typeof(DefaultApiConventions))] [assembly: ApiConventionType(typeof(DefaultApiConventions))]
@ -8,8 +9,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixWithConventionAddsMissingStatusCodes : ControllerBase public class CodeFixWithConventionAddsMissingStatusCodes : ControllerBase
{ {
[ProducesResponseType(202)] [ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public ActionResult<string> GetItem(int id) public ActionResult<string> GetItem(int id)
{ {

View File

@ -1,11 +1,13 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{ {
[ApiController] [ApiController]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixWithConventionMethodAddsMissingStatusCodes : ControllerBase public class CodeFixWithConventionMethodAddsMissingStatusCodes : ControllerBase
{ {
[ProducesResponseType(202)] [ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public ActionResult<string> GetItem(int id) public ActionResult<string> GetItem(int id)
{ {