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 addUsingDirective = false;
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))
@ -64,7 +67,23 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
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)
@ -75,12 +94,37 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
var methodSyntax = methodReturnStatement.FirstAncestorOrSelf<MethodDeclarationSyntax>();
var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken);
var statusCodesType = semanticModel.Compilation.GetTypeByMetadataName(ApiSymbolNames.HttpStatusCodes);
var statusCodeConstants = GetStatusCodeConstants(statusCodesType);
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;
}
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)
{
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;
}
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(
SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute)
.WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.AttributeArgumentList().AddArguments(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(statusCode)))));
SyntaxFactory.AttributeArgument(statusCodeSyntax)));
}
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()
@ -124,22 +185,25 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
private readonly struct CodeActionContext
{
public CodeActionContext(
SemanticModel semanticModel,
public CodeActionContext(SemanticModel semanticModel,
ApiControllerSymbolCache symbolCache,
IMethodSymbol method,
MethodDeclarationSyntax methodSyntax,
Dictionary<int, string> statusCodeConstants,
CancellationToken cancellationToken)
{
SemanticModel = semanticModel;
SymbolCache = symbolCache;
Method = method;
MethodSyntax = methodSyntax;
StatusCodeConstants = statusCodeConstants;
CancellationToken = cancellationToken;
}
public MethodDeclarationSyntax MethodSyntax { get; }
public Dictionary<int, string> StatusCodeConstants { get; }
public IMethodSymbol Method { 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 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.
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
@ -10,8 +11,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// <see cref="ActionContext.ModelState"/> and short-circuits the pipeline
/// with an Unsupported Media Type (415) response.
/// </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 />
public void OnActionExecuting(ActionExecutingContext context)
{
@ -32,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
foreach (var kvp in modelState)
{
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];
if (error.Exception is UnsupportedContentTypeException)

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using BasicWebSite.Models;
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]
public async Task ActionsReturnBadRequest_UsesProblemDescriptionProviderAndApiConventionsToConfigureErrorResponse()
{

View File

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

View File

@ -1,4 +1,5 @@

using Microsoft.AspNetCore.Http;
[assembly: Microsoft.AspNetCore.Mvc.ApiConventionType(typeof(Microsoft.AspNetCore.Mvc.DefaultApiConventions))]
namespace TestApp._OUTPUT_
@ -17,9 +18,9 @@ namespace TestApp._OUTPUT_
{
public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController
{
[Microsoft.AspNetCore.Mvc.ProducesResponseType(202)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(400)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(404)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status202Accepted)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status400BadRequest)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status404NotFound)]
[Microsoft.AspNetCore.Mvc.ProducesDefaultResponseType]
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]
[Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase
{
[ProducesResponseType(404)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetItem(int id)
{
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]
[Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase
{
[ProducesResponseType(404)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
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]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesController : ControllerBase
{
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public IActionResult GetItem(int id)
{

View File

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

View File

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