Merge remote-tracking branch 'origin/release/2.2'
This commit is contained in:
commit
2f0aee6736
|
|
@ -39,5 +39,6 @@
|
|||
-->
|
||||
<ItemGroup Condition="'$(BenchmarksTargetFramework)' != ''">
|
||||
<PackageReference Include="Microsoft.AspNetCore.All" Version="$(MicrosoftAspNetCoreAllPackageVersion)" />
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Razor" Version="$(MicrosoftAspNetCoreAllPackageVersion)" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
-->
|
||||
<ItemGroup Condition="'$(BenchmarksTargetFramework)' != ''">
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="$(MicrosoftNETCoreApp22PackageVersion)" />
|
||||
<PackageReference Include="Microsoft.NET.Sdk.Razor" Version="$(MicrosoftNETCoreApp22PackageVersion)" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
|
|
|
|||
|
|
@ -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 Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
|
|
@ -9,16 +10,18 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
private readonly int? _statusCode;
|
||||
|
||||
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement)
|
||||
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, ITypeSymbol returnType)
|
||||
{
|
||||
ReturnStatement = returnStatement;
|
||||
ReturnType = returnType;
|
||||
_statusCode = null;
|
||||
}
|
||||
|
||||
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode)
|
||||
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode, ITypeSymbol returnType)
|
||||
{
|
||||
ReturnStatement = returnStatement;
|
||||
_statusCode = statusCode;
|
||||
ReturnType = returnType;
|
||||
}
|
||||
|
||||
public ReturnStatementSyntax ReturnStatement { get; }
|
||||
|
|
@ -26,5 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
public int StatusCode => _statusCode.Value;
|
||||
|
||||
public bool IsDefaultResponse => _statusCode == null;
|
||||
|
||||
public ITypeSymbol ReturnType { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,300 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
public static class ActualApiResponseMetadataFactory
|
||||
{
|
||||
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
|
||||
|
||||
/// <summary>
|
||||
/// This method looks at individual return statments and attempts to parse the status code and the return type.
|
||||
/// Given a <see cref="MethodDeclarationSyntax"/> for an action, this method inspects return statements in the body.
|
||||
/// If the returned type is not assignable from IActionResult, it assumes that an "object" value is being returned. e.g. return new Person();
|
||||
/// For return statements returning an action result, it attempts to infer the status code and return type. Helper methods in controller,
|
||||
/// values set in initializer and new-ing up an IActionResult instance are supported.
|
||||
/// </summary>
|
||||
internal static bool TryGetActualResponseMetadata(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
SemanticModel semanticModel,
|
||||
MethodDeclarationSyntax methodSyntax,
|
||||
CancellationToken cancellationToken,
|
||||
out IList<ActualApiResponseMetadata> actualResponseMetadata)
|
||||
{
|
||||
actualResponseMetadata = new List<ActualApiResponseMetadata>();
|
||||
|
||||
var allReturnStatementsReadable = true;
|
||||
|
||||
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
|
||||
{
|
||||
if (returnStatementSyntax.IsMissing || returnStatementSyntax.Expression == null || returnStatementSyntax.Expression.IsMissing)
|
||||
{
|
||||
// Ignore malformed return statements.
|
||||
allReturnStatementsReadable = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var responseMetadata = InspectReturnStatementSyntax(
|
||||
symbolCache,
|
||||
semanticModel,
|
||||
returnStatementSyntax,
|
||||
cancellationToken);
|
||||
|
||||
if (responseMetadata != null)
|
||||
{
|
||||
actualResponseMetadata.Add(responseMetadata.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
allReturnStatementsReadable = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allReturnStatementsReadable;
|
||||
}
|
||||
|
||||
internal static ActualApiResponseMetadata? InspectReturnStatementSyntax(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
SemanticModel semanticModel,
|
||||
ReturnStatementSyntax returnStatementSyntax,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var returnExpression = returnStatementSyntax.Expression;
|
||||
var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken);
|
||||
if (typeInfo.Type == null || typeInfo.Type.TypeKind == TypeKind.Error)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var statementReturnType = typeInfo.Type;
|
||||
|
||||
if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType))
|
||||
{
|
||||
// Return expression is not an instance of IActionResult. Must be returning the "model".
|
||||
return new ActualApiResponseMetadata(returnStatementSyntax, statementReturnType);
|
||||
}
|
||||
|
||||
var defaultStatusCodeAttribute = statementReturnType
|
||||
.GetAttributes(symbolCache.DefaultStatusCodeAttribute, inherit: true)
|
||||
.FirstOrDefault();
|
||||
|
||||
var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
|
||||
ITypeSymbol returnType = null;
|
||||
switch (returnExpression)
|
||||
{
|
||||
case InvocationExpressionSyntax invocation:
|
||||
{
|
||||
// Covers the 'return StatusCode(200)' case.
|
||||
var result = InspectMethodArguments(symbolCache, semanticModel, invocation.Expression, invocation.ArgumentList, cancellationToken);
|
||||
statusCode = result.statusCode ?? statusCode;
|
||||
returnType = result.returnType;
|
||||
break;
|
||||
}
|
||||
|
||||
case ObjectCreationExpressionSyntax creation:
|
||||
{
|
||||
// Read values from 'return new StatusCodeResult(200) case.
|
||||
var result = InspectMethodArguments(symbolCache, semanticModel, creation, creation.ArgumentList, cancellationToken);
|
||||
statusCode = result.statusCode ?? statusCode;
|
||||
returnType = result.returnType;
|
||||
|
||||
// Read values from property assignments e.g. 'return new ObjectResult(...) { StatusCode = 200 }'.
|
||||
// Property assignments override constructor assigned values and defaults.
|
||||
result = InspectInitializers(symbolCache, semanticModel, creation.Initializer, cancellationToken);
|
||||
statusCode = result.statusCode ?? statusCode;
|
||||
returnType = result.returnType ?? returnType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ActualApiResponseMetadata(returnStatementSyntax, statusCode.Value, returnType);
|
||||
}
|
||||
|
||||
private static (int? statusCode, ITypeSymbol returnType) InspectInitializers(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
SemanticModel semanticModel,
|
||||
InitializerExpressionSyntax initializer,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int? statusCode = null;
|
||||
ITypeSymbol typeSymbol = null;
|
||||
|
||||
for (var i = 0; initializer != null && i < initializer.Expressions.Count; i++)
|
||||
{
|
||||
var expression = initializer.Expressions[i];
|
||||
|
||||
if (!(expression is AssignmentExpressionSyntax assignment) ||
|
||||
!(assignment.Left is IdentifierNameSyntax identifier))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var symbolInfo = semanticModel.GetSymbolInfo(identifier, cancellationToken);
|
||||
if (symbolInfo.Symbol is IPropertySymbol property)
|
||||
{
|
||||
if (IsInterfaceImplementation(property, symbolCache.StatusCodeActionResultStatusProperty) &&
|
||||
TryGetExpressionStatusCode(semanticModel, assignment.Right, cancellationToken, out var statusCodeValue))
|
||||
{
|
||||
// Look for assignments to IStatusCodeActionResult.StatusCode
|
||||
statusCode = statusCodeValue;
|
||||
}
|
||||
else if (HasAttributeNamed(property, ApiSymbolNames.ActionResultObjectValueAttribute))
|
||||
{
|
||||
// Look for assignment to a property annotated with [ActionResultObjectValue]
|
||||
typeSymbol = GetExpressionObjectType(semanticModel, assignment.Right, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (statusCode, typeSymbol);
|
||||
}
|
||||
|
||||
private static (int? statusCode, ITypeSymbol returnType) InspectMethodArguments(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
SemanticModel semanticModel,
|
||||
ExpressionSyntax expression,
|
||||
BaseArgumentListSyntax argumentList,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int? statusCode = null;
|
||||
ITypeSymbol typeSymbol = null;
|
||||
|
||||
var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken);
|
||||
|
||||
if (symbolInfo.Symbol is IMethodSymbol method)
|
||||
{
|
||||
for (var i = 0; i < method.Parameters.Length; i++)
|
||||
{
|
||||
var parameter = method.Parameters[i];
|
||||
if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultStatusCodeAttribute))
|
||||
{
|
||||
var argument = argumentList.Arguments[parameter.Ordinal];
|
||||
if (TryGetExpressionStatusCode(semanticModel, argument.Expression, cancellationToken, out var statusCodeValue))
|
||||
{
|
||||
statusCode = statusCodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (HasAttributeNamed(parameter, ApiSymbolNames.ActionResultObjectValueAttribute))
|
||||
{
|
||||
var argument = argumentList.Arguments[parameter.Ordinal];
|
||||
typeSymbol = GetExpressionObjectType(semanticModel, argument.Expression, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (statusCode, typeSymbol);
|
||||
}
|
||||
|
||||
private static ITypeSymbol GetExpressionObjectType(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken)
|
||||
{
|
||||
var typeInfo = semanticModel.GetTypeInfo(expression, cancellationToken);
|
||||
return typeInfo.Type;
|
||||
}
|
||||
|
||||
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) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.ParenthesizedLambdaExpression) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.SimpleLambdaExpression) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.AnonymousMethodExpression);
|
||||
}
|
||||
|
||||
internal static int? GetDefaultStatusCode(AttributeData attribute)
|
||||
{
|
||||
if (attribute != null &&
|
||||
attribute.ConstructorArguments.Length == 1 &&
|
||||
attribute.ConstructorArguments[0].Kind == TypedConstantKind.Primitive &&
|
||||
attribute.ConstructorArguments[0].Value is int statusCode)
|
||||
{
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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 HasAttributeNamed(ISymbol symbol, string attributeName)
|
||||
{
|
||||
var attributes = symbol.GetAttributes();
|
||||
var length = attributes.Length;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
if (attributes[i].AttributeClass.Name == attributeName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,17 @@ using Microsoft.CodeAnalysis.Simplification;
|
|||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CodeAction"/> that adds one or more <c>ProducesResponseType</c> attributes on the action.
|
||||
/// 1) It get status codes from ProducesResponseType, ProducesDefaultResponseType, and conventions applied to the action to get the declared metadata.
|
||||
/// 2) It inspects return statements to get actual metadata.
|
||||
/// Diffing the two gets us a list of undocumented status codes.
|
||||
/// We'll attempt to generate a [ProducesResponseType(typeof(SomeModel), 4xx)] if
|
||||
/// a) the status code is 4xx or later.
|
||||
/// b) the return statement included a return type.
|
||||
/// c) the return type wasn't the error type (specified by ProducesErrorResponseType or implicit ProblemDetails)
|
||||
/// In all other cases, we generate [ProducesResponseType(StatusCode)]
|
||||
/// </summary>
|
||||
internal sealed class AddResponseTypeAttributeCodeFixAction : CodeAction
|
||||
{
|
||||
private readonly Document _document;
|
||||
|
|
@ -32,9 +43,10 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
var context = await CreateCodeActionContext(cancellationToken).ConfigureAwait(false);
|
||||
var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(context.SymbolCache, context.Method);
|
||||
var errorResponseType = SymbolApiResponseMetadataProvider.GetErrorResponseType(context.SymbolCache, context.Method);
|
||||
|
||||
var statusCodes = CalculateStatusCodesToApply(context, declaredResponseMetadata);
|
||||
if (statusCodes.Count == 0)
|
||||
var results = CalculateStatusCodesToApply(context, declaredResponseMetadata);
|
||||
if (results.Count == 0)
|
||||
{
|
||||
return _document;
|
||||
}
|
||||
|
|
@ -42,9 +54,22 @@ 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))
|
||||
foreach (var (statusCode, returnType) in results.OrderBy(s => s.statusCode))
|
||||
{
|
||||
documentEditor.AddAttribute(context.MethodSyntax, CreateProducesResponseTypeAttribute(context, statusCode, out var addUsing));
|
||||
AttributeSyntax attributeSyntax;
|
||||
bool addUsing;
|
||||
|
||||
if (statusCode >= 400 && returnType != null && returnType != errorResponseType)
|
||||
{
|
||||
// If a returnType was discovered and is different from the errorResponseType, use it in the result.
|
||||
attributeSyntax = CreateProducesResponseTypeAttribute(context, statusCode, returnType, out addUsing);
|
||||
}
|
||||
else
|
||||
{
|
||||
attributeSyntax = CreateProducesResponseTypeAttribute(context, statusCode, out addUsing);
|
||||
}
|
||||
|
||||
documentEditor.AddAttribute(context.MethodSyntax, attributeSyntax);
|
||||
addUsingDirective |= addUsing;
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +128,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
return codeActionContext;
|
||||
}
|
||||
|
||||
private static Dictionary<int, string> GetStatusCodeConstants(INamespaceOrTypeSymbol statusCodesType)
|
||||
private static Dictionary<int, string> GetStatusCodeConstants(INamedTypeSymbol statusCodesType)
|
||||
{
|
||||
var statusCodeConstants = new Dictionary<int, string>();
|
||||
|
||||
|
|
@ -125,15 +150,15 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
return statusCodeConstants;
|
||||
}
|
||||
|
||||
private ICollection<int> CalculateStatusCodesToApply(CodeActionContext context, IList<DeclaredApiResponseMetadata> declaredResponseMetadata)
|
||||
private ICollection<(int statusCode, ITypeSymbol typeSymbol)> CalculateStatusCodesToApply(in CodeActionContext context, IList<DeclaredApiResponseMetadata> declaredResponseMetadata)
|
||||
{
|
||||
if (!SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata))
|
||||
if (!ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata))
|
||||
{
|
||||
// If we cannot parse metadata correctly, don't offer fixes.
|
||||
return Array.Empty<int>();
|
||||
return Array.Empty<(int, ITypeSymbol)>();
|
||||
}
|
||||
|
||||
var statusCodes = new HashSet<int>();
|
||||
var statusCodes = new Dictionary<int, (int, ITypeSymbol)>();
|
||||
foreach (var metadata in actualResponseMetadata)
|
||||
{
|
||||
if (DeclaredApiResponseMetadata.TryGetDeclaredMetadata(declaredResponseMetadata, metadata, result: out var declaredMetadata) &&
|
||||
|
|
@ -143,20 +168,39 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
continue;
|
||||
}
|
||||
|
||||
statusCodes.Add(metadata.IsDefaultResponse ? 200 : metadata.StatusCode);
|
||||
var statusCode = metadata.IsDefaultResponse ? 200 : metadata.StatusCode;
|
||||
statusCodes.Add(statusCode, (statusCode, metadata.ReturnType));
|
||||
}
|
||||
|
||||
return statusCodes;
|
||||
return statusCodes.Values;
|
||||
}
|
||||
|
||||
private static AttributeSyntax CreateProducesResponseTypeAttribute(CodeActionContext context, int statusCode, out bool addUsingDirective)
|
||||
private static AttributeSyntax CreateProducesResponseTypeAttribute(in CodeActionContext context, int statusCode, out bool addUsingDirective)
|
||||
{
|
||||
// [ProducesResponseType(StatusCodes.Status400NotFound)]
|
||||
var statusCodeSyntax = CreateStatusCodeSyntax(context, statusCode, out addUsingDirective);
|
||||
|
||||
return SyntaxFactory.Attribute(
|
||||
SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute)
|
||||
.WithAdditionalAnnotations(Simplifier.Annotation),
|
||||
SyntaxFactory.AttributeArgumentList().AddArguments(
|
||||
|
||||
SyntaxFactory.AttributeArgument(statusCodeSyntax)));
|
||||
}
|
||||
|
||||
private static AttributeSyntax CreateProducesResponseTypeAttribute(in CodeActionContext context, int statusCode, ITypeSymbol typeSymbol, out bool addUsingDirective)
|
||||
{
|
||||
// [ProducesResponseType(typeof(ReturnType), StatusCodes.Status400NotFound)]
|
||||
var statusCodeSyntax = CreateStatusCodeSyntax(context, statusCode, out addUsingDirective);
|
||||
var responseTypeAttribute = SyntaxFactory.TypeOfExpression(
|
||||
SyntaxFactory.ParseTypeName(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
|
||||
.WithAdditionalAnnotations(Simplifier.Annotation));
|
||||
|
||||
return SyntaxFactory.Attribute(
|
||||
SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute)
|
||||
.WithAdditionalAnnotations(Simplifier.Annotation),
|
||||
SyntaxFactory.AttributeArgumentList().AddArguments(
|
||||
SyntaxFactory.AttributeArgument(responseTypeAttribute),
|
||||
SyntaxFactory.AttributeArgument(statusCodeSyntax)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
}
|
||||
|
||||
var returnStatementSyntax = (ReturnStatementSyntax)returnOperation.Syntax;
|
||||
var actualMetadata = SymbolApiResponseMetadataProvider.InspectReturnStatementSyntax(
|
||||
var actualMetadata = ActualApiResponseMetadataFactory.InspectReturnStatementSyntax(
|
||||
symbolCache,
|
||||
semanticModel,
|
||||
returnStatementSyntax,
|
||||
|
|
|
|||
|
|
@ -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.Diagnostics;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
|
|
@ -22,11 +21,11 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
ModelStateDictionary = compilation.GetTypeByMetadataName(ApiSymbolNames.ModelStateDictionary);
|
||||
NonActionAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.NonActionAttribute);
|
||||
NonControllerAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.NonControllerAttribute);
|
||||
ProblemDetails = compilation.GetTypeByMetadataName(ApiSymbolNames.ProblemDetails);
|
||||
ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesDefaultResponseTypeAttribute);
|
||||
ProducesErrorResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesErrorResponseTypeAttribute);
|
||||
ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesResponseTypeAttribute);
|
||||
|
||||
StatusCodeValueAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.StatusCodeValueAttribute);
|
||||
|
||||
var statusCodeActionResult = compilation.GetTypeByMetadataName(ApiSymbolNames.IStatusCodeActionResult);
|
||||
StatusCodeActionResultStatusProperty = (IPropertySymbol)statusCodeActionResult?.GetMembers("StatusCode")[0];
|
||||
|
||||
|
|
@ -61,10 +60,12 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
|
||||
public INamedTypeSymbol NonControllerAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol ProblemDetails { get; }
|
||||
|
||||
public INamedTypeSymbol ProducesDefaultResponseTypeAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol ProducesResponseTypeAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol StatusCodeValueAttribute { get; }
|
||||
public INamedTypeSymbol ProducesErrorResponseTypeAttribute { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
}
|
||||
|
||||
var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||
var hasUnreadableStatusCodes = !SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata);
|
||||
var hasUnreadableStatusCodes = !ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata);
|
||||
|
||||
var hasUndocumentedStatusCodes = false;
|
||||
foreach (var actualMetadata in actualResponseMetadata)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
internal static class ApiSymbolNames
|
||||
{
|
||||
public const string ActionResultStatusCodeAttribute = "ActionResultStatusCodeAttribute";
|
||||
|
||||
public const string ActionResultObjectValueAttribute = "ActionResultObjectValueAttribute";
|
||||
|
||||
public const string AllowAnonymousAttribute = "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute";
|
||||
|
||||
public const string ApiConventionMethodAttribute = "Microsoft.AspNetCore.Mvc.ApiConventionMethodAttribute";
|
||||
|
|
@ -31,12 +35,14 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
|
||||
public const string NonControllerAttribute = "Microsoft.AspNetCore.Mvc.NonControllerAttribute";
|
||||
|
||||
public const string ProblemDetails = "Microsoft.AspNetCore.Mvc.ProblemDetails";
|
||||
|
||||
public const string ProducesDefaultResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute";
|
||||
|
||||
public const string ProducesErrorResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesErrorResponseTypeAttribute";
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
|
|
@ -15,14 +12,13 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
private const string StatusCodeProperty = "StatusCode";
|
||||
private const string StatusCodeConstructorParameter = "statusCode";
|
||||
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
|
||||
private static readonly IList<DeclaredApiResponseMetadata> DefaultResponseMetadatas = new[]
|
||||
{
|
||||
DeclaredApiResponseMetadata.ImplicitResponse,
|
||||
};
|
||||
|
||||
internal static IList<DeclaredApiResponseMetadata> GetDeclaredResponseMetadata(
|
||||
ApiControllerSymbolCache symbolCache,
|
||||
public static IList<DeclaredApiResponseMetadata> GetDeclaredResponseMetadata(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
IMethodSymbol method)
|
||||
{
|
||||
var metadataItems = GetResponseMetadataFromMethodAttributes(symbolCache, method);
|
||||
|
|
@ -44,8 +40,29 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
return metadataItems;
|
||||
}
|
||||
|
||||
public static ITypeSymbol GetErrorResponseType(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
IMethodSymbol method)
|
||||
{
|
||||
var errorTypeAttribute =
|
||||
method.GetAttributes(symbolCache.ProducesErrorResponseTypeAttribute).FirstOrDefault() ??
|
||||
method.ContainingType.GetAttributes(symbolCache.ProducesErrorResponseTypeAttribute).FirstOrDefault() ??
|
||||
method.ContainingAssembly.GetAttributes(symbolCache.ProducesErrorResponseTypeAttribute).FirstOrDefault();
|
||||
|
||||
ITypeSymbol errorType = symbolCache.ProblemDetails;
|
||||
if (errorTypeAttribute != null &&
|
||||
errorTypeAttribute.ConstructorArguments.Length == 1 &&
|
||||
errorTypeAttribute.ConstructorArguments[0].Kind == TypedConstantKind.Type &&
|
||||
errorTypeAttribute.ConstructorArguments[0].Value is ITypeSymbol typeSymbol)
|
||||
{
|
||||
errorType = typeSymbol;
|
||||
}
|
||||
|
||||
return errorType;
|
||||
}
|
||||
|
||||
private static IList<DeclaredApiResponseMetadata> GetResponseMetadataFromConventions(
|
||||
ApiControllerSymbolCache symbolCache,
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
IMethodSymbol method,
|
||||
IReadOnlyList<ITypeSymbol> conventionTypes)
|
||||
{
|
||||
|
|
@ -63,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
return Array.Empty<DeclaredApiResponseMetadata>();
|
||||
}
|
||||
|
||||
private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
||||
private static IMethodSymbol GetMethodFromConventionMethodAttribute(in ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
||||
{
|
||||
var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true)
|
||||
.FirstOrDefault();
|
||||
|
|
@ -97,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
}
|
||||
|
||||
private static IMethodSymbol MatchConventionMethod(
|
||||
ApiControllerSymbolCache symbolCache,
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
IMethodSymbol method,
|
||||
IReadOnlyList<ITypeSymbol> conventionTypes)
|
||||
{
|
||||
|
|
@ -120,7 +137,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
return null;
|
||||
}
|
||||
|
||||
private static IList<DeclaredApiResponseMetadata> GetResponseMetadataFromMethodAttributes(ApiControllerSymbolCache symbolCache, IMethodSymbol methodSymbol)
|
||||
private static IList<DeclaredApiResponseMetadata> GetResponseMetadataFromMethodAttributes(in ApiControllerSymbolCache symbolCache, IMethodSymbol methodSymbol)
|
||||
{
|
||||
var metadataItems = new List<DeclaredApiResponseMetadata>();
|
||||
var responseMetadataAttributes = methodSymbol.GetAttributes(symbolCache.ProducesResponseTypeAttribute, inherit: true);
|
||||
|
|
@ -141,7 +158,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
return metadataItems;
|
||||
}
|
||||
|
||||
internal static IReadOnlyList<ITypeSymbol> GetConventionTypes(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
||||
internal static IReadOnlyList<ITypeSymbol> GetConventionTypes(in ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
||||
{
|
||||
var attributes = method.ContainingType.GetAttributes(symbolCache.ApiConventionTypeAttribute).ToArray();
|
||||
if (attributes.Length == 0)
|
||||
|
|
@ -209,255 +226,5 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
|
||||
return DefaultStatusCode;
|
||||
}
|
||||
|
||||
internal static bool TryGetActualResponseMetadata(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
SemanticModel semanticModel,
|
||||
MethodDeclarationSyntax methodSyntax,
|
||||
CancellationToken cancellationToken,
|
||||
out IList<ActualApiResponseMetadata> actualResponseMetadata)
|
||||
{
|
||||
actualResponseMetadata = new List<ActualApiResponseMetadata>();
|
||||
|
||||
var allReturnStatementsReadable = true;
|
||||
|
||||
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,
|
||||
returnStatementSyntax,
|
||||
cancellationToken);
|
||||
|
||||
if (responseMetadata != null)
|
||||
{
|
||||
actualResponseMetadata.Add(responseMetadata.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
allReturnStatementsReadable = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allReturnStatementsReadable;
|
||||
}
|
||||
|
||||
internal static ActualApiResponseMetadata? InspectReturnStatementSyntax(
|
||||
in ApiControllerSymbolCache symbolCache,
|
||||
SemanticModel semanticModel,
|
||||
ReturnStatementSyntax returnStatementSyntax,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var returnExpression = returnStatementSyntax.Expression;
|
||||
var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken);
|
||||
if (typeInfo.Type.TypeKind == TypeKind.Error)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var statementReturnType = typeInfo.Type;
|
||||
|
||||
var defaultStatusCodeAttribute = statementReturnType
|
||||
.GetAttributes(symbolCache.DefaultStatusCodeAttribute, inherit: true)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (defaultStatusCodeAttribute != 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, defaultStatusCode.Value);
|
||||
}
|
||||
|
||||
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) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.ParenthesizedLambdaExpression) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.SimpleLambdaExpression) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.AnonymousMethodExpression);
|
||||
}
|
||||
|
||||
internal static int? GetDefaultStatusCode(AttributeData attribute)
|
||||
{
|
||||
if (attribute != null &&
|
||||
attribute.ConstructorArguments.Length == 1 &&
|
||||
attribute.ConstructorArguments[0].Kind == TypedConstantKind.Primitive &&
|
||||
attribute.ConstructorArguments[0].Value is int statusCode)
|
||||
{
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
string actionName,
|
||||
string controllerName,
|
||||
object routeValues,
|
||||
object value)
|
||||
[ActionResultObjectValue] object value)
|
||||
: base(value)
|
||||
{
|
||||
ActionName = actionName;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <param name="routeValues">The route data to use for generating the URL.</param>
|
||||
/// <param name="value">The value to format in the entity body.</param>
|
||||
public AcceptedAtRouteResult(object routeValues, object value)
|
||||
public AcceptedAtRouteResult(object routeValues, [ActionResultObjectValue] object value)
|
||||
: this(routeName: null, routeValues: routeValues, value: value)
|
||||
{
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public AcceptedAtRouteResult(
|
||||
string routeName,
|
||||
object routeValues,
|
||||
object value)
|
||||
[ActionResultObjectValue] object value)
|
||||
: base(value)
|
||||
{
|
||||
RouteName = routeName;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <param name="location">The location at which the status of requested content can be monitored.</param>
|
||||
/// <param name="value">The value to format in the entity body.</param>
|
||||
public AcceptedResult(string location, object value)
|
||||
public AcceptedResult(string location, [ActionResultObjectValue] object value)
|
||||
: base(value)
|
||||
{
|
||||
Location = location;
|
||||
|
|
@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="locationUri">The location at which the status of requested content can be monitored
|
||||
/// It is an optional parameter and may be null</param>
|
||||
/// <param name="value">The value to format in the entity body.</param>
|
||||
public AcceptedResult(Uri locationUri, object value)
|
||||
public AcceptedResult(Uri locationUri, [ActionResultObjectValue] object value)
|
||||
: base(value)
|
||||
{
|
||||
if (locationUri == null)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Creates a new <see cref="BadRequestObjectResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="error">Contains the errors to be returned to the client.</param>
|
||||
public BadRequestObjectResult(object error)
|
||||
public BadRequestObjectResult([ActionResultObjectValue] object error)
|
||||
: base(error)
|
||||
{
|
||||
StatusCode = DefaultStatusCode;
|
||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Creates a new <see cref="BadRequestObjectResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="modelState"><see cref="ModelStateDictionary"/> containing the validation errors.</param>
|
||||
public BadRequestObjectResult(ModelStateDictionary modelState)
|
||||
public BadRequestObjectResult([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||
: base(new SerializableError(modelState))
|
||||
{
|
||||
if (modelState == null)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Creates a new <see cref="ConflictObjectResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="error">Contains the errors to be returned to the client.</param>
|
||||
public ConflictObjectResult(object error)
|
||||
public ConflictObjectResult([ActionResultObjectValue] object error)
|
||||
: base(error)
|
||||
{
|
||||
StatusCode = DefaultStatusCode;
|
||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Creates a new <see cref="ConflictObjectResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="modelState"><see cref="ModelStateDictionary"/> containing the validation errors.</param>
|
||||
public ConflictObjectResult(ModelStateDictionary modelState)
|
||||
public ConflictObjectResult([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||
: base(new SerializableError(modelState))
|
||||
{
|
||||
if (modelState == null)
|
||||
|
|
|
|||
|
|
@ -201,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([StatusCodeValue] int statusCode)
|
||||
public virtual StatusCodeResult StatusCode([ActionResultStatusCode] int statusCode)
|
||||
=> new StatusCodeResult(statusCode);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -211,7 +211,7 @@ 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([StatusCodeValue] int statusCode, object value)
|
||||
public virtual ObjectResult StatusCode([ActionResultStatusCode] int statusCode, [ActionResultObjectValue] object value)
|
||||
{
|
||||
var result = new ObjectResult(value)
|
||||
{
|
||||
|
|
@ -304,9 +304,10 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="OkObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual OkObjectResult Ok(object value)
|
||||
public virtual OkObjectResult Ok([ActionResultObjectValue] object value)
|
||||
=> new OkObjectResult(value);
|
||||
|
||||
#region RedirectResult variants
|
||||
/// <summary>
|
||||
/// Creates a <see cref="RedirectResult"/> object that redirects (<see cref="StatusCodes.Status302Found"/>)
|
||||
/// to the specified <paramref name="url"/>.
|
||||
|
|
@ -1077,7 +1078,9 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
preserveMethod: true,
|
||||
fragment: fragment);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region FileResult variants
|
||||
/// <summary>
|
||||
/// Returns a file with the specified <paramref name="fileContents" /> as content (<see cref="StatusCodes.Status200OK"/>),
|
||||
/// and the specified <paramref name="contentType" /> as the Content-Type.
|
||||
|
|
@ -1698,6 +1701,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
EnableRangeProcessing = enableRangeProcessing,
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Creates an <see cref="UnauthorizedResult"/> that produces an <see cref="StatusCodes.Status401Unauthorized"/> response.
|
||||
|
|
@ -1712,7 +1716,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <returns>The created <see cref="UnauthorizedObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual UnauthorizedObjectResult Unauthorized(object value)
|
||||
public virtual UnauthorizedObjectResult Unauthorized([ActionResultObjectValue] object value)
|
||||
=> new UnauthorizedObjectResult(value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1728,7 +1732,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <returns>The created <see cref="NotFoundObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual NotFoundObjectResult NotFound(object value)
|
||||
public virtual NotFoundObjectResult NotFound([ActionResultObjectValue] object value)
|
||||
=> new NotFoundObjectResult(value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1745,7 +1749,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="error">An error object to be returned to the client.</param>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual BadRequestObjectResult BadRequest(object error)
|
||||
public virtual BadRequestObjectResult BadRequest([ActionResultObjectValue] object error)
|
||||
=> new BadRequestObjectResult(error);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1754,7 +1758,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="modelState">The <see cref="ModelStateDictionary" /> containing errors to be returned to the client.</param>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual BadRequestObjectResult BadRequest(ModelStateDictionary modelState)
|
||||
public virtual BadRequestObjectResult BadRequest([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||
{
|
||||
if (modelState == null)
|
||||
{
|
||||
|
|
@ -1780,7 +1784,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="error">An error object to be returned to the client.</param>
|
||||
/// <returns>The created <see cref="UnprocessableEntityObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual UnprocessableEntityObjectResult UnprocessableEntity(object error)
|
||||
public virtual UnprocessableEntityObjectResult UnprocessableEntity([ActionResultObjectValue] object error)
|
||||
{
|
||||
return new UnprocessableEntityObjectResult(error);
|
||||
}
|
||||
|
|
@ -1791,7 +1795,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="modelState">The <see cref="ModelStateDictionary" /> containing errors to be returned to the client.</param>
|
||||
/// <returns>The created <see cref="UnprocessableEntityObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual UnprocessableEntityObjectResult UnprocessableEntity(ModelStateDictionary modelState)
|
||||
public virtual UnprocessableEntityObjectResult UnprocessableEntity([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||
{
|
||||
if (modelState == null)
|
||||
{
|
||||
|
|
@ -1815,7 +1819,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="error">Contains errors to be returned to the client.</param>
|
||||
/// <returns>The created <see cref="ConflictObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ConflictObjectResult Conflict(object error)
|
||||
public virtual ConflictObjectResult Conflict([ActionResultObjectValue] object error)
|
||||
=> new ConflictObjectResult(error);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1824,7 +1828,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="modelState">The <see cref="ModelStateDictionary" /> containing errors to be returned to the client.</param>
|
||||
/// <returns>The created <see cref="ConflictObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ConflictObjectResult Conflict(ModelStateDictionary modelState)
|
||||
public virtual ConflictObjectResult Conflict([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||
=> new ConflictObjectResult(modelState);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1832,7 +1836,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ActionResult ValidationProblem(ValidationProblemDetails descriptor)
|
||||
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ValidationProblemDetails descriptor)
|
||||
{
|
||||
if (descriptor == null)
|
||||
{
|
||||
|
|
@ -1847,7 +1851,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <returns>The created <see cref="BadRequestObjectResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual ActionResult ValidationProblem(ModelStateDictionary modelStateDictionary)
|
||||
public virtual ActionResult ValidationProblem([ActionResultObjectValue] ModelStateDictionary modelStateDictionary)
|
||||
{
|
||||
if (modelStateDictionary == null)
|
||||
{
|
||||
|
|
@ -1877,7 +1881,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="CreatedResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual CreatedResult Created(string uri, object value)
|
||||
public virtual CreatedResult Created(string uri, [ActionResultObjectValue] object value)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
|
|
@ -1894,7 +1898,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="CreatedResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual CreatedResult Created(Uri uri, object value)
|
||||
public virtual CreatedResult Created(Uri uri, [ActionResultObjectValue] object value)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
|
|
@ -1911,7 +1915,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="CreatedAtActionResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual CreatedAtActionResult CreatedAtAction(string actionName, object value)
|
||||
public virtual CreatedAtActionResult CreatedAtAction(string actionName, [ActionResultObjectValue] object value)
|
||||
=> CreatedAtAction(actionName, routeValues: null, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1922,7 +1926,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="CreatedAtActionResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual CreatedAtActionResult CreatedAtAction(string actionName, object routeValues, object value)
|
||||
public virtual CreatedAtActionResult CreatedAtAction(string actionName, object routeValues, [ActionResultObjectValue] object value)
|
||||
=> CreatedAtAction(actionName, controllerName: null, routeValues: routeValues, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1938,7 +1942,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
string actionName,
|
||||
string controllerName,
|
||||
object routeValues,
|
||||
object value)
|
||||
[ActionResultObjectValue] object value)
|
||||
=> new CreatedAtActionResult(actionName, controllerName, routeValues, value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1948,7 +1952,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="CreatedAtRouteResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual CreatedAtRouteResult CreatedAtRoute(string routeName, object value)
|
||||
public virtual CreatedAtRouteResult CreatedAtRoute(string routeName, [ActionResultObjectValue] object value)
|
||||
=> CreatedAtRoute(routeName, routeValues: null, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1958,7 +1962,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="CreatedAtRouteResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual CreatedAtRouteResult CreatedAtRoute(object routeValues, object value)
|
||||
public virtual CreatedAtRouteResult CreatedAtRoute(object routeValues, [ActionResultObjectValue] object value)
|
||||
=> CreatedAtRoute(routeName: null, routeValues: routeValues, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1969,7 +1973,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The content value to format in the entity body.</param>
|
||||
/// <returns>The created <see cref="CreatedAtRouteResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual CreatedAtRouteResult CreatedAtRoute(string routeName, object routeValues, object value)
|
||||
public virtual CreatedAtRouteResult CreatedAtRoute(string routeName, object routeValues, [ActionResultObjectValue] object value)
|
||||
=> new CreatedAtRouteResult(routeName, routeValues, value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1986,7 +1990,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The optional content value to format in the entity body; may be null.</param>
|
||||
/// <returns>The created <see cref="AcceptedResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedResult Accepted(object value)
|
||||
public virtual AcceptedResult Accepted([ActionResultObjectValue] object value)
|
||||
=> new AcceptedResult(location: null, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2023,7 +2027,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The optional content value to format in the entity body; may be null.</param>
|
||||
/// <returns>The created <see cref="AcceptedResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedResult Accepted(string uri, object value)
|
||||
public virtual AcceptedResult Accepted(string uri, [ActionResultObjectValue] object value)
|
||||
=> new AcceptedResult(uri, value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2033,7 +2037,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The optional content value to format in the entity body; may be null.</param>
|
||||
/// <returns>The created <see cref="AcceptedResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedResult Accepted(Uri uri, object value)
|
||||
public virtual AcceptedResult Accepted(Uri uri, [ActionResultObjectValue] object value)
|
||||
{
|
||||
if (uri == null)
|
||||
{
|
||||
|
|
@ -2069,7 +2073,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The optional content value to format in the entity body; may be null.</param>
|
||||
/// <returns>The created <see cref="AcceptedAtActionResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, object value)
|
||||
public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, [ActionResultObjectValue] object value)
|
||||
=> AcceptedAtAction(actionName, routeValues: null, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2080,7 +2084,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="routeValues">The route data to use for generating the URL.</param>
|
||||
/// <returns>The created <see cref="AcceptedAtActionResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName, object routeValues)
|
||||
public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, string controllerName, [ActionResultObjectValue] object routeValues)
|
||||
=> AcceptedAtAction(actionName, controllerName, routeValues, value: null);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2091,7 +2095,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The optional content value to format in the entity body; may be null.</param>
|
||||
/// <returns>The created <see cref="AcceptedAtActionResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, object routeValues, object value)
|
||||
public virtual AcceptedAtActionResult AcceptedAtAction(string actionName, object routeValues, [ActionResultObjectValue] object value)
|
||||
=> AcceptedAtAction(actionName, controllerName: null, routeValues: routeValues, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2107,7 +2111,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
string actionName,
|
||||
string controllerName,
|
||||
object routeValues,
|
||||
object value)
|
||||
[ActionResultObjectValue] object value)
|
||||
=> new AcceptedAtActionResult(actionName, controllerName, routeValues, value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2116,7 +2120,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="routeValues">The route data to use for generating the URL.</param>
|
||||
/// <returns>The created <see cref="AcceptedAtRouteResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedAtRouteResult AcceptedAtRoute(object routeValues)
|
||||
public virtual AcceptedAtRouteResult AcceptedAtRoute([ActionResultObjectValue] object routeValues)
|
||||
=> AcceptedAtRoute(routeName: null, routeValues: routeValues, value: null);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2145,7 +2149,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The optional content value to format in the entity body; may be null.</param>
|
||||
/// <returns>The created <see cref="AcceptedAtRouteResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedAtRouteResult AcceptedAtRoute(object routeValues, object value)
|
||||
public virtual AcceptedAtRouteResult AcceptedAtRoute(object routeValues, [ActionResultObjectValue] object value)
|
||||
=> AcceptedAtRoute(routeName: null, routeValues: routeValues, value: value);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2156,7 +2160,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <param name="value">The optional content value to format in the entity body; may be null.</param>
|
||||
/// <returns>The created <see cref="AcceptedAtRouteResult"/> for the response.</returns>
|
||||
[NonAction]
|
||||
public virtual AcceptedAtRouteResult AcceptedAtRoute(string routeName, object routeValues, object value)
|
||||
public virtual AcceptedAtRouteResult AcceptedAtRoute(string routeName, object routeValues, [ActionResultObjectValue] object value)
|
||||
=> new AcceptedAtRouteResult(routeName, routeValues, value);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
string actionName,
|
||||
string controllerName,
|
||||
object routeValues,
|
||||
object value)
|
||||
[ActionResultObjectValue] object value)
|
||||
: base(value)
|
||||
{
|
||||
ActionName = actionName;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// </summary>
|
||||
/// <param name="routeValues">The route data to use for generating the URL.</param>
|
||||
/// <param name="value">The value to format in the entity body.</param>
|
||||
public CreatedAtRouteResult(object routeValues, object value)
|
||||
public CreatedAtRouteResult(object routeValues, [ActionResultObjectValue] object value)
|
||||
: this(routeName: null, routeValues: routeValues, value: value)
|
||||
{
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public CreatedAtRouteResult(
|
||||
string routeName,
|
||||
object routeValues,
|
||||
object value)
|
||||
[ActionResultObjectValue] object value)
|
||||
: base(value)
|
||||
{
|
||||
RouteName = routeName;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute annoted on ActionResult constructor, helper method parameters, and properties to indicate
|
||||
/// that the parameter or property is used to set the "value" for ActionResult.
|
||||
/// <para>
|
||||
/// Analyzers match this parameter by type name. This allows users to annotate custom results \ custom helpers
|
||||
/// with a user defined attribute without having to expose this type.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This attribute is intentionally marked Inherited=false since the analyzer does not walk the inheritance graph.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// BadObjectResult([ActionResultObjectValueAttribute] object value)
|
||||
/// ObjectResult { [ActionResultObjectValueAttribute] public object Value { get; set; } }
|
||||
/// </example>
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
|
||||
internal sealed class ActionResultObjectValueAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute annoted on ActionResult constructor and helper method parameters to indicate
|
||||
/// that the parameter is used to set the "statusCode" for the ActionResult.
|
||||
/// <para>
|
||||
/// Analyzers match this parameter by type name. This allows users to annotate custom results \ custom helpers
|
||||
/// with a user defined attribute without having to expose this type.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This attribute is intentionally marked Inherited=false since the analyzer does not walk the inheritance graph.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// StatusCodeResult([ActionResultStatusCodeParameter] int statusCode)
|
||||
/// </example>
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
|
||||
internal sealed class ActionResultStatusCodeAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
// 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,7 +18,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Creates a new <see cref="NotFoundObjectResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to format in the entity body.</param>
|
||||
public NotFoundObjectResult(object value)
|
||||
public NotFoundObjectResult([ActionResultObjectValue] object value)
|
||||
: base(value)
|
||||
{
|
||||
StatusCode = DefaultStatusCode;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
ContentTypes = new MediaTypeCollection();
|
||||
}
|
||||
|
||||
[ActionResultObjectValue]
|
||||
public object Value { get; set; }
|
||||
|
||||
public FormatterCollection<IOutputFormatter> Formatters { get; set; }
|
||||
|
|
|
|||
|
|
@ -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([StatusCodeValue] int statusCode)
|
||||
public StatusCodeResult([ActionResultStatusCode] int statusCode)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// <summary>
|
||||
/// Creates a new <see cref="UnauthorizedObjectResult"/> instance.
|
||||
/// </summary>
|
||||
public UnauthorizedObjectResult(object value) : base(value)
|
||||
public UnauthorizedObjectResult([ActionResultObjectValue] object value) : base(value)
|
||||
{
|
||||
StatusCode = DefaultStatusCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Creates a new <see cref="UnprocessableEntityObjectResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="modelState"><see cref="ModelStateDictionary"/> containing the validation errors.</param>
|
||||
public UnprocessableEntityObjectResult(ModelStateDictionary modelState)
|
||||
public UnprocessableEntityObjectResult([ActionResultObjectValue] ModelStateDictionary modelState)
|
||||
: this(new SerializableError(modelState))
|
||||
{
|
||||
}
|
||||
|
|
@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
/// Creates a new <see cref="UnprocessableEntityObjectResult"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="error">Contains errors to be returned to the client.</param>
|
||||
public UnprocessableEntityObjectResult(object error)
|
||||
public UnprocessableEntityObjectResult([ActionResultObjectValue] object error)
|
||||
: base(error)
|
||||
{
|
||||
StatusCode = DefaultStatusCode;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,369 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Analyzer.Testing;
|
||||
using Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.ActualApiResponseMetadataFactoryTest;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
public class ActualApiResponseMetadataFactoryTest
|
||||
{
|
||||
private static readonly string Namespace = typeof(ActualApiResponseMetadataFactoryTest).Namespace;
|
||||
|
||||
[Fact]
|
||||
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingStatusCodeConstants()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation("GetDefaultStatusCodeTest");
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingStatusCodesConstants).FullName).GetAttributes()[0];
|
||||
|
||||
// Act
|
||||
var actual = ActualApiResponseMetadataFactory.GetDefaultStatusCode(attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(412, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingHttpStatusCast()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation("GetDefaultStatusCodeTest");
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingHttpStatusCodeCast).FullName).GetAttributes()[0];
|
||||
|
||||
// Act
|
||||
var actual = ActualApiResponseMetadataFactory.GetDefaultStatusCode(attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(302, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound()
|
||||
{
|
||||
// Arrange & Act
|
||||
var source = @"
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class TestController : ControllerBase
|
||||
{
|
||||
public IActionResult Get(int id)
|
||||
{
|
||||
return new DoesNotExist(id);
|
||||
}
|
||||
}
|
||||
}";
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { source });
|
||||
var compilation = await project.GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var returnType = compilation.GetTypeByMetadataName($"{Namespace}.TestController");
|
||||
var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||
|
||||
var method = (IMethodSymbol)returnType.GetMembers().First();
|
||||
var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||
var returnStatement = methodSyntax.DescendantNodes().OfType<ReturnStatementSyntax>().First();
|
||||
|
||||
var actualResponseMetadata = ActualApiResponseMetadataFactory.InspectReturnStatementSyntax(
|
||||
symbolCache,
|
||||
compilation.GetSemanticModel(syntaxTree),
|
||||
returnStatement,
|
||||
CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.Null(actualResponseMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsStatusCodeFromDefaultStatusCodeAttributeOnActionResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(401, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsStatusCodeFromStatusCodePropertyAssignment()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(201, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsStatusCodeFromConstructorAssignment()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(204, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsStatusCodeFromHelperMethod()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(302, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_UsesExplicitlySpecifiedStatusCode_ForActionResultWithDefaultStatusCode()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(422, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReadsStatusCodeConstant()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(423, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_DoesNotReadLocalFieldWithConstantValue()
|
||||
{
|
||||
// This is a gap in the analyzer. We're using this to document the current behavior and not an expecation.
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.Null(actualResponseMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_FallsBackToDefaultStatusCode_WhenAppliedStatusCodeCannotBeRead()
|
||||
{
|
||||
// This is a gap in the analyzer. We're using this to document the current behavior and not an expecation.
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(400, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_SetsReturnType_WhenLiteralTypeIsSpecifiedInConstructor()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata?.ReturnType);
|
||||
Assert.Equal("TestModel", actualResponseMetadata.Value.ReturnType.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_SetsReturnType_WhenLocalValueIsSpecifiedInConstructor()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata?.ReturnType);
|
||||
Assert.Equal("TestModel", actualResponseMetadata.Value.ReturnType.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_SetsReturnType_WhenValueIsReturned()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata?.ReturnType);
|
||||
Assert.Equal("TestModel", actualResponseMetadata.Value.ReturnType.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsNullReturnType_IfValueIsNotSpecified()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Null(actualResponseMetadata.Value.ReturnType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningOkResult()
|
||||
{
|
||||
// Arrange
|
||||
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningOkResult);
|
||||
|
||||
// Act
|
||||
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(success);
|
||||
Assert.Collection(
|
||||
responseMetadatas,
|
||||
metadata =>
|
||||
{
|
||||
Assert.False(metadata.IsDefaultResponse);
|
||||
Assert.Equal(200, metadata.StatusCode);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningModel()
|
||||
{
|
||||
// Arrange
|
||||
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningModel);
|
||||
|
||||
// Act
|
||||
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(success);
|
||||
Assert.Collection(
|
||||
responseMetadatas,
|
||||
metadata =>
|
||||
{
|
||||
Assert.True(metadata.IsDefaultResponse);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel()
|
||||
{
|
||||
// Arrange
|
||||
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||
var methodName = nameof(TryGetActualResponseMetadataController.ActionReturningNotFoundAndModel);
|
||||
|
||||
// Act
|
||||
var (success, responseMetadatas, testSource) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(success);
|
||||
Assert.Collection(
|
||||
responseMetadatas,
|
||||
metadata =>
|
||||
{
|
||||
Assert.False(metadata.IsDefaultResponse);
|
||||
Assert.Equal(204, metadata.StatusCode);
|
||||
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM1"], metadata.ReturnStatement.GetLocation());
|
||||
|
||||
},
|
||||
metadata =>
|
||||
{
|
||||
Assert.True(metadata.IsDefaultResponse);
|
||||
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM2"], metadata.ReturnStatement.GetLocation());
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<(bool result, IList<ActualApiResponseMetadata> responseMetadatas, TestSource testSource)> TryGetActualResponseMetadata(string typeName, string methodName)
|
||||
{
|
||||
var testSource = MvcTestSource.Read(GetType().Name, "TryGetActualResponseMetadataTests");
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
|
||||
|
||||
var compilation = await GetCompilation("TryGetActualResponseMetadataTests");
|
||||
|
||||
var type = compilation.GetTypeByMetadataName(typeName);
|
||||
var method = (IMethodSymbol)type.GetMembers(methodName).First();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var syntaxTree = method.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||
var methodSyntax = (MethodDeclarationSyntax)syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||
var semanticModel = compilation.GetSemanticModel(syntaxTree);
|
||||
|
||||
var result = ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, CancellationToken.None, out var responseMetadatas);
|
||||
|
||||
return (result, responseMetadatas, testSource);
|
||||
}
|
||||
|
||||
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation("InspectReturnExpressionTests");
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var controllerType = compilation.GetTypeByMetadataName(typeof(TestFiles.InspectReturnExpressionTests.TestController).FullName);
|
||||
var syntaxTree = controllerType.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||
|
||||
var method = (IMethodSymbol)Assert.Single(controllerType.GetMembers(test));
|
||||
var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||
var returnStatement = methodSyntax.DescendantNodes().OfType<ReturnStatementSyntax>().First();
|
||||
|
||||
return ActualApiResponseMetadataFactory.InspectReturnStatementSyntax(
|
||||
symbolCache,
|
||||
compilation.GetSemanticModel(syntaxTree),
|
||||
returnStatement,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax(string source, string test)
|
||||
{
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { source });
|
||||
var compilation = await project.GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var returnType = compilation.GetTypeByMetadataName($"{Namespace}.{test}");
|
||||
var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||
|
||||
var method = (IMethodSymbol)returnType.GetMembers().First();
|
||||
var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||
var returnStatement = methodSyntax.DescendantNodes().OfType<ReturnStatementSyntax>().First();
|
||||
|
||||
return ActualApiResponseMetadataFactory.InspectReturnStatementSyntax(
|
||||
symbolCache,
|
||||
compilation.GetSemanticModel(syntaxTree),
|
||||
returnStatement,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
private Task<Compilation> GetCompilation(string test)
|
||||
{
|
||||
var testSource = MvcTestSource.Read(GetType().Name, test);
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
|
||||
|
||||
return project.GetCompilationAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,9 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
[Fact]
|
||||
public Task CodeFixAddsMissingStatusCodes() => RunTest();
|
||||
|
||||
[Fact]
|
||||
public Task CodeFixAddsMissingStatusCodesAndTypes() => RunTest();
|
||||
|
||||
[Fact]
|
||||
public Task CodeFixWithConventionAddsMissingStatusCodes() => RunTest();
|
||||
|
||||
|
|
@ -36,6 +39,9 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
[Fact]
|
||||
public Task CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants() => RunTest();
|
||||
|
||||
[Fact]
|
||||
public Task CodeFixAddsResponseTypeWhenDifferentFromErrorType() => RunTest();
|
||||
|
||||
[Fact]
|
||||
public Task CodeFixAddsStatusCodesFromMethodParameters() => RunTest();
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,71 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
public Task NoDiagnosticsAreReturned_ForReturnStatementsInLocalFunctions()
|
||||
=> RunNoDiagnosticsAreReturned();
|
||||
|
||||
[Fact]
|
||||
public async Task DiagnosticsAreReturned_ForIncompleteActionResults()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[ApiController]
|
||||
[Route(""[controller]/[action]"")
|
||||
public class TestController : ControllerBase
|
||||
{
|
||||
public IActionResult Get(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
/*MM*/return NotFound();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}";
|
||||
var testSource = TestSource.Read(source);
|
||||
var expectedLocation = testSource.DefaultMarkerLocation;
|
||||
|
||||
// Act
|
||||
var result = await Executor.GetDiagnosticsAsync(testSource.Source);
|
||||
|
||||
// Assert
|
||||
var diagnostic = Assert.Single(result, d => d.Id == ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode.Id);
|
||||
AnalyzerAssert.DiagnosticLocation(expectedLocation, diagnostic.Location);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoDiagnosticsAreReturned_WhenActionDoesNotCompile()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
namespace Test
|
||||
{
|
||||
[ApiController]
|
||||
[Route(""[controller]/[action]"")
|
||||
public class TestController : ControllerBase
|
||||
{
|
||||
public IActionResult Get(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}";
|
||||
|
||||
// Act
|
||||
var result = await Executor.GetDiagnosticsAsync(source);
|
||||
|
||||
// Assert
|
||||
Assert.DoesNotContain(result, d => d.Id == ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode()
|
||||
=> RunTest(ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode, 404);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -35,7 +35,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ImplicitResponse;
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(200, AttributeData, Mock.Of<IMethodSymbol>());
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of<IMethodSymbol>());
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(201, AttributeData, Mock.Of<IMethodSymbol>());
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 200, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -99,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesResponseType(302, AttributeData, Mock.Of<IMethodSymbol>());
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 302);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 302, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, actualStatusCode);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, actualStatusCode, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -130,7 +130,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 204);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, 204, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
{
|
||||
// Arrange
|
||||
var declaredMetadata = DeclaredApiResponseMetadata.ForProducesDefaultResponse(AttributeData, Mock.Of<IMethodSymbol>());
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement);
|
||||
var actualMetadata = new ActualApiResponseMetadata(ReturnStatement, null);
|
||||
|
||||
// Act
|
||||
var matches = declaredMetadata.Matches(actualMetadata);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Analyzer.Testing;
|
||||
using Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
|
|
@ -218,12 +214,12 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetResponseMetadata_WIthProducesResponseTypeAndApiConventionMethod()
|
||||
public async Task GetResponseMetadata_WithProducesResponseTypeAndApiConventionMethod()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_WIthProducesResponseTypeAndApiConventionMethod)).First();
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_WithProducesResponseTypeAndApiConventionMethod)).First();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
|
|
@ -362,193 +358,75 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingStatusCodeConstants()
|
||||
public async Task GetErrorResponseType_ReturnsProblemDetails_IfNoAttributeIsDiscovered()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation("GetDefaultStatusCodeTest");
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingStatusCodesConstants).FullName).GetAttributes()[0];
|
||||
var compilation = await GetCompilation(nameof(GetErrorResponseType_ReturnsProblemDetails_IfNoAttributeIsDiscovered));
|
||||
var expected = compilation.GetTypeByMetadataName(typeof(ProblemDetails).FullName);
|
||||
|
||||
// Act
|
||||
var actual = SymbolApiResponseMetadataProvider.GetDefaultStatusCode(attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(412, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingHttpStatusCast()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation("GetDefaultStatusCodeTest");
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingHttpStatusCodeCast).FullName).GetAttributes()[0];
|
||||
|
||||
// Act
|
||||
var actual = SymbolApiResponseMetadataProvider.GetDefaultStatusCode(attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(302, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound()
|
||||
{
|
||||
// Arrange & Act
|
||||
var source = @"
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound : ControllerBase
|
||||
{
|
||||
public IActionResult Get(int id)
|
||||
{
|
||||
return new DoesNotExist(id);
|
||||
}
|
||||
}
|
||||
}";
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax(source, nameof(InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound));
|
||||
|
||||
// Assert
|
||||
Assert.Null(actualResponseMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsStatusCodeFromDefaultStatusCodeAttributeOnActionResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.Equal(401, actualResponseMetadata.Value.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResult()
|
||||
{
|
||||
// Arrange & Act
|
||||
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualResponseMetadata);
|
||||
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningOkResult()
|
||||
{
|
||||
// Arrange
|
||||
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningOkResult);
|
||||
|
||||
// Act
|
||||
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(success);
|
||||
Assert.Collection(
|
||||
responseMetadatas,
|
||||
metadata =>
|
||||
{
|
||||
Assert.False(metadata.IsDefaultResponse);
|
||||
Assert.Equal(200, metadata.StatusCode);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetActualResponseMetadata_ActionWithActionResultOfTReturningModel()
|
||||
{
|
||||
// Arrange
|
||||
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||
var methodName = nameof(TryGetActualResponseMetadataController.ActionWithActionResultOfTReturningModel);
|
||||
|
||||
// Act
|
||||
var (success, responseMetadatas, _) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(success);
|
||||
Assert.Collection(
|
||||
responseMetadatas,
|
||||
metadata =>
|
||||
{
|
||||
Assert.True(metadata.IsDefaultResponse);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryGetActualResponseMetadata_ActionReturningNotFoundAndModel()
|
||||
{
|
||||
// Arrange
|
||||
var typeName = typeof(TryGetActualResponseMetadataController).FullName;
|
||||
var methodName = nameof(TryGetActualResponseMetadataController.ActionReturningNotFoundAndModel);
|
||||
|
||||
// Act
|
||||
var (success, responseMetadatas, testSource) = await TryGetActualResponseMetadata(typeName, methodName);
|
||||
|
||||
// Assert
|
||||
Assert.True(success);
|
||||
Assert.Collection(
|
||||
responseMetadatas,
|
||||
metadata =>
|
||||
{
|
||||
Assert.False(metadata.IsDefaultResponse);
|
||||
Assert.Equal(204, metadata.StatusCode);
|
||||
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM1"], metadata.ReturnStatement.GetLocation());
|
||||
|
||||
},
|
||||
metadata =>
|
||||
{
|
||||
Assert.True(metadata.IsDefaultResponse);
|
||||
AnalyzerAssert.DiagnosticLocation(testSource.MarkerLocations["MM2"], metadata.ReturnStatement.GetLocation());
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<(bool result, IList<ActualApiResponseMetadata> responseMetadatas, TestSource testSource)> TryGetActualResponseMetadata(string typeName, string methodName)
|
||||
{
|
||||
var testSource = MvcTestSource.Read(GetType().Name, "TryGetActualResponseMetadataTests");
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
|
||||
|
||||
var compilation = await GetCompilation("TryGetActualResponseMetadataTests");
|
||||
|
||||
var type = compilation.GetTypeByMetadataName(typeName);
|
||||
var method = (IMethodSymbol)type.GetMembers(methodName).First();
|
||||
var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsProblemDetails_IfNoAttributeIsDiscoveredController).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers("Action").First();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var syntaxTree = method.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||
var methodSyntax = (MethodDeclarationSyntax)syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||
var semanticModel = compilation.GetSemanticModel(syntaxTree);
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method);
|
||||
|
||||
var result = SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, CancellationToken.None, out var responseMetadatas);
|
||||
|
||||
return (result, responseMetadatas, testSource);
|
||||
// Assert
|
||||
Assert.Same(expected, result);
|
||||
}
|
||||
|
||||
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
|
||||
[Fact]
|
||||
public async Task GetErrorResponseType_ReturnsTypeDefinedAtAssembly()
|
||||
{
|
||||
// Arrange
|
||||
var testSource = MvcTestSource.Read(GetType().Name, test);
|
||||
return await RunInspectReturnStatementSyntax(testSource.Source, test);
|
||||
}
|
||||
var compilation = await GetCompilation(nameof(GetErrorResponseType_ReturnsTypeDefinedAtAssembly));
|
||||
var expected = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtAssemblyModel).FullName);
|
||||
|
||||
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax(string source, string test)
|
||||
{
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { source });
|
||||
var compilation = await project.GetCompilationAsync();
|
||||
var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtAssemblyController).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers("Action").First();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var returnType = compilation.GetTypeByMetadataName($"{Namespace}.{test}");
|
||||
var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree;
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method);
|
||||
|
||||
var method = (IMethodSymbol)returnType.GetMembers().First();
|
||||
var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
|
||||
var returnStatement = methodSyntax.DescendantNodes().OfType<ReturnStatementSyntax>().First();
|
||||
// Assert
|
||||
Assert.Same(expected, result);
|
||||
}
|
||||
|
||||
return SymbolApiResponseMetadataProvider.InspectReturnStatementSyntax(
|
||||
symbolCache,
|
||||
compilation.GetSemanticModel(syntaxTree),
|
||||
returnStatement,
|
||||
CancellationToken.None);
|
||||
[Fact]
|
||||
public async Task GetErrorResponseType_ReturnsTypeDefinedAtController()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation(nameof(GetErrorResponseType_ReturnsTypeDefinedAtController));
|
||||
var expected = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtControllerModel).FullName);
|
||||
|
||||
var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtControllerController).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers("Action").First();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetErrorResponseType_ReturnsTypeDefinedAtAction()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation(nameof(GetErrorResponseType_ReturnsTypeDefinedAtAction));
|
||||
var expected = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtActionModel).FullName);
|
||||
|
||||
var type = compilation.GetTypeByMetadataName(typeof(GetErrorResponseType_ReturnsTypeDefinedAtActionController).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers("Action").First();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetErrorResponseType(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.Same(expected, result);
|
||||
}
|
||||
|
||||
private Task<Compilation> GetResponseMetadataCompilation() => GetCompilation("GetResponseMetadataTests");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.InspectReturnExpressionTests
|
||||
{
|
||||
public class TestController : ControllerBase
|
||||
{
|
||||
public object InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResult()
|
||||
{
|
||||
return new TestModel();
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_ReturnsStatusCodeFromDefaultStatusCodeAttributeOnActionResult()
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_ReturnsStatusCodeFromStatusCodePropertyAssignment()
|
||||
{
|
||||
return new ObjectResult(new object()) { StatusCode = 201 };
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_ReturnsStatusCodeFromConstructorAssignment()
|
||||
{
|
||||
return new StatusCodeResult(204);
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_ReturnsStatusCodeFromHelperMethod()
|
||||
{
|
||||
return StatusCode(302);
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_UsesExplicitlySpecifiedStatusCode_ForActionResultWithDefaultStatusCode()
|
||||
{
|
||||
return new BadRequestObjectResult(new object())
|
||||
{
|
||||
StatusCode = StatusCodes.Status422UnprocessableEntity,
|
||||
};
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_ReadsStatusCodeConstant()
|
||||
{
|
||||
return StatusCode(StatusCodes.Status423Locked);
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_DoesNotReadLocalFieldWithConstantValue()
|
||||
{
|
||||
var statusCode = StatusCodes.Status429TooManyRequests;
|
||||
return StatusCode(statusCode);
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_FallsBackToDefaultStatusCode_WhenAppliedStatusCodeCannotBeRead()
|
||||
{
|
||||
var statusCode = StatusCodes.Status422UnprocessableEntity;
|
||||
return new BadRequestObjectResult(new object()) { StatusCode = statusCode };
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_SetsReturnType_WhenLiteralTypeIsSpecifiedInConstructor()
|
||||
{
|
||||
return new BadRequestObjectResult(new TestModel());
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_SetsReturnType_WhenLocalValueIsSpecifiedInConstructor()
|
||||
{
|
||||
var local = new TestModel();
|
||||
return new BadRequestObjectResult(local);
|
||||
}
|
||||
|
||||
public IActionResult InspectReturnExpression_ReturnsNullReturnType_IfValueIsNotSpecified()
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
public ActionResult<TestModel> InspectReturnExpression_SetsReturnType_WhenValueIsReturned()
|
||||
{
|
||||
var local = new TestModel();
|
||||
return local;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestModel { }
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.ActualApiResponseMetadataFactoryTest
|
||||
{
|
||||
public class TryGetActualResponseMetadataController : ControllerBase
|
||||
{
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class CodeFixAddsMissingStatusCodesAndTypes : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public IActionResult GetItem(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class CodeFixAddsMissingStatusCodesAndTypes : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ModelBinding.ModelStateDictionary), StatusCodes.Status400BadRequest)]
|
||||
[ProducesDefaultResponseType]
|
||||
public IActionResult GetItem(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
|
||||
{
|
||||
[ProducesErrorResponseType(typeof(CodeFixAddsResponseTypeWhenDifferentErrorModel))]
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class CodeFixAddsResponseTypeWhenDifferentFromErrorType : ControllerBase
|
||||
{
|
||||
public IActionResult GetItem(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound(new CodeFixAddsResponseTypeWhenDifferentErrorModel());
|
||||
}
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
var validationProblemDetails = new ValidationProblemDetails(ModelState);
|
||||
return BadRequest(validationProblemDetails);
|
||||
}
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
}
|
||||
|
||||
public class CodeFixAddsResponseTypeWhenDifferentErrorModel { }
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
|
||||
{
|
||||
[ProducesErrorResponseType(typeof(CodeFixAddsResponseTypeWhenDifferentErrorModel))]
|
||||
[ApiController]
|
||||
[Route("[controller]/[action]")]
|
||||
public class CodeFixAddsResponseTypeWhenDifferentFromErrorType : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesDefaultResponseType]
|
||||
public IActionResult GetItem(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound(new CodeFixAddsResponseTypeWhenDifferentErrorModel());
|
||||
}
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
var validationProblemDetails = new ValidationProblemDetails(ModelState);
|
||||
return BadRequest(validationProblemDetails);
|
||||
}
|
||||
|
||||
return Ok(new object());
|
||||
}
|
||||
}
|
||||
|
||||
public class CodeFixAddsResponseTypeWhenDifferentErrorModel { }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
|
||||
{
|
||||
public class GetErrorResponseType_ReturnsProblemDetails_IfNoAttributeIsDiscoveredController
|
||||
{
|
||||
public void Action() { }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
|
||||
{
|
||||
[ProducesErrorResponseType(typeof(ModelStateDictionary))]
|
||||
public class GetErrorResponseType_ReturnsTypeDefinedAtActionController
|
||||
{
|
||||
[ProducesErrorResponseType(typeof(GetErrorResponseType_ReturnsTypeDefinedAtActionModel))]
|
||||
public void Action() { }
|
||||
}
|
||||
|
||||
public class GetErrorResponseType_ReturnsTypeDefinedAtActionModel { }
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[assembly: ProducesErrorResponseType(typeof(Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest.GetErrorResponseType_ReturnsTypeDefinedAtAssemblyModel))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
|
||||
{
|
||||
public class GetErrorResponseType_ReturnsTypeDefinedAtAssemblyController
|
||||
{
|
||||
public void Action() { }
|
||||
}
|
||||
|
||||
public class GetErrorResponseType_ReturnsTypeDefinedAtAssemblyModel { }
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers.TestFiles.SymbolApiResponseMetadataProviderTest
|
||||
{
|
||||
[ProducesErrorResponseType(typeof(GetErrorResponseType_ReturnsTypeDefinedAtControllerModel))]
|
||||
public class GetErrorResponseType_ReturnsTypeDefinedAtControllerController
|
||||
{
|
||||
public void Action() { }
|
||||
}
|
||||
|
||||
public class GetErrorResponseType_ReturnsTypeDefinedAtControllerModel { }
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
|||
|
||||
[ProducesResponseType(204)]
|
||||
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Find))]
|
||||
public IActionResult GetResponseMetadata_WIthProducesResponseTypeAndApiConventionMethod() => null;
|
||||
public IActionResult GetResponseMetadata_WithProducesResponseTypeAndApiConventionMethod() => null;
|
||||
}
|
||||
|
||||
public class Person { }
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
public class InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResult : ControllerBase
|
||||
{
|
||||
public object Get()
|
||||
{
|
||||
return new InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResultModel();
|
||||
}
|
||||
}
|
||||
|
||||
public class InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResultModel { }
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
|
||||
{
|
||||
public class InspectReturnExpression_ReturnsStatusCodeFromDefaultStatusCodeAttributeOnActionResult : ControllerBase
|
||||
{
|
||||
public IActionResult Get()
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue